├── .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 [![Build Status](https://travis-ci.org/Karasiq/proxyutils.svg?branch=master)](https://travis-ci.org/Karasiq/proxyutils) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.karasiq/proxyutils_2.12/badge.svg)](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 | --------------------------------------------------------------------------------