Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions portalProxy/src/main/kotlin/IpUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package de.binarynoise.captiveportalautologin.portalproxy.portal
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.UnknownHostException
import io.vertx.core.http.HttpServerRequest

fun HttpServerRequest.getRealRemoteIP(): String {
val ip = remoteAddress().host()

val headers = headers()
if (isLanIp(ip) && "X-Real-IP" in headers) {
val realIp = headers["X-Real-IP"]
if (realIp != null) return realIp.split(",").first().trim()
}
return ip
}

fun isLanIp(ipString: String): Boolean {
val addr = parseIp(ipString) ?: return false

return when (addr) {
is Inet4Address -> isPrivateIPv4(addr)
is Inet6Address -> isLocalIPv6(addr)
else -> false
}
}

fun parseIp(ipString: String): InetAddress? = try {
InetAddress.getByName(ipString.trim())
} catch (_: UnknownHostException) {
null
} catch (_: SecurityException) {
null
}


val loopbackV6 = ByteArray(16) { if (it == 15) 1 else 0 }
fun isLocalIPv6(addr: Inet6Address): Boolean {
val bytes = addr.address
val b0 = bytes[0].toInt()
val b1 = bytes[1].toInt()

// fc00::/7 (1111 110x)
val isUniqueLocal = (b0 and 0xFE) == 0xFC
// fe80::/10 (1111 1110 10xx xxxx)
val isLinkLocal = (b0 == 0xFE) && ((b1 and 0xC0) == 0x80)
val isLoopback = bytes.contentEquals(loopbackV6)

return isUniqueLocal || isLinkLocal || isLoopback
}

fun isPrivateIPv4(addr: Inet4Address): Boolean {
val bytes = addr.address
val b0 = bytes[0].toInt() and 0xFF
val b1 = bytes[1].toInt() and 0xFF

return when (b0) {
// 10.0.0.0/8
10 -> true
// 127.0.0.1/8
127 -> true
// 172.16.0.0/12
172 if (b1 in 16..31) -> true
// 192.168.0.0/16
192 if b1 == 168 -> true
else -> false
}
}
36 changes: 26 additions & 10 deletions portalProxy/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package de.binarynoise.captiveportalautologin.portalproxy

import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.launch
import de.binarynoise.captiveportalautologin.portalproxy.portal.portalHost
import de.binarynoise.captiveportalautologin.portalproxy.portal.portalPort
import de.binarynoise.captiveportalautologin.portalproxy.portal.portalRouter
import de.binarynoise.captiveportalautologin.portalproxy.proxy.forward
import de.binarynoise.captiveportalautologin.portalproxy.proxy.forwardConnect
import de.binarynoise.captiveportalautologin.portalproxy.proxy.proxyPort
import de.binarynoise.logger.Logger
import de.binarynoise.logger.Logger.log
import io.vertx.core.Vertx
Expand All @@ -18,21 +19,22 @@ import io.vertx.kotlin.coroutines.dispatcher

class MainVerticle : CoroutineVerticle() {
override suspend fun start() {
val router = Router.router(vertx)

// Portal routes
router.route().virtualHost(portalHost).handler { ctx ->
val portalRouter = Router.router(vertx)
portalRouter.route().handler { ctx ->
log("route portal")
ctx.next()
}.subRouter(portalRouter(vertx))

// Proxy routes
router.route().handler { ctx ->
val proxyRouter = Router.router(vertx)
proxyRouter.route().handler { ctx ->
log("route /http")
forward(ctx.request())
}

val requestHandler: (HttpServerRequest) -> Unit = { request ->
val proxyRequestHandler: (HttpServerRequest) -> Unit = { request ->
launch(vertx.dispatcher() + EmptyCoroutineContext) {
log(buildString {
append("< ")
Expand All @@ -56,7 +58,7 @@ class MainVerticle : CoroutineVerticle() {
if (request.method() == HttpMethod.CONNECT) {
forwardConnect(request, vertx)
} else {
router.handle(request)
proxyRouter.handle(request)
}

log(buildString {
Expand All @@ -71,20 +73,34 @@ class MainVerticle : CoroutineVerticle() {
}
}

val portalRequestHandler: (HttpServerRequest) -> Unit = { request ->
launch(vertx.dispatcher() + EmptyCoroutineContext) {
portalRouter.handle(request)
}
}

val exceptionHandler = { t: Throwable ->
log("Unhandled exception during connection", t)
}
val invalidRequestHandler = { r: HttpServerRequest ->
log("Invalid request: $r")
}

val server = vertx.createHttpServer()
.requestHandler(requestHandler)
val proxyServer = vertx.createHttpServer()
.requestHandler(proxyRequestHandler)
.exceptionHandler(exceptionHandler)
.invalidRequestHandler(invalidRequestHandler)
.listen(proxyPort, "::")
.coAwait()
log("Started proxy server on port " + proxyServer.actualPort())

val portalServer = vertx.createHttpServer()
.requestHandler(portalRequestHandler)
.exceptionHandler(exceptionHandler)
.invalidRequestHandler(invalidRequestHandler)
.listen(8000, "::")
.listen(portalPort, "::")
.coAwait()
log("Started server on port " + server.actualPort())
log("Started portal server on port " + portalServer.actualPort())
}
}

Expand Down
28 changes: 20 additions & 8 deletions portalProxy/src/main/kotlin/Portal.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.binarynoise.captiveportalautologin.portalproxy.portal

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.html.*
import kotlinx.html.stream.*
Expand All @@ -10,8 +10,9 @@ import io.vertx.core.http.HttpServerRequest
import io.vertx.ext.web.Router
import io.vertx.kotlin.coroutines.coroutineRouter

const val portalHost = "binarynoise.de"
const val portalPort = 8000
const val portalPort = 8001
// TODO: use [System.getenv("PORTAL_HOST")] here
const val friendlyHost = "portal.binarynoise.de"

private val database = ConcurrentHashMap<String, Boolean>()

Expand All @@ -26,15 +27,17 @@ fun CoroutineScope.portalRouter(vertx: Vertx): Router {

// Login route
router.route("/login").handler { ctx ->
val ip = ctx.request().remoteAddress().host()
val ip = ctx.request().getRealRemoteIP()

database[ip] = false
log("logged in $ip")
ctx.response().putHeader("Location", "/").setStatusCode(302).end()
}

// Logout route
router.route("/logout").handler { ctx ->
val ip = ctx.request().remoteAddress().host()
val ip = ctx.request().getRealRemoteIP()

database[ip] = true
log("logged out $ip")
redirect(ctx.request())
Expand All @@ -49,12 +52,17 @@ fun CoroutineScope.portalRouter(vertx: Vertx): Router {
return router
}

fun getPortalHost(request: HttpServerRequest?) : String {
return request?.getHeader("Host") ?: friendlyHost
}

fun redirect(request: HttpServerRequest) {
request.response().putHeader("Location", "http://$portalHost:$portalPort/").setStatusCode(303).end()
val host = getPortalHost(request)
request.response().putHeader("Location", "http://$host:$portalPort/").setStatusCode(303).end()
}

fun checkCaptured(request: HttpServerRequest): Boolean {
val ip = request.remoteAddress().host()
val ip = request.getRealRemoteIP()
return database[ip] ?: true
}

Expand Down Expand Up @@ -89,6 +97,10 @@ private fun servePortalPage(request: HttpServerRequest) {
body {
h1 { +"Captive Portal" }
p { +"You are currently ${if (captured) "captured" else "not captured"}" }
p {
+"Your IP is "
code { +request.getRealRemoteIP() }
}

form("/login") {
p {
Expand All @@ -105,7 +117,7 @@ private fun servePortalPage(request: HttpServerRequest) {
p {
+"This page can be opened again at"
br()
val href = "http://$portalHost:$portalPort/"
val href = "http://${getPortalHost(request)}:$portalPort/"
a(href = href) { +href }
}
}
Expand Down
1 change: 1 addition & 0 deletions portalProxy/src/main/kotlin/Proxy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.vertx.kotlin.coroutines.coAwait

private val allowlistDomain = listOf("am-i-captured.binarynoise.de", "www.google.com")
private val allowlistPort = listOf("80", "443")
val proxyPort = 8000

fun forward(request: HttpServerRequest) {
if (checkCaptured(request)) {
Expand Down
Loading