mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-18 21:34:39 +01:00
parent
6bc0943a62
commit
02c4505948
4
.github/workflows/Linux_2.12_RPC_Tests.yml
vendored
4
.github/workflows/Linux_2.12_RPC_Tests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Linux 2.12 bitcoind, eclair, and lnd rpc tests
|
||||
name: Linux 2.12 bitcoind, eclair, tor, and lnd rpc tests
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
|
||||
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.12.13 downloadBitcoind downloadEclair downloadLnd coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test
|
||||
run: sbt ++2.12.13 downloadBitcoind downloadEclair downloadLnd coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test torTest/test
|
||||
|
4
.github/workflows/Linux_2.13_RPC_Tests.yml
vendored
4
.github/workflows/Linux_2.13_RPC_Tests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Linux 2.13 bitcoind, eclair, and lnd rpc tests
|
||||
name: Linux 2.13 bitcoind, eclair, tor, and lnd rpc tests
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
|
||||
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.5 downloadBitcoind downloadEclair downloadLnd coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test
|
||||
run: sbt ++2.13.5 downloadBitcoind downloadEclair downloadLnd coverage bitcoindRpcTest/test bitcoindRpc/coverageReport bitcoindRpc/coverageAggregate bitcoindRpc/coveralls eclairRpcTest/test eclairRpc/coverageReport eclairRpc/coverageAggregate eclairRpc/coveralls lndRpcTest/test torTest/test
|
||||
|
13
build.sbt
13
build.sbt
@ -11,7 +11,6 @@ Test / flywayClean / aggregate := true
|
||||
|
||||
lazy val Benchmark = config("bench") extend Test
|
||||
|
||||
|
||||
lazy val benchSettings: Seq[Def.SettingsDefinition] = {
|
||||
//for scalameter
|
||||
//https://scalameter.github.io/home/download/
|
||||
@ -148,6 +147,14 @@ lazy val lndRpc = project
|
||||
.in(file("lnd-rpc"))
|
||||
.dependsOn(asyncUtilsJVM, bitcoindRpc)
|
||||
|
||||
lazy val tor = project
|
||||
.in(file("tor"))
|
||||
.dependsOn(cryptoJVM)
|
||||
|
||||
lazy val torTest = project
|
||||
.in(file("tor-test"))
|
||||
.dependsOn(tor, testkit)
|
||||
|
||||
lazy val jsProjects: Vector[ProjectReference] =
|
||||
Vector(asyncUtilsJS,
|
||||
asyncUtilsTestJS,
|
||||
@ -209,6 +216,8 @@ lazy val `bitcoin-s` = project
|
||||
serverRoutes,
|
||||
lndRpc,
|
||||
lndRpcTest,
|
||||
tor,
|
||||
torTest,
|
||||
scripts
|
||||
)
|
||||
.dependsOn(
|
||||
@ -256,6 +265,8 @@ lazy val `bitcoin-s` = project
|
||||
serverRoutes,
|
||||
lndRpc,
|
||||
lndRpcTest,
|
||||
tor,
|
||||
torTest,
|
||||
scripts
|
||||
)
|
||||
.settings(CommonSettings.settings: _*)
|
||||
|
@ -33,3 +33,32 @@ object InetAddress {
|
||||
InetAddressImpl(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
trait TorAddress extends InetAddress {
|
||||
|
||||
override def ipv4Bytes: ByteVector = throw new IllegalArgumentException(
|
||||
"Tor address cannot be an IPv4 address")
|
||||
}
|
||||
|
||||
object TorAddress {
|
||||
val TOR_V2_ADDR_LENGTH = 10
|
||||
val TOR_V3_ADDR_LENGTH = 32
|
||||
}
|
||||
|
||||
trait Tor2Address extends TorAddress
|
||||
|
||||
object Tor2Address {
|
||||
|
||||
private case class Tor2AddressImpl(bytes: ByteVector) extends Tor2Address {
|
||||
require(bytes.size == TorAddress.TOR_V2_ADDR_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
trait Tor3Address extends TorAddress
|
||||
|
||||
object Tor3Address {
|
||||
|
||||
private case class Tor3AddressImpl(bytes: ByteVector) extends Tor2Address {
|
||||
require(bytes.size == TorAddress.TOR_V3_ADDR_LENGTH)
|
||||
}
|
||||
}
|
||||
|
@ -623,10 +623,10 @@ object AddrV2Message extends Factory[AddrV2Message] {
|
||||
final val IPV6_ADDR_LENGTH: Int = 16
|
||||
|
||||
final val TOR_V2_NETWORK_BYTE: Byte = 0x03
|
||||
final val TOR_V2_ADDR_LENGTH: Int = 10
|
||||
final val TOR_V2_ADDR_LENGTH: Int = TorAddress.TOR_V2_ADDR_LENGTH
|
||||
|
||||
final val TOR_V3_NETWORK_BYTE: Byte = 0x04
|
||||
final val TOR_V3_ADDR_LENGTH: Int = 32
|
||||
final val TOR_V3_ADDR_LENGTH: Int = TorAddress.TOR_V3_ADDR_LENGTH
|
||||
|
||||
final val I2P_NETWORK_BYTE: Byte = 0x05
|
||||
final val I2P_ADDR_LENGTH: Int = 32
|
||||
|
@ -101,10 +101,11 @@ object Deps {
|
||||
"org.scalafx" %% "scalafx" % V.scalaFxV withSources () withJavadoc ()
|
||||
|
||||
lazy val arch = System.getProperty("os.arch")
|
||||
|
||||
lazy val osName = System.getProperty("os.name") match {
|
||||
case n if n.startsWith("Linux") => "linux"
|
||||
case n if n.startsWith("Mac") =>
|
||||
if (arch == "aarch64" ) {
|
||||
case n if n.startsWith("Linux") => "linux"
|
||||
case n if n.startsWith("Mac") =>
|
||||
if (arch == "aarch64") {
|
||||
//needed to accommodate the different chip
|
||||
//arch for M1
|
||||
s"mac-${arch}"
|
||||
@ -114,6 +115,7 @@ object Deps {
|
||||
case n if n.startsWith("Windows") => "win"
|
||||
case x => throw new Exception(s"Unknown platform $x!")
|
||||
}
|
||||
|
||||
// Not sure if all of these are needed, some might be possible to remove
|
||||
lazy val javaFxBase =
|
||||
"org.openjfx" % s"javafx-base" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
@ -426,6 +428,13 @@ object Deps {
|
||||
Compile.grizzledSlf4j
|
||||
)
|
||||
|
||||
val tor: Def.Initialize[List[ModuleID]] = Def.setting {
|
||||
List(
|
||||
Compile.akkaActor,
|
||||
Compile.scodec.value
|
||||
)
|
||||
}
|
||||
|
||||
val lndRpc = List(
|
||||
Compile.akkaHttp,
|
||||
Compile.akkaHttp2,
|
||||
|
@ -0,0 +1,337 @@
|
||||
package org.bitcoins.tor
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.io.Tcp.Connected
|
||||
import akka.testkit.{ImplicitSender, TestActorRef, TestKit}
|
||||
import akka.util.ByteString
|
||||
import org.bitcoins.crypto.CryptoUtil
|
||||
import org.scalatest._
|
||||
import org.scalatest.funsuite.AnyFunSuiteLike
|
||||
import scodec.bits._
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.{Files, Paths}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Promise}
|
||||
|
||||
class TorProtocolHandlerSpec
|
||||
extends TestKit(ActorSystem("test"))
|
||||
with TestSuite
|
||||
with BeforeAndAfterAll
|
||||
with AnyFunSuiteLike
|
||||
with ImplicitSender {
|
||||
|
||||
import TorProtocolHandler._
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
val LocalHost = new InetSocketAddress("localhost", 8888)
|
||||
val PASSWORD = "foobar"
|
||||
|
||||
val ClientNonce =
|
||||
hex"8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA"
|
||||
val tempDir = Files.createTempDirectory("tor-test-").toAbsolutePath.toString
|
||||
val PkFilePath = Paths.get(tempDir, "testtorpk.dat")
|
||||
val CookieFilePath = Paths.get(tempDir, "testtorcookie.dat")
|
||||
|
||||
val AuthCookie =
|
||||
hex"AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485"
|
||||
|
||||
override def withFixture(test: NoArgTest) = {
|
||||
PkFilePath.toFile.delete()
|
||||
super.withFixture(test) // Invoke the test function
|
||||
}
|
||||
|
||||
ignore("connect to real tor daemon") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
val protocolHandlerProps =
|
||||
TorProtocolHandler.props(version = OnionServiceVersion("v2"),
|
||||
authentication = Password(PASSWORD),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress))
|
||||
|
||||
val controller =
|
||||
TestActorRef(TorController.props(new InetSocketAddress("localhost", 9051),
|
||||
protocolHandlerProps),
|
||||
"tor")
|
||||
|
||||
val address = Await.result(promiseOnionAddress.future, 30.seconds)
|
||||
println(address)
|
||||
}
|
||||
|
||||
test("happy path v2") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(version = OnionServiceVersion("v2"),
|
||||
authentication = Password(PASSWORD),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=HASHEDPASSWORD\r\n" +
|
||||
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n"""))
|
||||
protocolHandler ! ByteString(
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-ServiceID=z4zif3fy7fe7bpg3\r\n" +
|
||||
"250-PrivateKey=RSA1024:private-key\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
protocolHandler ! GetOnionAddress
|
||||
val addr = expectMsgType[Option[InetSocketAddress]]
|
||||
|
||||
assert(addr.nonEmpty)
|
||||
|
||||
assertAddressesEqual(
|
||||
addr.get,
|
||||
InetSocketAddress.createUnresolved("z4zif3fy7fe7bpg3.onion", 9999))
|
||||
|
||||
val address = Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
assertAddressesEqual(
|
||||
address,
|
||||
InetSocketAddress.createUnresolved("z4zif3fy7fe7bpg3.onion", 9999))
|
||||
|
||||
assert(readString(PkFilePath) === "RSA1024:private-key")
|
||||
}
|
||||
|
||||
test("happy path v3") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(version = OnionServiceVersion("v3"),
|
||||
authentication = Password(PASSWORD),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=HASHEDPASSWORD\r\n" +
|
||||
"250-VERSION Tor=\"0.3.4.8\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n"""))
|
||||
protocolHandler ! ByteString(
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-ServiceID=mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd\r\n" +
|
||||
"250-PrivateKey=ED25519-V3:private-key\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
protocolHandler ! GetOnionAddress
|
||||
val addr = expectMsgType[Option[InetSocketAddress]]
|
||||
|
||||
assert(addr.nonEmpty)
|
||||
|
||||
assertAddressesEqual(
|
||||
addr.get,
|
||||
InetSocketAddress.createUnresolved(
|
||||
"mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion",
|
||||
9999))
|
||||
|
||||
val address = Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
assertAddressesEqual(
|
||||
address,
|
||||
InetSocketAddress.createUnresolved(
|
||||
"mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion",
|
||||
9999))
|
||||
|
||||
assert(readString(PkFilePath) === "ED25519-V3:private-key")
|
||||
}
|
||||
|
||||
test("v2/v3 compatibility check against tor version") {
|
||||
assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6"))
|
||||
assert(!OnionServiceVersion.isCompatible(V3, "0.3.3.5"))
|
||||
assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6-devel"))
|
||||
assert(OnionServiceVersion.isCompatible(V3, "0.4"))
|
||||
assert(!OnionServiceVersion.isCompatible(V3, "0.2"))
|
||||
assert(OnionServiceVersion.isCompatible(V3, "0.5.1.2.3.4"))
|
||||
|
||||
}
|
||||
|
||||
test("authentication method errors") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(version = OnionServiceVersion("v2"),
|
||||
authentication = Password(PASSWORD),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
|
||||
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
assert(intercept[TorException] {
|
||||
Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
} === TorException(
|
||||
"cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'"))
|
||||
}
|
||||
|
||||
test("invalid server hash") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
Files.write(CookieFilePath, CryptoUtil.randomBytes(32).toArray)
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(
|
||||
version = OnionServiceVersion("v2"),
|
||||
authentication = SafeCookie(ClientNonce),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)
|
||||
))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
|
||||
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(
|
||||
"AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
|
||||
)
|
||||
|
||||
assert(intercept[TorException] {
|
||||
Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
} === TorException("unexpected server hash"))
|
||||
}
|
||||
|
||||
test("AUTHENTICATE failure") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
Files.write(CookieFilePath, AuthCookie.toArray)
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(
|
||||
version = OnionServiceVersion("v2"),
|
||||
authentication = SafeCookie(ClientNonce),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)
|
||||
))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
|
||||
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(
|
||||
"AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(
|
||||
"AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"515 Authentication failed: Safe cookie response did not match expected value.\r\n"
|
||||
)
|
||||
|
||||
assert(intercept[TorException] {
|
||||
Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
} === TorException(
|
||||
"server returned error: 515 Authentication failed: Safe cookie response did not match expected value."))
|
||||
}
|
||||
|
||||
test("ADD_ONION failure") {
|
||||
val promiseOnionAddress = Promise[InetSocketAddress]()
|
||||
|
||||
Files.write(CookieFilePath, AuthCookie.toArray)
|
||||
|
||||
val protocolHandler = TestActorRef(
|
||||
props(
|
||||
version = OnionServiceVersion("v2"),
|
||||
authentication = SafeCookie(ClientNonce),
|
||||
privateKeyPath = PkFilePath,
|
||||
virtualPort = 9999,
|
||||
onionAdded = Some(promiseOnionAddress)
|
||||
))
|
||||
|
||||
protocolHandler ! Connected(LocalHost, LocalHost)
|
||||
|
||||
expectMsg(ByteString("PROTOCOLINFO 1\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250-PROTOCOLINFO 1\r\n" +
|
||||
"250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" +
|
||||
"250-VERSION Tor=\"0.3.3.5\"\r\n" +
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(
|
||||
"AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString(
|
||||
"AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"250 OK\r\n"
|
||||
)
|
||||
|
||||
expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n"))
|
||||
protocolHandler ! ByteString(
|
||||
"513 Invalid argument\r\n"
|
||||
)
|
||||
|
||||
val t = intercept[TorException] {
|
||||
Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
}
|
||||
|
||||
assert(intercept[TorException] {
|
||||
Await.result(promiseOnionAddress.future, 3.seconds)
|
||||
} === TorException("server returned error: 513 Invalid argument"))
|
||||
}
|
||||
|
||||
private def assertAddressesEqual(
|
||||
actual: InetSocketAddress,
|
||||
expected: InetSocketAddress) = {
|
||||
assert(
|
||||
actual.getHostString == expected.getHostString && actual.getPort == expected.getPort)
|
||||
}
|
||||
|
||||
}
|
0
tor/README.md
Normal file
0
tor/README.md
Normal file
238
tor/src/main/scala/org/bitcoins/tor/Socks5Connection.scala
Normal file
238
tor/src/main/scala/org/bitcoins/tor/Socks5Connection.scala
Normal file
@ -0,0 +1,238 @@
|
||||
package org.bitcoins.tor
|
||||
|
||||
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated}
|
||||
import akka.io.Tcp
|
||||
import akka.util.ByteString
|
||||
import Socks5Connection.{Credentials, Socks5Connect}
|
||||
import org.bitcoins.crypto.CryptoUtil
|
||||
|
||||
/** Simple socks 5 client. It should be given a new connection, and will
|
||||
*
|
||||
* Created by rorp
|
||||
*
|
||||
* @param connection underlying TcpConnection
|
||||
* @param credentials_opt optional username/password for authentication
|
||||
*/
|
||||
class Socks5Connection(
|
||||
connection: ActorRef,
|
||||
credentials_opt: Option[Credentials],
|
||||
command: Socks5Connect)
|
||||
extends Actor
|
||||
with ActorLogging {
|
||||
|
||||
import Socks5Connection._
|
||||
|
||||
context watch connection
|
||||
|
||||
val passwordAuth: Boolean = credentials_opt.isDefined
|
||||
|
||||
var isConnected: Boolean = false
|
||||
|
||||
connection ! Tcp.Register(self)
|
||||
connection ! Tcp.ResumeReading
|
||||
connection ! Tcp.Write(socks5Greeting(passwordAuth))
|
||||
|
||||
override def receive: Receive = greetings
|
||||
|
||||
def greetings: Receive = { case Tcp.Received(data) =>
|
||||
if (data(0) != 0x05) {
|
||||
throw Socks5Error("Invalid SOCKS5 proxy response")
|
||||
} else if (
|
||||
(!passwordAuth && data(1) != NoAuth) || (passwordAuth && data(
|
||||
1) != PasswordAuth)
|
||||
) {
|
||||
throw Socks5Error("Unrecognized SOCKS5 auth method")
|
||||
} else {
|
||||
if (data(1) == PasswordAuth) {
|
||||
context become authenticate
|
||||
val credentials = credentials_opt.getOrElse(
|
||||
throw Socks5Error("credentials are not defined"))
|
||||
connection ! Tcp.Write(
|
||||
socks5PasswordAuthenticationRequest(credentials.username,
|
||||
credentials.password))
|
||||
connection ! Tcp.ResumeReading
|
||||
} else {
|
||||
context become connectionRequest
|
||||
connection ! Tcp.Write(socks5ConnectionRequest(command.address))
|
||||
connection ! Tcp.ResumeReading
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def authenticate: Receive = { case Tcp.Received(data) =>
|
||||
if (data(0) != 0x01) {
|
||||
throw Socks5Error("Invalid SOCKS5 proxy response")
|
||||
} else if (data(1) != 0) {
|
||||
throw Socks5Error("SOCKS5 authentication failed")
|
||||
}
|
||||
context become connectionRequest
|
||||
connection ! Tcp.Write(socks5ConnectionRequest(command.address))
|
||||
connection ! Tcp.ResumeReading
|
||||
}
|
||||
|
||||
def connectionRequest: Receive = { case Tcp.Received(data) =>
|
||||
if (data(0) != 0x05) {
|
||||
throw Socks5Error("Invalid SOCKS5 proxy response")
|
||||
} else {
|
||||
val status = data(1)
|
||||
if (status != 0) {
|
||||
throw Socks5Error(
|
||||
connectErrors.getOrElse(status, s"Unknown SOCKS5 error $status"))
|
||||
}
|
||||
val connectedAddress = data(3) match {
|
||||
case 0x01 =>
|
||||
val ip = Array(data(4), data(5), data(6), data(7))
|
||||
val port = data(8).toInt << 8 | data(9)
|
||||
new InetSocketAddress(InetAddress.getByAddress(ip), port)
|
||||
case 0x03 =>
|
||||
val len = data(4)
|
||||
val start = 5
|
||||
val end = start + len
|
||||
val domain = data.slice(start, end).utf8String
|
||||
val port = data(end).toInt << 8 | data(end + 1)
|
||||
new InetSocketAddress(domain, port)
|
||||
case 0x04 =>
|
||||
val ip = Array.ofDim[Byte](16)
|
||||
data.copyToArray(ip, 4, 4 + ip.length)
|
||||
val port = data(4 + ip.length).toInt << 8 | data(4 + ip.length + 1)
|
||||
new InetSocketAddress(InetAddress.getByAddress(ip), port)
|
||||
case _ => throw Socks5Error(s"Unrecognized address type")
|
||||
}
|
||||
context become connected
|
||||
context.parent ! Socks5Connected(connectedAddress)
|
||||
isConnected = true
|
||||
}
|
||||
}
|
||||
|
||||
def connected: Receive = { case Tcp.Register(handler, _, _) =>
|
||||
context become registered(handler)
|
||||
}
|
||||
|
||||
def registered(handler: ActorRef): Receive = {
|
||||
case c: Tcp.Command => connection ! c
|
||||
case e: Tcp.Event => handler ! e
|
||||
}
|
||||
|
||||
override def unhandled(message: Any): Unit = message match {
|
||||
case Terminated(actor) if actor == connection => context stop self
|
||||
case _: Tcp.ConnectionClosed => context stop self
|
||||
case _ => log.warning(s"unhandled message=$message")
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
super.postStop()
|
||||
connection ! Tcp.Close
|
||||
if (!isConnected) {
|
||||
context.parent ! command.failureMessage
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Socks5Connection {
|
||||
|
||||
def props(
|
||||
tcpConnection: ActorRef,
|
||||
credentials_opt: Option[Credentials],
|
||||
command: Socks5Connect): Props = Props(
|
||||
new Socks5Connection(tcpConnection, credentials_opt, command))
|
||||
|
||||
case class Socks5Connect(address: InetSocketAddress) extends Tcp.Command
|
||||
|
||||
case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event
|
||||
|
||||
case class Socks5Error(message: String) extends RuntimeException(message)
|
||||
|
||||
case class Credentials(username: String, password: String) {
|
||||
require(username.length < 256, "username is too long")
|
||||
require(password.length < 256, "password is too long")
|
||||
}
|
||||
|
||||
val NoAuth: Byte = 0x00
|
||||
val PasswordAuth: Byte = 0x02
|
||||
|
||||
val connectErrors: Map[Byte, String] = Map[Byte, String](
|
||||
(0x00, "Request granted"),
|
||||
(0x01, "General failure"),
|
||||
(0x02, "Connection not allowed by ruleset"),
|
||||
(0x03, "Network unreachable"),
|
||||
(0x04, "Host unreachable"),
|
||||
(0x05, "Connection refused by destination host"),
|
||||
(0x06, "TTL expired"),
|
||||
(0x07, "Command not supported / protocol error"),
|
||||
(0x08, "Address type not supported")
|
||||
)
|
||||
|
||||
def socks5Greeting(passwordAuth: Boolean) = ByteString(
|
||||
0x05, // SOCKS version
|
||||
0x01, // number of authentication methods supported
|
||||
if (passwordAuth) PasswordAuth else NoAuth
|
||||
) // auth method
|
||||
|
||||
def socks5PasswordAuthenticationRequest(
|
||||
username: String,
|
||||
password: String): ByteString =
|
||||
ByteString(0x01, // version of username/password authentication
|
||||
username.length.toByte) ++
|
||||
ByteString(username) ++
|
||||
ByteString(password.length.toByte) ++
|
||||
ByteString(password)
|
||||
|
||||
def socks5ConnectionRequest(address: InetSocketAddress): ByteString = {
|
||||
ByteString(0x05, // SOCKS version
|
||||
0x01, // establish a TCP/IP stream connection
|
||||
0x00) ++ // reserved
|
||||
addressToByteString(address) ++
|
||||
portToByteString(address.getPort)
|
||||
}
|
||||
|
||||
def inetAddressToByteString(inet: InetAddress): ByteString = inet match {
|
||||
case a: Inet4Address =>
|
||||
ByteString(
|
||||
0x01 // IPv4 address
|
||||
) ++ ByteString(a.getAddress)
|
||||
case a: Inet6Address =>
|
||||
ByteString(
|
||||
0x04 // IPv6 address
|
||||
) ++ ByteString(a.getAddress)
|
||||
case _ => throw Socks5Error("Unknown InetAddress")
|
||||
}
|
||||
|
||||
def addressToByteString(address: InetSocketAddress): ByteString = Option(
|
||||
address.getAddress) match {
|
||||
case None =>
|
||||
// unresolved address, use SOCKS5 resolver
|
||||
val host = address.getHostString
|
||||
ByteString(0x03, // Domain name
|
||||
host.length.toByte) ++
|
||||
ByteString(host)
|
||||
case Some(inetAddress) =>
|
||||
inetAddressToByteString(inetAddress)
|
||||
}
|
||||
|
||||
def portToByteString(port: Int): ByteString =
|
||||
ByteString((port & 0x0000ff00) >> 8, port & 0x000000ff)
|
||||
}
|
||||
|
||||
case class Socks5ProxyParams(
|
||||
address: InetSocketAddress,
|
||||
credentials_opt: Option[Credentials],
|
||||
randomizeCredentials: Boolean,
|
||||
useForIPv4: Boolean,
|
||||
useForIPv6: Boolean,
|
||||
useForTor: Boolean)
|
||||
|
||||
object Socks5ProxyParams {
|
||||
|
||||
def proxyCredentials(
|
||||
proxyParams: Socks5ProxyParams): Option[Socks5Connection.Credentials] =
|
||||
if (proxyParams.randomizeCredentials) {
|
||||
// randomize credentials for every proxy connection to enable Tor stream isolation
|
||||
Some(
|
||||
Socks5Connection.Credentials(CryptoUtil.randomBytes(16).toHex,
|
||||
CryptoUtil.randomBytes(16).toHex))
|
||||
} else {
|
||||
proxyParams.credentials_opt
|
||||
}
|
||||
}
|
75
tor/src/main/scala/org/bitcoins/tor/TorController.scala
Normal file
75
tor/src/main/scala/org/bitcoins/tor/TorController.scala
Normal file
@ -0,0 +1,75 @@
|
||||
package org.bitcoins.tor
|
||||
|
||||
import akka.actor.{
|
||||
Actor,
|
||||
ActorLogging,
|
||||
OneForOneStrategy,
|
||||
Props,
|
||||
SupervisorStrategy,
|
||||
Terminated
|
||||
}
|
||||
import akka.io.{IO, Tcp}
|
||||
import akka.util.ByteString
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/** Created by rorp
|
||||
*
|
||||
* @param address Tor control address
|
||||
* @param protocolHandlerProps Tor protocol handler props
|
||||
* @param ec execution context
|
||||
*/
|
||||
class TorController(address: InetSocketAddress, protocolHandlerProps: Props)
|
||||
extends Actor
|
||||
with ActorLogging {
|
||||
|
||||
import TorController._
|
||||
import Tcp._
|
||||
import context.system
|
||||
|
||||
IO(Tcp) ! Connect(address)
|
||||
|
||||
def receive = {
|
||||
case e @ CommandFailed(_: Connect) =>
|
||||
e.cause match {
|
||||
case Some(ex) => log.error(ex, "Cannot connect")
|
||||
case _ => log.error("Cannot connect")
|
||||
}
|
||||
context stop self
|
||||
case c: Connected =>
|
||||
val protocolHandler = context actorOf protocolHandlerProps
|
||||
protocolHandler ! c
|
||||
val connection = sender()
|
||||
connection ! Register(self)
|
||||
context watch connection
|
||||
context become {
|
||||
case data: ByteString =>
|
||||
connection ! Write(data)
|
||||
case CommandFailed(_: Write) =>
|
||||
// O/S buffer was full
|
||||
protocolHandler ! SendFailed
|
||||
log.error("Tor command failed")
|
||||
case Received(data) =>
|
||||
protocolHandler ! data
|
||||
case _: ConnectionClosed =>
|
||||
context stop self
|
||||
case Terminated(actor) if actor == connection =>
|
||||
context stop self
|
||||
}
|
||||
}
|
||||
|
||||
// we should not restart a failing tor session
|
||||
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) {
|
||||
case _ => SupervisorStrategy.Escalate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object TorController {
|
||||
|
||||
def props(address: InetSocketAddress, protocolHandlerProps: Props) =
|
||||
Props(new TorController(address, protocolHandlerProps))
|
||||
|
||||
case object SendFailed
|
||||
|
||||
}
|
349
tor/src/main/scala/org/bitcoins/tor/TorProtocolHandler.scala
Normal file
349
tor/src/main/scala/org/bitcoins/tor/TorProtocolHandler.scala
Normal file
@ -0,0 +1,349 @@
|
||||
package org.bitcoins.tor
|
||||
|
||||
import java.nio.file.attribute.PosixFilePermissions
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.util
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||
import akka.io.Tcp.Connected
|
||||
import akka.util.ByteString
|
||||
import TorProtocolHandler.{Authentication, OnionServiceVersion}
|
||||
import org.bitcoins.crypto.CryptoUtil
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
case class TorException(private val msg: String)
|
||||
extends RuntimeException(s"Tor error: $msg")
|
||||
|
||||
/** Created by rorp
|
||||
*
|
||||
* Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt
|
||||
*
|
||||
* @param onionServiceVersion v2 or v3
|
||||
* @param authentication Tor controller auth mechanism (password or safecookie)
|
||||
* @param privateKeyPath path to a file that contains a Tor private key
|
||||
* @param virtualPort port for the public hidden service (typically 9735)
|
||||
* @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty
|
||||
* @param onionAdded a Promise to track creation of the endpoint
|
||||
*/
|
||||
class TorProtocolHandler(
|
||||
onionServiceVersion: OnionServiceVersion,
|
||||
authentication: Authentication,
|
||||
privateKeyPath: Path,
|
||||
virtualPort: Int,
|
||||
targets: Seq[String],
|
||||
onionAdded: Option[Promise[InetSocketAddress]])
|
||||
extends Actor
|
||||
with Stash
|
||||
with ActorLogging {
|
||||
|
||||
import TorProtocolHandler._
|
||||
|
||||
private var receiver: ActorRef = _
|
||||
|
||||
private var address: Option[InetSocketAddress] = None
|
||||
|
||||
override def receive: Receive = { case Connected(_, _) =>
|
||||
receiver = sender()
|
||||
sendCommand("PROTOCOLINFO 1")
|
||||
context become protocolInfo
|
||||
}
|
||||
|
||||
def protocolInfo: Receive = { case data: ByteString =>
|
||||
val res = parseResponse(readResponse(data))
|
||||
val methods: String =
|
||||
res.getOrElse("METHODS", throw TorException("auth methods not found"))
|
||||
val torVersion = unquote(
|
||||
res.getOrElse("Tor", throw TorException("version not found")))
|
||||
log.info(s"Tor version $torVersion")
|
||||
if (!OnionServiceVersion.isCompatible(onionServiceVersion, torVersion)) {
|
||||
throw TorException(
|
||||
s"version $torVersion does not support onion service $onionServiceVersion")
|
||||
}
|
||||
if (!Authentication.isCompatible(authentication, methods)) {
|
||||
throw TorException(
|
||||
s"cannot use authentication '$authentication', supported methods are '$methods'")
|
||||
}
|
||||
authentication match {
|
||||
case Password(password) =>
|
||||
sendCommand(s"""AUTHENTICATE "$password"""")
|
||||
context become authenticate
|
||||
case SafeCookie(nonce) =>
|
||||
val cookieFile = Paths.get(
|
||||
unquote(
|
||||
res.getOrElse("COOKIEFILE",
|
||||
throw TorException("cookie file not found"))))
|
||||
sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${nonce.toHex}")
|
||||
context become cookieChallenge(cookieFile, nonce)
|
||||
}
|
||||
}
|
||||
|
||||
def cookieChallenge(cookieFile: Path, clientNonce: ByteVector): Receive = {
|
||||
case data: ByteString =>
|
||||
val res = parseResponse(readResponse(data))
|
||||
val clientHash = computeClientHash(
|
||||
ByteVector.fromValidHex(res
|
||||
.getOrElse("SERVERHASH", throw TorException("server hash not found"))
|
||||
.toLowerCase),
|
||||
ByteVector.fromValidHex(
|
||||
res
|
||||
.getOrElse("SERVERNONCE",
|
||||
throw TorException("server nonce not found"))
|
||||
.toLowerCase),
|
||||
clientNonce,
|
||||
cookieFile
|
||||
)
|
||||
sendCommand(s"AUTHENTICATE ${clientHash.toHex}")
|
||||
context become authenticate
|
||||
}
|
||||
|
||||
def authenticate: Receive = { case data: ByteString =>
|
||||
readResponse(data)
|
||||
sendCommand(s"ADD_ONION $computeKey $computePort")
|
||||
context become addOnion
|
||||
}
|
||||
|
||||
def addOnion: Receive = { case data: ByteString =>
|
||||
val res = readResponse(data)
|
||||
if (ok(res)) {
|
||||
val serviceId = processOnionResponse(parseResponse(res))
|
||||
address = Some(
|
||||
InetSocketAddress.createUnresolved(s"$serviceId.onion", virtualPort))
|
||||
onionAdded.foreach(_.success(address.get))
|
||||
log.debug("Onion address: {}", address.get)
|
||||
}
|
||||
}
|
||||
|
||||
override def aroundReceive(receive: Receive, msg: Any): Unit = try {
|
||||
super.aroundReceive(receive, msg)
|
||||
} catch {
|
||||
case t: Throwable =>
|
||||
onionAdded.map(_.tryFailure(t))
|
||||
()
|
||||
}
|
||||
|
||||
override def unhandled(message: Any): Unit = message match {
|
||||
case GetOnionAddress =>
|
||||
sender() ! address
|
||||
}
|
||||
|
||||
private def processOnionResponse(res: Map[String, String]): String = {
|
||||
val serviceId =
|
||||
res.getOrElse("ServiceID", throw TorException("service ID not found"))
|
||||
val privateKey = res.get("PrivateKey")
|
||||
privateKey.foreach { pk =>
|
||||
writeString(privateKeyPath, pk)
|
||||
setPermissions(privateKeyPath, "rw-------")
|
||||
}
|
||||
serviceId
|
||||
}
|
||||
|
||||
private def computeKey: String = {
|
||||
if (privateKeyPath.toFile.exists()) {
|
||||
readString(privateKeyPath)
|
||||
} else {
|
||||
onionServiceVersion match {
|
||||
case V2 => "NEW:RSA1024"
|
||||
case V3 => "NEW:ED25519-V3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def computePort: String = {
|
||||
if (targets.isEmpty) {
|
||||
s"Port=$virtualPort,$virtualPort"
|
||||
} else {
|
||||
targets.map(p => s"Port=$virtualPort,$p").mkString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
private def computeClientHash(
|
||||
serverHash: ByteVector,
|
||||
serverNonce: ByteVector,
|
||||
clientNonce: ByteVector,
|
||||
cookieFile: Path): ByteVector = {
|
||||
if (serverHash.length != 32)
|
||||
throw TorException("invalid server hash length")
|
||||
if (serverNonce.length != 32)
|
||||
throw TorException("invalid server nonce length")
|
||||
|
||||
val cookie = ByteVector.view(Files.readAllBytes(cookieFile))
|
||||
|
||||
val message = cookie ++ clientNonce ++ serverNonce
|
||||
|
||||
val computedServerHash = hmacSHA256(ServerKey, message)
|
||||
if (computedServerHash != serverHash) {
|
||||
throw TorException("unexpected server hash")
|
||||
}
|
||||
|
||||
hmacSHA256(ClientKey, message)
|
||||
}
|
||||
|
||||
private def sendCommand(cmd: String): Unit = {
|
||||
receiver ! ByteString(s"$cmd\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
object TorProtocolHandler {
|
||||
|
||||
def props(
|
||||
version: OnionServiceVersion,
|
||||
authentication: Authentication,
|
||||
privateKeyPath: Path,
|
||||
virtualPort: Int,
|
||||
targets: Seq[String] = Seq(),
|
||||
onionAdded: Option[Promise[InetSocketAddress]] = None): Props =
|
||||
Props(
|
||||
new TorProtocolHandler(version,
|
||||
authentication,
|
||||
privateKeyPath,
|
||||
virtualPort,
|
||||
targets,
|
||||
onionAdded))
|
||||
|
||||
// those are defined in the spec
|
||||
private val ServerKey = ByteVector.view(
|
||||
"Tor safe cookie authentication server-to-controller hash".getBytes())
|
||||
|
||||
private val ClientKey = ByteVector.view(
|
||||
"Tor safe cookie authentication controller-to-server hash".getBytes())
|
||||
|
||||
// @formatter:off
|
||||
sealed trait OnionServiceVersion
|
||||
case object V2 extends OnionServiceVersion
|
||||
case object V3 extends OnionServiceVersion
|
||||
// @formatter:on
|
||||
|
||||
object OnionServiceVersion {
|
||||
|
||||
def apply(s: String): OnionServiceVersion = s match {
|
||||
case "v2" | "V2" => V2
|
||||
case "v3" | "V3" => V3
|
||||
case _ => throw TorException(s"unknown protocol version `$s`")
|
||||
}
|
||||
|
||||
def isCompatible(
|
||||
onionServiceVersion: OnionServiceVersion,
|
||||
torVersion: String): Boolean =
|
||||
onionServiceVersion match {
|
||||
case V2 => true
|
||||
case V3 =>
|
||||
torVersion
|
||||
.split("\\.")
|
||||
.map(
|
||||
_.split('-').head
|
||||
) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.)
|
||||
.map(d => Try(d.toInt).getOrElse(0))
|
||||
.zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6
|
||||
.foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left
|
||||
case (Some(res), _) =>
|
||||
Some(
|
||||
res
|
||||
) // we stop the comparison as soon as there is a difference
|
||||
case (None, (v, vref)) =>
|
||||
if (v > vref) Some(true)
|
||||
else if (v < vref) Some(false)
|
||||
else None
|
||||
}
|
||||
.getOrElse(true) // if version == 0.3.3.6 then result will be None
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait Authentication
|
||||
|
||||
case class Password(password: String) extends Authentication {
|
||||
override def toString = "password"
|
||||
}
|
||||
|
||||
case class SafeCookie(nonce: ByteVector = CryptoUtil.randomBytes(32))
|
||||
extends Authentication {
|
||||
override def toString = "safecookie"
|
||||
}
|
||||
|
||||
object Authentication {
|
||||
|
||||
def isCompatible(authentication: Authentication, methods: String): Boolean =
|
||||
authentication match {
|
||||
case _: Password => methods.contains("HASHEDPASSWORD")
|
||||
case _: SafeCookie => methods.contains("SAFECOOKIE")
|
||||
}
|
||||
}
|
||||
|
||||
case object GetOnionAddress
|
||||
|
||||
def readString(path: Path): String = Files.readAllLines(path).get(0)
|
||||
|
||||
def writeString(path: Path, string: String): Unit = {
|
||||
Files.write(path, util.Arrays.asList(string))
|
||||
()
|
||||
}
|
||||
|
||||
def setPermissions(path: Path, permissionString: String): Unit =
|
||||
try {
|
||||
Files.setPosixFilePermissions(
|
||||
path,
|
||||
PosixFilePermissions.fromString(permissionString))
|
||||
()
|
||||
} catch {
|
||||
case _: UnsupportedOperationException => () // we are on windows
|
||||
}
|
||||
|
||||
def unquote(s: String): String = s
|
||||
.stripSuffix("\"")
|
||||
.stripPrefix("\"")
|
||||
.replace("""\\""", """\""")
|
||||
.replace("""\"""", "\"")
|
||||
|
||||
private val r1 = """(\d+)-(.*)""".r
|
||||
private val r2 = """(\d+) (.*)""".r
|
||||
|
||||
def readResponse(bstr: ByteString): Seq[(Int, String)] = {
|
||||
val lines = bstr.utf8String
|
||||
.split('\n')
|
||||
.map(_.stripSuffix("\r"))
|
||||
.filterNot(_.isEmpty)
|
||||
.map {
|
||||
case r1(c, msg) => (c.toInt, msg)
|
||||
case r2(c, msg) => (c.toInt, msg)
|
||||
case x @ _ => throw TorException(s"unknown response line format: `$x`")
|
||||
}
|
||||
.toSeq
|
||||
if (!ok(lines)) {
|
||||
throw TorException(
|
||||
s"server returned error: ${status(lines)} ${reason(lines)}")
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
def ok(res: Seq[(Int, String)]): Boolean = status(res) == 250
|
||||
|
||||
def status(res: Seq[(Int, String)]): Int =
|
||||
res.lastOption.map(_._1).getOrElse(-1)
|
||||
|
||||
def reason(res: Seq[(Int, String)]): String =
|
||||
res.lastOption.map(_._2).getOrElse("Unknown error")
|
||||
|
||||
private val r = """([^=]+)=(.+)""".r
|
||||
|
||||
def parseResponse(lines: Seq[(Int, String)]): Map[String, String] = {
|
||||
lines.flatMap { case (_, message) =>
|
||||
message
|
||||
.split(" ")
|
||||
.collect { case r(k, v) =>
|
||||
(k, v)
|
||||
}
|
||||
}.toMap
|
||||
}
|
||||
|
||||
def hmacSHA256(key: ByteVector, message: ByteVector): ByteVector = {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val secretKey = new SecretKeySpec(key.toArray, "HmacSHA256")
|
||||
mac.init(secretKey)
|
||||
ByteVector.view(mac.doFinal(message.toArray))
|
||||
}
|
||||
}
|
5
tor/tor.sbt
Normal file
5
tor/tor.sbt
Normal file
@ -0,0 +1,5 @@
|
||||
name := "bitcoin-s-tor"
|
||||
|
||||
libraryDependencies ++= Deps.tor.value
|
||||
|
||||
CommonSettings.prodSettings
|
Loading…
Reference in New Issue
Block a user