Skip to content

Commit

Permalink
Implemented CONNECT proxying
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvanzijst committed Aug 17, 2021
1 parent d75f570 commit 8004431
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 11 deletions.
78 changes: 73 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# TLS Proxy Server in Scala
# TLS (HTTPS) Proxy Server in Scala

Very simple HTTPS proxy server written in Scala 2.12.
Very simple HTTPS proxy server written in Scala 2.12 with no dependencies
beyond `scala-logging`

Can be used as a library with no dependencies beyond `scala-logging`, or as a
standalone program.
Can be used as a library, or as a standalone program.

## Standalone

```
$ sbt run
Expand All @@ -13,4 +15,70 @@ $ sbt run
[info] Set current project to tlsproxy (in build file:/Users/erik/work/tlsproxy/)
[info] Running tlsproxy.Main
14:31:46.707 [run-main-0] INFO tlsproxy.ServerHandler - Listening on port 3128...
```
18:03:15.466 [main] INFO tlsproxy.ServerHandler - Listening on port 3128...
18:03:22.651 [main] ERROR tlsproxy.TlsProxyHandler - /0:0:0:0:0:0:0:1:49672 -> google.com:443: error: connection closed: java.io.IOException: Connection reset by peer
18:04:22.806 [main] INFO tlsproxy.TlsProxyHandler - /0:0:0:0:0:0:0:1:49818 -> www.google.com/172.217.6.36:443 finished (up: 581, down: 4294)
18:04:56.131 [main] INFO tlsproxy.TlsProxyHandler - /0:0:0:0:0:0:0:1:49807 -> nginx.org/52.58.199.22:443 finished (up: 568, down: 187)
```

Now configure `localhost:3128` as proxy in your browser.

```
$ curl -I -x localhost:3128 https://woefdram.nl
HTTP/1.1 200 Connection Accepted
Proxy-Agent: TlsProxy/1.0 (github.com/erikvanzijst/scala_tlsproxy)
Content-Type: text/plain; charset=us-ascii
Content-Length: 0
HTTP/2 200
server: nginx/1.18.0
date: Tue, 17 Aug 2021 16:19:04 GMT
content-type: text/html
content-length: 612
last-modified: Tue, 21 Apr 2020 14:09:01 GMT
etag: "5e9efe7d-264"
accept-ranges: bytes
```

## Library

To use it as a library in-process:

```scala
import tlsproxy.TlsProxy

new TlsProxy(3128).run()
```

The `run()` does not create any threads and run the entire proxy on the
calling thread. It does not return.

To move it to the background, pass it to a `Thread` or `Executor`:

```scala
import tlsproxy.TlsProxy
import java.util.concurrent.Executors

val executor = Executors.newSingleThreadExecutor()
executor.submit(new TlsProxy(3128))
```

## Caveat emptor

This is only implements the `CONNECT` method and can therefor only proxy HTTPS
requests. It does not support unencrypted proxy requests using `GET`.

Proxy requests for HTTP (non-TLS) `GET` requests result in an error and the
connection getting closed:

```
18:08:53.604 [main] ERROR tlsproxy.TlsProxyHandler - /0:0:0:0:0:0:0:1:51043 -> unconnected: error: connection closed: java.io.IOException: Malformed request
```

## Robustness (or lack thereof)

* This implementation is totally susceptible to all kinds of [slowloris attacks](https://en.wikipedia.org/wiki/Slowloris_(computer_security).
* It does not support client authentication
* Uses only 1 thread and cannot currently scale to multiple cores
* Does not restrict non-standard upstream ports
* Undoubtedly riddled with bugs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.typesafe.scalalogging.StrictLogging

import scala.collection.JavaConverters._

class ClientHandler(selector: Selector, socketChannel: SocketChannel) extends KeyHandler with StrictLogging {
class EchoHandler(selector: Selector, socketChannel: SocketChannel) extends KeyHandler with StrictLogging {
socketChannel.configureBlocking(false)
private val peer = socketChannel.getRemoteAddress
private val buffer = ByteBuffer.allocate((1 << 16) - 1)
Expand Down Expand Up @@ -54,10 +54,11 @@ class ClientHandler(selector: Selector, socketChannel: SocketChannel) extends Ke
}

def close(): Unit =
shutdown = true
if (selectionKey.isValid) {
selectionKey.cancel()
socketChannel.close()
logger.info("{} connection closed (total connected clients: {})",
peer, selector.keys().asScala.count(_.attachment().isInstanceOf[ClientHandler]) - 1)
peer, selector.keys().asScala.count(_.attachment().isInstanceOf[EchoHandler]) - 1)
}
}
9 changes: 9 additions & 0 deletions src/main/scala/tlsproxy/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ package tlsproxy

import java.util.concurrent.{ExecutorService, Executors}

import ch.qos.logback.classic.Level
import com.typesafe.scalalogging.StrictLogging
import org.slf4j.LoggerFactory
import scopt.OptionParser

object Main extends StrictLogging {

// Suppress debug when running from the shell
Seq("tlsproxy.TlsProxyHandler", "tlsproxy.ServerHandler", "tlsproxy.Pipe")
.map(LoggerFactory.getLogger)
.map(_.asInstanceOf[ch.qos.logback.classic.Logger])
.foreach(_.setLevel(Level.INFO))

def main(args: Array[String]): Unit = {

case class Config(port: Int = 3128)
Expand Down
50 changes: 50 additions & 0 deletions src/main/scala/tlsproxy/Pipe.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package tlsproxy

import java.nio.ByteBuffer
import java.nio.channels.{SelectionKey, SocketChannel}

import com.typesafe.scalalogging.StrictLogging

class Pipe(fromKey: SelectionKey, fromChannel: SocketChannel, toKey: SelectionKey, toChannel: SocketChannel)
extends KeyHandler with StrictLogging {

private val buffer = ByteBuffer.allocate(1 << 16)
private var shutdown = false
private var count: Long = 0

def bytes: Long = count

def isClosed: Boolean = shutdown && buffer.position() == 0

override def process(): Unit = {

if (fromKey.isValid && fromKey.isReadable && !shutdown) {
val len = fromChannel.read(buffer)
if (len == -1) {
logger.debug("{} -> {} EOF reached", fromChannel.getRemoteAddress, toChannel.getRemoteAddress)
shutdown = true
} else {
count = count + len
}
}

if (toKey.isValid && toKey.isWritable) {
buffer.flip()
toChannel.write(buffer)
buffer.compact()
}

if (shutdown && buffer.position() == 0) toChannel.shutdownOutput()

if (toKey.isValid) {
toKey.interestOps(
if (buffer.position() > 0) toKey.interestOps() | SelectionKey.OP_WRITE
else toKey.interestOps() & ~SelectionKey.OP_WRITE)
}
if (fromKey.isValid) {
fromKey.interestOps(
if (!buffer.hasRemaining || shutdown) fromKey.interestOps() & ~SelectionKey.OP_READ
else fromKey.interestOps() | SelectionKey.OP_READ)
}
}
}
6 changes: 3 additions & 3 deletions src/main/scala/tlsproxy/ServerHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class ServerHandler(selector: Selector, port: Int) extends KeyHandler with Stric

override def process(): Unit = {
val channel = serverSocketChannel.accept()
new ClientHandler(selector, channel)
logger.info("New incoming connection from {} (total connected clients: {})",
channel.getRemoteAddress, selector.keys().asScala.count(_.attachment().isInstanceOf[ClientHandler]))
new TlsProxyHandler(selector, channel)
logger.debug("New incoming connection from {} (total connected clients: {})",
channel.getRemoteAddress, selector.keys().asScala.count(_.attachment().isInstanceOf[TlsProxyHandler]))
}
}
4 changes: 3 additions & 1 deletion src/main/scala/tlsproxy/TlsProxy.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package tlsproxy

import java.nio.channels.Selector

import ch.qos.logback.classic.Level
import com.typesafe.scalalogging.StrictLogging
import org.slf4j.LoggerFactory

trait KeyHandler {
def process(): Unit
Expand All @@ -18,7 +20,7 @@ class TlsProxy(port: Int) extends StrictLogging with Runnable with AutoCloseable

override def run(): Unit = {
val selector = Selector.open
val server = new ServerHandler(selector, port)
new ServerHandler(selector, port)

while (true) {
if (selector.select(5000) > 0) {
Expand Down
Loading

0 comments on commit 8004431

Please sign in to comment.