├── .gitignore
├── .travis.yml
├── LICENSE
├── README.MD
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
└── scala
│ └── com
│ └── karasiq
│ ├── parsers
│ ├── ByteFragment.scala
│ ├── ByteRange.scala
│ ├── ParserException.scala
│ ├── RegexByteExtractor.scala
│ ├── http
│ │ ├── HttpConnect.scala
│ │ ├── HttpHeaders.scala
│ │ ├── HttpRequest.scala
│ │ └── HttpResponse.scala
│ └── socks
│ │ ├── SocksClient.scala
│ │ ├── SocksServer.scala
│ │ └── internal
│ │ ├── Address.scala
│ │ ├── LengthString.scala
│ │ ├── NullTerminatedString.scala
│ │ ├── Port.scala
│ │ └── Socks4AInvalidIP.scala
│ └── proxy
│ ├── ProxyChain.scala
│ ├── ProxyException.scala
│ ├── client
│ ├── HttpProxyClientStage.scala
│ └── SocksProxyClientStage.scala
│ └── server
│ ├── ProxyConnectionRequest.scala
│ └── ProxyServerStage.scala
└── test
├── resources
└── reference.conf
└── scala
└── com
└── github
└── karasiq
└── proxy
└── tests
├── ActorSpec.scala
├── client
└── ProxyChainTest.scala
├── rfc
├── HttpConnectRfcTest.scala
└── SocksRfcTest.scala
└── server
└── ProxyServerTest.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | .idea
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - 2.11.11
4 | - 2.12.3
5 | jdk:
6 | - oraclejdk8
7 | script:
8 | - sbt ++$TRAVIS_SCALA_VERSION compile "testOnly com.github.karasiq.proxy.tests.rfc.* com.github.karasiq.proxy.tests.server.*"
9 | sudo: false
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Karasiq
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # proxyutils [](https://travis-ci.org/Karasiq/proxyutils) [](https://maven-badges.herokuapp.com/maven-central/com.github.karasiq/proxyutils_2.12)
2 | Scala HTTP/SOCKS proxy library, based on akka-streams.
3 |
4 |
5 | # Example usage
6 | See https://github.com/Karasiq/proxychain/blob/dd7189782c12fb60628b0c945b43cc4330eb5cdd/src/main/scala/com/karasiq/proxychain/app/Boot.scala#L64-L68
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | val projectName = "proxyutils"
2 |
3 | lazy val commonSettings = Seq(
4 | organization := "com.github.karasiq",
5 | version := "2.0.14",
6 | isSnapshot := version.value.endsWith("SNAPSHOT"),
7 | scalaVersion := "2.12.3",
8 | crossScalaVersions := Seq("2.11.11", scalaVersion.value),
9 | resolvers += "softprops-maven" at "http://dl.bintray.com/content/softprops/maven"
10 | // resolvers += Resolver.sonatypeRepo("snapshots")
11 | )
12 |
13 | lazy val publishSettings = Seq(
14 | publishMavenStyle := true,
15 | publishTo := {
16 | val nexus = "https://oss.sonatype.org/"
17 | if (isSnapshot.value)
18 | Some("snapshots" at nexus + "content/repositories/snapshots")
19 | else
20 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
21 | },
22 | publishArtifact in Test := false,
23 | pomIncludeRepository := { _ ⇒ false },
24 | licenses := Seq("Apache License, Version 2.0" → url("http://opensource.org/licenses/Apache-2.0")),
25 | homepage := Some(url(s"https://github.com/Karasiq/$projectName")),
26 | pomExtra :=
27 | git@github.com:Karasiq/{projectName}.git
28 | scm:git:git@github.com:Karasiq/{projectName}.git
29 |
30 |
31 |
32 | karasiq
33 | Piston Karasiq
34 | https://github.com/Karasiq
35 |
36 |
37 | )
38 |
39 | lazy val proxyutils = (project in file("."))
40 | .settings(
41 | commonSettings,
42 | publishSettings,
43 | name := projectName,
44 | libraryDependencies ++= {
45 | val akkaV = "2.5.4"
46 | val akkaHttpV = "10.0.10"
47 | Seq(
48 | "commons-io" % "commons-io" % "2.5",
49 | "org.apache.httpcomponents" % "httpclient" % "4.5.3",
50 | "com.typesafe.akka" %% "akka-actor" % akkaV,
51 | "com.typesafe.akka" %% "akka-stream" % akkaV,
52 | "com.typesafe.akka" %% "akka-http" % akkaHttpV,
53 | "com.typesafe.akka" %% "akka-testkit" % akkaV % "test",
54 | "com.typesafe.akka" %% "akka-stream-testkit" % akkaV % "test",
55 | "com.github.karasiq" %% "commons-akka" % "1.0.7",
56 | "com.github.karasiq" %% "cryptoutils" % "1.4.3",
57 | "org.scalatest" %% "scalatest" % "3.0.4" % "test",
58 | "org.bouncycastle" % "bcprov-jdk15on" % "1.58" % "provided",
59 | "org.bouncycastle" % "bcpkix-jdk15on" % "1.58" % "provided"
60 | )
61 | }
62 | )
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 1.1.0
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | logLevel := Level.Warn
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/ByteFragment.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import akka.util.ByteString
6 |
7 | import scala.language.implicitConversions
8 |
9 | /**
10 | * Generic bytes serializer/deserializer
11 | * @tparam T Result type
12 | */
13 | trait ByteFragment[T] {
14 | final type Extractor = PartialFunction[Seq[Byte], (T, Seq[Byte])]
15 |
16 | def fromBytes: Extractor
17 |
18 | def toBytes(value: T): ByteString
19 |
20 | final def unapply(bytes: Seq[Byte]): Option[(T, Seq[Byte])] = {
21 | bytes match {
22 | case bs if fromBytes.isDefinedAt(bs) ⇒
23 | Some(fromBytes(bs))
24 |
25 | case _ ⇒
26 | None
27 | }
28 | }
29 |
30 | final def apply(t: T): ByteString = toBytes(t)
31 | }
32 |
33 | object ByteFragment {
34 | def wrap(f: ⇒ ByteBuffer): ByteString = {
35 | val buffer = f
36 | buffer.flip()
37 | ByteString(buffer)
38 | }
39 |
40 | def create(size: Int)(f: ByteBuffer ⇒ ByteBuffer): ByteString = {
41 | wrap(f(ByteBuffer.allocate(size)))
42 | }
43 |
44 | implicit def bytesToByteString(b: Seq[Byte]): ByteString = ByteString(b.toArray)
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/ByteRange.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers
2 |
3 | trait ByteRange[T] {
4 | def fromByte: PartialFunction[Byte, T]
5 | def toByte: PartialFunction[T, Byte]
6 |
7 | final def unapply(b: Byte): Option[T] = {
8 | fromByte.lift.apply(b)
9 | }
10 |
11 | final def apply(t: T): Byte = {
12 | toByte(t)
13 | }
14 |
15 | final def apply(b: Byte): T = unapply(b).getOrElse(throw new ParserException("Not in range: " + b))
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/ParserException.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers
2 |
3 | /**
4 | * Generic parser exception
5 | */
6 | class ParserException(message: String = null, cause: Throwable = null) extends Exception(message, cause)
7 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/RegexByteExtractor.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers
2 |
3 | import akka.util.ByteString
4 |
5 | import scala.util.matching.Regex
6 |
7 | final class RegexByteExtractor(r: Regex) {
8 | def unapply(b: Seq[Byte]): Option[(Regex.Match, Seq[Byte])] = {
9 | val str = ByteString(b.toArray).utf8String
10 | r.findFirstMatchIn(str) match {
11 | case Some(m) ⇒
12 | Some(m → ByteString(str.drop(m.group(0).length)))
13 |
14 | case _ ⇒
15 | None
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/http/HttpConnect.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.http
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import akka.util.ByteString
6 | import com.karasiq.networkutils.http.headers.{Host, HttpHeader}
7 | import com.karasiq.networkutils.url.URLParser
8 |
9 |
10 | object HttpConnect {
11 | private def portOption(port: Int) = Some(port).filter(_ != -1)
12 |
13 | def addressOf(u: String): InetSocketAddress = {
14 | val url = URLParser.withDefaultProtocol(u)
15 | val (host, port) = url.getHost → portOption(url.getPort).orElse(portOption(url.getDefaultPort)).getOrElse(80)
16 | InetSocketAddress.createUnresolved(host, port)
17 | }
18 |
19 | private def withHostHeader(address: InetSocketAddress, headers: Seq[HttpHeader]) = {
20 | if (headers.exists(_.name == Host.name)) headers else headers :+ Host(address)
21 | }
22 |
23 | def apply(address: InetSocketAddress, headers: Seq[HttpHeader]): ByteString = {
24 | HttpRequest((HttpMethod.CONNECT, s"${address.getHostString}:${address.getPort}", withHostHeader(address, headers)))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/http/HttpHeaders.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.http
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.networkutils.http.headers.HttpHeader
5 |
6 | private[http] object HttpHeaders {
7 | private def asByteString(b: Seq[Byte]) = ByteString(b:_*)
8 |
9 | private def headersEnd: String = "\r\n\r\n"
10 |
11 | def unapply(b: Seq[Byte]): Option[(Seq[HttpHeader], Seq[Byte])] = {
12 | val regex = """(?s)(?:\r\n)?([\w-]+): ((?:(?!\r\n[\w-]+: )(?!\r\n\r\n).)+)""".r
13 | asByteString(b).utf8String.split(headersEnd, 2).toSeq match {
14 | case h +: rest ⇒
15 | val headers = regex.findAllMatchIn(h).collect {
16 | case regex(name, value) ⇒
17 | HttpHeader(name, value.lines.map(_.dropWhile(_ == ' ')).mkString("\r\n"))
18 | }
19 | Some(headers.toVector → ByteString(rest.mkString))
20 |
21 | case _ ⇒
22 | None
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/http/HttpRequest.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.http
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.networkutils.http.headers.HttpHeader
5 | import com.karasiq.parsers.{ByteFragment, RegexByteExtractor}
6 |
7 | object HttpMethod extends Enumeration {
8 | val GET, HEAD, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, TRACE = Value
9 | }
10 |
11 | object HttpRequest extends ByteFragment[(HttpMethod.Value, String, Seq[HttpHeader])] {
12 | private val regex = new RegexByteExtractor("""^([A-Z]+) ((?:https?://|)[^\s]+) HTTP/1\.[01]\r\n""".r)
13 |
14 | override def fromBytes: Extractor = {
15 | case regex(result, HttpHeaders(headers, rest)) ⇒
16 | (HttpMethod.withName(result.group(1)), result.group(2), headers) → rest
17 | }
18 |
19 | override def toBytes(value: (HttpMethod.Value, String, Seq[HttpHeader])): ByteString = value match {
20 | case (method, address, headers) ⇒
21 | val connect = s"$method $address HTTP/1.1\r\n"
22 | ByteString(connect + HttpHeader.formatHeaders(headers) + "\r\n")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/http/HttpResponse.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.http
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.networkutils.http.HttpStatus
5 | import com.karasiq.networkutils.http.headers.HttpHeader
6 | import com.karasiq.parsers.{ByteFragment, RegexByteExtractor}
7 |
8 | object HttpResponse extends ByteFragment[(HttpStatus, Seq[HttpHeader])] {
9 | private val regex = new RegexByteExtractor("""^HTTP/1\.[01] (\d+) ([^\r\n]+)(?=\r\n)""".r)
10 |
11 | override def fromBytes: Extractor = {
12 | case regex(result, HttpHeaders(headers, rest)) ⇒
13 | (HttpStatus(result.group(1).toInt, result.group(2)), headers) → rest
14 | }
15 |
16 | override def toBytes(value: (HttpStatus, Seq[HttpHeader])): ByteString = value match {
17 | case (status, headers) ⇒
18 | val statusString = s"HTTP/1.1 ${status.code} ${status.message}\r\n"
19 | ByteString(statusString + HttpHeader.formatHeaders(headers) + "\r\n")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/SocksClient.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import akka.util.ByteString
6 | import com.karasiq.parsers._
7 | import com.karasiq.parsers.socks.internal.{NullTerminatedString, _}
8 |
9 | /**
10 | * Serializers for SOCKS client
11 | */
12 | object SocksClient {
13 | sealed trait SocksVersion {
14 | def code: Byte
15 | }
16 |
17 | object SocksVersion extends ByteRange[SocksVersion] {
18 | def of(bs: ByteString): SocksVersion = apply(bs.head) // Extract from first byte
19 |
20 | case object SocksV4 extends SocksVersion {
21 | override def code: Byte = 0x04
22 | }
23 |
24 | case object SocksV5 extends SocksVersion {
25 | override def code: Byte = 0x05
26 | }
27 |
28 | override def fromByte: PartialFunction[Byte, SocksVersion] = {
29 | case 0x04 ⇒ SocksV4
30 | case 0x05 ⇒ SocksV5
31 | }
32 |
33 | override def toByte: PartialFunction[SocksVersion, Byte] = {
34 | case v ⇒ v.code
35 | }
36 | }
37 |
38 | sealed trait AuthMethod {
39 | def code: Byte
40 | }
41 |
42 | object AuthMethod extends ByteRange[AuthMethod] {
43 | case object NoAuth extends AuthMethod {
44 | override def code: Byte = 0
45 | override def toString: String = "No authentication"
46 | }
47 |
48 | case object GSSAPI extends AuthMethod {
49 | override def code: Byte = 1
50 | override def toString: String = "GSSAPI"
51 | }
52 |
53 | case object UsernamePassword extends AuthMethod {
54 | override def code: Byte = 2
55 | override def toString: String = "Username/Password"
56 | }
57 |
58 | case class IANA(override val code: Byte) extends AuthMethod {
59 | override def toString: String = s"Method assigned by IANA: $code"
60 | }
61 | case class PrivateUse(override val code: Byte) extends AuthMethod {
62 | override def toString: String = s"Method reserved for private use: $code"
63 | }
64 |
65 | override def fromByte: PartialFunction[Byte, AuthMethod] = {
66 | case 0x00 ⇒ NoAuth
67 | case 0x01 ⇒ GSSAPI
68 | case 0x02 ⇒ UsernamePassword
69 | case r if (0x03 to 0x7F).contains(r) ⇒ IANA(r)
70 | case r if (0x80 to 0xFE).contains(r) ⇒ PrivateUse(r)
71 | }
72 |
73 | override def toByte: PartialFunction[AuthMethod, Byte] = {
74 | case authMethod ⇒
75 | authMethod.code
76 | }
77 | }
78 |
79 | object AuthRequest extends ByteFragment[Seq[AuthMethod]] {
80 | override def fromBytes: Extractor = {
81 | case SocksVersion(SocksVersion.SocksV5) +: authMethodsCount +: bytes ⇒
82 | bytes.take(authMethodsCount).collect {
83 | case AuthMethod(method) ⇒
84 | method
85 | } → bytes.drop(authMethodsCount)
86 | }
87 |
88 | override def toBytes(authMethods: Seq[AuthMethod]): ByteString = {
89 | ByteString(SocksVersion(SocksVersion.SocksV5), authMethods.length.toByte) ++ authMethods.map(_.code)
90 | }
91 | }
92 |
93 | sealed trait Command {
94 | def code: Byte
95 | }
96 |
97 | object Command {
98 | case object TcpConnection extends Command {
99 | override def code: Byte = 0x01
100 | }
101 | case object TcpBind extends Command {
102 | override def code: Byte = 0x02
103 | }
104 | case object UdpAssociate extends Command {
105 | override def code: Byte = 0x03
106 | }
107 |
108 | def apply(b: Byte): Command = {
109 | unapply(b).getOrElse(throw new ParserException("Invalid SOCKS command: " + b))
110 | }
111 |
112 | def unapply(b: Byte): Option[Command] = {
113 | val pf: PartialFunction[Byte, Command] = {
114 | case 0x01 ⇒ TcpConnection
115 | case 0x02 ⇒ TcpBind
116 | case 0x03 ⇒ UdpAssociate
117 | }
118 | pf.lift.apply(b)
119 | }
120 | }
121 |
122 | object ConnectionRequest extends ByteFragment[(SocksVersion, Command, InetSocketAddress, String)] {
123 | override def fromBytes: Extractor = {
124 | case SocksVersion(SocksVersion.SocksV4) +: Command(command) +: (Address.V4(address, NullTerminatedString(userId, rest))) if command != Command.UdpAssociate ⇒
125 | (SocksVersion.SocksV4, command, address, userId) → (if (address.isUnresolved) rest.drop(address.getHostString.length + 1) else rest)
126 |
127 | case SocksVersion(SocksVersion.SocksV5) +: Command(command) +: 0x00 +: (Address.V5(address, rest)) ⇒
128 | (SocksVersion.SocksV5, command, address, "") → rest
129 | }
130 |
131 | private def socks4a(address: InetSocketAddress, userId: String): ByteString = {
132 | Port(address.getPort) ++ Socks4AInvalidIP() ++ NullTerminatedString(userId) ++ NullTerminatedString(address.getHostString)
133 | }
134 |
135 | override def toBytes(value: (SocksVersion, Command, InetSocketAddress, String)): ByteString = value match {
136 | case (socksVersion, command, address, userId) ⇒
137 | val head = ByteString(SocksVersion(socksVersion), command.code)
138 | socksVersion match {
139 | case SocksVersion.SocksV4 if address.isUnresolved ⇒ // SOCKS4A
140 | head ++ socks4a(address, userId)
141 |
142 | case v @ SocksVersion.SocksV4 ⇒ // SOCKS4
143 | head ++ Address(v, address) ++ NullTerminatedString(userId)
144 |
145 | case v @ SocksVersion.SocksV5 ⇒ // SOCKS5
146 | head ++ ByteString(0) ++ Address(v, address)
147 | }
148 | }
149 | }
150 |
151 | object UsernameAuthRequest extends ByteFragment[(String, String)] {
152 | override def fromBytes: Extractor = {
153 | case 0x01 +: (LengthString(username, LengthString(password, rest))) ⇒
154 | (username, password) → rest
155 | }
156 |
157 | override def toBytes(value: (String, String)): ByteString = value match {
158 | case (username, password) ⇒
159 | ByteString(0x01) ++ LengthString(username) ++ LengthString(password)
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/SocksServer.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import scala.language.implicitConversions
6 |
7 | import akka.util.ByteString
8 |
9 | import com.karasiq.parsers.{ByteFragment, ParserException}
10 | import com.karasiq.parsers.socks.SocksClient.{AuthMethod, SocksVersion}
11 | import com.karasiq.parsers.socks.SocksClient.SocksVersion.SocksV5
12 | import com.karasiq.parsers.socks.internal.Address
13 |
14 | /**
15 | * Serializers for SOCKS server
16 | */
17 | object SocksServer {
18 | implicit def statusToByte(c: ConnectionStatus): Byte = c.code
19 |
20 | /**
21 | * Server auth method selection
22 | */
23 | object AuthMethodResponse extends ByteFragment[AuthMethod] {
24 | override def fromBytes: Extractor = {
25 | AuthStatusResponse.fromBytes.andThen {
26 | case (status, rest) ⇒
27 | AuthMethod(status) → rest
28 | }
29 | }
30 |
31 | override def toBytes(value: AuthMethod): ByteString = {
32 | ByteString(SocksV5.code, value.code)
33 | }
34 |
35 | /**
36 | * Response for "no supported methods provided"
37 | */
38 | def notSupported: ByteString = ByteString(SocksVersion.SocksV5.code, 0xFF)
39 | }
40 |
41 | /**
42 | * Server authentication status
43 | */
44 | object AuthStatusResponse extends ByteFragment[Byte] {
45 | override def fromBytes: Extractor = {
46 | case _ +: statusCode +: rest ⇒
47 | statusCode → rest
48 | }
49 |
50 | override def toBytes(value: Byte): ByteString = {
51 | ByteString(value)
52 | }
53 | }
54 |
55 | /**
56 | * Server connection response
57 | */
58 | object ConnectionStatusResponse extends ByteFragment[(SocksVersion, Option[InetSocketAddress], ConnectionStatus)] {
59 | override def fromBytes: Extractor = {
60 | // SOCKS5
61 | case SocksVersion(v @ SocksVersion.SocksV5) +: ConnectionStatus(status: ConnectionSuccess) +: 0x00 +: (Address.V5(address, rest)) ⇒
62 | (v, Some(address), status) → rest
63 |
64 | case SocksVersion(v @ SocksVersion.SocksV5) +: ConnectionStatus(status) +: 0x00 +: rest ⇒
65 | (v, None, status) → rest
66 |
67 | // SOCKS4
68 | case 0x00 +: ConnectionStatus(status) +: _ +: _ +: _ +: _ +: _ +: _ +: rest ⇒
69 | (SocksVersion.SocksV4, None, status) → rest
70 | }
71 |
72 | private def emptyAddress: ByteString = ByteString(
73 | 0x01, // Type
74 | 0x00, 0x00, 0x00, 0x00, // IPv4
75 | 0x00, 0x00 // Port
76 | )
77 |
78 | override def toBytes(value: (SocksVersion, Option[InetSocketAddress], ConnectionStatus)): ByteString = value match {
79 | case (version, address, status) ⇒
80 | version match {
81 | // SOCKS5
82 | case SocksVersion.SocksV5 ⇒
83 | val statusSerialized = ByteString(version.code, status.code, 0x00)
84 | address.fold(statusSerialized ++ emptyAddress)(statusSerialized ++ Address.V5(_))
85 |
86 | // SOCKS4
87 | case SocksVersion.SocksV4 ⇒
88 | ByteString(0x00, status.code, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
89 | }
90 | }
91 | }
92 |
93 | trait ConnectionStatus {
94 | def code: Byte
95 | def message: String
96 | override def toString: String = message
97 | }
98 |
99 | case class ConnectionSuccess(code: Byte, message: String) extends ConnectionStatus
100 | case class ConnectionError(code: Byte, message: String) extends ConnectionStatus
101 |
102 | /**
103 | * Server status codes
104 | */
105 | object Codes {
106 | /**
107 | * Generic success code
108 | * @param v SOCKS version
109 | */
110 | def success(v: SocksVersion): ConnectionStatus = v match {
111 | case SocksVersion.SocksV4 ⇒
112 | Socks4.REQUEST_GRANTED
113 |
114 | case SocksVersion.SocksV5 ⇒
115 | Socks5.REQUEST_GRANTED
116 | }
117 |
118 | /**
119 | * Generic failure code
120 | * @param v SOCKS version
121 | */
122 | def failure(v: SocksVersion): ConnectionStatus = v match {
123 | case SocksVersion.SocksV4 ⇒
124 | Socks4.REQUEST_FAILED
125 |
126 | case SocksVersion.SocksV5 ⇒
127 | Socks5.GENERAL_FAILURE
128 | }
129 |
130 | /**
131 | * SOCKS4 codes
132 | */
133 | object Socks4 {
134 | val REQUEST_GRANTED: ConnectionStatus = ConnectionSuccess(0x5a, "Request granted")
135 | val REQUEST_FAILED: ConnectionStatus = ConnectionError(0x5b, "Request failed")
136 | }
137 |
138 | /**
139 | * SOCKS5 codes
140 | */
141 | object Socks5 {
142 | val REQUEST_GRANTED: ConnectionStatus = ConnectionSuccess(0x00, "Request granted")
143 | val GENERAL_FAILURE: ConnectionStatus = ConnectionError(0x01, "General failure")
144 | val CONN_NOT_ALLOWED: ConnectionStatus = ConnectionError(0x02, "Connection not allowed by ruleset")
145 | val NETWORK_UNREACHABLE: ConnectionStatus = ConnectionError(0x03, "Network unreachable")
146 | val HOST_UNREACHABLE: ConnectionStatus = ConnectionError(0x04, "Host unreachable")
147 | val CONN_REFUSED: ConnectionStatus = ConnectionError(0x05, "Connection refused by destination host")
148 | val TTL_EXPIRED: ConnectionStatus = ConnectionError(0x06, "TTL expired")
149 | val COMMAND_NOT_SUPPORTED: ConnectionStatus = ConnectionError(0x07, "Command not supported / protocol error")
150 | val ADDR_TYPE_NOT_SUPPORTED: ConnectionStatus = ConnectionError(0x08, "Address type not supported")
151 | }
152 | }
153 |
154 | object ConnectionStatus {
155 | import Codes._
156 |
157 | def apply(b: Byte): ConnectionStatus = unapply(b).getOrElse(throw new ParserException("Invalid status code: " + b))
158 |
159 | def unapply(b: Byte): Option[ConnectionStatus] = {
160 | val pf: PartialFunction[Byte, ConnectionStatus] = {
161 | // SOCKS4
162 | case 0x5a ⇒ Socks4.REQUEST_GRANTED
163 | case 0x5b ⇒ Socks4.REQUEST_FAILED
164 |
165 | case 0x00 ⇒ Socks5.REQUEST_GRANTED
166 | case 0x01 ⇒ Socks5.GENERAL_FAILURE
167 | case 0x02 ⇒ Socks5.CONN_NOT_ALLOWED
168 | case 0x03 ⇒ Socks5.NETWORK_UNREACHABLE
169 | case 0x04 ⇒ Socks5.HOST_UNREACHABLE
170 | case 0x05 ⇒ Socks5.CONN_REFUSED
171 | case 0x06 ⇒ Socks5.TTL_EXPIRED
172 | case 0x07 ⇒ Socks5.COMMAND_NOT_SUPPORTED
173 | case 0x08 ⇒ Socks5.ADDR_TYPE_NOT_SUPPORTED
174 | }
175 |
176 | pf.lift.apply(b)
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/internal/Address.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks.internal
2 |
3 | import java.net.{InetAddress, InetSocketAddress}
4 |
5 | import akka.util.ByteString
6 |
7 | import com.karasiq.parsers.{ByteFragment, ByteRange}
8 | import com.karasiq.parsers.socks.SocksClient.SocksVersion
9 |
10 | private[socks] object Address {
11 | sealed trait AddressType {
12 | def code: Byte
13 | }
14 |
15 | object AddressType extends ByteRange[AddressType] {
16 | case object IPv4Address extends AddressType {
17 | override def code: Byte = 0x01
18 | }
19 | case object DomainName extends AddressType {
20 | override def code: Byte = 0x03
21 | }
22 | case object IPv6Address extends AddressType {
23 | override def code: Byte = 0x04
24 | }
25 |
26 | override def fromByte: PartialFunction[Byte, AddressType] = {
27 | case 0x01 ⇒ IPv4Address
28 | case 0x03 ⇒ DomainName
29 | case 0x04 ⇒ IPv6Address
30 | }
31 |
32 | override def toByte: PartialFunction[AddressType, Byte] = {
33 | case addressType ⇒
34 | addressType.code
35 | }
36 | }
37 |
38 | @inline
39 | private def readIP(b: Seq[Byte]): InetAddress = InetAddress.getByAddress(b.toArray)
40 |
41 | private object IPv4 extends ByteFragment[InetAddress] {
42 | override def toBytes(address: InetAddress): ByteString = {
43 | assert(address.getAddress.length == 4, s"Not an IPv4 address: $address")
44 | ByteString(address.getAddress)
45 | }
46 |
47 | override def fromBytes: Extractor = {
48 | case bytes if bytes.length >= 4 ⇒
49 | readIP(bytes.take(4)) → bytes.drop(4)
50 | }
51 | }
52 |
53 | private object IPv6 extends ByteFragment[InetAddress] {
54 | override def toBytes(address: InetAddress) = {
55 | assert(address.getAddress.length == 16, s"Not an IPv6 address: $address")
56 | ByteString(address.getAddress)
57 | }
58 |
59 | override def fromBytes: Extractor = {
60 | case bytes if bytes.length >= 16 ⇒
61 | readIP(bytes.take(16)) → bytes.drop(16)
62 | }
63 | }
64 |
65 | object V4 extends ByteFragment[InetSocketAddress] {
66 | override def toBytes(address: InetSocketAddress): ByteString = {
67 | Port(address.getPort) ++ IPv4(InetAddress.getByName(address.getHostString))
68 | }
69 |
70 | override def fromBytes: Extractor = {
71 | case Port(port, Socks4AInvalidIP(_, rest @ NullTerminatedString(_, NullTerminatedString(domain, _)))) ⇒ // SOCKS4A
72 | InetSocketAddress.createUnresolved(domain, port) → rest
73 |
74 | case Port(port, IPv4(address, rest)) ⇒ // SOCKS4
75 | new InetSocketAddress(address, port) → rest
76 | }
77 | }
78 |
79 | object V5 extends ByteFragment[InetSocketAddress] {
80 | import AddressType._
81 |
82 | override def toBytes(address: InetSocketAddress): ByteString = {
83 | val addr: ByteString = if (address.isUnresolved) {
84 | ByteString(AddressType.DomainName.code) ++ LengthString(address.getHostString)
85 | } else address.getAddress match { // IP address
86 | case a if a.getAddress.length == 16 ⇒
87 | ByteString(AddressType.IPv6Address.code) ++ IPv6(a)
88 | case a if a.getAddress.length == 4 ⇒
89 | ByteString(AddressType.IPv4Address.code) ++ IPv4(a)
90 | }
91 |
92 | addr ++ Port(address.getPort)
93 | }
94 |
95 | override def fromBytes: Extractor = {
96 | case AddressType(IPv4Address) +: (IPv4(address, Port(port, rest))) ⇒
97 | new InetSocketAddress(address, port) → rest
98 |
99 | case AddressType(IPv6Address) +: (IPv6(address, Port(port, rest))) ⇒
100 | new InetSocketAddress(address, port) → rest
101 |
102 | case AddressType(DomainName) +: (LengthString(host, Port(port, rest))) ⇒
103 | InetSocketAddress.createUnresolved(host, port) → rest
104 | }
105 | }
106 |
107 | def apply(version: SocksVersion, address: InetSocketAddress): ByteString = version match {
108 | case SocksVersion.SocksV4 ⇒ this.V4(address)
109 | case SocksVersion.SocksV5 ⇒ this.V5(address)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/internal/LengthString.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks.internal
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.parsers.ByteFragment
5 |
6 | private[socks] object LengthString extends ByteFragment[String] {
7 | override def toBytes(s: String): ByteString = {
8 | ByteString(s.length.toByte) ++ ByteString(s)
9 | }
10 |
11 | override def fromBytes: Extractor = {
12 | case length +: string if string.length >= length ⇒
13 | ByteString(string.take(length).toArray).utf8String → string.drop(length)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/internal/NullTerminatedString.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks.internal
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.parsers.ByteFragment
5 |
6 | /**
7 | * Null-byte-terminated string extractor
8 | */
9 | private[socks] object NullTerminatedString extends ByteFragment[String] {
10 | override def toBytes(s: String): ByteString = {
11 | ByteString(s) ++ ByteString(0)
12 | }
13 |
14 | override def fromBytes: Extractor = {
15 | case bytes if bytes.nonEmpty && bytes.contains(0) ⇒
16 | val (keep, drop) = bytes.splitAt(bytes.indexOf(0))
17 | ByteString(keep:_*).utf8String → drop.tail
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/internal/Port.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks.internal
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.parsers.ByteFragment
5 |
6 | private[socks] object Port extends ByteFragment[Int] {
7 | override def toBytes(port: Int): ByteString = {
8 | ByteFragment.create(2)(_.putShort(port.toShort))
9 | }
10 |
11 | override def fromBytes: Extractor = {
12 | case bytes if bytes.length >= 2 ⇒
13 | readPort(bytes) → bytes.drop(2)
14 | }
15 |
16 | @inline
17 | private def readPort(b: Seq[Byte]): Int = {
18 | val array: Array[Byte] = (ByteString(0x00, 0x00) ++ b.take(2)).toArray
19 | BigInt(array).intValue()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/parsers/socks/internal/Socks4AInvalidIP.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.parsers.socks.internal
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.parsers.ByteFragment
5 |
6 | private[socks] object Socks4AInvalidIP extends ByteFragment[Seq[Byte]] {
7 | def apply(): ByteString = ByteString(0x00, 0x00, 0x00, 0x01)
8 |
9 | override def toBytes(value: Seq[Byte]): ByteString = {
10 | ByteString(value:_*)
11 | }
12 |
13 | override def fromBytes: PartialFunction[Seq[Byte], (Seq[Byte], Seq[Byte])] = {
14 | case bs @ (0x00 +: 0x00 +: 0x00 +: last +: rest) if last != 0x00 ⇒
15 | bs.take(4) → rest
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/ProxyChain.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import scala.annotation.tailrec
6 | import scala.collection.JavaConverters._
7 | import scala.concurrent.{ExecutionContext, Future}
8 | import scala.concurrent.duration._
9 | import scala.language.{implicitConversions, postfixOps}
10 | import scala.util.Random
11 |
12 | import akka.Done
13 | import akka.actor.ActorSystem
14 | import akka.event.Logging
15 | import akka.http.scaladsl.HttpsConnectionContext
16 | import akka.io.Tcp.SO
17 | import akka.stream.{BidiShape, FlowShape, TLSClosing, TLSRole}
18 | import akka.stream.TLSProtocol.{SendBytes, SessionBytes, SslTlsInbound}
19 | import akka.stream.scaladsl.{BidiFlow, Flow, GraphDSL, Keep, Tcp, TLS}
20 | import akka.util.ByteString
21 | import com.typesafe.config.{Config, ConfigException}
22 |
23 | import com.karasiq.networkutils.proxy.Proxy
24 | import com.karasiq.parsers.socks.SocksClient.SocksVersion
25 | import com.karasiq.proxy.client.{HttpProxyClientStage, SocksProxyClientStage}
26 |
27 | object ProxyChain {
28 | private[proxy] val TlsPrefix = "tls-"
29 |
30 | private[proxy] def proxyStage(address: InetSocketAddress, proxy: Proxy, tlsContext: Option[HttpsConnectionContext] = None)
31 | (implicit as: ActorSystem): BidiFlow[ByteString, ByteString, ByteString, ByteString, Future[Done]] = {
32 | val log = Logging(as, "ProxyStage")
33 | def stageForScheme(scheme: String) = {
34 | scheme.toLowerCase match {
35 | case "socks4" | "socks4a" ⇒
36 | new SocksProxyClientStage(log, address, SocksVersion.SocksV4, Some(proxy))
37 |
38 | case "socks5" | "socks" ⇒
39 | new SocksProxyClientStage(log, address, SocksVersion.SocksV5, Some(proxy))
40 |
41 | case "http" | "https" ⇒
42 | new HttpProxyClientStage(log, address, Some(proxy))
43 |
44 | case _ ⇒
45 | throw new IllegalArgumentException(s"Unknown proxy protocol: $scheme")
46 | }
47 | }
48 |
49 | val graph = {
50 | if (proxy.scheme.startsWith(TlsPrefix) && tlsContext.nonEmpty) {
51 | val tls = TLS(tlsContext.get.sslContext, tlsContext.get.firstSession, TLSRole.client, TLSClosing.ignoreComplete, Some(proxy.host → proxy.port))
52 | BidiFlow.fromGraph(GraphDSL.create(stageForScheme(proxy.scheme.split(TlsPrefix, 2).last), tls)(Keep.left) { implicit builder ⇒ (connection, tls) ⇒
53 | import GraphDSL.Implicits._
54 | val bytesIn = builder.add(Flow[SslTlsInbound].collect { case SessionBytes(_, bytes) ⇒ bytes })
55 | val bytesOut = builder.add(Flow[ByteString].map(SendBytes))
56 | connection.out1 ~> bytesOut ~> tls.in1
57 | tls.out2 ~> bytesIn ~> connection.in1
58 | BidiShape(tls.in2, tls.out1, connection.in2, connection.out2)
59 | })
60 | } else {
61 | stageForScheme(proxy.scheme)
62 | }
63 | }
64 |
65 | BidiFlow.fromGraph(graph).named("proxyStage")
66 | }
67 |
68 | def connect(destination: InetSocketAddress, proxies: Seq[Proxy],
69 | tlsContext: Option[HttpsConnectionContext] = None)
70 | (implicit as: ActorSystem, ec: ExecutionContext): Flow[ByteString, ByteString, (Future[Tcp.OutgoingConnection], Future[Done])] = {
71 | val address = proxies.headOption.fold(destination)(_.toInetSocketAddress)
72 | val connection = Tcp().outgoingConnection(address, options = List(SO.KeepAlive(true), SO.TcpNoDelay(true)),
73 | connectTimeout = 15 seconds, idleTimeout = 5 minutes)
74 | createFlow(connection, destination, proxies, tlsContext)
75 | }
76 |
77 | def createFlow[Mat](flow: Flow[ByteString, ByteString, Mat],
78 | destination: InetSocketAddress, proxies: Seq[Proxy],
79 | tlsContext: Option[HttpsConnectionContext] = None)
80 | (implicit as: ActorSystem, ec: ExecutionContext): Flow[ByteString, ByteString, (Mat, Future[Done])] = {
81 | val flowWithDone = flow.mapMaterializedValue(_ → Future.successful(Done))
82 | if (proxies.isEmpty) {
83 | flowWithDone
84 | } else {
85 | @tailrec
86 | def connect(currentFlow: Flow[ByteString, ByteString, (Mat, Future[Done])], proxies: Seq[Proxy]): Flow[ByteString, ByteString, (Mat, Future[Done])] = {
87 | if (proxies.isEmpty) {
88 | currentFlow
89 | } else {
90 | val (proxy, address) = proxies match {
91 | case currentProxy +: nextProxy +: _ ⇒
92 | currentProxy → nextProxy.toInetSocketAddress
93 |
94 | case currentProxy +: Nil ⇒
95 | currentProxy → destination
96 |
97 | case _ ⇒
98 | throw new IllegalArgumentException(s"Invalid proxy chain: $proxies")
99 | }
100 | val connectedFlow = Flow.fromGraph(GraphDSL.create(currentFlow, proxyStage(address, proxy, tlsContext)) {
101 | case ((mat, ps1), ps2) ⇒ mat → ps1.flatMap(_ ⇒ ps2)
102 | } { implicit b ⇒ (connection, stage) ⇒
103 | import GraphDSL.Implicits._
104 | connection.out ~> stage.in1
105 | stage.out1 ~> connection.in
106 | FlowShape(stage.in2, stage.out2)
107 | })
108 | connect(connectedFlow, proxies.tail)
109 | }
110 | }
111 | connect(flowWithDone, proxies).named("proxyConnection")
112 | }
113 | }
114 |
115 | def select(proxies: Seq[Proxy], randomize: Boolean = false, hops: Int = 0): Seq[Proxy] = {
116 | val ordered = if (randomize) Random.shuffle(proxies) else proxies
117 | if (hops == 0) ordered else ordered.take(hops)
118 | }
119 |
120 | private[proxy] def configSelect(config: Config): Seq[Proxy] = {
121 | val proxies = config.getStringList("proxies").asScala
122 | select(proxies.map(s ⇒ Proxy(if (s.contains("://")) s else s"http://$s")), config.getBoolean("randomize"), config.getInt("hops"))
123 | }
124 |
125 | @throws[ConfigException]("if invalid config provided")
126 | def fromConfig(config: Config): Seq[Proxy] = {
127 | val configs = Seq(config.getConfig("entry"), config.getConfig("middle"), config.getConfig("exit"))
128 | configs.flatMap(configSelect)
129 | }
130 | }
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/ProxyException.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy
2 |
3 | import java.io.IOException
4 |
5 | /**
6 | * Generic proxy exception
7 | */
8 | class ProxyException(message: String = null, cause: Throwable = null) extends IOException(message, cause)
9 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/client/HttpProxyClientStage.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy.client
2 |
3 | import java.net.InetSocketAddress
4 | import java.nio.charset.StandardCharsets
5 |
6 | import akka.Done
7 | import akka.event.LoggingAdapter
8 | import akka.stream._
9 | import akka.stream.stage._
10 | import akka.util.ByteString
11 | import com.karasiq.networkutils.http.headers.{HttpHeader, `Proxy-Authorization`}
12 | import com.karasiq.networkutils.proxy.Proxy
13 | import com.karasiq.parsers.http.{HttpConnect, HttpResponse}
14 | import com.karasiq.proxy.ProxyException
15 |
16 | import scala.concurrent.{Future, Promise}
17 |
18 | final class HttpProxyClientStage(log: LoggingAdapter, destination: InetSocketAddress, proxy: Option[Proxy] = None) extends GraphStageWithMaterializedValue[BidiShape[ByteString, ByteString, ByteString, ByteString], Future[Done]] {
19 | val input = Inlet[ByteString]("HttpProxyClient.tcpIn")
20 | val output = Outlet[ByteString]("HttpProxyClient.tcpOut")
21 | val proxyInput = Inlet[ByteString]("HttpProxyClient.dataIn")
22 | val proxyOutput = Outlet[ByteString]("HttpProxyClient.dataOut")
23 |
24 | def shape = BidiShape(input, output, proxyInput, proxyOutput)
25 |
26 | def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = {
27 | val promise = Promise[Done]
28 | val logic = new GraphStageLogic(shape) {
29 | val bufferSize = 8192
30 | val terminator = ByteString("\r\n\r\n", StandardCharsets.US_ASCII.name())
31 | var buffer = ByteString.empty
32 | val proxyString = proxy.getOrElse("")
33 |
34 | def sendRequest(): Unit = {
35 | val auth: Seq[HttpHeader] = proxy.flatMap(_.userInfo).map(userInfo ⇒ `Proxy-Authorization`.basic(userInfo)).toVector
36 | val request = HttpConnect(destination, auth)
37 | log.debug(s"Sending request to HTTP proxy {}: {}", proxyString, request.utf8String)
38 | emit(output, request, () ⇒ pull(input))
39 | }
40 |
41 | def parseResponse(): Unit = {
42 | val headersEnd = buffer.indexOfSlice(terminator)
43 | if (headersEnd != -1) {
44 | val (keep, drop) = buffer.splitAt(headersEnd + terminator.length)
45 | buffer = drop
46 | keep match {
47 | case response @ HttpResponse((status, headers), _) ⇒
48 | log.debug("Received response from HTTP proxy {}: {}", proxyString, response.utf8String)
49 | if (status.code != 200) {
50 | failStage(new ProxyException(s"HTTP CONNECT rejected by $proxyString: ${status.code} ${status.message}"))
51 | } else {
52 | log.debug("Connection established to {} through HTTP proxy: {}", destination, proxyString)
53 | passAlong(input, proxyOutput)
54 | passAlong(proxyInput, output, doFinish = false, doPull = true)
55 | promise.success(Done)
56 | if (buffer.nonEmpty) {
57 | emit(proxyOutput, buffer, () ⇒ if (!hasBeenPulled(input)) pull(input))
58 | buffer = ByteString.empty
59 | } else {
60 | pull(input)
61 | }
62 | }
63 |
64 | case bs: ByteString ⇒
65 | failStage(new ProxyException(s"Bad HTTPS proxy response: ${bs.utf8String}"))
66 | }
67 | } else {
68 | log.debug("Incomplete response from HTTP proxy {}: {}", proxyString, buffer.utf8String)
69 | pull(input)
70 | }
71 | }
72 |
73 | override def preStart() = {
74 | super.preStart()
75 | sendRequest()
76 | }
77 |
78 | override def postStop() = {
79 | promise.tryFailure(new ProxyException("Proxy connection closed"))
80 | super.postStop()
81 | }
82 |
83 | setHandler(input, new InHandler {
84 | def onPush() = {
85 | buffer ++= grab(input)
86 | if (buffer.length > bufferSize) failStage(BufferOverflowException("HTTP proxy headers size limit reached"))
87 | parseResponse()
88 | }
89 | })
90 |
91 | setHandler(proxyInput, eagerTerminateInput)
92 | setHandler(output, eagerTerminateOutput)
93 | setHandler(proxyOutput, eagerTerminateOutput)
94 | }
95 | (logic, promise.future)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/client/SocksProxyClientStage.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy.client
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import scala.concurrent.{Future, Promise}
6 |
7 | import akka.Done
8 | import akka.event.LoggingAdapter
9 | import akka.stream._
10 | import akka.stream.stage._
11 | import akka.util.ByteString
12 |
13 | import com.karasiq.networkutils.proxy.Proxy
14 | import com.karasiq.parsers.socks.SocksClient.{AuthMethod, _}
15 | import com.karasiq.parsers.socks.SocksServer.{AuthMethodResponse, AuthStatusResponse, Codes, ConnectionStatusResponse}
16 | import com.karasiq.proxy.ProxyException
17 |
18 | final class SocksProxyClientStage(log: LoggingAdapter, destination: InetSocketAddress, version: SocksVersion = SocksVersion.SocksV5, proxy: Option[Proxy] = None) extends GraphStageWithMaterializedValue[BidiShape[ByteString, ByteString, ByteString, ByteString], Future[Done]] {
19 | val input = Inlet[ByteString]("SocksProxyClient.tcpIn")
20 | val output = Outlet[ByteString]("SocksProxyClient.tcpOut")
21 | val proxyInput = Inlet[ByteString]("SocksProxyClient.dataIn")
22 | val proxyOutput = Outlet[ByteString]("SocksProxyClient.dataOut")
23 |
24 | def shape = BidiShape(input, output, proxyInput, proxyOutput)
25 |
26 | def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = {
27 | val promise = Promise[Done]
28 | val logic = new GraphStageLogic(shape) {
29 | object Stage extends Enumeration {
30 | val NotConnecting, AuthMethod, Auth, Request = Value
31 | }
32 |
33 | val maxBufferSize = 4096
34 | var buffer = ByteString.empty
35 | var stage = Stage.NotConnecting
36 | val proxyString = proxy.getOrElse("")
37 |
38 | def authCredentials: (String, String) = {
39 | val loginPassword = for (p ← proxy; string ← p.userInfo; Array(login, password) ← Some(string.split(":", 2)))
40 | yield (login, password)
41 | loginPassword
42 | .orElse(proxy.flatMap(_.userInfo).map(_ → ""))
43 | .getOrElse("" → "")
44 | }
45 |
46 | def writeBuffer(data: ByteString): Unit = {
47 | if (buffer.length + data.length > maxBufferSize) {
48 | failStage(BufferOverflowException("Socks TCP buffer overflow"))
49 | } else {
50 | buffer ++= data
51 | }
52 | }
53 |
54 | def sendAuthMethodRequest(): Unit = {
55 | stage = Stage.AuthMethod
56 | log.debug("Requesting available auth methods from SOCKS proxy: {}", proxyString)
57 |
58 | val authMethods = if (proxy.exists(_.userInfo.isDefined))
59 | Seq(AuthMethod.NoAuth, AuthMethod.UsernamePassword)
60 | else
61 | Seq(AuthMethod.NoAuth)
62 |
63 | emit(output, AuthRequest(authMethods), () ⇒ pull(input))
64 | }
65 |
66 | def sendAuthRequest(): Unit = {
67 | log.debug(s"Sending auth request to SOCKS proxy: {}", proxyString)
68 | val (login, password) = authCredentials
69 | stage = Stage.Auth
70 | emit(output, UsernameAuthRequest(login → password), () ⇒ pull(input))
71 | }
72 |
73 | def sendConnectionRequest(): Unit = {
74 | log.debug("Sending connection request to SOCKS proxy {} -> {}", proxyString, destination)
75 | stage = Stage.Request
76 | val (userId, _) = authCredentials
77 | emit(output, ConnectionRequest((version, Command.TcpConnection, destination, userId)), () ⇒ pull(input))
78 | }
79 |
80 | def parseResponse(data: ByteString): Unit = {
81 | writeBuffer(data)
82 | stage match {
83 | case Stage.AuthMethod ⇒
84 | buffer match {
85 | case AuthMethodResponse(AuthMethod.NoAuth, rest) ⇒
86 | buffer = ByteString(rest:_*)
87 | log.debug("SOCKS proxy has no authentication: {}", proxyString)
88 | sendConnectionRequest()
89 |
90 | case AuthMethodResponse(AuthMethod.UsernamePassword, rest) ⇒
91 | buffer = ByteString(rest:_*)
92 | log.debug("SOCKS proxy requested username/password authentication: {}", proxyString)
93 | sendAuthRequest()
94 |
95 | case AuthMethodResponse(method, _) ⇒
96 | failStage(new ProxyException(s"Authentication method not supported: $method"))
97 |
98 | case _ ⇒
99 | pull(input)
100 | }
101 |
102 | case Stage.Auth ⇒
103 | buffer match {
104 | case AuthStatusResponse(0x00, rest) ⇒
105 | buffer = ByteString(rest:_*)
106 | log.debug("SOCKS proxy accepted authentication: {}", proxyString)
107 | sendConnectionRequest()
108 |
109 | case AuthStatusResponse(_, _) ⇒
110 | failStage(new ProxyException("SOCKS authentication rejected"))
111 |
112 | case _ ⇒
113 | pull(input)
114 | }
115 |
116 | case Stage.Request ⇒
117 | buffer match {
118 | case ConnectionStatusResponse((`version`, address, status), rest) ⇒
119 | buffer = ByteString(rest:_*)
120 | if ((version == SocksVersion.SocksV5 && status != Codes.Socks5.REQUEST_GRANTED) || (version == SocksVersion.SocksV4 && status != Codes.Socks4.REQUEST_GRANTED)) {
121 | failStage(new ProxyException(s"SOCKS request rejected: $status"))
122 | } else {
123 | log.debug("Connection established to {} through SOCKS proxy: {}", destination, proxyString)
124 | stage = Stage.NotConnecting
125 | passAlong(input, proxyOutput)
126 | passAlong(proxyInput, output, doFinish = false, doPull = true)
127 | promise.success(Done)
128 | if (buffer.nonEmpty) {
129 | emit(proxyOutput, buffer, () ⇒ if (!hasBeenPulled(input)) pull(input))
130 | buffer = ByteString.empty
131 | } else {
132 | pull(input)
133 | }
134 | }
135 |
136 | case _ ⇒
137 | }
138 |
139 | case _ ⇒
140 | failStage(new IllegalArgumentException("Invalid stage"))
141 | }
142 | }
143 |
144 | override def preStart() = {
145 | super.preStart()
146 | version match {
147 | case SocksVersion.SocksV5 ⇒
148 | sendAuthMethodRequest()
149 |
150 | case SocksVersion.SocksV4 ⇒
151 | sendConnectionRequest()
152 | }
153 | }
154 |
155 | override def postStop() = {
156 | promise.tryFailure(new ProxyException("Proxy connection closed"))
157 | super.postStop()
158 | }
159 |
160 | setHandler(input, new InHandler {
161 | def onPush() = parseResponse(grab(input))
162 | })
163 | setHandler(proxyInput, eagerTerminateInput)
164 | setHandler(output, eagerTerminateOutput)
165 | setHandler(proxyOutput, eagerTerminateOutput)
166 | }
167 | (logic, promise.future)
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/server/ProxyConnectionRequest.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy.server
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import akka.util.ByteString
6 |
7 | import com.karasiq.networkutils.http.HttpStatus
8 | import com.karasiq.parsers.http.HttpResponse
9 | import com.karasiq.parsers.socks.SocksClient.SocksVersion.{SocksV4, SocksV5}
10 | import com.karasiq.parsers.socks.SocksServer.{Codes, _}
11 |
12 | case class ProxyConnectionRequest(scheme: String, address: InetSocketAddress)
13 |
14 | object ProxyConnectionRequest {
15 | def successResponse(request: ProxyConnectionRequest): ByteString = {
16 | request.scheme match {
17 | case "http" ⇒
18 | ByteString.empty
19 |
20 | case "https" ⇒
21 | HttpResponse((HttpStatus(200, "Connection established"), Nil))
22 |
23 | case "socks" | "socks5" ⇒
24 | ConnectionStatusResponse(SocksV5, None, Codes.success(SocksV5))
25 |
26 | case "socks4" ⇒
27 | ConnectionStatusResponse(SocksV4, None, Codes.success(SocksV4))
28 |
29 | case _ ⇒
30 | throw new IllegalArgumentException(s"Invalid proxy connection request: $request")
31 | }
32 | }
33 |
34 | def failureResponse(request: ProxyConnectionRequest): ByteString = {
35 | request.scheme match {
36 | case "http" | "https" ⇒
37 | HttpResponse((HttpStatus(502, "Bad Gateway"), Nil))
38 |
39 | case "socks" | "socks5" ⇒
40 | ConnectionStatusResponse(SocksV5, None, Codes.failure(SocksV5))
41 |
42 | case "socks4" ⇒
43 | ConnectionStatusResponse(SocksV4, None, Codes.failure(SocksV4))
44 |
45 | case _ ⇒
46 | throw new IllegalArgumentException(s"Invalid proxy connection request: $request")
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/scala/com/karasiq/proxy/server/ProxyServerStage.scala:
--------------------------------------------------------------------------------
1 | package com.karasiq.proxy.server
2 |
3 | import java.io.IOException
4 |
5 | import akka.NotUsed
6 | import akka.http.scaladsl.HttpsConnectionContext
7 | import akka.stream.TLSProtocol.{SendBytes, SessionBytes, SslTlsInbound}
8 | import akka.stream._
9 | import akka.stream.scaladsl._
10 | import akka.stream.stage._
11 | import akka.util.ByteString
12 | import com.karasiq.networkutils.http.HttpStatus
13 | import com.karasiq.networkutils.url.URLParser
14 | import com.karasiq.parsers.http.{HttpConnect, HttpMethod, HttpRequest, HttpResponse}
15 | import com.karasiq.parsers.socks.SocksClient._
16 | import com.karasiq.parsers.socks.SocksServer.{AuthMethodResponse, _}
17 | import com.karasiq.proxy.ProxyException
18 |
19 | import scala.concurrent.{Future, Promise}
20 |
21 | object ProxyServer {
22 | def withTls(tlsContext: HttpsConnectionContext): Flow[ByteString, ByteString, Future[(ProxyConnectionRequest, Flow[ByteString, ByteString, NotUsed])]] = {
23 | Flow.fromGraph(GraphDSL.create(new ProxyServerStage) { implicit builder ⇒ stage ⇒
24 | import GraphDSL.Implicits._
25 | val tlsInbound = builder.add(Flow[SslTlsInbound].collect { case SessionBytes(_, bytes) ⇒ bytes })
26 | val tlsOutbound = builder.add(Flow[ByteString].map(SendBytes))
27 | val tls = builder.add(TLS(tlsContext.sslContext, tlsContext.firstSession, TLSRole.server))
28 | tls.out2 ~> tlsInbound ~> stage
29 | stage ~> tlsOutbound ~> tls.in1
30 | FlowShape(tls.in2, tls.out1)
31 | })
32 | }
33 |
34 | def apply(): Flow[ByteString, ByteString, Future[(ProxyConnectionRequest, Flow[ByteString, ByteString, NotUsed])]] = {
35 | Flow.fromGraph(new ProxyServerStage)
36 | }
37 |
38 | def withResponse[Mat](flow: Flow[ByteString, ByteString, Mat], response: ByteString): Flow[ByteString, ByteString, Mat] = {
39 | Flow.fromGraph(GraphDSL.create(flow) { implicit builder ⇒ connection ⇒
40 | import GraphDSL.Implicits._
41 | val inputHead = builder.add(Source.single(response))
42 | val input = builder.add(Concat[ByteString]())
43 | inputHead ~> input.in(0)
44 | input ~> connection
45 | FlowShape(input.in(1), connection.out)
46 | })
47 | }
48 |
49 | def withSuccess[Mat](flow: Flow[ByteString, ByteString, Mat], request: ProxyConnectionRequest): Flow[ByteString, ByteString, Mat] = {
50 | if (request.scheme == "http") flow else withResponse(flow, ProxyConnectionRequest.successResponse(request))
51 | }
52 |
53 | def withFailure[Mat](flow: Flow[ByteString, ByteString, Mat], request: ProxyConnectionRequest): Flow[ByteString, ByteString, Mat] = {
54 | withResponse(flow, ProxyConnectionRequest.failureResponse(request))
55 | }
56 | }
57 |
58 | private[proxy] final class ProxyServerStage extends GraphStageWithMaterializedValue[FlowShape[ByteString, ByteString], Future[(ProxyConnectionRequest, Flow[ByteString, ByteString, NotUsed])]] {
59 | val tcpInput = Inlet[ByteString]("ProxyServer.tcpIn")
60 | val tcpOutput = Outlet[ByteString]("ProxyServer.tcpOut")
61 | val promise = Promise[(ProxyConnectionRequest, Flow[ByteString, ByteString, NotUsed])]()
62 |
63 | val shape = new FlowShape(tcpInput, tcpOutput)
64 |
65 | def createLogicAndMaterializedValue(inheritedAttributes: Attributes) = {
66 | val logic = new GraphStageLogic(shape) {
67 | val bufferSize = 8192
68 | var buffer = ByteString.empty
69 |
70 | override def postStop() = {
71 | promise.tryFailure(new IOException("Connection closed"))
72 | super.postStop()
73 | }
74 |
75 | override def preStart() = {
76 | super.preStart()
77 | pull(tcpInput)
78 | }
79 |
80 | def failConnection(ex: Exception): Unit = {
81 | promise.tryFailure(ex)
82 | failStage(ex)
83 | }
84 |
85 | def writeBuffer(data: ByteString): Unit = {
86 | if (buffer.length > bufferSize) {
87 | failConnection(BufferOverflowException("Buffer overflow"))
88 | } else {
89 | buffer ++= data
90 | }
91 | }
92 |
93 | def emitRequest(request: ProxyConnectionRequest): Unit = {
94 | val inlet = new SubSinkInlet[ByteString]("ProxyServer.tcpInConnected")
95 | val outlet = new SubSourceOutlet[ByteString]("ProxyServer.tcpOutConnected")
96 | var outletEmitting = false
97 | val outletHandler = new OutHandler {
98 | @scala.throws[Exception](classOf[Exception])
99 | def onPull() = ()
100 |
101 | override def onDownstreamFinish() = {
102 | cancel(tcpInput)
103 | }
104 | }
105 | def outletEmit(data: ByteString, andThen: () ⇒ Unit): Unit = {
106 | if (outlet.isAvailable) {
107 | outlet.push(data)
108 | andThen()
109 | } else {
110 | outletEmitting = true
111 | outlet.setHandler(new OutHandler {
112 | def onPull() = {
113 | outletEmitting = false
114 | outlet.push(data)
115 | outlet.setHandler(outletHandler)
116 | andThen()
117 | }
118 |
119 | override def onDownstreamFinish() = {
120 | outletHandler.onDownstreamFinish()
121 | }
122 | })
123 | }
124 | }
125 |
126 | setHandler(tcpInput, new InHandler {
127 | def onPush() = outletEmit(grab(tcpInput), () ⇒ if (isClosed(tcpInput)) outlet.complete() else pull(tcpInput))
128 | override def onUpstreamFinish() = if (!outletEmitting) outlet.complete()
129 | })
130 |
131 | setHandler(tcpOutput, new OutHandler {
132 | def onPull() = if (!inlet.hasBeenPulled && !inlet.isClosed) inlet.pull()
133 | override def onDownstreamFinish() = inlet.cancel()
134 | })
135 |
136 | inlet.setHandler(new InHandler {
137 | def onPush() = emit(tcpOutput, inlet.grab(), () ⇒ if (!inlet.hasBeenPulled && !inlet.isClosed) inlet.pull())
138 | override def onUpstreamFinish() = complete(tcpOutput)
139 | })
140 | outlet.setHandler(outletHandler)
141 |
142 | if (buffer.nonEmpty) {
143 | outletEmit(buffer, () ⇒ if (isClosed(tcpInput)) outlet.complete() else pull(tcpInput))
144 | buffer = ByteString.empty
145 | } else {
146 | tryPull(tcpInput)
147 | }
148 |
149 | promise.success(request → Flow.fromSinkAndSource(inlet.sink, outlet.source))
150 | inlet.pull()
151 | }
152 |
153 | def processBuffer(): Unit = {
154 | buffer match {
155 | case ConnectionRequest((socksVersion, command, address, userId), rest) ⇒
156 | buffer = ByteString(rest:_*)
157 | if (command != Command.TcpConnection) {
158 | val code = if (socksVersion == SocksVersion.SocksV5) Codes.Socks5.COMMAND_NOT_SUPPORTED else Codes.failure(socksVersion)
159 | emit(tcpOutput, ConnectionStatusResponse(socksVersion, None, code), () ⇒ {
160 | val ex = new ProxyException("Command not supported")
161 | failConnection(ex)
162 | })
163 | } else {
164 | emitRequest(ProxyConnectionRequest(if (socksVersion == SocksVersion.SocksV5) "socks" else "socks4", address))
165 | }
166 |
167 | case AuthRequest(methods, rest) ⇒
168 | buffer = ByteString(rest:_*)
169 | if (methods.contains(AuthMethod.NoAuth)) {
170 | emit(tcpOutput, AuthMethodResponse(AuthMethod.NoAuth), () ⇒ pull(tcpInput))
171 | } else {
172 | emit(tcpOutput, AuthMethodResponse.notSupported, () ⇒ {
173 | val ex = new ProxyException("No valid authentication methods provided")
174 | failConnection(ex)
175 | })
176 | }
177 |
178 | case HttpRequest((method, url, headers), rest) ⇒
179 | buffer = ByteString(rest:_*)
180 | val address = HttpConnect.addressOf(url)
181 | if (address.getHostString.isEmpty) { // Plain HTTP request
182 | emit(tcpOutput, HttpResponse(HttpStatus(400, "Bad Request"), Nil) ++ ByteString("Request not supported"), () ⇒ {
183 | val ex = new ProxyException("Plain HTTP not supported")
184 | failConnection(ex)
185 | })
186 | } else {
187 | if (method != HttpMethod.CONNECT) {
188 | // Replicate request
189 | val path = Option(URLParser.withDefaultProtocol(url).getFile).filter(_.nonEmpty).getOrElse("/")
190 | val request = HttpRequest((method, path, headers))
191 | buffer = request ++ buffer
192 | emitRequest(ProxyConnectionRequest("http", address))
193 | } else {
194 | emitRequest(ProxyConnectionRequest("https", address))
195 | }
196 | }
197 |
198 | case _ ⇒
199 | pull(tcpInput)
200 | }
201 | }
202 |
203 | setHandler(tcpInput, new InHandler {
204 | def onPush() = {
205 | val data = grab(tcpInput)
206 | writeBuffer(data)
207 | processBuffer()
208 | }
209 | })
210 |
211 | setHandler(tcpOutput, eagerTerminateOutput)
212 | }
213 | (logic, promise.future)
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/test/resources/reference.conf:
--------------------------------------------------------------------------------
1 | karasiq.proxy-chain-test {
2 | url = "http://example.com/"
3 | ok-status = "HTTP/1.1 200 OK"
4 |
5 | proxies = [
6 | "http://127.0.0.1:9999" // HTTP proxy
7 | "socks://127.0.0.1:1080" // Socks proxy
8 | "tls-socks://127.0.0.1:4562" // TLS-SOCKS proxy
9 | "http://127.0.0.1:9050" // Second HTTP proxy
10 | ]
11 | }
12 |
13 | akka.loglevel = DEBUG
--------------------------------------------------------------------------------
/src/test/scala/com/github/karasiq/proxy/tests/ActorSpec.scala:
--------------------------------------------------------------------------------
1 | package com.github.karasiq.proxy.tests
2 |
3 | import akka.actor.ActorSystem
4 | import akka.stream.ActorMaterializer
5 | import akka.testkit.TestKit
6 | import org.scalatest.{BeforeAndAfterAll, Matchers, Suite}
7 |
8 | abstract class ActorSpec extends TestKit(ActorSystem("test")) with Suite with Matchers with BeforeAndAfterAll {
9 | implicit val materializer = ActorMaterializer()
10 |
11 | override protected def afterAll(): Unit = {
12 | TestKit.shutdownActorSystem(system)
13 | super.afterAll()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/scala/com/github/karasiq/proxy/tests/client/ProxyChainTest.scala:
--------------------------------------------------------------------------------
1 | package com.github.karasiq.proxy.tests.client
2 |
3 | import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
4 | import java.net.{InetSocketAddress, URI}
5 | import java.security.SecureRandom
6 |
7 | import scala.collection.JavaConverters._
8 | import scala.concurrent.Future
9 | import scala.language.postfixOps
10 |
11 | import akka.Done
12 | import akka.http.scaladsl.{ConnectionContext, HttpsConnectionContext}
13 | import akka.stream.scaladsl.{Flow, Keep, Source, Tcp}
14 | import akka.stream.testkit.scaladsl.TestSink
15 | import akka.util.ByteString
16 | import com.github.karasiq.proxy.tests.ActorSpec
17 | import org.scalatest.FlatSpecLike
18 | import org.scalatest.tags.Network
19 |
20 | import com.karasiq.networkutils.http.headers.HttpHeader
21 | import com.karasiq.networkutils.proxy.Proxy
22 | import com.karasiq.parsers.http.{HttpMethod, HttpRequest}
23 | import com.karasiq.proxy._
24 | import com.karasiq.tls.TLSKeyStore
25 |
26 | // You need to have running proxies to run this test
27 | @Network
28 | class ProxyChainTest extends ActorSpec with FlatSpecLike {
29 | import system.dispatcher
30 |
31 | "Stream connector" should "connect to HTTP proxy" in {
32 | val Some(proxy) = Settings.testProxies.find(_.scheme == "http")
33 | readFrom(ProxyChain.connect(Settings.testHost, Seq(proxy)))
34 | }
35 |
36 | it should "connect to SOCKS proxy" in {
37 | val Some(proxy) = Settings.testProxies.find(_.scheme == "socks")
38 | readFrom(ProxyChain.connect(Settings.testHost, Seq(proxy)))
39 | }
40 |
41 | it should "connect to TLS-SOCKS proxy" in {
42 | val Some(proxy) = Settings.testProxies.find(_.scheme == "tls-socks")
43 | readFrom(ProxyChain.connect(Settings.testHost, Seq(proxy), Some(Settings.tlsContext)))
44 | }
45 |
46 | it should "connect through proxy chain" in {
47 | readFrom(ProxyChain.connect(Settings.testHost, Settings.testProxies, Some(Settings.tlsContext)))
48 | }
49 |
50 | private[this] object Settings {
51 | private[this] val config = system.settings.config.getConfig("karasiq.proxy-chain-test")
52 |
53 | val (testHost, testUrl) = {
54 | val uri = new URI(config.getString("url"))
55 | (InetSocketAddress.createUnresolved(uri.getHost, Some(uri.getPort).filter(_ != -1).getOrElse(80)), uri.getPath)
56 | }
57 |
58 | val testProxies: List[Proxy] = {
59 | import com.karasiq.networkutils.uri._
60 |
61 | config.getStringList("proxies").asScala
62 | .map(url ⇒ Proxy(url))
63 | .toList
64 | }
65 |
66 | lazy val tlsContext: HttpsConnectionContext = {
67 | val keyStore = TLSKeyStore()
68 | val sslContext = SSLContext.getInstance("TLS")
69 | val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
70 | keyManagerFactory.init(keyStore.keyStore, keyStore.password.toCharArray)
71 |
72 | val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
73 | trustManagerFactory.init(keyStore.keyStore)
74 | sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, SecureRandom.getInstanceStrong)
75 |
76 | ConnectionContext.https(sslContext)
77 | }
78 |
79 | val okStatus = config.getString("ok-status")
80 | }
81 |
82 | private[this] def checkResponse(bytes: ByteString): Unit = {
83 | val response = bytes.utf8String
84 | println(response)
85 | assert(response.startsWith(Settings.okStatus))
86 | }
87 |
88 | private[this] def readFrom(flow: Flow[ByteString, ByteString, (Future[Tcp.OutgoingConnection], Future[Done])]): Unit = {
89 | val request = HttpRequest((HttpMethod.GET, Settings.testUrl, Seq(HttpHeader("Host" → Settings.testHost.getHostString))))
90 | val probe = Source.single(request)
91 | .viaMat(flow)(Keep.right)
92 | .runWith(TestSink.probe)
93 |
94 | checkResponse(probe.requestNext())
95 | probe.cancel()
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/scala/com/github/karasiq/proxy/tests/rfc/HttpConnectRfcTest.scala:
--------------------------------------------------------------------------------
1 | package com.github.karasiq.proxy.tests.rfc
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import akka.util.ByteString
6 | import com.karasiq.networkutils.http.HttpStatus
7 | import com.karasiq.networkutils.http.headers._
8 | import com.karasiq.parsers.http._
9 | import org.scalatest.{FlatSpec, Matchers}
10 |
11 | class HttpConnectRfcTest extends FlatSpec with Matchers {
12 | "HTTP CONNECT parser" should "parse request" in {
13 | ByteString("CONNECT host.com:443 HTTP/1.1\r\nHost: host.com\r\nTest-header: test\r\n\r\n") match {
14 | case HttpRequest((method, url, headers), Nil) ⇒
15 | url shouldBe "host.com:443"
16 | headers.toList shouldBe List(HttpHeader("Host: host.com"), HttpHeader("Test-header: test"))
17 | }
18 | }
19 |
20 | it should "parse response" in {
21 | ByteString("HTTP/1.1 123 Test message\r\nTest-header: header\r\n\r\n") match {
22 | case HttpResponse((status, headers), Nil) ⇒
23 | status shouldBe HttpStatus(123, "Test message")
24 | headers.toList shouldBe List(HttpHeader("Test-header: header"))
25 | }
26 | }
27 |
28 | it should "create request" in {
29 | val address = InetSocketAddress.createUnresolved("host.com", 443)
30 | val data = HttpConnect(address, Seq(Host(address), HttpHeader("Test-Header", "test")))
31 | data.utf8String shouldBe "CONNECT host.com:443 HTTP/1.1\r\nHost: host.com:443\r\nTest-Header: test\r\n\r\n"
32 | }
33 |
34 | it should "create response" in {
35 | val data = HttpResponse((HttpStatus(123, "Test code"), Seq(HttpHeader("Host: host.com"), HttpHeader("Test-Header: test"))))
36 | data.utf8String shouldBe "HTTP/1.1 123 Test code\r\nHost: host.com\r\nTest-Header: test\r\n\r\n"
37 | }
38 |
39 | it should "parse Proxy-Authorization header" in {
40 | HttpHeader("Proxy-Authorization", "Basic dXNlcjpwYXNz") match {
41 | case `Proxy-Authorization`("user:pass") ⇒
42 | // Pass
43 | }
44 | }
45 |
46 | it should "create valid Proxy-Authorization header" in {
47 | `Proxy-Authorization`.basic("user:pass") shouldBe HttpHeader("Proxy-Authorization", "Basic dXNlcjpwYXNz")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/scala/com/github/karasiq/proxy/tests/rfc/SocksRfcTest.scala:
--------------------------------------------------------------------------------
1 | package com.github.karasiq.proxy.tests.rfc
2 |
3 | import akka.util.ByteString
4 | import com.karasiq.parsers.socks.SocksClient._
5 | import com.karasiq.parsers.socks.SocksServer._
6 | import org.scalatest.FlatSpec
7 |
8 | class SocksRfcTest extends FlatSpec {
9 | "SOCKS parser" should "parse SOCKS5 auth request" in {
10 | ByteString(0x05, 0x01, 0x00) match {
11 | case AuthRequest(AuthMethod.NoAuth +: Nil, Nil) ⇒
12 | // Pass
13 | }
14 | }
15 |
16 | it should "parse SOCKS5 connection request" in {
17 | ByteString(0x05, 0x03, 0x00, 0x01, 127, 0, 0, 1, 0x1F, 0x90) match {
18 | case ConnectionRequest((SocksVersion.SocksV5, command, address, _), Nil) ⇒
19 | assert(command == Command.UdpAssociate)
20 | assert(address.getHostString == "127.0.0.1")
21 | assert(address.getPort == 8080)
22 | }
23 | }
24 |
25 | it should "parse SOCKS4 connection request" in {
26 | ByteString(0x04, 0x01, 0x1F, 0x90, 127, 0, 0, 1) ++ ByteString("user") ++ ByteString(0x00) match {
27 | case ConnectionRequest((SocksVersion.SocksV4, Command.TcpConnection, address, "user"), Nil) ⇒
28 | assert(address.getHostString == "127.0.0.1")
29 | assert(address.getPort == 8080)
30 | }
31 | }
32 |
33 | it should "parse SOCKS4A connection request" in {
34 | ByteString(0x04, 0x01, 0x1F, 0x90, 0, 0, 0, 1) ++ ByteString("user") ++ ByteString(0x00) ++ ByteString("host.com") ++ ByteString(0x00) match {
35 | case ConnectionRequest((SocksVersion.SocksV4, Command.TcpConnection, address, "user"), data) ⇒
36 | assert(address.getHostString == "host.com")
37 | assert(address.getPort == 8080)
38 | }
39 | }
40 |
41 | it should "parse SOCKS5 auth method response" in {
42 | ByteString(0x05, 0x00) match {
43 | case AuthMethodResponse(AuthMethod.NoAuth, Nil) ⇒
44 | // Pass
45 | }
46 | }
47 |
48 | it should "parse SOCKS5 connection response" in {
49 | ByteString(0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0, 80) match {
50 | case ConnectionStatusResponse((SocksVersion.SocksV5, Some(address), ConnectionSuccess(code @ 0x00, message @ "Request granted")), Nil) ⇒
51 | assert(address.getHostString == "127.0.0.1")
52 | assert(address.getPort == 80)
53 | }
54 | }
55 |
56 | it should "parse SOCKS4 connection response" in {
57 | ByteString(0x00, Codes.Socks4.REQUEST_FAILED.code, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) match {
58 | case ConnectionStatusResponse((SocksVersion.SocksV4, None, Codes.Socks4.REQUEST_FAILED), Nil) ⇒
59 | // Pass
60 | }
61 | }
62 |
63 |
64 | it should "parse username/password auth request" in {
65 | val (username, password) = "admin" → "password"
66 |
67 | ByteString(0x01, username.length.toByte) ++ ByteString(username) ++ ByteString(password.length.toByte) ++ ByteString(password) match {
68 | case UsernameAuthRequest((`username`, `password`), Nil) ⇒
69 | // Pass
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/test/scala/com/github/karasiq/proxy/tests/server/ProxyServerTest.scala:
--------------------------------------------------------------------------------
1 | package com.github.karasiq.proxy.tests.server
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import scala.concurrent.Await
6 | import scala.concurrent.duration._
7 | import scala.language.postfixOps
8 |
9 | import akka.stream.scaladsl.{Keep, Source}
10 | import akka.stream.testkit.scaladsl.TestSink
11 | import akka.util.ByteString
12 | import com.github.karasiq.proxy.tests.ActorSpec
13 | import org.scalatest.FlatSpecLike
14 |
15 | import com.karasiq.networkutils.http.HttpStatus
16 | import com.karasiq.parsers.http.{HttpMethod, HttpRequest, HttpResponse}
17 | import com.karasiq.parsers.socks.SocksClient
18 | import com.karasiq.parsers.socks.SocksClient.ConnectionRequest
19 | import com.karasiq.parsers.socks.SocksClient.SocksVersion.{SocksV4, SocksV5}
20 | import com.karasiq.proxy.ProxyException
21 | import com.karasiq.proxy.server.{ProxyConnectionRequest, ProxyServer}
22 |
23 | class ProxyServerTest extends ActorSpec with FlatSpecLike {
24 | "Proxy server" should "accept HTTP CONNECT" in {
25 | HttpRequest((HttpMethod.CONNECT, "http://example.com", Nil)) should beAcceptedAs(ProxyConnectionRequest("https", InetSocketAddress.createUnresolved("example.com", 80)))
26 | }
27 |
28 | it should "fail on plain HTTP" in {
29 | val expectedAnswer = HttpResponse(HttpStatus(400, "Bad Request"), Nil) ++ ByteString("Request not supported")
30 | val (future, probe) = Source.single(HttpRequest((HttpMethod.GET, "/", Nil)))
31 | .viaMat(ProxyServer())(Keep.right)
32 | .toMat(TestSink.probe)(Keep.both)
33 | .run()
34 | probe.requestNext(expectedAnswer)
35 | probe.expectError()
36 | intercept[ProxyException](Await.result(future, 10 seconds))
37 | }
38 |
39 | it should "accept SOCKS5" in {
40 | ConnectionRequest((SocksV5, SocksClient.Command.TcpConnection, InetSocketAddress.createUnresolved("example.com", 80), "")) should beAcceptedAs(ProxyConnectionRequest("socks", InetSocketAddress.createUnresolved("example.com", 80)))
41 | }
42 |
43 | it should "accept SOCKS4" in {
44 | ConnectionRequest((SocksV4, SocksClient.Command.TcpConnection, InetSocketAddress.createUnresolved("example.com", 80), "")) should beAcceptedAs(ProxyConnectionRequest("socks4", InetSocketAddress.createUnresolved("example.com", 80)))
45 | }
46 |
47 | private[this] def beAcceptedAs(expect: ProxyConnectionRequest) = {
48 | equal(expect).matcher[ProxyConnectionRequest].compose { (request: ByteString) ⇒
49 | val testIn = ByteString("Test bytes sent to server")
50 | val testOut = ByteString("Test bytes sent to client")
51 | val (future, serverProbe) = Source(Vector(request, testIn))
52 | .viaMat(ProxyServer())(Keep.right)
53 | .toMat(TestSink.probe)(Keep.both)
54 | .run()
55 |
56 | val (parsedRequest, flow) = Await.result(future, 10 seconds)
57 | parsedRequest shouldBe expect
58 | val (_, clientProbe) = flow.runWith(Source.single(testOut), TestSink.probe)
59 |
60 | clientProbe.requestNext(testIn)
61 | serverProbe.requestNext(testOut)
62 | clientProbe.expectComplete()
63 | serverProbe.expectComplete()
64 | parsedRequest
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------