Skip to content
Merged
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
52 changes: 21 additions & 31 deletions .github/workflows/android-build-debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,24 @@ jobs:
runs-on: self-hosted

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/*.gradle', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build ARM64 Debug APK
run: ./gradlew :app:assembleArm64Debug

- name: Upload ARM64 Debug APK
uses: actions/upload-artifact@v4
with:
name: arm64-debug-apk
path: app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
retention-days: 30
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build ARM64 Debug APK
run: ./gradlew :app:assembleArm64Debug

- name: Upload ARM64 Debug APK
uses: actions/upload-artifact@v4
with:
name: arm64-debug-apk
path: app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
retention-days: 30
52 changes: 21 additions & 31 deletions .github/workflows/linux-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,24 @@ jobs:
runs-on: self-hosted

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/*.gradle', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build Linux Shadow Jar
run: ./gradlew :linux:shadowJar

- name: Upload ARM64 Debug APK
uses: actions/upload-artifact@v4
with:
name: linux-shadow-jar
path: linux/build/libs/linux-shadow.jar
retention-days: 30
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build Linux Shadow Jar
run: ./gradlew :linux:shadowJar

- name: Upload ARM64 Debug APK
uses: actions/upload-artifact@v4
with:
name: linux-shadow-jar
path: linux/build/libs/linux-shadow.jar
retention-days: 30
72 changes: 31 additions & 41 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,34 @@ jobs:
runs-on: self-hosted

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/*.gradle', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run unit tests
run: ./gradlew test

- name: Run lint checks
run: ./gradlew lint

- name: Run all verification checks
run: ./gradlew check

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
**/build/reports/tests/**
**/build/reports/lint-results*.html
**/*.sarif
retention-days: 30
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run unit tests
run: ./gradlew test

- name: Run lint checks
run: ./gradlew lint

- name: Run all verification checks
run: ./gradlew check

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
**/build/reports/tests/**
**/build/reports/lint-results*.html
**/*.sarif
retention-days: 30
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import okhttp3.Response
@Experimental
object BinarynoisePortalProxy : PortalLiberator {
override fun canSolve(response: Response): Boolean {
return response.requestUrl.host == "binarynoise.de" && response.requestUrl.port == 8000
return response.requestUrl.host == "portal.binarynoise.de" && response.requestUrl.port == 8001
}

override fun solve(client: OkHttpClient, response: Response, cookies: Set<Cookie>) {
Expand Down
44 changes: 44 additions & 0 deletions portalProxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# PortalProxy

A HTTP/HTTPS proxy server with captive portal functionality.

## Overview

PortalProxy provides:

- HTTP/HTTPS proxy server with configurable port
- Captive portal for network authentication
- IP-based access control and allowlisting
- CONNECT tunneling for HTTPS traffic
- Real-time IP tracking and login/logout functionality

## Environment Variables

### Server Configuration

| Variable | Default | Description |
|---------------|---------|---------------------------------------------|
| `PROXY_PORT` | `8000` | Port for the proxy server |
| `PORTAL_PORT` | `8001` | Port for the captive portal web interface |
| `PORTAL_HOST` | `null` | Friendly hostname for the portal (optional) |

### Access Control

| Variable | Default | Description |
|--------------------------|---------|--------------------------------------------------------------|
| `PROXY_ALLOWLIST_DOMAIN` | `null` | Comma-separated list of allowed domains for CONNECT requests |
| `PROXY_ALLOWLIST_PORT` | `null` | Comma-separated list of allowed ports for CONNECT requests |

Providing empty values for `PROXY_ALLOWLIST_DOMAIN` or `PROXY_ALLOWLIST_PORT` disables allowlisting.

## Usage

### Basic Setup

```bash
./gradlew :portalProxy:run
```

### Portal Interface

Access the captive portal at `http://localhost:8001/` (or your configured `PORTAL_PORT`).
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
}
}
Loading
Loading