Skip to content

Commit

Permalink
akka tls support
Browse files Browse the repository at this point in the history
  • Loading branch information
Karasiq committed May 29, 2016
1 parent 9e1f364 commit 9d6987a
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 191 deletions.
7 changes: 4 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name := "proxychain"

organization := "com.github.karasiq"

version := "2.0.0"
version := "2.0.1"

isSnapshot := version.value.endsWith("SNAPSHOT")

Expand All @@ -19,9 +19,10 @@ libraryDependencies ++= {
"org.apache.httpcomponents" % "httpclient" % "4.3.3",
"com.typesafe.akka" %% "akka-actor" % akkaV,
"com.typesafe.akka" %% "akka-stream" % akkaV,
"com.typesafe.akka" %% "akka-http-experimental" % akkaV,
"org.scalatest" %% "scalatest" % "2.2.4" % "test",
"com.github.karasiq" %% "cryptoutils" % "1.2",
"com.github.karasiq" %% "proxyutils" % "2.0.2",
"com.github.karasiq" %% "cryptoutils" % "1.4.0",
"com.github.karasiq" %% "proxyutils" % "2.0.3",
"com.github.karasiq" %% "coffeescript" % "1.0"
)
}
Expand Down
26 changes: 22 additions & 4 deletions src/main/scala/com/karasiq/proxychain/AppConfig.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.karasiq.proxychain

import java.net.InetSocketAddress
import java.security.SecureRandom
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}

import akka.http.scaladsl.{ConnectionContext, HttpsConnectionContext}
import akka.stream.TLSClientAuth
import com.karasiq.fileutils.PathUtils._
import com.karasiq.networkutils.proxy.Proxy
import com.karasiq.proxy.ProxyChain
import com.karasiq.tls.x509.CertificateVerifier
import com.karasiq.tls.x509.{CertificateVerifier, TrustStore}
import com.karasiq.tls.{TLS, TLSKeyStore}
import com.typesafe.config.{Config, ConfigFactory}

Expand Down Expand Up @@ -35,18 +39,32 @@ object AppConfig {
}

def apply(): AppConfig = apply(externalConfig().getConfig("proxyChain"))

case class TLSConfig(keyStore: TLSKeyStore, verifier: CertificateVerifier, keySet: TLS.KeySet, clientAuth: Boolean)

def tlsConfig(): TLSConfig = {
val config = AppConfig.externalConfig().getConfig("proxyChain.tls")

val verifier = CertificateVerifier.fromTrustStore(CertificateVerifier.trustStore(config.getString("trust-store")))
val verifier = CertificateVerifier.fromTrustStore(TrustStore.fromFile(config.getString("trust-store")))
val keyStore = new TLSKeyStore(TLSKeyStore.keyStore(config.getString("key-store"), config.getString("key-store-pass")), config.getString("key-store-pass"))
val clientAuth = config.getBoolean("client-auth")
val keySet = keyStore.getKeySet(config.getString("key"))
TLSConfig(keyStore, verifier, keySet, clientAuth)
}

def tlsContext(server: Boolean = false): HttpsConnectionContext = {
// TODO: Auto configuration (http://doc.akka.io/docs/akka/current/scala/http/server-side-https-support.html)
val tlsConfig = this.tlsConfig()
val sslContext = SSLContext.getInstance("TLS")
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(tlsConfig.keyStore.keyStore, tlsConfig.keyStore.password.toCharArray)

val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
trustManagerFactory.init(tlsConfig.keyStore.keyStore)
sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, SecureRandom.getInstanceStrong)

ConnectionContext.https(sslContext, clientAuth = if (server && tlsConfig.clientAuth) Some(TLSClientAuth.need) else None)
}
}

trait AppConfig {
Expand Down
7 changes: 4 additions & 3 deletions src/main/scala/com/karasiq/proxychain/app/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.karasiq.proxychain.app

import java.net.InetSocketAddress

import akka.actor.{ActorSystem, Props}
import akka.actor.ActorSystem
import akka.event.Logging
import akka.io.Tcp.Bind
import akka.io.{IO, Tcp}
Expand Down Expand Up @@ -36,13 +36,14 @@ object Boot extends App {

val port = cfg.getInt("port")
if (port != 0) {
val server = actorSystem.actorOf(Props(classOf[Server], config), "proxychain-server")
val server = actorSystem.actorOf(Server.props(config, tls = false), "proxychain-server")
IO(Tcp)(actorSystem).tell(Bind(server, new InetSocketAddress(host, port)), server)
}

val tlsPort = cfg.getInt("tls.port")
if (tlsPort != 0) {
val server = actorSystem.actorOf(Props(classOf[TLSServer], new InetSocketAddress(host, tlsPort), config), "proxychain-tls-server")
val server = actorSystem.actorOf(Server.props(config, tls = true), "proxychain-tls-server")
IO(Tcp)(actorSystem).tell(Bind(server, new InetSocketAddress(host, tlsPort)), server)
}

Runtime.getRuntime.addShutdownHook(new Thread(new Runnable {
Expand Down
29 changes: 18 additions & 11 deletions src/main/scala/com/karasiq/proxychain/app/Handler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.io.IOException
import java.net.InetSocketAddress

import akka.Done
import akka.actor.SupervisorStrategy.{Resume, Stop}
import akka.actor.SupervisorStrategy.Stop
import akka.actor._
import akka.io.Tcp
import akka.io.Tcp._
Expand All @@ -15,7 +15,6 @@ import akka.util.ByteString
import com.karasiq.networkutils.http.HttpStatus
import com.karasiq.networkutils.http.headers.HttpHeader
import com.karasiq.networkutils.url.URLParser
import com.karasiq.parsers.ParserException
import com.karasiq.parsers.http.{HttpConnect, HttpMethod, HttpRequest, HttpResponse}
import com.karasiq.parsers.socks.SocksClient._
import com.karasiq.parsers.socks.SocksServer._
Expand All @@ -33,7 +32,7 @@ import scala.util.{Failure, Success, Try, control}
/**
* Proxy connection handler
*/
class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor with ActorLogging {
private[app] class Handler(connection: ActorRef, cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor with ActorLogging {
import context.{dispatcher, system}

implicit val actorMaterializer = ActorMaterializer(ActorMaterializerSettings(system))(system)
Expand All @@ -42,6 +41,12 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
var authenticated = false
val firewall: Firewall = cfg.firewall()

@scala.throws[Exception](classOf[Exception])
override def preStart() = {
super.preStart()
context.watch(connection)
}

private def write(connection: ActorRef, bytes: ByteString) = connection ! Write(bytes)

private def openConnection(connection: ActorRef, address: InetSocketAddress): Future[(OutgoingConnection, Subscriber[ByteString], Publisher[ByteString])] = {
Expand All @@ -51,8 +56,9 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
val promise = Promise[(OutgoingConnection, Subscriber[ByteString], Publisher[ByteString])]
val futures = chains.map { chain
val ((proxyInput, (connFuture, proxyFuture)), proxyOutput) = Source.asSubscriber[ByteString]
.idleTimeout(30 seconds)
.viaMat(ProxyChain.connect(address, chain:_*))(Keep.both)
.initialTimeout(10 seconds)
.idleTimeout(5 minutes)
.viaMat(ProxyChain.connect(address, chain, Some(AppConfig.tlsContext())))(Keep.both)
.toMat(Sink.asPublisher[ByteString](fanout = false))(Keep.both)
.run()

Expand Down Expand Up @@ -123,7 +129,6 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
}

private def connectThen(address: InetSocketAddress)(onComplete: Try[(OutgoingConnection, Subscriber[ByteString], Publisher[ByteString])] Unit): Unit = {
val connection = sender()
val ctx = context
connection ! SuspendReading
context.become(onClose)
Expand Down Expand Up @@ -154,19 +159,19 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
case ConnectionRequest((socksVersion, command, address, userId), rest)
authenticated = true
buffer = ByteString(rest:_*)
val connection = sender()
context.watch(connection)
if (command != Command.TcpConnection) {
// Command not supported
val code = if (socksVersion == SocksVersion.SocksV5) Codes.Socks5.COMMAND_NOT_SUPPORTED else Codes.failure(socksVersion)
write(connection, ConnectionStatusResponse(socksVersion, None, code))
connection ! Close
context.stop(self)
} else if (firewall.connectionIsAllowed(clientAddress, address)) {
log.info("{} connection request: {}", socksVersion, address)
connectThen(address) {
case Failure(e)
write(connection, ConnectionStatusResponse(socksVersion, None, Codes.failure(socksVersion)))
connection ! Close
context.stop(self)

case Success((proxyConnection, _, _))
write(connection, ConnectionStatusResponse(socksVersion, Some(proxyConnection.localAddress), Codes.success(socksVersion)))
Expand All @@ -176,34 +181,36 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
val code = if (socksVersion == SocksVersion.SocksV5) Codes.Socks5.CONN_NOT_ALLOWED else Codes.failure(socksVersion)
write(connection, ConnectionStatusResponse(socksVersion, None, code))
connection ! Close
context.stop(self)
}

case AuthRequest(methods, rest) if !authenticated
buffer = ByteString(rest:_*)
val connection = sender()
if (methods.contains(AuthMethod.NoAuth)) {
authenticated = true
write(connection, AuthMethodResponse(AuthMethod.NoAuth))
} else {
log.error("No valid authentication methods provided")
write(connection, AuthMethodResponse.notSupported)
connection ! Close
context.stop(self)
}

case HttpRequest((method, url, headers), rest)
buffer = ByteString(rest:_*)
val connection = sender()
val address = HttpConnect.addressOf(url)

if (address.getHostString.isEmpty) { // Plain HTTP request
write(connection, HttpResponse(HttpStatus(404, "Not Found"), Nil) ++ dummyPage())
connection ! Close
context.stop(self)
} else if (firewall.connectionIsAllowed(clientAddress, address)) {
log.info("HTTP connection request: {}", address)
connectThen(address) {
case Failure(e)
write(connection, HttpResponse(HttpStatus(400, "Bad Request"), Nil) ++ ByteString("Connection failed"))
connection ! Close
context.stop(self)

case Success((_, input, output))
method match {
Expand All @@ -220,14 +227,14 @@ class Handler(cfg: AppConfig, clientAddress: InetSocketAddress) extends Actor wi
log.warning("HTTP connection from {} rejected: {}", clientAddress, address)
write(connection, HttpResponse(HttpStatus(403, "Forbidden"), Nil) ++ ByteString("Connection not allowed"))
connection ! Close
context.stop(self)
}
}
}

override def receive = waitConnection.orElse(onClose)

override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
case _: ParserException Resume // Invalid request
case _: IOException Stop // Connection error
}
}
98 changes: 10 additions & 88 deletions src/main/scala/com/karasiq/proxychain/app/Server.scala
Original file line number Diff line number Diff line change
@@ -1,108 +1,30 @@
package com.karasiq.proxychain.app

import java.io.IOException
import java.net.InetSocketAddress
import java.nio.channels.{ServerSocketChannel, SocketChannel}
import java.util.concurrent.Executors

import akka.actor._
import akka.event.Logging
import com.karasiq.proxychain.AppConfig
import com.karasiq.tls.TLSServerWrapper
import org.apache.commons.io.IOUtils

import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, control}
import scala.language.postfixOps

private[app] object Server {
def props(cfg: AppConfig, tls: Boolean = false): Props = {
Props(classOf[Server], cfg, tls)
}
}

class Server(cfg: AppConfig) extends Actor with ActorLogging {
private[app] class Server(cfg: AppConfig, tls: Boolean) extends Actor with ActorLogging {
import akka.io.Tcp._

def receive = {
case Bound(address)
log.info("Proxy server running on {}", address)
log.info("{} server running on {}", if (tls) "TLS-Proxy" else "Proxy", address)

case CommandFailed(_: Bind)
context.stop(self)

case c @ Connected(remote, local) // New connection accepted
val handler = context.actorOf(Props(classOf[Handler], cfg, remote))
val connection = sender()
val handler = context.actorOf(Props(if (tls) classOf[TLSHandler] else classOf[Handler], connection, cfg, remote))
connection ! Register(handler)
}
}

// TLS tamper
class TLSServer(address: InetSocketAddress, cfg: AppConfig) extends Actor with ActorLogging {
import akka.io.Tcp._
val serverSocket = ServerSocketChannel.open()
private case class Accepted(socket: SocketChannel)

private val tlsConfig = AppConfig.tlsConfig()

private val acceptor = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor())

@throws[Exception](classOf[Exception])
override def preStart(): Unit = {
super.preStart()
serverSocket.bind(address)
acceptor.execute(new Runnable {
override def run(): Unit = {
control.Exception.ignoring(classOf[IOException]) {
while (serverSocket.isOpen) {
self ! Accepted(serverSocket.accept())
}
}
}
})
log.info("TLS-proxy server running on {}", address)
}


@throws[Exception](classOf[Exception])
override def postStop(): Unit = {
super.postStop()
IOUtils.closeQuietly(serverSocket)
acceptor.shutdown()
}

def receive = {
case Accepted(socket) // New connection accepted
import context.dispatcher
val handler = context.actorOf(Props(classOf[Handler], cfg, socket.getRemoteAddress.asInstanceOf[InetSocketAddress]))
val catcher = control.Exception.allCatch.withApply { exc
context.stop(handler)
IOUtils.closeQuietly(socket)
}

catcher {
val log = Logging(context.system, handler)

val tlsSocket = Promise[SocketChannel]()
tlsSocket.future.onComplete {
case Success(sc)
log.debug("TLS handshake finished")
val actor = context.actorOf(Props(classOf[TLSHandlerTamper], sc))
actor ! Register(handler)

case Failure(exc)
log.error(exc, "Error opening TLS socket")
handler ! ErrorClosed
IOUtils.closeQuietly(socket)
}

val serverWrapper = new TLSServerWrapper(tlsConfig.keySet, tlsConfig.clientAuth, tlsConfig.verifier) {
override protected def onInfo(message: String): Unit = {
log.debug(message)
}

override protected def onError(message: String, exc: Throwable): Unit = {
log.error(exc, message)
}
}

tlsSocket.completeWith(Future {
serverWrapper(socket)
})
}
}
}
Loading

0 comments on commit 9d6987a

Please sign in to comment.