diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..47f97d2f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+- package-ecosystem: maven
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+- package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..943b4227
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,84 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ main ]
+ paths-ignore:
+ - '.idea/**'
+ - '.run/**'
+ - '**/*.md'
+ - 'src/site/**'
+ - '**/.editorconfig'
+ - '**/.gitattributes'
+ - '**/.gitignore'
+ - '/*.txt'
+ - '/*.bash'
+ - 'release'
+ pull_request:
+ paths-ignore:
+ - '.idea/**'
+ - '.run/**'
+ - '**/*.md'
+ - 'src/site/**'
+ - '**/.editorconfig'
+ - '**/.gitattributes'
+ - '**/.gitignore'
+ - '/*.txt'
+ - '/*.bash'
+ - 'release'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ platform: [ubuntu-latest, macos-latest, windows-latest]
+ fail-fast: false
+
+ runs-on: ${{ matrix.platform }}
+ timeout-minutes: 7
+
+ steps:
+ - uses: actions/checkout@v7
+ - name: Set up JDK 17
+ uses: actions/setup-java@v6
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+ - name: Check Java
+ run: java -version
+ - name: Check Maven
+ run: mvn --batch-mode --version
+ - name: Compile code
+ run: mvn --batch-mode compile
+ - name: Branch name [env]
+ run: echo running on branch ${GITHUB_REF##*/}
+ - name: Run smoke tests
+ run: mvn --batch-mode package -Psmoke-test
+ - name: Run slow tests
+ if: success()
+ run: mvn --batch-mode package -Pslow-tests
+
+ - name: Generate Maven test report
+ if: failure()
+ run: mvn --batch-mode surefire-report:report-only
+ - name: Upload test report
+ uses: actions/upload-artifact@v7
+ if: failure()
+ with:
+ name: test-report-${{matrix.platform}}
+ retention-days: 5
+ path: |
+ target/surefire-reports
+ target/site
+ target/*.hprof
+ target/*.txt
+ target/*.jks
+ target/*_cert*
diff --git a/.gitignore b/.gitignore
index eb7fa26c..146483be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-log.txt
*.swp
*.settings
*.classpath
@@ -9,10 +8,13 @@ log.txt
*.DS_Store
*.orig
target/
-.idea/
+dependency-reduced-pom.xml
jmeter.log
lib/
LittleProxy.pro
/bin
-/*.jks
performance/site/
+.claude/settings.local.json
+/logs
+/.opencode
+/.sisyphus
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..f46b8fe8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/*
+
+# things we want to be shared
+!/dictionaries
+!/inspectionProfiles
+!/scopes
+!/.gitignore
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..02b2d7f2
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/tests.run.xml b/.run/tests.run.xml
new file mode 100644
index 00000000..9d1c328e
--- /dev/null
+++ b/.run/tests.run.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 34ea22c1..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-sudo: false
-
-language: java
-dist: trusty
-jdk:
- - oraclejdk8
-
-cache:
- directories:
- - $HOME/.m2
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..cf1605a6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,64 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project
+
+LittleProxy is a high-performance HTTP/HTTPS proxy library written on top of Netty.
+It is consumed as an embedded library (`io.github.littleproxy:littleproxy`) and can also run as a standalone
+executable via the shaded jar (`Launcher` main class).
+
+The active fork is maintained at https://github.com/LittleProxy/LittleProxy.
+
+## Build & Test Commands
+
+- Build & run all tests: `mvn test`
+- Package (produces the shaded runnable jar): `mvn clean package`
+- Skip tests while packaging: `mvn clean package -DskipTests`
+- Smoke tests only (fast subset, excludes `slow-test` tag): `mvn package -Psmoke-test`
+- Slow tests only (tagged `@Tag("slow-test")`): `mvn package -Pslow-tests`
+- Single test class: `mvn test -Dtest=MitmProxyTest`
+- Single test method: `mvn test -Dtest=MitmProxyTest#testProxyConnects`
+- Run the shaded jar locally: `./run.bash --server --config ./config/littleproxy.properties` (wraps `mvn package -Dmaven.test.skip=true` + `java -jar`)
+
+CI (`.github/workflows/main.yml`) runs `-Psmoke-test` then `-Pslow-tests` on Ubuntu/macOS/Windows with JDK 17.
+
+## Toolchain Constraints
+
+- **Main source targets Java 11** (`11`); **tests compile with Java 17** (`compile-tests-with-java17` execution). New main-source code must not use language features newer than Java 11.
+- Source is auto-formatted by **Spotless (google-java-format 1.28.0)** in the `compile` phase — it rewrites files on every build. Don't hand-align imports; just run the build.
+- **ErrorProne** runs during compilation with `-Xep:MissingSummary:OFF -Xep:JdkObsolete:OFF -Xep:ReferenceEquality:OFF -Xep:OperatorPrecedence:OFF`. Other checks are active.
+- `maven-enforcer-plugin` bans `junit:junit` and `org.hamcrest:hamcrest-core` — use JUnit Jupiter + AssertJ.
+
+## High-Level Architecture
+
+The proxy is built around two mirror Netty channel handlers and a small state machine.
+
+- **`DefaultHttpProxyServer`** (`impl/`) — bootstrap/entry point; owns the `ServerGroup` (shared Netty event loops), `HttpFiltersSource`, optional `MitmManager`, `ChainedProxyManager`, and `ProxyAuthenticator`. `HttpProxyServerBootstrap` is the fluent builder users interact with.
+- **`ClientToProxyConnection`** (`impl/`) — one per inbound client channel. Decodes HTTP requests, runs the filter chain, resolves/reuses a per-(host,port) `ProxyToServerConnection`, and writes responses back to the client. Also handles `CONNECT` (tunnel vs. MITM branch) and proxy auth.
+- **`ProxyToServerConnection`** (`impl/`) — one per upstream target. Drives `ConnectionFlow` during setup, then proxies request/response chunks. Caches chained-proxy fallback state.
+- **`ConnectionFlow` / `ConnectionFlowStep`** (`impl/`) — ordered async step machine used to set up outbound connections: `ConnectChannel` → optional chained-proxy handshake (HTTP CONNECT / SOCKS4 / SOCKS5) → optional `StartTunneling` or `EncryptChannel` (MITM) → `RespondCONNECTSuccessful` → `MitmEncryptClientChannel`. Each step returns a Netty `Future` and advances via callbacks.
+- **`ProxyConnection`** (`impl/`) — abstract base for both connections; owns the `ConnectionState` transitions (`AWAITING_INITIAL`, `AWAITING_CHUNK`, `CONNECTING`, `HANDSHAKING`, `NEGOTIATING_CONNECT`, `AWAITING_PROXY_AUTHENTICATION`, `DISCONNECT_REQUESTED`, `DISCONNECTED`). Saturation callbacks implement backpressure (pause client reads when any upstream is saturated, and vice versa).
+- **`HttpFilters` / `HttpFiltersSource`** (top-level package) — primary extension point. A new `HttpFilters` instance is created per request via `filterRequest(...)`. Callback order (request → server connect → response) is documented in `LittleProxy_Request_Handling_Architecture.md`; returning a non-null `HttpResponse` from `clientToProxyRequest`/`proxyToServerRequest` short-circuits the request.
+- **`MitmManager` + `SslEngineSource`** — supply `SSLEngine`s for HTTPS interception. The in-tree `extras/SelfSignedMitmManager` is demo-grade; production setups typically plug in `LittleProxy-mitm` or the BrowserMob `mitm` module (see README).
+- **`ChainedProxyManager` / `ChainedProxy`** — per-request upstream proxy selection. Returning `ChainedProxyAdapter.FALLBACK_TO_DIRECT_CONNECTION` or an empty queue fails over to a direct connection; otherwise connection failures walk the queue.
+- **Netty pipelines** — client-side: `HAProxyMessageDecoder?` → `HttpRequestDecoder` → optional `HttpObjectAggregator` → monitors → `IdleStateHandler` → `ClientToProxyConnection`. Server-side mirrors this with `HttpRequestEncoder` / `HeadAwareHttpResponseDecoder` plus optional `GlobalTrafficShapingHandler` for throttling. When a `CONNECT` tunnel is established (without MITM), HTTP codecs are removed and data flows as raw bytes via `readRaw`/`write`.
+- **`extras/`** contains production-adjacent but optional implementations (`ActivityLogger`, `LogFormat`, `SelfSignedMitmManager`, `HAProxyMessageEncoder`, `TrustingTrustManager`). Core must not depend on `extras`.
+
+For diagrams and the full lifecycle of CONNECT/MITM/filter callbacks, see `LittleProxy_Request_Handling_Architecture.md`.
+
+## Testing Notes
+
+- Always use SLF4J (`org.slf4j.Logger`/`LoggerFactory`) for logging, not log4j directly — this is the logging API used throughout the codebase.
+- Tests are JUnit Jupiter + AssertJ + Mockito; Jetty and WireMock are used as real backends (do not mock them away).
+- Long-running or timing-sensitive tests are marked `@Tag("slow-test")` and excluded from the default/smoke profile.
+- Integration tests start real proxy instances on ephemeral ports; prefer extending `AbstractProxyTest` / `BaseProxyTest` / `BaseChainedProxyTest` rather than duplicating setup.
+- Test resources include keystores and `log4j.xml` under `src/test/resources/`.
+- Prefer `final` fields for test fixtures (e.g. `private final Foo foo = mock();`) over non-final fields assigned in `@BeforeEach`.
+- Prefer initializing fields inline at declaration over assigning them in `@BeforeEach`, when possible (e.g. only fall back to `@BeforeEach` when a value depends on another mock's stubbing or on a checked exception).
+- Never use `any()`/`any(Class)` in a `verify(...)` clause — pass the actual expected argument instead, e.g. `verify(throwingTracker).serverDisconnected(fullFlowContext, hostAddress);` not `verify(throwingTracker).serverDisconnected(any(), any());`. `any()` in `verify` only checks that some call happened, not that it happened with the right arguments.
+- Avoid reflection to reach a private field/method from a test. Best: test through the class's public API. If that's not practical, widen the member to package-private (test class lives in the same package) rather than reaching in via reflection — less magic, and the IDE/compiler catch renames.
+
+## Release
+
+Release steps (version bumps in `pom.xml` + `README.md`, `deploy.bash`, tag, publish on Sonatype Central) are documented in `CONTRIBUTING.md`. Do not bump versions unless a release is being cut.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..25571393
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,2 @@
+# CLAUDE.md
+@AGENTS.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..de47ecdc
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+
+Contribution guide for LittleProxy
+
+## Project scope
+
+LittleProxy is intended to be a LITTLE library, not a kitchen-sink proxy with every conceivable feature.
+Before starting work on a new feature, please discuss it first in
+[GitHub Discussions](https://github.com/LittleProxy/LittleProxy/discussions) or as a
+[GitHub Issue](https://github.com/LittleProxy/LittleProxy/issues), so we can agree whether it fits the
+project's scope before you invest time implementing it.
+
+If you need a richer feature set, consider [BrowserUp Proxy](https://github.com/valfirst/browserup-proxy),
+which is built on top of LittleProxy and provides more functionality.
+
+## How to start
+
+ git clone git@github.com:LittleProxy/LittleProxy.git
+ cd LittleProxy
+ mvn test
+
+## Release
+
+* Update the release notes (file RELEASE_NOTES.md)
+* Change version in the pom.xml file (e.g. "2.4.5-SNAPSHOT" -> "2.4.5")
+* Change version in README.md (e.g. "2.4.4" -> "2.4.5")
+* Run `mvn clean install`
+* Run `deploy.bash` (takes a while, showing "Waiting until Deployment **** is published")
+* Log into https://central.sonatype.com/publishing/deployments and ensure that the deployment status is "Published".
+* Commit the changes (e.g. with message "release LittleProxy 2.4.5") and make sure the CI build passes
+* Run `git tag v2.4.5 && git push --tags`
+* Create a new release in GitHub (go to `https://github.com/LittleProxy/LittleProxy/releases/new`)
+* Update the version in the pom.xml file to the next SNAPSHOT version (e.g. "2.4.5" -> "2.4.6-SNAPSHOT").
+* Commit the pom.xml change (e.g. with commit message "working on LittleProxy 2.4.6")
+* Announce the release in https://groups.google.com/forum/#!forum/littleproxy2
+
+## How-to
+
+### Check the built JAR target version
+> mvn clean package -DskipTests
+> javap -verbose -classpath target/littleproxy-*SNAPSHOT.jar org.littleshoot.proxy.ProxyAuthenticator | grep "major"
+
+* major version: 55 -> Java 11
+* major version: 61 -> Java 17
\ No newline at end of file
diff --git a/LittleProxy_Request_Handling_Architecture.md b/LittleProxy_Request_Handling_Architecture.md
new file mode 100644
index 00000000..e699a3f3
--- /dev/null
+++ b/LittleProxy_Request_Handling_Architecture.md
@@ -0,0 +1,551 @@
+# LittleProxy Request Handling Architecture
+
+This document explains with detailed diagrams how LittleProxy handles HTTP/HTTPS requests.
+
+## Architecture Overview
+
+```mermaid
+flowchart TB
+ subgraph Client["🖥️ Client (Browser)"]
+ C[Client Application]
+ end
+
+ subgraph LittleProxy["🔧 LittleProxy Server"]
+ direction TB
+
+ subgraph ServerGroup["ServerGroup (Thread Pools)"]
+ CAT["ClientToProxy Acceptor"]
+ CWT["ClientToProxy Worker"]
+ SWT["ProxyToServer Worker"]
+ end
+
+ subgraph ClientToProxy["ClientToProxyConnection"]
+ CP_PIPELINE["HTTP Pipeline Decoder → Encoder"]
+ CP_HANDLER["Main Handler"]
+ AUTH["Authentication"]
+ end
+
+ subgraph ProxyToServer["ProxyToServerConnection"]
+ PS_PIPELINE["HTTP Pipeline Encoder → Decoder"]
+ PS_HANDLER["Main Handler"]
+ CONN_FLOW["ConnectionFlow"]
+ end
+
+ subgraph Filters["🎛️ HttpFilters"]
+ F1["clientToProxyRequest()"]
+ F2["proxyToServerRequest()"]
+ F3["serverToProxyResponse()"]
+ F4["proxyToClientResponse()"]
+ end
+ end
+
+ subgraph Upstream["🌐 Upstream"]
+ direction TB
+ CHAINED["Chained Proxy (Optional)"]
+ TARGET["Target Server"]
+ end
+
+ C -->|"HTTP/HTTPS Request"| CAT
+ CAT --> CWT
+ CWT --> ClientToProxy
+ CP_PIPELINE --> CP_HANDLER
+ CP_HANDLER --> F1
+ F1 -->|"Short-circuit?"| CP_HANDLER
+ F1 -->|"Continue"| F2
+ F2 --> ProxyToServer
+ ProxyToServer --> CONN_FLOW
+ CONN_FLOW -->|"Connect + SSL/SOCKS"| PS_PIPELINE
+ PS_PIPELINE --> PS_HANDLER
+ PS_HANDLER -->|"HTTP Proxy"| CHAINED
+ PS_HANDLER -->|"Direct"| TARGET
+ CHAINED --> TARGET
+ TARGET -->|"Response"| PS_HANDLER
+ PS_HANDLER --> F3
+ F3 --> CP_HANDLER
+ CP_HANDLER --> F4
+ F4 -->|"Final Response"| C
+```
+
+## Main Class Diagram
+
+```mermaid
+classDiagram
+ class ProxyConnection~I~ {
+ <>
+ -ConnectionState currentState
+ -boolean tunneling
+ -SSLEngine sslEngine
+ +read(Object msg)
+ +write(Object msg)
+ +connected()
+ +disconnected()
+ +encrypt(SSLEngine)
+ +become(ConnectionState)
+ }
+
+ class ClientToProxyConnection {
+ -Map~String,ProxyToServerConnection~ serverConnections
+ -HttpFilters currentFilters
+ -HttpRequest currentRequest
+ -boolean mitming
+ +readHTTPInitial(HttpRequest)
+ +readHTTPChunk(HttpContent)
+ +respond(ProxyToServerConnection, HttpFilters, HttpRequest, HttpResponse, HttpObject)
+ +serverConnectionSucceeded()
+ +serverConnectionFailed()
+ }
+
+ class ProxyToServerConnection {
+ -ClientToProxyConnection clientConnection
+ -ChainedProxy chainedProxy
+ -ConnectionFlow connectionFlow
+ -Queue~ChainedProxy~ availableChainedProxies
+ -HttpRequest initialRequest
+ +readHTTPInitial(HttpResponse)
+ +write(Object, HttpFilters)
+ +connectionSucceeded()
+ +connectionFailed()
+ +initializeConnectionFlow()
+ }
+
+ class ConnectionFlow {
+ -Deque~ConnectionFlowStep~ steps
+ -ConnectionFlowStep currentStep
+ +start()
+ +advance()
+ +succeed()
+ +fail()
+ }
+
+ class ConnectionFlowStep~T~ {
+ <>
+ -ProxyConnection connection
+ -ConnectionState state
+ +execute() Future
+ +onSuccess(ConnectionFlow)
+ +read(ConnectionFlow, Object)
+ }
+
+ class DefaultHttpProxyServer {
+ -ServerGroup serverGroup
+ -HttpFiltersSource filtersSource
+ -MitmManager mitmManager
+ -ChainedProxyManager chainProxyManager
+ -ProxyAuthenticator proxyAuthenticator
+ +start()
+ +stop()
+ }
+
+ class HttpFilters {
+ <>
+ +clientToProxyRequest(HttpObject)
+ +proxyToServerRequest(HttpObject)
+ +serverToProxyResponse(HttpObject)
+ +proxyToClientResponse(HttpObject)
+ }
+
+ ProxyConnection <|-- ClientToProxyConnection
+ ProxyConnection <|-- ProxyToServerConnection
+ ClientToProxyConnection "1" --> "0..*" ProxyToServerConnection : manages
+ ProxyToServerConnection --> ConnectionFlow : uses
+ ConnectionFlow "1" --> "0..*" ConnectionFlowStep : contains
+ ClientToProxyConnection --> HttpFilters : applies
+ ProxyToServerConnection --> HttpFilters : applies
+ DefaultHttpProxyServer --> ClientToProxyConnection : creates
+ DefaultHttpProxyServer --> HttpFilters : provides
+```
+
+## HTTP Request Lifecycle (Non-CONNECT)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor Client
+ participant C2P as ClientToProxyConnection
+ participant Filters as HttpFilters
+ participant P2S as ProxyToServerConnection
+ participant CF as ConnectionFlow
+ participant Target as Target Server
+
+ Note over Client,Target: Standard HTTP Request (GET/POST/...)
+
+ Client->>C2P: HTTP Request
+ activate C2P
+
+ C2P->>Filters: clientToProxyRequest(request)
+ alt Short-circuit response
+ Filters-->>C2P: HttpResponse (short-circuit)
+ C2P->>C2P: respondWithShortCircuitResponse()
+ C2P-->>Client: Filtered Response
+ else Continue processing
+ Filters-->>C2P: null (continue)
+
+ C2P->>C2P: identifyHostAndPort()
+ C2P->>C2P: find/reuse ProxyToServerConnection
+
+ C2P->>Filters: proxyToServerRequest(request)
+ Filters-->>C2P: null (continue)
+
+ C2P->>P2S: write(request, filters)
+ activate P2S
+
+ alt Existing connection
+ P2S->>Target: Send request
+ else New connection
+ P2S->>CF: initializeConnectionFlow()
+ CF->>CF: start() → ConnectChannel
+ CF->>Target: TCP Connection
+ CF->>CF: succeed()
+ CF-->>P2S: connectionSucceeded()
+ P2S->>Target: Send request
+ end
+
+ P2S->>Filters: proxyToServerRequestSending()
+ P2S->>Filters: proxyToServerRequestSent()
+ deactivate P2S
+
+ Target-->>P2S: HTTP Response
+ activate P2S
+ P2S->>Filters: serverToProxyResponseReceiving()
+ P2S->>Filters: serverToProxyResponse(response)
+ Filters-->>P2S: modified response
+
+ P2S->>C2P: respond(filters, request, response, object)
+ deactivate P2S
+
+ C2P->>Filters: proxyToClientResponse(response)
+ Filters-->>C2P: final response
+ C2P->>C2P: modifyResponseHeaders()
+ C2P-->>Client: Final Response
+ deactivate C2P
+
+ P2S->>Filters: serverToProxyResponseReceived()
+ end
+```
+
+## CONNECT Request Lifecycle (HTTPS Tunneling)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor Client
+ participant C2P as ClientToProxyConnection
+ participant P2S as ProxyToServerConnection
+ participant CF as ConnectionFlow
+ participant Target as Target Server
+
+ Note over Client,Target: HTTPS Tunneling (without MITM)
+
+ Client->>C2P: CONNECT host:443
+ activate C2P
+ C2P->>C2P: doReadHTTPInitial()
+ C2P->>P2S: create() + write(connectRequest)
+ activate P2S
+
+ P2S->>CF: initializeConnectionFlow()
+ CF->>CF: start()
+
+ Note right of CF: ConnectionFlow Steps
+ CF->>Target: 1. ConnectChannel (TCP)
+ CF->>Target: 2. StartTunneling (tunnel mode)
+
+ CF-->>C2P: RespondCONNECTSuccessful
+ C2P-->>Client: 200 Connection established
+
+ CF->>C2P: StartTunneling
+ CF->>CF: succeed()
+ CF-->>P2S: connectionSucceeded()
+ deactivate P2S
+
+ Note over Client,Target: Active Tunnel - raw data
+
+ Client->>C2P: Raw SSL/TLS data
+ C2P->>P2S: readRaw() → write()
+ P2S->>Target: Raw SSL/TLS data
+
+ Target-->>P2S: Raw SSL/TLS data
+ P2S->>C2P: readRaw() → write()
+ C2P-->>Client: Raw SSL/TLS data
+
+ deactivate C2P
+```
+
+## Connection Flow (with MITM)
+
+```mermaid
+flowchart TB
+ subgraph "ConnectionFlow Steps"
+ START(["Start"]) --> CONNECT["1️⃣ ConnectChannel TCP Connection"]
+ CONNECT --> CHAIN_PROXY{"Chained Proxy ?"}
+
+ CHAIN_PROXY -->|"Yes - HTTP"| HTTP_PROXY["HTTPCONNECTWithChainedProxy"]
+ CHAIN_PROXY -->|"Yes - SOCKS4"| SOCKS4["SOCKS4CONNECTWithChainedProxy"]
+ CHAIN_PROXY -->|"Yes - SOCKS5"| SOCKS5["SOCKS5InitialRequest → SOCKS5SendPasswordCredentials → SOCKS5CONNECTRequest"]
+ CHAIN_PROXY -->|"No"| CHECK_CONNECT{"CONNECT Request ?"}
+
+ HTTP_PROXY --> CHECK_CONNECT
+ SOCKS4 --> CHECK_CONNECT
+ SOCKS5 --> CHECK_CONNECT
+
+ CHECK_CONNECT -->|"No"| END_SUCCESS(["Success AWAITING_INITIAL"])
+
+ CHECK_CONNECT -->|"Yes"| MITM_CHECK{"MITM Enabled ?"}
+
+ MITM_CHECK -->|"Yes"| ENCRYPT_SERVER["EncryptChannel (SSL to server)"]
+ ENCRYPT_SERVER --> RESPOND_OK1["RespondCONNECTSuccessful 200 to client"]
+ RESPOND_OK1 --> ENCRYPT_CLIENT["MitmEncryptClientChannel (SSL to client)"]
+ ENCRYPT_CLIENT --> END_MITM(["MITM Success AWAITING_INITIAL"])
+
+ MITM_CHECK -->|"No"| TUNNEL_SERVER["StartTunneling (server side)"]
+ TUNNEL_SERVER --> RESPOND_OK2["RespondCONNECTSuccessful 200 to client"]
+ RESPOND_OK2 --> TUNNEL_CLIENT["StartTunneling (client side)"]
+ TUNNEL_CLIENT --> END_TUNNEL(["Tunnel Success Raw bytes mode"])
+ end
+
+ style START fill:#90EE90
+ style END_SUCCESS fill:#90EE90
+ style END_MITM fill:#FFD700
+ style END_TUNNEL fill:#87CEEB
+```
+
+## Netty Pipeline - Client Side (Inbound)
+
+```mermaid
+flowchart LR
+ subgraph "ClientToProxy Pipeline"
+ direction LR
+ IN["📥 Inbound"]
+ OUT["📤 Outbound"]
+
+ IN --> BYTES_READ["bytesReadMonitor 📊 Read stats"]
+ BYTES_READ --> BYTES_WRITE["bytesWrittenMonitor 📊 Write stats"]
+ BYTES_WRITE --> ENCODER["HttpResponseEncoder 📤 Encode responses"]
+
+ PROXY_DEC["HAProxyMessageDecoder 🌐 Proxy Protocol"]
+ DECODER["HttpRequestDecoder 📥 Decode requests"]
+ AGG["HttpObjectAggregator 📦 Buffering (opt)"]
+ REQ_MON["requestReadMonitor 📊 Request stats"]
+ RES_MON["responseWrittenMonitor 📊 Response stats"]
+ IDLE["IdleStateHandler ⏱️ Timeout"]
+ HANDLER["ClientToProxyConnection 🎯 Main handler"]
+
+ ENCODER --> PROXY_DEC
+ PROXY_DEC --> DECODER
+ DECODER --> AGG
+ AGG --> REQ_MON
+ REQ_MON --> RES_MON
+ RES_MON --> IDLE
+ IDLE --> HANDLER
+ HANDLER --> OUT
+ end
+
+ style HANDLER fill:#FFD700,stroke:#FF8C00,stroke-width:3px
+```
+
+## Netty Pipeline - Server Side (Outbound)
+
+```mermaid
+flowchart LR
+ subgraph "ProxyToServer Pipeline"
+ direction LR
+ IN["📥 Inbound"]
+ OUT["📤 Outbound"]
+
+ IN --> BYTES_READ["bytesReadMonitor 📊 Read stats"]
+ BYTES_READ --> BYTES_WRITE["bytesWrittenMonitor 📊 Write stats"]
+ BYTES_WRITE --> TRAFFIC["GlobalTrafficShapingHandler 🚦 Throttling (opt)"]
+
+ TRAFFIC --> PROXY_ENC["HAProxyMessageEncoder 🌐 Proxy Protocol"]
+ PROXY_ENC --> ENCODER["HttpRequestEncoder 📤 Encode requests"]
+ DECODER["HeadAwareHttpResponseDecoder 📥 Decode responses"]
+ AGG["HttpObjectAggregator 📦 Buffering (opt)"]
+ RES_MON["responseReadMonitor 📊 Response stats"]
+ REQ_MON["requestWrittenMonitor 📊 Request stats"]
+ IDLE["IdleStateHandler ⏱️ Timeout"]
+ HANDLER["ProxyToServerConnection 🎯 Main handler"]
+
+ ENCODER --> DECODER
+ DECODER --> AGG
+ AGG --> RES_MON
+ RES_MON --> REQ_MON
+ REQ_MON --> IDLE
+ IDLE --> HANDLER
+ HANDLER --> OUT
+ end
+
+ style HANDLER fill:#87CEEB,stroke:#4682B4,stroke-width:3px
+```
+
+## Connection State Machine
+
+```mermaid
+stateDiagram-v2
+ [*] --> AWAITING_INITIAL: Connection accepted
+
+ AWAITING_INITIAL --> AWAITING_CHUNK: Chunked request received
+ AWAITING_CHUNK --> AWAITING_CHUNK: Chunk received
+ AWAITING_CHUNK --> AWAITING_INITIAL: LastHttpContent received
+
+ AWAITING_INITIAL --> CONNECTING: New server connection
+ CONNECTING --> AWAITING_CONNECT_OK: TCP connection OK
+ CONNECTING --> DISCONNECTED: Connection failed
+
+ AWAITING_CONNECT_OK --> HANDSHAKING: SSL/SOCKS/CONNECT in progress
+ AWAITING_CONNECT_OK --> AWAITING_INITIAL: Connect OK (no TLS)
+
+ HANDSHAKING --> AWAITING_INITIAL: SSL handshake succeeded
+ HANDSHAKING --> DISCONNECTED: Handshake failed
+
+ AWAITING_INITIAL --> NEGOTIATING_CONNECT: CONNECT request received
+ NEGOTIATING_CONNECT --> AWAITING_INITIAL: Tunnel established
+
+ AWAITING_INITIAL --> AWAITING_PROXY_AUTHENTICATION: Auth required
+ AWAITING_PROXY_AUTHENTICATION --> AWAITING_INITIAL: Auth succeeded
+ AWAITING_PROXY_AUTHENTICATION --> DISCONNECT_REQUESTED: Auth failed
+
+ AWAITING_INITIAL --> DISCONNECT_REQUESTED: Close requested
+ AWAITING_CHUNK --> DISCONNECT_REQUESTED: Close requested
+
+ DISCONNECT_REQUESTED --> DISCONNECTED: Disconnect
+ DISCONNECTED --> [*]: End
+```
+
+## Filter Chain (HttpFilters)
+
+```mermaid
+flowchart TB
+ subgraph "Filter Chain Execution Order"
+ direction TB
+
+ REQ["📝 Client Request"] --> F1["1. clientToProxyRequest() 📍 Initial interception Short-circuit possible"]
+
+ F1 -->|"Short-circuit"| RESP_FINAL["🔚 Client Response"]
+ F1 -->|"Continue"| F2["2. proxyToServerRequest() 📍 Modify before sending to server Short-circuit possible"]
+
+ F2 -->|"Short-circuit"| RESP_FINAL
+ F2 -->|"Continue"| CONN["🔌 Connect to Server"]
+
+ CONN --> F3["3. serverToProxyResponse() 📍 Modify server response Return null = disconnect"]
+
+ F3 -->|"null (force disconnect)"| DISCONNECT["❌ Force disconnect"]
+ F3 -->|"Continue"| F4["4. proxyToClientResponse() 📍 Final response modification Return null = disconnect"]
+
+ F4 -->|"null (force disconnect)"| DISCONNECT
+ F4 -->|"Continue"| RESP_FINAL
+ end
+
+ subgraph "Callback Notifications (Order)"
+ direction TB
+ N1["clientToProxyRequest"] --> N2["proxyToServerConnectionQueued"]
+ N2 --> N3["proxyToServerResolutionStarted"]
+ N3 --> N4["proxyToServerResolutionSucceeded/Failed"]
+ N4 --> N5["proxyToServerRequest"]
+ N5 --> N6["proxyToServerConnectionStarted"]
+ N6 --> N7["proxyToServerConnectionSSLHandshakeStarted (if HTTPS)"]
+ N7 --> N8["proxyToServerConnectionSucceeded/Failed"]
+ N8 --> N9["proxyToServerRequestSending"]
+ N9 --> N10["proxyToServerRequestSent"]
+ N10 --> N11["serverToProxyResponseReceiving"]
+ N11 --> N12["serverToProxyResponse"]
+ N12 --> N13["serverToProxyResponseReceived"]
+ N13 --> N14["proxyToClientResponse"]
+ end
+
+ style F1 fill:#FFD700
+ style F2 fill:#FFD700
+ style F3 fill:#87CEEB
+ style F4 fill:#87CEEB
+```
+
+## Chained Proxy Management
+
+```mermaid
+flowchart TB
+ subgraph "ChainedProxy Resolution"
+ START(["New request"]) --> RESOLVE["ChainedProxyManager.lookupChainedProxies()"]
+
+ RESOLVE --> HAS_PROXY{"Proxies available ?"}
+ HAS_PROXY -->|"No"| NULL["Returns null 502 Bad Gateway"]
+ HAS_PROXY -->|"Yes"| QUEUE["Proxy queue ConcurrentLinkedQueue"]
+
+ QUEUE --> CREATE["Create ProxyToServerConnection with first proxy"]
+ CREATE --> CONNECT["Attempt connection"]
+
+ CONNECT --> SUCCESS{"Connection OK ?"}
+ SUCCESS -->|"Yes"| USE["Use this proxy"]
+ SUCCESS -->|"No"| FALLBACK["ChainedProxy.connectionFailed()"]
+
+ FALLBACK --> NEXT_PROXY{"More proxies in queue ?"}
+ NEXT_PROXY -->|"Yes"| NEXT["Take next proxy"]
+ NEXT --> CONNECT
+
+ NEXT_PROXY -->|"No"| FALLBACK_DIRECT["FALLBACK_TO_DIRECT_CONNECTION"]
+ FALLBACK_DIRECT --> DIRECT["Direct connection (no proxy)"]
+ DIRECT --> DIRECT_SUCCESS{"Direct OK ?"}
+ DIRECT_SUCCESS -->|"Yes"| USE
+ DIRECT_SUCCESS -->|"No"| FAIL["502 Bad Gateway"]
+ end
+
+ subgraph "Proxy Types"
+ HTTP["HTTP Proxy → CONNECT request"]
+ SOCKS4["SOCKS4 Proxy → CONNECT command"]
+ SOCKS5["SOCKS5 Proxy → Auth + CONNECT command"]
+ end
+
+ style USE fill:#90EE90
+ style FAIL fill:#FF6B6B
+ style NULL fill:#FF6B6B
+```
+
+## Backpressure Management
+
+```mermaid
+flowchart TB
+ subgraph "Backpressure Management"
+ C2P_SAT["ClientToProxy saturated"] --> STOP_ALL["Stop reading ALL ProxyToServer connections"]
+
+ P2S_SAT["ProxyToServer saturated"] --> STOP_CLIENT["Stop reading ClientToProxy"]
+
+ C2P_WRITE["ClientToProxy writable"] --> CHECK_ALL{"All servers writable ?"}
+ CHECK_ALL -->|"Yes"| RESUME_CLIENT["Resume reading ClientToProxy"]
+ CHECK_ALL -->|"No"| WAIT["Wait"]
+
+ P2S_WRITE["ProxyToServer writable"] --> CHECK_SAT{"All servers non-saturated ?"}
+ CHECK_SAT -->|"Yes"| RESUME_ALL["Resume reading ALL ProxyToServer + Client"]
+ CHECK_SAT -->|"No"| KEEP_WAIT["Keep waiting"]
+ end
+
+ style STOP_ALL fill:#FFD700
+ style STOP_CLIENT fill:#FFD700
+ style RESUME_CLIENT fill:#90EE90
+ style RESUME_ALL fill:#90EE90
+```
+
+## Key Components Summary
+
+| Component | Role | File |
+|-----------|------|------|
+| **DefaultHttpProxyServer** | Entry point, bootstrap, configuration | `DefaultHttpProxyServer.java` |
+| **ClientToProxyConnection** | Manages incoming client connections | `ClientToProxyConnection.java` |
+| **ProxyToServerConnection** | Manages outgoing server connections | `ProxyToServerConnection.java` |
+| **ConnectionFlow** | Orchestration of connection steps | `ConnectionFlow.java` |
+| **ConnectionFlowStep** | Individual step in the connection flow | `ConnectionFlowStep.java` |
+| **HttpFilters** | Interface for filtering/modifying requests/responses | `HttpFilters.java` |
+| **ProxyConnection** | Abstract base class for connections | `ProxyConnection.java` |
+| **ServerGroup** | Netty thread pool management | `ServerGroup.java` |
+
+## Key Architecture Points
+
+1. **Separation of Concerns**: The [`ClientToProxyConnection`](src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java:88) class manages the client side, while [`ProxyToServerConnection`](src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java:104) manages the server side.
+
+2. **Connection Reuse**: Only one [`ProxyToServerConnection`](src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java:104) per host:port is maintained and reused for HTTP requests.
+
+3. **Tunnel Mode**: For CONNECT requests (HTTPS), HTTP encoders/decoders are removed and data passes through in raw bytes mode.
+
+4. **MITM (Man-In-The-Middle)**: Allows decrypting HTTPS traffic by acting as an SSL server on the client side and SSL client on the server side.
+
+5. **Chained Proxies**: Support for HTTP, SOCKS4, and SOCKS5 with automatic fallback mechanism.
+
+6. **Filters**: Extensible filter chain allowing modification of requests/responses at different stages.
+
+7. **Backpressure**: Saturation management mechanism to prevent memory overload.
diff --git a/Netty_4_Upgrade_Notes.md b/Netty_4_Upgrade_Notes.md
index 53fab01d..414fc93f 100644
--- a/Netty_4_Upgrade_Notes.md
+++ b/Netty_4_Upgrade_Notes.md
@@ -21,7 +21,7 @@ Relevant Changes
* DefaultChannelGroup?
-* SimpleChannelUpstreamHandler -> SimpleChanneInboundHandler
+* SimpleChannelUpstreamHandler -> SimpleChannelInboundHandler
* InterestOps is gone - what does this mean to setReadable() and
channelInterestChanged() ?
diff --git a/PERFORMANCE_AND_LOGGING.md b/PERFORMANCE_AND_LOGGING.md
new file mode 100644
index 00000000..2a5584a3
--- /dev/null
+++ b/PERFORMANCE_AND_LOGGING.md
@@ -0,0 +1,666 @@
+# LittleProxy Performance and Logging Guide
+
+This guide covers logging performance optimization techniques and configuration options for LittleProxy.
+
+## Table of Contents
+
+- [Logging Modes](#logging-modes)
+ - [Synchronous Logging](#synchronous-logging)
+ - [Asynchronous Logging](#asynchronous-logging)
+- [Performance Considerations](#performance-considerations)
+- [Logging Configuration](#logging-configuration)
+- [Log Filtering and Rate Limiting](#log-filtering-and-rate-limiting)
+ - [BurstFilter Configuration](#burstfilter-configuration)
+ - [Custom Filter Implementation](#custom-filter-implementation)
+- [Activity Logging](#activity-logging)
+- [Best Practices](#best-practices)
+- [Troubleshooting](#troubleshooting)
+
+## Logging Modes
+
+### Synchronous Logging
+
+**Default Mode**: Synchronous logging is the default behavior and provides reliable logging with immediate disk writes.
+
+**Characteristics:**
+- ✅ Simple and reliable
+- ✅ Logs are immediately written to disk
+- ❌ Higher I/O overhead
+- ❌ Can impact proxy performance under heavy load
+- ❌ Slower response times during peak traffic
+
+**Configuration File**: `src/main/resources/littleproxy_default_log4j2.xml`
+
+**Appender Type**: `RollingFile` with immediate flush
+
+**Example Usage:**
+```bash
+./run.bash --server --config ./config/littleproxy.properties --port 9092
+```
+
+### Asynchronous Logging
+
+**Performance Mode**: Asynchronous logging significantly improves performance by buffering log events and writing them in batches.
+
+**Characteristics:**
+- ✅ Much lower I/O overhead
+- ✅ Better performance under heavy load
+- ✅ Reduced disk I/O operations
+- ✅ Configurable buffer sizes
+- ❌ Slight risk of log loss on JVM crash
+- ❌ Logs may be delayed during shutdown
+
+**Configuration File**: `src/main/resources/littleproxy_async_log4j2.xml`
+
+**Appender Type**: `RollingRandomAccessFile` with `immediateFlush="false"`
+
+**Async Features:**
+- `AsyncRoot` for root logger
+- `AsyncLogger` for all specific loggers
+- `includeLocation="true"` for better debugging
+
+**Performance Optimizations:**
+- **Buffer Size**: Configurable via Log4j2 system properties
+- **Batch Writing**: Logs are written in batches rather than individually
+- **Reduced I/O**: `immediateFlush="false"` reduces disk operations
+- **Larger Files**: 250MB file size vs 50MB in sync mode reduces rollover frequency
+
+**Example Usage:**
+```bash
+# Using the async_logging_default flag
+./run.bash --async_logging_default --server --config ./config/littleproxy.properties --port 9092
+
+# Direct Java command
+java -server -XX:+HeapDumpOnOutOfMemoryError -Xmx800m \
+ -jar ./target/littleproxy-2.9.1-littleproxy-shade.jar \
+ --server --config ./config/littleproxy.properties --port 9092 \
+ --log_config ./target/classes/littleproxy_async_log4j2.xml
+```
+
+## Performance Considerations
+
+### When to Use Synchronous Logging
+
+- **Development environments** where immediate log visibility is important
+- **Debugging scenarios** where you need real-time log output
+- **Low-traffic productions** where performance impact is negligible
+- **Compliance requirements** that mandate immediate log persistence
+
+### When to Use Asynchronous Logging
+
+- **High-traffic productions** where performance is critical
+- **Load testing environments** to get accurate performance metrics
+- **Resource-constrained systems** where I/O reduction is needed
+- **Burst traffic scenarios** where logging can become a bottleneck
+
+### Performance Impact Comparison
+
+| Metric | Synchronous | Asynchronous | Improvement |
+|--------|-------------|--------------|-------------|
+| **Throughput** | Baseline | +30-50% | 1.3-1.5x |
+| **Latency** | Baseline | -40-60% | 0.4-0.6x |
+| **Disk I/O** | High | Low | 5-10x less |
+| **CPU Usage** | Moderate | Lower | 10-20% less |
+| **Memory Usage** | Low | Slightly higher | Buffer overhead |
+
+## Logging Configuration
+
+### Default Configuration (Synchronous)
+
+```xml
+
+
+
+ %d{ISO8601} %-5p [%t] %c{2} (%F:%L).%M() - %m%n
+
+
+
+
+
+
+```
+
+### Async Configuration
+
+```xml
+
+
+
+ %d{ISO8601} %-5p [%t] %c{2} (%F:%L).%M() - %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Advanced Configuration Options
+
+**Log4j2 System Properties:**
+
+```bash
+# Increase async logger ring buffer size (default: 262144)
+java -Dlog4j2.AsyncLogger.RingBufferSize=1048576 ...
+
+# Increase async logger queue size
+java -Dlog4j2.AsyncLoggerConfig.RingBufferSize=1048576 ...
+
+# Disable location tracking for better performance
+java -Dlog4j2.includeLocation=false ...
+```
+
+## Log Filtering and Rate Limiting
+
+### BurstFilter Configuration
+
+Log4j2 provides a `BurstFilter` that can limit the number of log events within a time period to prevent log flooding.
+
+**Example Configuration:**
+
+```xml
+
+
+
+
+
+
+
+
+
+```
+
+**BurstFilter Parameters:**
+
+- `level`: The log level to filter (INFO, DEBUG, WARN, etc.)
+- `rate`: Maximum number of log events per second
+- `maxBurst`: Maximum number of events allowed in a burst
+
+**Example Scenarios:**
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### Custom Filter Implementation
+
+For more sophisticated filtering, you can implement custom Log4j2 filters using Java or Groovy scripts.
+
+#### Java Custom Filter Example
+
+**Example Custom Filter:**
+
+```java
+package org.littleshoot.proxy.logging;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.filter.AbstractFilter;
+
+/**
+ * Custom filter to exclude specific request patterns
+ */
+public class RequestPatternFilter extends AbstractFilter {
+
+ private final String[] excludedPatterns;
+
+ public RequestPatternFilter(String[] excludedPatterns) {
+ this.excludedPatterns = excludedPatterns;
+ }
+
+ @Override
+ public Result filter(LogEvent event) {
+ String message = event.getMessage().getFormattedMessage();
+
+ // Check if message matches any excluded pattern
+ for (String pattern : excludedPatterns) {
+ if (message.contains(pattern)) {
+ return Result.DENY; // Exclude this log
+ }
+ }
+
+ return Result.NEUTRAL; // Allow this log
+ }
+
+ @Override
+ public Result filter(org.apache.logging.log4j.core.Logger logger, org.apache.logging.log4j.Level level,
+ org.apache.logging.log4j.Marker marker, String msg, Object... params) {
+ return filterLogEvent(level, marker, msg, params);
+ }
+
+ @Override
+ public Result filter(org.apache.logging.log4j.core.Logger logger, org.apache.logging.log4j.Level level,
+ org.apache.logging.log4j.Marker marker, Object msg, Throwable t) {
+ return filterLogEvent(level, marker, msg, t);
+ }
+
+ @Override
+ public Result filter(org.apache.logging.log4j.core.Logger logger, org.apache.logging.log4j.Level level,
+ org.apache.logging.log4j.Marker marker, Message msg, Throwable t) {
+ return filterLogEvent(level, marker, msg, t);
+ }
+
+ private Result filterLogEvent(org.apache.logging.log4j.Level level, org.apache.logging.log4j.Marker marker,
+ Object msg, Object... params) {
+ if (msg instanceof String message) {
+ for (String pattern : excludedPatterns) {
+ if (message.contains(pattern)) {
+ return Result.DENY;
+ }
+ }
+ }
+ return Result.NEUTRAL;
+ }
+}
+```
+
+#### Groovy Script Filter Example
+
+Log4j2 supports Groovy scripts for dynamic filtering without compilation. This is perfect for sampling, conditional logging, or complex logic.
+
+**Example: Sampling Filter (log only 10% of messages)**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Example: Conditional Filter (exclude health checks)**
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+**Example: Time-Based Filter (business hours only)**
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+**Groovy Script Filter Benefits:**
+
+- ✅ **No compilation needed**: Scripts are interpreted at runtime
+- ✅ **Dynamic logic**: Can use complex conditions and external data
+- ✅ **Easy to modify**: Change filtering logic without recompiling
+- ✅ **Powerful**: Access to full Groovy language features
+- ✅ **Performance**: Still efficient for most use cases
+
+**Script Filter Parameters:**
+
+- `onMatch`: What to do when script returns true (ACCEPT/DENY/NEUTRAL)
+- `onMismatch`: What to do when script returns false (ACCEPT/DENY/NEUTRAL)
+- `language`: Script language (groovy, javascript, etc.)
+
+**Available Variables in Script:**
+
+- `logEvent`: The current LogEvent object
+- `loggerName`: Name of the logger
+- `level`: Log level
+- `message`: Formatted message
+- `marker`: Marker (if any)
+- `throwable`: Exception (if any)
+
+**Performance Considerations:**
+
+- Script filters add some overhead compared to compiled filters
+- Use for complex logic that's hard to implement in Java
+- Test script performance before deploying to production
+- Consider caching results for repeated patterns
+
+**XML Configuration for Custom Filter:**
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### Filter Examples by Use Case
+
+**1. Exclude Health Check Requests:**
+```xml
+
+```
+
+**2. Limit Debug Logs in Production:**
+```xml
+
+```
+
+**3. Filter by Request Type:**
+```xml
+
+```
+
+**4. Rate Limiting for Specific Loggers:**
+```xml
+
+
+
+```
+
+## Activity Logging
+
+Activity logging in LittleProxy captures HTTP request/response details. This can be a significant performance factor.
+
+### Activity Log Formats
+
+**CLF (Common Log Format):**
+```bash
+--activity_log_format CLF
+```
+
+**ELF (Extended Log Format):**
+```bash
+--activity_log_format ELF
+```
+
+**JSON:**
+```bash
+--activity_log_format JSON
+```
+
+**SQUID:**
+```bash
+--activity_log_format SQUID
+```
+
+**W3C:**
+```bash
+--activity_log_format W3C
+```
+
+**LTSV (Labeled Tab-Separated Values):**
+```bash
+--activity_log_format LTSV
+```
+
+**CSV (Comma-Separated Values):**
+```bash
+--activity_log_format CSV
+```
+
+**HAPROXY:**
+```bash
+--activity_log_format HAPROXY
+```
+
+### Activity Logging Performance Impact
+
+| Format | Performance | Use Case |
+|--------|-------------|----------|
+| **CLF** | ⚡ Fastest | Production, high volume |
+| **ELF** | ⚡ Fast | Extended logging needs |
+| **JSON** | 🏃 Moderate | Analytics, structured logging |
+| **SQUID** | 🏃 Moderate | Squid proxy compatibility |
+| **W3C** | 🏃 Moderate | Web standards compliance |
+| **LTSV** | 🏃 Moderate | Machine-readable logs |
+| **CSV** | 🏃 Moderate | Spreadsheet analysis |
+| **HAPROXY** | 🏃 Moderate | HAProxy compatibility |
+
+### Activity Logging Best Practices
+
+1. **Disable in Development**: omit the `--activity_log_format` flag when not needed
+2. **Use CLF in Production**: Fastest format for high-volume scenarios
+3. **Sample Activity Logs**: Consider sampling (every Nth request)
+4. **Separate Activity Logs**: Use different files for access logs vs application logs
+
+**Example with Activity Logging:**
+```bash
+# Async logging with CLF activity format (best performance)
+./run.bash --async_logging_default --server --config ./config/littleproxy.properties \
+ --port 9092 --activity_log_format CLF
+
+# Sync logging with JSON activity format (structured logging)
+./run.bash --server --config ./config/littleproxy.properties \
+ --port 9092 --activity_log_format JSON
+```
+
+## Best Practices
+
+### Logging Configuration
+
+1. **Use Async for Production**: Always use `--async_logging_default` in production
+2. **Keep Sync for Development**: Use default sync logging during development
+3. **Monitor Log Growth**: Set appropriate file sizes and rotation policies
+4. **Test Configuration**: Validate Log4j2 configuration before deployment
+
+### Performance Optimization
+
+1. **Tune Buffer Sizes**: Adjust `log4j2.AsyncLogger.RingBufferSize` based on load
+2. **Limit Location Info**: Use `includeLocation="false"` for production
+3. **Filter Early**: Apply filters at the appender level
+4. **Use Appropriate Levels**: DEBUG for development, INFO for production
+
+### Monitoring and Maintenance
+
+1. **Monitor Log Files**: Check disk usage regularly
+2. **Rotate Logs**: Configure proper rotation policies
+3. **Archive Old Logs**: Implement log archiving strategy
+4. **Alert on Errors**: Set up monitoring for ERROR level logs
+
+## Troubleshooting
+
+### Common Issues and Solutions
+
+**Issue: No logs appearing with async mode**
+- **Solution**: Check that `littleproxy_async_log4j2.xml` is in the correct location
+- **Solution**: Verify file permissions on log directory
+- **Solution**: Check for Log4j2 configuration errors
+
+**Issue: High CPU usage with logging**
+- **Solution**: Switch to async logging mode
+- **Solution**: Reduce log level from DEBUG to INFO
+- **Solution**: Apply BurstFilter to limit log volume
+
+**Issue: Disk full due to logs**
+- **Solution**: Configure proper rotation policies
+- **Solution**: Increase file size limits
+- **Solution**: Implement log archiving
+
+**Issue: Log4j2 configuration errors**
+- **Solution**: Check XML syntax
+- **Solution**: Validate with `status="TRACE"` in configuration
+- **Solution**: Ensure all referenced appenders exist
+
+### Debugging Log4j2 Configuration
+
+Add debug output to Log4j2:
+
+```xml
+
+
+
+```
+
+**Debug Levels:**
+- `OFF`: No internal logging
+- `ERROR`: Only errors
+- `WARN`: Warnings and errors
+- `INFO`: Informational messages
+- `DEBUG`: Debug information
+- `TRACE`: Verbose debugging
+
+### Checking Async Logger Status
+
+```bash
+# Check async logger buffer status
+java -Dlog4j2.AsyncLoggerConfig.StatusLogger.level=INFO \
+ -jar ./target/littleproxy-2.9.1-littleproxy-shade.jar \
+ --server --config ./config/littleproxy.properties --port 9092
+```
+
+## Advanced Topics
+
+### Custom Appender Implementation
+
+For specialized logging needs, implement custom appenders:
+
+```java
+@Plugin(name = "CustomAppender", category = "Core", elementType = "appender", printObject = true)
+public class CustomAppender extends AbstractAppender {
+
+ protected CustomAppender(String name, Filter filter, Layout extends Serializable> layout) {
+ super(name, filter, layout);
+ }
+
+ @Override
+ public void append(LogEvent event) {
+ // Custom logging logic
+ byte[] bytes = getLayout().toByteArray(event);
+ // Send to custom destination (database, network, etc.)
+ }
+}
+```
+
+### Dynamic Log Level Adjustment
+
+Change log levels at runtime:
+
+```java
+// Get the logger context
+LoggerContext context = (LoggerContext) LogManager.getContext(false);
+Configuration config = context.getConfiguration();
+
+// Adjust log level
+LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
+loggerConfig.setLevel(Level.DEBUG);
+
+// Update configuration
+context.updateLoggers();
+```
+
+### Log Enrichment
+
+Add contextual information to logs:
+
+```java
+// Use ThreadContext to add contextual data
+ThreadContext.put("requestId", UUID.randomUUID().toString());
+ThreadContext.put("clientIp", clientAddress);
+
+try {
+ // Process request - logs will include context data
+ logger.info("Processing request");
+} finally {
+ ThreadContext.clear();
+}
+```
+
+**Pattern Layout with Context:**
+```xml
+
+```
+
+## Summary
+
+This guide provides comprehensive information on optimizing LittleProxy logging performance:
+
+- **Synchronous vs Asynchronous**: Choose based on your performance needs
+- **Configuration Options**: Default and async configurations provided
+- **Filtering**: BurstFilter and custom filters for rate limiting
+- **Activity Logging**: Format options and performance considerations
+- **Best Practices**: Production-ready recommendations
+- **Troubleshooting**: Common issues and solutions
+
+For most production environments, **asynchronous logging with CLF activity format** provides the best balance of performance and functionality:
+
+```bash
+./run.bash --async_logging_default --server --config ./config/littleproxy.properties \
+ --port 9092 --activity_log_format CLF
+```
\ No newline at end of file
diff --git a/README.md b/README.md
index 6e0eb4e5..2111d9ae 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,447 @@
-[](https://travis-ci.com/mrog/LittleProxy)
-[](https://depshield.github.io)
-
This is an updated fork of adamfisk's LittleProxy. The original project appears
-to have been abondoned. Because it's so incredibly useful, it's being brought
+to have been abandoned. Because it's so incredibly useful, it's being brought
back to life in this repository.
LittleProxy is a high performance HTTP proxy written in Java atop Trustin Lee's
excellent [Netty](http://netty.io) event-based networking library. It's quite
-stable, performs well, and is easy to integrate into your projects.
+stable, performs well, and is easy to integrate into your projects.
+
+# Usage
+
+## Command Line
-One option is to clone LittleProxy and run it from the command line. This is as simple as:
+One option is to clone LittleProxy and run it from the command line. This is as simple as running the following commands :
```
-$ git clone git@github.com:mrog/LittleProxy.git
+$ git clone git@github.com:LittleProxy/LittleProxy.git
$ cd LittleProxy
$ ./run.bash
```
-You can embed LittleProxy in your own projects through Maven with the following:
+### Options
+
+Multiple options can be passed to the script as arguments. The following options are supported :
+
+#### Config File
+
+This will start LittleProxy with the configuration (path relative to the working directory or absolute)
+specified in the given file.
+
+```bash
+$ ./run.bash --config path/to/config/littleproxy.properties
+```
+
+You can, for example, run the shell script at the root project directory as a server, pointing
+to the provided _littleproxy.properties_ file :
+
+```bash
+$ ./run.bash --server --config ./config/littleproxy.properties
+```
+
+##### config file description
+
+The config file is a properties file with the following properties :
+- `dnssec` : boolean value to enable/disable DNSSEC validation (default : `false`)
+- `transparent` : boolean value to enable/disable transparent proxy mode (default : `false`)
+- `idleConnectionTimeout` : integer value to set the idle connection timeout in seconds (default : `-1`, i.e. no timeout)
+- `connect_timeout` : integer value to set the connect timeout in seconds (default : `0`, i.e. no timeout)
+- `max_initial_line_length` : integer value to set the max initial line length in bytes (default : `8192`)
+- `max_header_size` : integer value to set the max header size in bytes (default : `16384`)
+- `max_chunk_size` : integer value to set the max chunk size in bytes (default : `16384`)
+- `server_connection_pool_type` : pool implementation used by the shared server connection pool (`CONCURRENT_MAP`) (default : `CONCURRENT_MAP`) -- only effective when `use_shared_server_connection_pool=true`
+- `max_total_connections` : integer value to set the maximum total pooled server connections (default : `200`) -- only effective when `use_shared_server_connection_pool=true`
+- `max_connections_per_host` : integer value to set the maximum pooled server connections per host:port (default : `10`) -- only effective when `use_shared_server_connection_pool=true`
+- `name` : string value to set the proxy server name (default : `LittleProxy`)
+- `address` : string value to set the proxy server address (default : `0.0.0.0:8080`)
+- `port` : integer value to set the proxy server port (default : `8080`)
+- `nic` : string value to set the network interface card (default : `0.0.0.0`)
+- `proxy_alias` : string value to set the proxy alias (default : hostname of the machine)
+- `allow_local_only` : boolean value to allow only local connections (default : `false`)
+- `authenticate_ssl_clients` : boolean value to enable/disable SSL client authentication (default : `false`)
+- `ssl_clients_trust_all_servers` : boolean value to trust all servers (default : `false`)
+- `ssl_clients_send_certs` : boolean value to send certificates (default : `false`)
+- `ssl_clients_key_store_file_path` : string value to set the key store file path (default : `null`)
+- `ssl_clients_key_store_alias` : string value to set the key store alias (default : `null`)
+- `ssl_clients_key_store_password` : string value to set the key store password (default : `null`)
+- `throttle_read_bytes_per_second` : integer value to set the throttle read bytes per second (default : `0`)
+- `throttle_write_bytes_per_second` : integer value to set the throttle write bytes per second (default : `0`)
+- `allow_requests_to_origin_server` : boolean value to allow requests to origin server (default : `false`)
+- `allow_proxy_protocol` : boolean value to allow proxy protocol (default : `false`)
+- `send_proxy_protocol` : boolean value to send proxy protocol header (default : `false`)
+- `activity_log_format` : string value to set the activity log format (CLF, ELF, JSON, LTSV, CSV, SQUID, HAPROXY) (default: disabled)
+
+Options set from the command line, override the ones set in the config file.
+
+> **Note**: For advanced logging configuration and performance optimization, see our [Performance and Logging Guide](PERFORMANCE_AND_LOGGING.md).
+
+##### littleproxy.properties Example
+
+````properties
+dnssec=true
+transparent=false
+idleConnectionTimeout=60
+connect_timeout=30
+max_initial_line_length=8192
+max_header_size=16384
+max_chunk_size=16384
+server_connection_pool_type=CONCURRENT_MAP
+max_total_connections=200
+max_connections_per_host=10
+name=LittleProxy
+address=192.168.1.100:8080
+port=8080
+nic=eth0
+proxy_alias=myproxy
+allow_local_only=false
+authenticate_ssl_clients=false
+ssl_clients_trust_all_servers=false
+ssl_clients_send_certs=false
+ssl_clients_key_store_file_path=/path/to/keystore.jks
+ssl_clients_key_store_alias=myalias
+ssl_clients_key_store_password=mypassword
+throttle_read_bytes_per_second=1024
+throttle_write_bytes_per_second=1024
+allow_requests_to_origin_server=true
+allow_proxy_protocol=true
+send_proxy_protocol=true
+activity_log_format=CLF
+````
+#### DNSSec
+
+This will start LittleProxy with DNSSEC validation enabled ; i.e, it will use secure DNS lookups for outbound
+connections.
+
+
+```bash
+$ ./run.bash --dnssec true
+```
+
+#### Log configuration file
+
+This will start LittleProxy with the specified log configuration file.
+Path of the log configuration file can be relative or absolute.
+
+If it is relative, it will be resolved relative to the current working directory :
+```bash
+$ ./run.bash --log_config ./log4j.xml
+```
+If it is absolute, it will be resolved as is :
+
+```bash
+$ ./run.bash --log_config /home/user/log4j.xml
+```
+
+#### Activity Log Format
+
+This will enable the activity tracker with the specified log format.
+Supported formats: `CLF`, `ELF`, `W3C`, `JSON`, `LTSV`, `CSV`, `SQUID`, `HAPROXY`.
+
+```bash
+$ ./run.bash --activity_log_format CLF
+```
+
+#### Port
+
+This will start LittleProxy on port `8080` by default.
+You can customize the port by passing a port number as an argument to the script :
+
+```bash
+$ ./run.bash --port 9090
+```
+
+#### NIC
+
+This will start LittleProxy on the default network interface. You can customize the network interface by passing
+a NIC name (`eth0` in the example below) as an argument to the script :
+
+```bash
+$ ./run.bash --nic eth0
+```
+
+#### MITM Manager
+
+If you pass this option, this will start LittleProxy with the default MITM manager (`SelfSignedMitmManager` implementation).
+It will generate a self-signed certificate for each domain you visit.
+
+```bash
+$ ./run.bash --mitm
+```
+#### name
+
+This will start LittleProxy with the specified name. This name will be used to name the threads.
+
+```bash
+$ ./run.bash --name MyProxy
+```
+
+#### address
+
+This will start LittleProxy binding to the specified address. IPV4,IPV6 and hostname addresses are supported.
+
+```bash
+$ ./run.bash --address 127.0.0.1:8080
+```
+#### nic
+
+This will start LittleProxy binding to the specified network interface.
+
+```bash
+$ ./run.bash --nic eth0
+```
+
+#### proxy_alias
+
+This will start LittleProxy with the specified proxy alias.
+The alias or pseudonym for this proxy, used when adding the `Via` header.
+
+```bash
+$ ./run.bash --proxy_alias MyProxy
+```
+
+#### allow_local_only
+
+This will start LittleProxy allowing only local connections (default is `false`).
+
+```bash
+$ ./run.bash --allow_local_only true
+```
+
+#### authenticate_ssl_clients
+
+This will start LittleProxy authenticating SSL clients (default is `false`).
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true```
+
+#### trust_all_servers
+
+This will start LittleProxy authenticating SSL clients and trusting all servers (default is `false`).
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true --trust_all_servers true
+```
+#### send_certs
+
+This will start LittleProxy authenticating SSL clients and sending certificates (default is `false`).
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true --send_certs true```
+
+#### ssl_client_keystore_path
+
+This will start LittleProxy authenticating SSL clients and using the specified keystore path.
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true --ssl_client_keystore_path /path/to/keystore`
+```
+#### ssl_client_keystore_alias
+
+This will start LittleProxy authenticating SSL clients and using the specified keystore alias.
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true --ssl_client_keystore_alias myalias```
+```
+#### ssl_client_keystore_password
+
+This will start LittleProxy authenticating SSL clients and using the specified keystore password.
+
+```bash
+$ ./run.bash --authenticate_ssl_clients true --ssl_client_keystore_password mypassword```
+
+#### throttle_read_bytes_per_second
+
+This will start LittleProxy throttling the read bytes per second.
+
+```bash
+$ ./run.bash --throttle_read_bytes_per_second 1024
+```
+
+#### throttle_write_bytes_per_second
+
+This will start LittleProxy throttling the write bytes per second.
+
+```bash
+$ ./run.bash --throttle_write_bytes_per_second 1024
+```
+
+#### allow_request_to_origin_server
+
+This will start LittleProxy allowing requests to the origin server.
+
+```bash
+$ ./run.bash --allow_request_to_origin_server true
+```
+
+#### allow_proxy_protocol
+
+This will start LittleProxy allowing the PROXY protocol.
+
+```bash
+$ ./run.bash --allow_proxy_protocol true
+```
+
+#### send_proxy_protocol
+
+This will start LittleProxy sending the PROXY protocol header.
+
+```bash
+$ ./run.bash --send_proxy_protocol true
+```
+
+#### client_to_proxy_worker_threads
+
+This will start LittleProxy with the specified number of client to proxy worker threads.
+
+```bash
+$ ./run.bash --client_to_proxy_worker_threads 10
+```
+
+#### proxy_to_server_worker_threads
+
+This will start LittleProxy with the specified number of proxy to server worker threads.
+
+```bash
+$ ./run.bash --proxy_to_server_worker_threads 10
+```
+
+#### acceptor_threads
+
+This will start LittleProxy with the specified number of acceptor threads.
+
+```bash
+$ ./run.bash --acceptor_threads 10
+```
+
+
+#### server
+
+This will start LittleProxy as a server, i.e it will not stop, until you stop the process running it (via a `kill`kill command).
+
+```bash
+$ ./run.bash --server
+```
+
+#### Help
+
+This will print the help message:
+
+```bash
+$ ./run.bash --help
+```
+
+## Embedding in your own projects
+
+You can embed LittleProxy in your own projects through Maven with the following :
```
- xyz.rogfam
+ io.github.littleproxylittleproxy
- 2.0.0-beta-5
+ 2.9.1
```
Or with Gradle like this
-`compile "xyz.rogfam:littleproxy:2.0.0-beta-5"`
+`implementation "io.github.littleproxy:littleproxy:2.9.1"`
Once you've included LittleProxy, you can start the server with the following:
```java
HttpProxyServer server =
- DefaultHttpProxyServer.bootstrap()
- .withPort(8080)
- .start();
+ DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .start();
+```
+
+### Shared server connection pool configuration
+
+LittleProxy supports pluggable shared server connection pools. This allows multiple client
+connections to reuse upstream connections and helps prevent connection explosion under load.
+
+```java
+HttpProxyServer server =
+ DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withSharedServerConnectionPool(true)
+ .withMaxConnections(500)
+ .withMaxConnectionsPerHost(50)
+ .withPoolIdleTimeout(Duration.ofSeconds(30))
+ .start();
```
+Available pool types:
+
+- `CONCURRENT_MAP`: lightweight default implementation
+
+#### Pool metrics
+
+Each server connection pool implementation exposes runtime metrics through `PoolMetrics`
+(`totalConnections`, `activeConnections`, `idleConnections`, `borrowCount`, `returnCount`,
+`evictionCount`, `validationFailureCount`).
+
+Implementation details:
+
+- `CONCURRENT_MAP`
+ - `totalConnections`: current number of tracked pooled server connections
+ - `activeConnections`: `totalConnections - idleConnections`
+ - `idleConnections`: connections currently waiting in the available queue
+ - `borrowCount` / `returnCount`: incremented on successful borrow/return
+ - `evictionCount`: incremented when idle-timeout eviction removes connections
+ - `validationFailureCount`: incremented when validation rejects a pooled connection
+
+These metrics are useful for capacity tuning, behavior validation during load tests, and
+troubleshooting connection reuse.
+
To intercept and manipulate HTTPS traffic, LittleProxy uses a man-in-the-middle (MITM) manager. LittleProxy's default
implementation (`SelfSignedMitmManager`) has a fairly limited feature set. For greater control over certificate impersonation,
-browser trust, the TLS handshake, and more, use a the LittleProxy-compatible MITM extension:
+browser trust, the TLS handshake, and more, use a LittleProxy-compatible MITM extension:
- [LittleProxy-mitm](https://github.com/ganskef/LittleProxy-mitm) - A LittleProxy MITM extension that aims to support every Java platform including Android
- [mitm](https://github.com/lightbody/browsermob-proxy/tree/master/mitm) - A LittleProxy MITM extension that supports elliptic curve cryptography and custom trust stores
-To filter HTTP traffic, you can add request and response filters using a
+To filter HTTP traffic, you can add request and response filters using a
`HttpFiltersSource(Adapter)`, for example:
```java
HttpProxyServer server =
- DefaultHttpProxyServer.bootstrap()
- .withPort(8080)
- .withFiltersSource(new HttpFiltersSourceAdapter() {
- public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
- return new HttpFiltersAdapter(originalRequest) {
- @Override
- public HttpResponse clientToProxyRequest(HttpObject httpObject) {
- // TODO: implement your filtering here
- return null;
- }
+ DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withFiltersSource(new HttpFiltersSourceAdapter() {
+ public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
+ return new HttpFiltersAdapter(originalRequest) {
+ @Override
+ public HttpResponse clientToProxyRequest(HttpObject httpObject) {
+ // TODO: implement your filtering here
+ return null;
+ }
- @Override
- public HttpObject serverToProxyResponse(HttpObject httpObject) {
- // TODO: implement your filtering here
- return httpObject;
+ @Override
+ public HttpObject serverToProxyResponse(HttpObject httpObject) {
+ // TODO: implement your filtering here
+ return httpObject;
+ }
+ };
}
- };
- }
- })
- .start();
+ })
+ .start();
```
-Please refer to the Javadoc of `org.littleshoot.proxy.HttpFilters` to see the
-methods you can use.
+Please refer to the Javadoc of `org.littleshoot.proxy.HttpFilters` to see the
+methods you can use.
-To enable aggregator and inflater you have to return a value greater than 0 in
-your `HttpFiltersSource#get(Request/Response)BufferSizeInBytes()` methods. This
-provides to you a `FullHttp(Request/Response)' with the complete content in your
-filter uncompressed. Otherwise you have to handle the chunks yourself.
+To enable aggregator and inflater you have to return a value greater than 0 in
+your `HttpFiltersSource#get(Request/Response)BufferSizeInBytes()` methods. This
+provides to you a `FullHttp(Request/Response)` with the complete content in your
+filter uncompressed. Otherwise, you have to handle the chunks yourself.
```java
@Override
- public int getMaximumResponseBufferSizeInBytes() {
- return 10 * 1024 * 1024;
- }
+public int getMaximumResponseBufferSizeInBytes() {
+ return 10 * 1024 * 1024;
+}
```
-This size limit applies to every connection. To disable aggregating by URL at
-*.iso or *dmg files for example, you can return in your filters source a filter
+This size limit applies to every connection. To disable aggregating by URL at
+*.iso or *dmg files for example, you can return in your filters source a filter
like this:
```java
@@ -106,39 +459,43 @@ return new HttpFiltersAdapter(originalRequest, serverCtx) {
}
};
```
-This enables huge downloads in an application, which regular handles size
-limited `FullHttpResponse`s to modify its content, HTML for example.
+This enables huge downloads in an application, which regular handles size
+limited `FullHttpResponse`s to modify its content, HTML for example.
-A proxy server like LittleProxy contains always a web server, too. If you get an
-URI without scheme, host and port in `originalRequest` it's a direct request to
-your proxy. You can return a `HttpFilters` implementation which answers
+A proxy server like LittleProxy contains always a web server, too. If you get a
+URI without scheme, host and port in `originalRequest` it's a direct request to
+your proxy. You can return a `HttpFilters` implementation which answers
responses with HTML content or redirects in `clientToProxyRequest` like this:
```java
+import java.nio.charset.StandardCharsets;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
public class AnswerRequestFilter extends HttpFiltersAdapter {
- private final String answer;
-
- public AnswerRequestFilter(HttpRequest originalRequest, String answer) {
- super(originalRequest, null);
- this.answer = answer;
- }
-
- @Override
- public HttpResponse clientToProxyRequest(HttpObject httpObject) {
- ByteBuf buffer = Unpooled.wrappedBuffer(answer.getBytes("UTF-8"));
- HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer);
- HttpHeaders.setContentLength(response, buffer.readableBytes());
- HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, "text/html");
- return response;
- }
+ private final String answer;
+
+ public AnswerRequestFilter(HttpRequest originalRequest, String answer) {
+ super(originalRequest, null);
+ this.answer = answer;
+ }
+
+ @Override
+ public HttpResponse clientToProxyRequest(HttpObject httpObject) {
+ ByteBuf buffer = Unpooled.wrappedBuffer(answer.getBytes(UTF_8));
+ HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer);
+ HttpHeaders.setContentLength(response, buffer.readableBytes());
+ HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, "text/html");
+ return response;
+ }
}
```
-On answering a redirect, you should add a Connection: close header, to avoid
+On answering a redirect, you should add a Connection: close header, to avoid
blocking behavior:
```java
HttpHeaders.setHeader(response, Names.CONNECTION, Values.CLOSE);
```
-With this trick, you can implement an UI to your application very easy.
+With this trick, you can implement a UI to your application very easy.
If you want to create additional proxy servers with similar configuration but
listening on different ports, you can clone an existing server. The cloned
@@ -149,8 +506,94 @@ stopped, all are stopped.
existingServer.clone().withPort(8081).start()
```
+
+### Logging Activity Tracker
+
+LittleProxy includes a `LoggingActivityTracker` that can log detailed information about each request and response handled by the proxy. It supports multiple standard log formats, which can be useful for integration with log analysis tools.
+
+To use it, wrap your functionality or simply add it to your server bootstrap:
+
+```java
+import org.littleshoot.proxy.extras.ActivityLogger;
+import org.littleshoot.proxy.extras.LoggingActivityTracker;
+import org.littleshoot.proxy.extras.LogFormat;
+
+// ...
+
+DefaultHttpProxyServer.bootstrap()
+ .
+
+withPort(8080)
+ .
+
+plusActivityTracker(new ActivityLogger(LogFormat.CLF)) // Use Common Log Format
+ .
+
+start();
+```
+
+#### Supported Log Formats
+
+The `LogFormat` enum provides several standard formats:
+
+* **`CLF` (Common Log Format)**: The standard NCSA Common log format.
+ * Example: `127.0.0.1 - - [24/Dec/2025:00:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234`
+* **`ELF` (Extended Log Format)**: Uses the NCSA Combined Log Format, which includes Referer and User-Agent.
+ * Example: `127.0.0.1 - - [date] "GET /..." 200 123 "http://referer" "Mozilla/5.0"`
+* **`W3C`**: A standard W3C Extended Log File Format (space-separated fields).
+ * Example: `2025-12-24 00:00:00 127.0.0.1 GET /index.html 200 1234 "Mozilla/5.0"`
+* **`JSON`**: Structured logging in JSON format, ideal for modern log aggregators (ELK, Splunk, etc.). Includes duration.
+ * Example: `{"timestamp":"...","client_ip":"127.0.0.1","method":"GET","duration":15,...}`
+* **`LTSV` (Labeled Tab-Separated Values)**: Efficient, human-readable, and machine-parsable format.
+ * Example: `time:2025-...\thost:127.0.0.1\tmethod:GET\t...`
+* **`CSV` (Comma-Separated Values)**: Standard CSV format for easy import into spreadsheets.
+ * Example: `"timestamp","127.0.0.1","GET",...`
+* **`SQUID`**: Squid native access log format. Useful for tools expecting Squid logs.
+* **`HAPROXY`**: A format mimicking HAProxy's HTTP logging, focusing on timing and status.
+
For examples of configuring logging, see [src/test/resources/log4j.xml](src/test/resources/log4j.xml).
+#### Customizing Logging Configuration
+
+You can customize the `log4j.xml` configuration to control how logs are output. This is particularly useful for separating access logs from system logs.
+
+**1. Standard Output (Default)**
+
+To print access logs to the console without standard Log4j prefixes (timestamps, thread names, etc.), use a specific appender for the tracker:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**2. Dedicated Access Log File**
+
+To write access logs to a separate file (e.g., `access.log`) and exclude them from the main log, use a `FileAppender`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
If you have questions, please visit our Google Group here:
https://groups.google.com/forum/#!forum/littleproxy2
@@ -159,7 +602,19 @@ https://groups.google.com/forum/#!forum/littleproxy2
accepting posts from new users. But it's still a great resource if you're
searching for older answers.)
-To subscribe, send an e-mail to [LittleProxy2+subscribe@googlegroups.com](mailto:LittleProxy2+subscribe@googlegroups.com).
+To subscribe, send an e-mail to [LittleProxy2+subscribe@googlegroups.com](mailto:LittleProxy2+subscribe@googlegroups.com).
+
+## Performance and Logging Guide
+
+For comprehensive information on logging performance optimization, including:
+
+- **Synchronous vs Asynchronous Logging**: Performance comparison and use cases
+- **Activity Log Formats**: All supported formats (CLF, ELF, JSON, SQUID, W3C, LTSV, CSV, HAPROXY)
+- **Log Filtering**: BurstFilter and custom filter implementations
+- **Groovy Script Filters**: Dynamic filtering with sampling and conditional logic
+- **Best Practices**: Production-ready recommendations and troubleshooting
+
+Please see our **[Performance and Logging Guide](PERFORMANCE_AND_LOGGING.md)**.
Acknowledgments
---------------
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index e286bc81..e2ec212a 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,327 @@
# Release Notes
+- 2.10.0 (Under construction, https://github.com/LittleProxy/LittleProxy/milestone/54?closed=1)
+ - TBD
+
+- 2.9.1 (28.08.2026, https://github.com/LittleProxy/LittleProxy/milestone/53?closed=1)
+ - Bump Netty from 4.2.16.Final to 4.2.17.Final (#782)
+ - Bump Selenium from 4.46.0 to 4.48.0 (#784) (#792)
+ - Bump Guava from 33.6.0-jre to 33.7.1-jre (#787) (#788)
+
+- 2.9.0 (06.08.2026, https://github.com/LittleProxy/LittleProxy/milestone/52?closed=1)
+ - #737 Fix order of handlers to support proxy protocol decoding for inbound requests (#729) by Krasimir Marinov
+ - fix "IllegalReferenceCountException: null" on ClientToProxyConnection (#625) by James Baldassari
+ - #71 Fix assumption about CONNECT in MITM mode should use SSL (#717) by Charles Lescot
+ - #77 Fix: CONNECT response not returned to HttpFilters (#721) by Charles Lescot
+ - #57 Fix: Upstream Socks Proxy Not Authenticating (#719) by Charles Lescot
+ - Bump netty.version from 4.2.15.Final to 4.2.16.Final (#770)
+ - Bump org.seleniumhq.selenium:selenium-java from 4.45.0 to 4.46.0 (#771)
+
+- 2.8.0 (27.06.2026, https://github.com/LittleProxy/LittleProxy/milestone/51?closed=1)
+ - Fix unsupported `com.lmax.disruptor` scope (#744) by Alexey Venderov
+ - Bump selenium from 4.41.0 to 4.45.0
+ - Bump jackson from 2.21.2 to 2.22.0
+ - Bump netty from 4.2.12.Final to 4.2.15.Final (#761)
+ - Bump dnsjava from 3.6.4 to 3.6.5 (#754)
+
+- 2.7.0 (06.04.2026, https://github.com/LittleProxy/LittleProxy/milestone/50?closed=1)
+ - fix WebSocket proxying by Andrei Solntsev
+ - add WebSocket frame observation hook by Andrei Solntsev
+ - #464 fix authentication bug when forwarding request with "Proxy-Authorization" header to a chained proxy (#691) by Charles Lescot
+ - quickly resolve localhost name (#698) by Andrei Solntsev
+ - #56 #439 fix timeout detection logic in ClientToProxyConnection (#699) by Charles Lescot
+ - #680 fix issue with generated jks and cert at root (#681) by Charles Lescot
+ - migration to log4j2 and async logging available (#684) by Charles Lescot
+ - enhance ActivityTracker interface with new lifecycle methods (#708) by Charles Lescot
+ - add unit test demonstrating how to do "internal redirect" (#68) (#718) by Charles Lescot
+
+- 2.6.0 (19.01.2026, https://github.com/LittleProxy/LittleProxy/milestone/49?closed=1)
+ - Feature/activity tracker logging (#668) by Charles Lescot
+ - add explicit logging support with "activity_log_format" option (CLI and config file) supporting CLF, ELF, JSON, LTSV, CSV, SQUID, HAPROXY formats (#668) by Charles Lescot
+ - move littleproxy.properties and log4j.xml in standard maven location (#667) by Charles Lescot
+ - fix: LittleProxy is not starting with jdk higher than jdk 11. (#669) by Charles Lescot
+ - migrate LittleProxy own tests from MockServer to WireMock (#670) by Charles Lescot
+ - bump Selenium from 4.39.0 to 4.40.0 (#676)
+
+- 2.5.0 (19.12.2025, https://github.com/LittleProxy/LittleProxy/milestone/48?closed=1)
+ - enhance documentation with run.bash script options (#659) by Charles Lescot
+ - add the --server option to Launch proxy as a server (#660) by Charles Lescot
+ - Add the --log-config option to launcher (#661) by Charles Lescot
+ - Add the --config option to launcher (#662) by Charles Lescot
+ - add options "--name", "--address", "--nic", "--allow_local_only", "--authenticate_ssl_clients", "--transparent", "--throttling", "--allow_requests_to_origin_server" (#666) by Charles Lescot
+ - add options "--proxy_alias", "--allow_proxy_protocol option", "--send_proxy_protocol", "--client_to_proxy_worker_threads", "--proxy_to_server_worker_threads" options (#666) by Charles Lescot
+ - remove unused and deprecated NetworkUtils.java (#663) by Charles Lescot
+ - remove "withListenOnAllAddresses" method from DefaultHttpProxyServer.java and HttpProxyServerBootstrap.java (#664) by Charles Lescot
+
+- 2.4.7 (15.12.2025, https://github.com/LittleProxy/LittleProxy/milestone/47?closed=1)
+ - Bump Netty from 4.2.7.Final to 4.2.9.Final (#655) (#658)
+ - bump Selenium from 4.38.0 to 4.39.0 (#653)
+
+- 2.4.6 (28.10.2025, https://github.com/LittleProxy/LittleProxy/milestone/46?closed=1)
+ - Bump Netty from 4.2.5.Final to 4.2.7.Final
+ - Bump Log4j from 2.25.1 to 2.25.2
+ - Bump Selenium from 4.35.0 to 4.38.0
+ - remove most Guava usages
+
+- 2.4.5 (08.09.2025, https://github.com/LittleProxy/LittleProxy/milestone/45?closed=1)
+ - fix compile warnings (#610) by leeyazhou
+ - bump Selenium from 4.34.0 to 4.35.0 (#604)
+ - bump Netty from 4.2.3.Final to 4.2.5.Final (#611) (#605)
+ - bump Jackson from 2.19.2 to 2.20.0 (#609)
+
+- 2.4.4 (10.08.2025, https://github.com/LittleProxy/LittleProxy/milestone/44?closed=1)
+ - Bump Netty from 4.2.2.Final to 4.2.3.Final (#596)
+ - Bump Jackson from 2.19.1 to 2.19.2 (#598)
+ - Bump Log4j from 2.25.0 to 2.25.1 (#595)
+
+- 2.4.3 (02.07.2025, https://github.com/LittleProxy/LittleProxy/milestone/43?closed=1)
+ - added "autoStop" property for ServerGroup (#590) -- thanks to Alex Panchenko
+ - fix problem when proxy is stopped quickly after starting (#590) -- thanks to Alex Panchenko
+ - Bump selenium from 4.32.0 to 4.34.0 (#588)
+ - Bump netty from 4.2.1.Final to 4.2.2.Final (#582)
+ - Bump jackson from 2.19.0 to 2.19.1 (#584)
+ - Bump log4j from 2.24.3 to 2.25.0 (#585)
+
+- 2.4.2 (08.05.2025, https://github.com/LittleProxy/LittleProxy/milestone/42?closed=1)
+ - fix memory leak in ProxyToServerConnection (#573)
+ - Bump selenium from 4.31.0 to 4.32.0 (#576)
+ - Bump netty from 4.2.0.Final to 4.2.1.Final (#577)
+
+- 2.4.1 (22.04.2025, https://github.com/LittleProxy/LittleProxy/milestone/41?closed=1)
+ - Bump netty from 4.1.116.Final to 4.2.0.Final (#550) (#564)
+ - Bump selenium from 4.27.0 to 4.31.0 (#546) (#560) (#566)
+ - Bump dnsjava from 3.6.2 to 3.6.3
+ - Bump slf4j from 2.0.16 to 2.0.17 (#549)
+ - Bump jackson from 2.18.2 to 2.18.3 (#554)
+
+- 2.4.0 (02.01.2025, https://github.com/LittleProxy/LittleProxy/milestone/40?closed=1)
+ - Migrate nullability annotations from JSR 305 to JSpecify (#533) (#534)
+ - Bump Netty from 4.1.115.Final to 4.1.116.Final (#530)
+ - Bump Log4j from 2.24.2 to 2.24.3 (#527)
+ - Bump Guava from 33.3.1-jre to 33.4.0-jre (#528)
+
+- 2.3.3 (04.12.2024, https://github.com/LittleProxy/LittleProxy/milestone/39?closed=1)
+ - Bump Netty from 4.1.114.Final to 4.1.115.Final (#521)
+ - Bump Selenium from 4.26.0 to 4.27.0 (#524)
+ - Bump Jackson from 2.18.1 to 2.18.2 (#525)
+
+- 2.3.2 (06.11.2024, https://github.com/LittleProxy/LittleProxy/milestone/38?closed=1)
+ - Expose proxy to server ctx to access client address -- thanks to Teodora Kostova (#520)
+ - Bump Netty from 4.1.113.Final to 4.1.114.Final
+ - Bump Selenium from 4.25.0 to 4.26.0
+
+- 2.3.1 (30.09.2024, https://github.com/LittleProxy/LittleProxy/milestone/37?closed=1)
+ - Bump org.seleniumhq.selenium:selenium-java from 4.24.0 to 4.25.0 (#492)
+ - Bump dnsjava:dnsjava from 3.6.1 to 3.6.2 (#491)
+ - Bump org.apache.logging.log4j:log4j-core from 2.23.1 to 2.24.1 (#489) (#497)
+
+- 2.3.0 (06.09.2024, https://github.com/LittleProxy/LittleProxy/milestone/36?closed=1)
+ - #487 remove UDP protocol support (#488)
+ - Bump Netty from 4.1.112.Final to 4.1.113.Final (#486)
+
+- 2.2.4 (04.09.2024, https://github.com/LittleProxy/LittleProxy/milestone/35?closed=1)
+ - Bump Selenium from 4.22.0 to 4.24.0
+ - Bump Netty from 4.1.111.Final to 4.1.112.Final
+ - Bump dnsjava from 3.5.3 to 3.6.1
+
+- 2.2.3 (21.06.2024, https://github.com/LittleProxy/LittleProxy/milestone/34?closed=1)
+ - Bump selenium from 4.21.0 to 4.22.0 (#433)
+ - #37 fix ClassCastException: "PooledUnsafeDirectByteBuf cannot be cast to HttpObject" (#434)
+
+- 2.2.2 (12.06.2024, https://github.com/LittleProxy/LittleProxy/milestone/33?closed=1)
+ - Bump selenium from 4.20.0 to 4.21.0
+ - Bump jackson from 2.17.0 to 2.17.1
+ - Bump netty from 4.1.109.Final to 4.1.111.Final
+
+- 2.2.1 (25.04.2024, https://github.com/LittleProxy/LittleProxy/milestone/32?closed=1)
+ - Bump Netty from 4.1.107.Final to 4.1.109.Final
+ - Bump Selenium from 4.18.1 to 4.20.0
+ - Bump Jackson from 2.16.1 to 2.17.0
+ - Bump Slf4j from 2.0.12 to 2.0.13
+ - Bump Log4j from 2.23.0 to 2.23.1
+ - Bump Guava from 33.0.0-jre to 33.1.0-jre
+
+- 2.2.0 (22.02.2024, https://github.com/LittleProxy/LittleProxy/milestone/31?closed=1)
+ - Move the project from groupId "xyz.rogfam" to "io.github.littleproxy"
+ - Migrate from JUnit4/Hamcrest to JUnit5/AssertJ (#373)
+ - Bump Netty from 4.1.106.Final to 4.1.107.Final (#376)
+ - Bump Selenium from 4.17.0 to 4.18.1 (#378) (#379)
+ - Bump log4j from 2.22.1 to 2.23.0 (#381)
+
+- 2.1.2 (07.02.2024, https://github.com/LittleProxy/LittleProxy/milestone/30?closed=1)
+ - Refactoring & code cleanup & setup IDEA inspections (#370) (#371)
+ - Bump Netty from 4.1.103.Final to 4.1.106.Final
+ - Bump slf4j from 2.0.9 to 2.0.12
+ - Bump log4j from 2.22.0 to 2.22.1 (#357)
+ - Bump Selenium from 4.16.1 to 4.17.0 (#367)
+ - Bump Guava from 32.1.3-jre to 33.0.0-jre
+
+- 2.1.1 (15.12.2023, https://github.com/LittleProxy/LittleProxy/milestone/29?closed=1)
+ - Bump Netty from 4.1.101.Final to 4.1.103.Final #345 #346
+ - Bump selenium from 4.15.0 to 4.16.1 #343 #344
+ - Bump log4j from 2.21.1 to 2.22.0 #336
+ - Bump commons-lang3 from 3.13.0 to 3.14.0 #338
+
+- 2.1.0 (20.11.2023, https://github.com/LittleProxy/LittleProxy/milestone/28?closed=1)
+ - Upgrade from Java 8 to Java 11+
+ - Bump Selenium from 4.13.0 to 4.15.0
+ - Bump Netty from 4.1.99.Final to 4.1.101.Final
+ - Bump Jackson from 2.15.2 to 2.16.0
+ - Bump Log4j from 2.20.0 to 2.21.1
+ - Bump Guava from 32.1.2-jre to 32.1.3-jre
+
+- 2.0.22 (08.10.2023, https://github.com/LittleProxy/LittleProxy/milestone/27?closed=1)
+ - #35 Fix Websocket race condition while protocol switching -- thanks to Craig Andrews for PR #308
+ - #307 bump Netty from 4.1.98.Final to 4.1.99.Final
+
+- 2.0.21 (27.09.2023, https://github.com/LittleProxy/LittleProxy/milestone/26?closed=1)
+ - #301 Always use a new connection for websockets -- thanks to Craig Andrews
+ - #299 fix problem with filtering proxy Authorization header -- thanks to Matthias Kraaz for PR #304
+ - #297 #306 Bump org.seleniumhq.selenium:selenium-java from 4.12.0 to 4.13.0
+ - #303 Bump `netty.version` from 4.1.97.Final to 4.1.98.Final.
+
+- 2.0.20 (04.09.2023, https://github.com/LittleProxy/LittleProxy/milestone/25?closed=1)
+ - #295 #131 fix memory leak "LEAK: ByteBuf.release() was not called..." -- thanks to Sujit Joshi for the fix
+ - #284 #291 Bump netty.version from 4.1.95.Final to 4.1.97.Final
+ - #286 #293 Bump org.seleniumhq.selenium:selenium-java from 4.10.0 to 4.12.0
+ - #285 Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.13.0
+ - #287 Bump com.google.guava:guava from 32.1.1-jre to 32.1.2-jre
+ - #294 Bump slf4j.version from 2.0.7 to 2.0.9
+
+- 2.0.19 (22.07.2023, https://github.com/LittleProxy/LittleProxy/milestone/24?closed=1)
+ - #283 fix memory leak: On proxy connection unregister, unregister downstream channels - thanks to Craig Andrews
+ - #274 Bump Selenium from 4.9.1 to 4.10.0 (see https://github.com/SeleniumHQ/selenium)
+ - #266 Bump Jackson from 2.15.1 to 2.15.2
+ - #281 Bump guava from 32.0.0-jre to 32.1.1-jre
+ - #282 Bump Netty from 4.1.93.Final to 4.1.95.Final
+ - #264 Migrate Jetty 9 to Jetty 11 - thanks to Valery Yatsynovich
+
+- 2.0.18 (29.05.2023, https://github.com/LittleProxy/LittleProxy/milestone/23?closed=1)
+ - Bump Selenium from 4.8.3 to 4.9.1 (see https://github.com/SeleniumHQ/selenium)
+ - #242 Bump Netty from 4.1.90.Final to 4.1.93.Final
+ - Bump Jackson from 2.14.2 to 2.15.1
+ - Bump guava from 31.1-jre to 32.0.0-jre
+
+- 2.0.17 (01.04.2023, https://github.com/LittleProxy/LittleProxy/milestone/22?closed=1)
+ - #235 Bump netty.version from 4.1.89.Final to 4.1.90.Final
+ - bump Jackson from 2.13.4 to latest 2.14.2 (fixes several CVEs)
+ - #236 Bump slf4j.version from 2.0.6 to 2.0.7
+ - #241 Bump selenium-java from 4.8.1 to 4.8.3
+
+- 2.0.16 (27.02.2023, https://github.com/LittleProxy/LittleProxy/milestone/21?closed=1)
+ - rename "master" branch to "main"
+ - #207 Remove redundant file generated by unit test -- thanks to Valery Yatsynovich
+ - #206 Export certificate to generated by SelfSignedMitmManager KeyStore directory -- thanks to Valery Yatsynovich
+ - Bump slf4j.version from 2.0.5 to 2.0.6
+ - Bump log4j-core from 2.19.0 to 2.20.0
+ - Bump selenium-java from 4.7.1 to 4.8.1
+ - Bump netty.version from 4.1.86.Final to 4.1.89.Final
+
+- 2.0.15 (14.12.2022, https://github.com/LittleProxy/LittleProxy/milestone/20?closed=1)
+ - Bump netty-codec-haproxy from 4.1.85.Final to 4.1.86.Final
+ - Bump selenium-java from 4.6.0 to 4.7.1
+ - Bump slf4j.version from 2.0.4 to 2.0.5
+ - Bump httpclient from 4.5.13 to 4.5.14
+
+- 2.0.14 (21.11.2022, https://github.com/LittleProxy/LittleProxy/milestone/19?closed=1)
+ - #184 Respectful KeyStore file path while generating certs by `SelfSignedMitmManager` -- thanks to Valery Yatsynovich
+ - #187 CI: run build on all major OS-s -- thanks to Valery Yatsynovich
+ - #183 Bump netty from 4.1.82.Final to 4.1.85.Final -- thanks to Valery Yatsynovich for fixing tests after upgrading Netty.
+ - #189 Bump slf4j.version from 2.0.3 to 2.0.4
+ - Bump jackson-databind from 2.13.2.2 to 2.13.4
+ - #191 Bump dnsjava from 3.5.1 to 3.5.2
+
+- 2.0.13 (04.10.2022)
+ - #170 restore transitive dependencies in generated pom -- thanks to Mateusz Pietryga for PR #171
+ - Bump slf4j from 2.0.1 to 2.0.3
+ - Bump selenium-java from 4.4.0 to 4.5.0
+
+- 2.0.12 (23.09.2022)
+ - #145 Restore Keep-Alive value when filtering short-circuit response -- thanks to krlvm for PR
+ - Bump netty from 4.1.79.Final to 4.1.82.Final
+ - Bump slf4j from 1.7.36 to 2.0.1
+ - Bump log4j-core from 2.18.0 to 2.19.0
+
+- 2.0.11 (13.08.2022)
+ - #131 fix memory leak: release byte buffer when closing request - see PR #141
+ - #142 fix some "modify response" problem, see https://github.com/adamfisk/LittleProxy/issues/359
+ - #144 HTTP CONNECT can't be Keep-Alive - thanks Michel Belleau for PR #144
+
+- 2.0.10 (20.07.2022)
+ - #135 Bump netty.version from 4.1.77.Final to 4.1.79.Final
+ - #132 Bump selenium-java from 4.1.4 to 4.3.0
+ - #118 Bump dnsjava from 3.5.0 to 3.5.1
+
+- 2.0.9 (10.05.2022)
+ - #115 reverted to maven-shade-plugin 3.2.4 (because 3.3.0 generated artifact without compile/runtime dependencies)
+
+- 2.0.8 (06.05.2022)
+ - #26 fixed TLS 1.3 handshake bug -- thanks Dan Powell for PR https://github.com/LittleProxy/LittleProxy/pull/26
+ - Bumped log4j-core from 2.17.0 to 2.17.2
+ - Bumped netty from 4.1.71 to 4.1.76
+ - Bumped slf4j from 1.7.30 to 1.7.36
+ - Bumped jackson from 2.11.3 to 2.12.6.1
+ - Bumped guava from 30.1-jre to 31.1-jre
+ - Bumped commons-cli from 1.4 to 1.5.0
+ - Relocated slf4j-log4j to slf4j-reload4j
+ - moved the project to https://github.com/LittleProxy/LittleProxy
+ - moved CI from Travis to https://github.com/LittleProxy/LittleProxy/actions
+
+- 2.0.7 (21.12.2021)
+ - Bumped log4j-core from 2.16.0 to 2.17.0
+
+- 2.0.6
+ - Use single Hamcrest dependency in tests
+ - Improve logging performance
+ - Bumped netty-codec from 4.1.63.Final to 4.1.68.Final
+ - Bump netty-codec-http from 4.1.68.Final to 4.1.71.Final
+ - Bumped log4j-core from 2.14.0 to 2.16.0
+ - Added public key file
+
+- 2.0.5
+ - Bumped jetty-server from 9.4.34.v20201102 to 9.4.41.v20210516.
+
+- 2.0.4
+ - Android compatibility fix (PR #76)
+ - Fix NoSuchElementException when switching protocols to WebSocket (PR #78)
+ - Prevent NullPointerException in ProxyUtils::isHEAD (PR #79)
+ - Fixes in ThrottlingTest, Upgrade to Netty 4.1.63.Final (PR #65)
+ - Fix NPEs in getReadThrottle and getWriteThrottle when globalTrafficShapingHandler is null (PR #80)
+
+- 2.0.3
+ - Upgrade guava to 30.1
+ - Threads are now set as daemon (not user, which is the default) threads so the JVM exits as expected when all other threads stop.
+ - Close thread pool if proxy fails to start
+
+- 2.0.2
+ - Support for WebSockets with MITM in transparent mode
+ - Support for per request conditional MITM
+
+- 2.0.1
+ - Removed beta tag from version
+ - Updated various dependency versions
+ - Re-ordered the release notes so the newest stuff is at the top
+
+- 2.0.0-beta-6
+ - Cleaned up old code to conform with newer version of Netty
+ - Deprecated UDT support because it's deprecated in Netty
+ - Removed performance test code because it seems to be confusing GitHub into thinking that this is a PHP project
+
+- 2.0.0-beta-5
+ - Treat an upstream SOCKS proxy as if it is the origin server
+ - Fixed memoryLeak in ClientToProxyConnection
+
+- 2.0.0-beta-4
+ - Allow users to set their own server group within the bootstrap helper
+ - Added support for chained SOCKS proxies
+
+- 2.0.0-beta-3
+ - Upgraded Netty, guava, Hamcrest, Jetty, Selenium, Apache commons cli and lang3
+ - Upgrade Maven plugins to the latest versions
+
+- 2.0.0-beta-2
+ - Added support for proxy protocol. See https://www.haproxy.com/blog/haproxy/proxy-protocol/ and https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt for protocol details.
+
- 2.0.0-beta-1
- New Maven coordinates
- Moved from Java 7 to 8
@@ -7,24 +329,3 @@
- **Breaking change:** Made client details available to ChainedProxyManager
- Refactored MITM manager to accept engine with user-defined parameters
- Added ability to load keystore from classpath
-
-- 2.0.0-beta-2
- - Added support for proxy protocol. See https://www.haproxy.com/blog/haproxy/proxy-protocol/ and https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt for protocol details.
-
-- 2.0.0-beta-3
- - Upgraded Netty, guava, Hamcrest, Jetty, Selenium, Apache commons cli and lang3
- - Upgrade Maven plugins to the latest versions
-
-- 2.0.0-beta-4
- - Allow users to set their own sesrvergroup within the bootstrap helper
- - Added support for chained SOCKS proxies
-
-- 2.0.0-beta-5
- - Treat an upstream SOCKS proxy as if it is the origin server
- - Fixed memoryLeak in ClientToProxyConnection
-
-- 2.0.0-beta-6
- - Cleaned up old code to conform with newer version of Netty
- - Deprecated UDT support because it's deprecated in Netty
- - Removed performance test code because it seems to be confusing GitHub into thinking that this is a PHP project.
-
\ No newline at end of file
diff --git a/config/littleproxy.properties b/config/littleproxy.properties
new file mode 100644
index 00000000..29e089c4
--- /dev/null
+++ b/config/littleproxy.properties
@@ -0,0 +1,3 @@
+name=MyLittleProxy
+idle_connection_timeout=40
+activity_log_format=ELF
\ No newline at end of file
diff --git a/docs/pooling-features.md b/docs/pooling-features.md
new file mode 100644
index 00000000..81d5f778
--- /dev/null
+++ b/docs/pooling-features.md
@@ -0,0 +1,677 @@
+# Server Connection Pooling — Full Feature Set
+
+This document describes **all** features on this branch that are not present in the
+main branch. The branch introduces a complete shared server connection pooling
+infrastructure (based on the design from PR #724) and extends it with MITM / HTTPS
+upstream support.
+
+---
+
+## Background
+
+On the main branch, every client connection gets its own dedicated
+`ProxyToServerConnection`. When client A and client B both connect to
+`http://example.com`, two separate TCP sockets are opened to the same server.
+When the traffic is MITM'd HTTPS, the upstream TLS connection is tied to the
+client's session for its entire lifetime and discarded on disconnect.
+
+This branch addresses the connection explosion problem by introducing a **shared
+server connection pool**, and extends it to HTTPS upstream connections intercepted
+via MITM.
+
+---
+
+## Base Connection Pooling (PR #724 Infrastructure)
+
+The entire pool infrastructure is new on this branch. On the main branch, all
+connections are created with `ProxyToServerConnection.create(...)` and tracked in a
+per-client `serverConnectionsByHostAndPort` map.
+
+### New Interfaces and Classes
+
+| Type | File | Purpose |
+|---|---|---|
+| Interface | `ServerConnectionPool` | Contract for pooling server connections |
+| Config | `ServerConnectionPoolConfig` | Configuration bean (pool type, sizes, timeouts) |
+| Config | `DefaultHttpProxyServerConfig` | Server-level config carrier that includes pool config |
+| Metrics | `PoolMetrics` | Active/idle/borrow/return/eviction counters |
+| Model | `PendingRequest` | Tracks a request awaiting a response (for HTTP pipelining) |
+| Enum | `ServerConnectionPoolType` | `CONCURRENT_MAP` |
+
+### Pool Implementations
+
+Single backend implementation:
+
+| Pool type | Class | Approach | Dependencies |
+|---|---|---|---|
+| `CONCURRENT_MAP` | `ConcurrentMapServerConnectionPool` | `ConcurrentHashMap` + per-host `Queue` of available connections | None (pure Netty/Java) |
+
+The implementation:
+- Implements `getOrCreateConnection(host, chainedProxyAddr, client, filters, request)` +
+ `releaseConnection(connection)` + `removeConnection(connection)` + `closeAll()`
+- Tracks pending requests per channel for HTTP pipelining support
+- Enforces per-host and global connection limits
+- Supports idle timeout eviction and optional connection validation on borrow
+- Exposes `getMetrics()` returning active/idle/total connections and cumulative operation counts
+
+### `ServerConnectionPool` Interface
+
+```java
+ProxyToServerConnection getOrCreateConnection(
+ String serverHostAndPort,
+ @Nullable InetSocketAddress chainedProxyAddress,
+ ClientToProxyConnection clientConnection,
+ HttpFilters initialFilters,
+ HttpRequest initialHttpRequest);
+
+void releaseConnection(ProxyToServerConnection connection);
+void removeConnection(ProxyToServerConnection connection);
+void registerPendingRequest(Channel, ClientToProxyConnection, HttpRequest, HttpFilters);
+PendingRequest removePendingRequest(Channel);
+PendingRequest peekPendingRequest(Channel);
+void drainPendingRequests(Channel);
+void closeAll();
+
+PoolMetrics getMetrics();
+
+// Computes a compound pool key: "host:port:"
+default String computePoolKey(String serverHostAndPort,
+ @Nullable InetSocketAddress chainedProxyAddress);
+```
+
+Pool keys incorporate the chained proxy address, so connections through different
+upstream proxies are isolated even when the target host is the same.
+
+### How Plain HTTP Pooling Works
+
+In `ClientToProxyConnection.doReadHTTPInitial()`, the selection logic was changed
+from a simple `serverConnectionsByHostAndPort.get(serverHostAndPort)` to a decision
+tree:
+
+```java
+boolean useSharedPool =
+ usePool
+ && !ProxyUtils.isCONNECT(httpRequest)
+ && !isTunneling()
+ && !isMitming()
+ && !ProxyUtils.isSwitchingToWebSocketProtocol(httpRequest);
+```
+
+When `useSharedPool` is true, `pool.getOrCreateConnection(...)` is called instead of
+`ProxyToServerConnection.create(...)`. The pool either returns an idle connection or
+creates a new one via `ProxyToServerConnection.createForPool(...)`, which attaches
+the `connectionPool` reference so the connection knows it is pool-managed.
+
+**Excluded from pooling:** Tunneling connections (non-MITM CONNECT, raw TCP tunnels)
+and WebSocket protocol upgrades are never pooled. Tunneling has no HTTP request/response
+boundary to trigger pool release — the connection stays dedicated for the tunnel's
+lifetime. WebSocket connections replace HTTP codecs with raw frame handlers after the
+upgrade handshake, so they cannot be returned to the pool. Both always use
+`serverConnectionsByHostAndPort` with dedicated `ProxyToServerConnection.create()`.
+
+On the response path, `markResponseComplete()` calls
+`connectionPool.releaseConnection(this)`, returning the connection to the available
+queue. HTTP pipelining is handled via `registerPendingRequest` /
+`removePendingRequest` — when a pipelined response arrives, the next pending request
+is dequeued and routed.
+
+### Connection Lifecycle for Pooled Connections
+
+| Event | Non-pooled (main branch) | Pooled (this branch) |
+|---|---|---|
+| New request arrives | `ProxyToServerConnection.create()` | `pool.getOrCreateConnection()` → creates or borrows |
+| Request sent | Stored in `currentHttpRequest` | Registered as `PendingRequest` in pool |
+| Response received | Forwarded to client | `removePendingRequest()` → forwarded to correct client |
+| Response complete | Connection stays in `AWAITING_INITIAL` | `releaseConnection()` → returned to pool |
+| Server disconnect | `clientConnection.serverDisconnected()` | `pool.removeConnection()` + drain pending |
+| Client disconnect | `serverConnection.disconnect()` (close) | `serverConnection.disconnect()` → `pool.removeConnection()` via `channelInactive` |
+
+### New Builder Methods on `HttpProxyServerBootstrap`
+
+```java
+.withSharedServerConnectionPool(boolean) // master switch
+.withServerConnectionPoolType(ServerConnectionPoolType) // CONCURRENT_MAP default
+.withMaxConnectionsPerHost(int) // default 10
+.withMaxConnections(int) // default 200
+.withPoolIdleTimeout(Duration) // null = no idle eviction
+```
+
+### How Pool Configuration Reaches the Server
+
+The configuration flows through three layers:
+
+1. `DefaultHttpProxyServerBootstrap` stores raw builder fields
+2. `build()` creates a `ServerConnectionPoolConfig` and a `DefaultHttpProxyServerConfig`
+3. `DefaultHttpProxyServer` reads the config on construction
+
+Properties file parsing in `DefaultHttpProxyServerBootstrap(Properties props)` maps
+each key:
+
+```properties
+use_shared_server_connection_pool=true
+server_connection_pool_type=CONCURRENT_MAP
+max_connections_per_host=10
+max_total_connections=200
+```
+
+### Refactoring: `DefaultHttpProxyServerConfig`
+
+A new `DefaultHttpProxyServerConfig` class was extracted to carry all server
+configuration as a single object. The old pattern of passing individual fields
+through the bootstrap → server constructor was replaced with a config object,
+enabling cleaner cloning and property-based construction. This class holds
+~25 fields including the `ServerConnectionPoolConfig`.
+
+### Changes to `ClientToProxyConnection`
+
+- **Request routing**: `doReadHTTPInitial()` now branches on `useSharedPool`. The
+ pooled path calls `pool.getOrCreateConnection()` with the resolved chained proxy
+ address.
+- **Connection tracking**: Pooled connections are not stored in
+ `serverConnectionsByHostAndPort` (they live in the pool instead).
+- **Backpressure** (`becameSaturated` / `becameWritable`): Both methods now also
+ check `currentServerConnection` (the transient reference set per request) in
+ addition to the `serverConnectionsByHostAndPort` values. This is necessary
+ because pooled connections are not in that map.
+- **`serverBecameWriteable`**: Now also checks `currentServerConnection` for
+ saturation before resuming client reads.
+- **`disconnected()`**: Now handles pooled connections by releasing them to the
+ pool instead of disconnecting them.
+- **`recordClientConnected()`**: Now called from `requestRead()` callback rather
+ than during CONNECT setup, fixing a timing issue with pooled connections.
+- **`getClientAddress()`**: Fixed a `ClassCastException` when `remoteAddress()`
+ returns a non-`InetSocketAddress` type.
+
+### Changes to `ProxyToServerConnection`
+
+- **`connectionPool` field**: All pooled connections carry a reference back to
+ their pool.
+- **`createForPool()` static factory**: Creates a connection with pool awareness,
+ resolving chained proxies and filters the same way as the non-pooled path.
+- **`getClientConnection()` method**: Routes responses to the correct client.
+ For pooled connections, uses the per-request `currentClientConnectionForRequest`
+ field (set before each write) instead of the constructor-injected
+ `clientConnection` reference.
+- **`write()` method**: When the connection is in pool-managed CONNECT reuse
+ state (not `DISCONNECTED` but needs a new flow), triggers `connectAndWrite()`
+ for the CONNECT request.
+- **`connectionSucceeded()`**: Explicitly releases the initial request reference
+ to prevent memory leaks with pooled connections (where `initialRequest` is
+ retained).
+- **`disconnected()`**: Pool-managed connections call
+ `connectionPool.removeConnection(this)` + `drainPendingRequests(channel)` before
+ notifying the client.
+- **`readRaw()`**: Uses `getClientConnection()` instead of `clientConnection`.
+- **All event recording methods** (`recordServerConnected`, `recordServerDisconnected`,
+ `recordConnectionSaturated`, etc.): Use `getClientConnection()` instead of
+ `clientConnection` to route activity tracker events to the correct client.
+- **`SendProxyProtocolHeader`**: Fixed to handle IPv6 addresses by selecting
+ `HAProxyProxiedProtocol.TCP6` when either endpoint uses an `Inet6Address`.
+
+### New `PendingRequest` Class
+
+A simple holder for a client connection, HTTP request, and filters, stored in a
+per-channel FIFO queue to support HTTP pipelining over pooled connections:
+
+```java
+class PendingRequest {
+ ClientToProxyConnection getClientConnection();
+ HttpRequest getRequest();
+ HttpFilters getFilters();
+}
+```
+
+---
+
+## HTTP-Only Pooling Scenario
+
+When the proxy handles only plain HTTP (no MITM manager, no CONNECT), pooling is fully
+determined by `useSharedServerConnectionPool`:
+
+```java
+HttpProxyServer server = DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withSharedServerConnectionPool(true)
+ .start();
+```
+
+### What gets pooled
+
+All non-CONNECT, non-tunneling, non-WebSocket HTTP requests go through the shared
+pool. Each request acquires a connection from the pool, the response flows back,
+and `markResponseComplete()` releases the connection immediately.
+
+### Connection lifecycle
+
+```
+Client A ── GET /api ──→ pool.getOrCreateConnection() ──→ [idle conn] or [new TCP]
+ ↓
+ response received → markResponseComplete() → releaseConnection()
+ ↓
+Client B ── GET /api ──→ pool.getOrCreateConnection() ──→ [same idle conn from A]
+```
+
+When the pool has an idle connection for the target `host:port`, it is reused
+directly — no new TCP socket. When all connections are busy, the pool either waits
+(blocking borrow) or creates a new one up to `maxConnectionsPerHost`.
+
+### Use case
+
+A high-traffic forward proxy serving many clients hitting the same REST APIs.
+Without pooling, each request opens a new TCP socket, does a TCP handshake (and
+potentially TLS), then tears it down. With pooling, sockets stay alive and are
+reused across clients, dramatically reducing latency and server load.
+
+### Backpressure in HTTP-only mode
+
+When using the pool, `currentServerConnection` is set on each request and cleared
+on response complete. The `becameSaturated()` / `becameWritable()` / `serverBecameWriteable()`
+methods in `ClientToProxyConnection` check this transient reference in addition to the
+`serverConnectionsByHostAndPort` map, because pooled connections are not stored in that map.
+This ensures backpressure signals flow correctly through the pooled connection to pause/resume
+client reads.
+
+### Pool sizing for HTTP-only
+
+For HTTP-only workloads, `maxConnectionsPerHost` (default 10) limits concurrent requests
+to any single origin. `maxConnections` (default 200) limits the total across all origins.
+If your clients make many concurrent requests to the same server, increase
+`maxConnectionsPerHost`. If you proxy to many different origins, increase `maxConnections`.
+
+---
+
+## Mixed HTTP + HTTPS (MITM) Pooling Scenario
+
+When the proxy handles both plain HTTP and MITM'd HTTPS traffic, the pool serves both,
+but the MITM paths require additional flags.
+
+### Configuration matrix
+
+```java
+// HTTP only: pool is used for all non-CONNECT requests
+HttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withSharedServerConnectionPool(true)
+ .start();
+
+// HTTP + MITM cross-client reuse: HTTPS upstream connections survive client sessions
+HttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withManInTheMiddle(myMitmManager)
+ .withSharedServerConnectionPool(true)
+ .withPoolSharedMitmConnections(true)
+ .start();
+
+// HTTP + MITM per-request: full pooling, no dedicated upstream per session
+HttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withManInTheMiddle(myMitmManager)
+ .withSharedServerConnectionPool(true)
+ .withPoolSharedMitmConnections(true)
+ .withPoolPerRequestInMitm(true)
+ .start();
+```
+
+### How the `useSharedPool` decision works
+
+In `ClientToProxyConnection.doReadHTTPInitial()`, every request goes through a
+single decision tree:
+
+```java
+boolean usePool = proxyServer.getServerConnectionPool() != null;
+boolean isConnect = ProxyUtils.isCONNECT(httpRequest);
+boolean poolSharedMitm = usePool && proxyServer.isPoolSharedMitmConnections();
+boolean poolPerRequest = usePool && proxyServer.isPoolPerRequestInMitm();
+
+boolean useSharedPool =
+ usePool
+ && !isTunneling()
+ && !ProxyUtils.isSwitchingToWebSocketProtocol(httpRequest)
+ && (poolPerRequest || !isMitming())
+ && (poolSharedMitm || !isConnect);
+```
+
+A request reaches the pool when:
+- The pool is enabled (`usePool`)
+- It is not a WebSocket upgrade or tunneling request — these are **always excluded**
+ because tunneling has no request/response boundary and WebSocket replaces HTTP codecs
+ with raw frame handlers, making pool return impossible
+- **For CONNECT requests**: only if `poolSharedMitmConnections=true`
+- **For MITM requests (after CONNECT)**: always if `poolPerRequestInMitm=true`;
+ otherwise the dedicated `serverConnectionsByHostAndPort` path is used
+- **For plain HTTP requests**: always (no MITM/CONNECT gating applies)
+
+### What happens during a CONNECT in mixed mode
+
+```
+CONNECT example.com:443
+ → useSharedPool? (only if poolSharedMitmConnections=true)
+ → Yes: pool.getOrCreateConnection()
+ → Pool returns idle connection (TCP + TLS already up) OR creates new one
+ → initializeConnectionFlow() with isReused check
+ → Reused: skip ConnectChannel + EncryptChannel → RespondCONNECTSuccessful → MitmEncryptClientChannel
+ → New: ConnectChannel → EncryptChannel → RespondCONNECTSuccessful → MitmEncryptClientChannel
+ → After CONNECT flow completes:
+ poolPerRequest? → releaseToPool() immediately
+ !poolPerRequest? → pinned to client session (mitmPooled=true), released on disconnect
+```
+
+While the CONNECT is being established, plain HTTP requests to different hosts
+continue to use the pool independently — the CONNECT flow does not block them.
+
+### What happens during an HTTP GET in mixed mode (after CONNECT)
+
+```
+GET /api (through existing MITM tunnel)
+ → useSharedPool?
+ → poolPerRequest=true: pool.getOrCreateConnection() → borrows a (possibly different) connection
+ → poolPerRequest=false: use dedicated serverConnectionsByHostAndPort entry
+ → Response complete:
+ → poolPerRequest=true: markResponseComplete() → releaseConnection()
+ → poolPerRequest=false: connection stays ready for next request
+```
+
+### Pool sizing for mixed workloads
+
+In mixed mode, the pool serves both plain HTTP requests and MITM upstream connections
+from the same pool. Each MITM upstream TLS connection counts toward the per-host and
+global limits. If you expect many concurrent MITM sessions to the same host, ensure
+`maxConnectionsPerHost` is high enough to accommodate both HTTP and HTTPS demand.
+
+Example: with `maxConnectionsPerHost=10`, if 8 HTTP requests and 5 MITM sessions all
+target `api.example.com:443`, the 11th request will fail to acquire a connection.
+Raise the limit or monitor `PoolMetrics` for borrow failures.
+
+### Separate pool keys for MITM vs. plain HTTP
+
+The pool key is computed by `ServerConnectionPool.computePoolKey()`:
+- Plain HTTP to `example.com:443`: key = `"example.com:443:direct"`
+- MITM CONNECT to `example.com:443`: key = `"example.com:443:direct"` (same key)
+
+This means a plain HTTP request and a MITM upstream connection to the same
+`host:port` compete for the same pool entries. This is intentional — they are
+connections to the same server and should be limited together.
+
+---
+
+## Feature 1: `poolSharedMitmConnections` — Cross-Client Upstream Reuse
+
+### What it does
+
+When enabled, the upstream TLS connection created during a MITM `CONNECT` handshake is
+stored in the shared connection pool (keyed by `host:port`). When a second (or third,
+etc.) client connects to the **same** target host, the pool returns the cached
+connection instead of opening a fresh TCP socket and TLS handshake.
+
+### Configuration
+
+```java
+HttpProxyServer server = DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withManInTheMiddle(myMitmManager)
+ .withSharedServerConnectionPool(true) // master switch
+ .withPoolSharedMitmConnections(true) // Phase 1
+ .start();
+```
+
+Or via properties file:
+
+```properties
+use_shared_server_connection_pool=true
+pool_shared_mitm_connections=true
+```
+
+### How it works
+
+- The pool guard `!isMitming()` is replaced with a combined condition that checks the
+ new flag (`poolSharedMitmConnections`).
+- During the CONNECT flow in `ClientToProxyConnection.doReadHTTPInitial()`, when
+ the flag is set, the connection is obtained via `pool.getOrCreateConnection()`
+ instead of `ProxyToServerConnection.create()` + `serverConnectionsByHostAndPort.put()`.
+- The existing `initializeConnectionFlow()` is extended: when the connection was
+ retrieved from the pool and its channel is already active (`channel != null &&
+ channel.isActive()`), the flow **skips** `ConnectChannel` and `EncryptChannel`
+ — the TCP socket and TLS handshake are already done from a previous session.
+- When the client disconnects, the server connection is released back to the pool
+ (via `releaseToPool()`) instead of being closed, making it available for the next
+ client.
+
+### Key implementation details
+
+- A new `isReused` boolean is computed at the top of `initializeConnectionFlow()`.
+ When true, `ConnectChannel` and all chained/send-proxy/encrypt steps are bypassed.
+- The `mitmPooled` flag on `ProxyToServerConnection` prevents `markResponseComplete()`
+ from releasing the connection after each individual HTTP response — the connection
+ stays pinned to the MITM session until the client disconnects.
+- `ProxyToServerConnection` gains a `connectionPool` field (null for non-pooled
+ connections), `createForPool()` static factory, `releaseToPool()`,
+ `isManagedByPool()`, `isConnected()`, `isAvailableForNewRequest()`, and
+ `getClientConnection()` — the latter routes responses to the correct client
+ even when the `clientConnection` field points to the original client that
+ created the connection.
+
+### Use case
+
+A browser opens 10 tabs to `https://api.example.com`. Each tab creates a separate
+client TCP connection through the proxy. Without pooling, 10 upstream TLS sockets
+are opened to `api.example.com:443`. With pooling, the first tab's upstream
+connection is reused for tabs 2–10.
+
+### Value
+
+- Reduces upstream TLS handshake overhead (CPU, latency)
+- Reduces server-side connection load
+- Lowers the number of concurrent outbound sockets
+- Most impactful for proxy deployments with many clients hitting the same origins
+
+## Feature 2: `poolPerRequestInMitm` — Per-Request (vs. Per-Session) Borrowing
+
+*Requires `poolSharedMitmConnections=true`.*
+
+### What it does
+
+Without this flag, the pooled connection is pinned to the client's MITM session for
+its lifetime — it is released back to the pool only when the client disconnects.
+With this flag, the connection is released back to the pool **after each individual
+HTTP request** completes, even while the client TCP session remains open. Subsequent
+HTTP requests through the same MITM tunnel acquire a (possibly different) connection
+from the pool.
+
+### Configuration
+
+```java
+HttpProxyServer server = DefaultHttpProxyServer.bootstrap()
+ .withPort(8080)
+ .withManInTheMiddle(myMitmManager)
+ .withSharedServerConnectionPool(true) // master switch
+ .withPoolSharedMitmConnections(true) // Phase 1
+ .withPoolPerRequestInMitm(true) // Phase 2
+ .start();
+```
+
+Or via properties file:
+
+```properties
+use_shared_server_connection_pool=true
+pool_shared_mitm_connections=true
+pool_per_request_in_mitm=true
+```
+
+### How it works
+
+- The `useSharedPool` condition in `ClientToProxyConnection.doReadHTTPInitial()`
+ is extended: when `poolPerRequestInMitm` is true, MITM requests (after the
+ CONNECT) go through the pool path.
+- The CONNECT response flow sets `releaseToPoolOnConnectComplete = true` on the
+ server connection. After the CONNECT flow completes (in
+ `connectionSucceeded()`), the connection is immediately released to the pool.
+- `serverConnectionsByHostAndPort` is NOT used for MITM connections when this
+ flag is set — every HTTP request goes through `pool.getOrCreateConnection()`.
+- `markResponseComplete()` calls `connectionPool.releaseConnection(this)` for
+ per-request connections (ones where `mitmPooled` is false). This returns the
+ connection to the available queue for another client's request.
+- HTTP pipelining is handled via `PendingRequest` tracking in the pool.
+- The `disconnected()` method in `ClientToProxyConnection` avoids a double-release:
+ per-request connections are already in the pool by the time the client
+ disconnects, so they just need `removeConnection()` on disconnect, not
+ `releaseToPool()`.
+
+### Key implementation details
+
+- The `releaseToPoolOnConnectComplete` transient flag on
+ `ProxyToServerConnection` is set during `initializeConnectionFlow()` and
+ consumed once in `connectionSucceeded()`. After release, subsequent HTTP
+ requests from the same MITM tunnel go through `doReadHTTPInitial()` → pool
+ path.
+- `setCurrentClientConnectionForRequest()` is called on the borrowed connection
+ to ensure responses are routed to the correct client.
+- A critical fix was made during development: `MitmEncryptClientChannel.execute()`
+ used the constructor field `clientConnection` (the original client that
+ initiated the CONNECT) instead of `getClientConnection()` (the client that
+ is making the current request). This caused
+ `testMultipleRequestsOverHTTPS` to fail with an SSL handshake error because
+ the encrypt was applied to the wrong channel. The fix was to use
+ `getClientConnection()` consistently.
+
+### Use case
+
+A client opens a single HTTPS connection and sends a GET, then sits idle for
+30 seconds, then sends a POST. Without per-request pooling, the upstream
+connection is held idle the whole time. With per-request pooling, it is
+returned to the pool after the GET, available for other clients, and
+re-acquired for the POST.
+
+### Value
+
+- Better connection utilization — idle time during a client session is
+ reclaimed for other clients
+- Enables a smaller connection pool to serve the same workload
+- Connections are not held captive by idle client sessions
+
+## Combined Scenario
+
+With both flags enabled, upstream connections are pooled across clients **and**
+released between requests within a single client session. This is the most
+aggressive connection-sharing mode, providing the highest potential connection
+reuse.
+
+## Pool Metrics
+
+The `ServerConnectionPool` interface exposes `getMetrics()`, returning a
+`PoolMetrics` object with:
+
+| Metric | Description |
+|------------------------|------------------------------------------|
+| `getTotalConnections()` | Total connections in the pool |
+| `getActiveConnections()`| Connections currently borrowed |
+| `getIdleConnections()` | Connections available for reuse |
+| `getBorrowCount()` | Cumulative borrow operations |
+| `getReturnCount()` | Cumulative return operations |
+| `getEvictionCount()` | Cumulative eviction operations |
+| `getValidationFailureCount()` | Connections that failed validation |
+
+These metrics are accessible from tests or monitoring code by casting
+`HttpProxyServer` to `DefaultHttpProxyServer` and calling
+`getServerConnectionPool().getMetrics()`.
+
+## Feature Interaction Matrix
+
+| `useSharedPool` | `poolSharedMitm` | `poolPerRequest` | Behavior |
+|---|---|---|---|
+| `false` | — | — | Legacy: dedicated connection per client per host. All connections are tracked in `serverConnectionsByHostAndPort` and closed on disconnect. Main-branch behavior. |
+| `true` | `false` | `false` | Plain HTTP is pooled. CONNECT and MITM use dedicated connections (main-branch pool behavior). |
+| `true` | `true` | `false` | Plain HTTP **and** MITM upstream connections are pooled across client sessions. Each MITM session holds one pooled connection until the client disconnects. |
+| `true` | `true` | `true` | Full pooling: plain HTTP and MITM connections are borrowed per request and released back to the pool between requests within the same client session. |
+
+(`poolPerRequestInMitm=true` without `poolSharedMitmConnections=true` has no
+effect — per-request mode requires the shared pool for MITM.)
+
+## Configuration Reference
+
+### Builder methods on `HttpProxyServerBootstrap`
+
+```java
+// Base pooling (PR #724 infrastructure)
+.withSharedServerConnectionPool(boolean)
+.withServerConnectionPoolType(ServerConnectionPoolType)
+.withMaxConnectionsPerHost(int)
+.withMaxConnections(int)
+.withPoolIdleTimeout(Duration)
+
+// MITM-specific (this branch)
+.withPoolSharedMitmConnections(boolean) // default false
+.withPoolPerRequestInMitm(boolean) // default false
+```
+
+### Properties file keys (for `--config`)
+
+```properties
+# Base pooling
+use_shared_server_connection_pool=true
+server_connection_pool_type=CONCURRENT_MAP
+max_connections_per_host=10
+max_total_connections=200
+
+# MITM-specific
+pool_shared_mitm_connections=true
+pool_per_request_in_mitm=true
+```
+
+## Test Coverage
+
+### Unit tests
+
+| Test class | Tests added | What it covers |
+|---|---|---|
+| `ServerConnectionPoolConfigTest` | 8 | Default values, fluent setters/getters, independence from `enabled` flag |
+| `DefaultHttpProxyServerBootstrapTest` | 5 | Property parsing for both flags via `Properties` constructor |
+
+### Integration tests
+
+| Test class | Tag | Tests | What it covers |
+|---|---|---|---|
+| `MitmWithSharedPoolTest` | — | 4 | GET, POST, cross-client reuse, pool metrics (borrow count) |
+| `MitmWithPerRequestPoolTest` | `slow-test` | 4 | GET, POST, sequential reuse, cross-client reuse |
+
+### Existing tests exercising the underlying pool infrastructure
+
+| Test class | Tests | What it covers |
+|---|---|---|
+| `ConcurrentMapServerConnectionPoolTest` | 24 | Pool implementation: borrow, release, eviction, pending requests |
+| `SharedConnectionPoolTest` | 13 | Integrated shared pool for plain HTTP |
+| `ServerConnectionPoolTypeTest` | 6 | Pool type selection |
+| `ClientToProxyConnectionShortCircuitTest` | 5 | Short-circuit filter response with pooled connections |
+| `ClientToProxyConnectionBackpressureTest` | 15 | Backpressure / saturation with pooled connections |
+
+## Excluded Protocols
+
+The following traffic types are never pooled and always use dedicated
+`ProxyToServerConnection` instances tracked in `serverConnectionsByHostAndPort`:
+
+| Protocol | Reason |
+|---|---|
+| **WebSocket** (`Upgrade: websocket`) | After the HTTP upgrade handshake, the HTTP codecs are replaced with `WebSocketFramePipeHandler`. The connection becomes a raw frame pipe between client and server with no HTTP request/response lifecycle to trigger pool release. |
+| **Tunneling** (non-MITM CONNECT, i.e. regular HTTPS) | Once the CONNECT response is sent, the connection enters raw TCP tunneling mode. There are no HTTP request/response boundaries — all data flows bidirectionally until the tunnel closes, so the connection cannot be returned to the pool. This is the default HTTPS proxy behavior (no intercept), and it is **never** pooled. Only MITM-intercepting HTTPS can be pooled (behind `poolSharedMitmConnections`). |
+| **Switching Protocols** (other `Upgrade` headers) | Same as WebSocket — HTTP codecs are removed and the connection switches to a different protocol, making pool return impossible. |
+
+The `useSharedPool` condition explicitly checks `!isTunneling()` and
+`!ProxyUtils.isSwitchingToWebSocketProtocol(httpRequest)` before every request.
+No configuration flag can override these exclusions.
+
+## HTTP/2 Compatibility Note
+
+The underlying pooling architecture (pool key generation, connection tracking,
+and connection lifecycle) is independent of HTTP version and could support HTTP/2
+multiplexed streams. Future HTTP/2 server connections would reuse the same
+`PoolMetrics` infrastructure to track success/failure of multiplexed requests,
+though ALPN and stream-tracking adjustments would be required beyond the current
+implementation.
+
+## Known Limitations
+
+- **Metrics API not on `HttpProxyServer` interface:** To access pool metrics
+ from client code, cast to `DefaultHttpProxyServer`. This is a minor API gap.
+- **No `encryptForMitm()` integration test:** The code path reached via
+ `disableSslForNonTls` (retry after failed TLS to a plain-text server) has
+ no integration test coverage. A bug in `encryptForMitm()` analogous to the
+ `MitmEncryptClientChannel` bug was fixed during development.
diff --git a/littleproxy.properties b/littleproxy.properties
deleted file mode 100644
index 452ac128..00000000
--- a/littleproxy.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-# Exposes proxy connection properties via JMX.
-jmx=false
-# Idle connections are disconnected after X seconds of inactivity
-idle_connection_timeout=70
\ No newline at end of file
diff --git a/littleproxy_cert b/littleproxy_cert
deleted file mode 100644
index d31898a2..00000000
Binary files a/littleproxy_cert and /dev/null differ
diff --git a/pom.xml b/pom.xml
index f46b2e48..e3e64b68 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,57 +1,97 @@
4.0.0
- xyz.rogfam
+ io.github.littleproxylittleproxyjar
- 2.0.0-beta-6-SNAPSHOT
+ 2.10.0-SNAPSHOTLittleProxy
LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
- https://github.com/mrog/LittleProxy
+ https://github.com/LittleProxy/LittleProxyUTF-8UTF-8github
- 4.1.41.Final
- 1.7.28
- 1.8
+ 11
+ 17
+
+
+ 3.27.7
+ 1.11.0
+ 1.6.0
+ 2.22.0
+ 3.20.0
+ 4.0.0
+ 3.6.5
+ 0.1.6
+ 2.42.0
+ 33.7.1-jre
+ 4.5.14
+ 2.22.2
+ 11.0.24
+ 6.1.3
+ 2.26.1
+ 5.23.0
+ 4.2.18.Final
+ 4.49.0
+ 2.0.19
+ 3.13.2The Apache Software License, Version 2.0
- http://www.apache.org/licenses/LICENSE-2.0
+ https://www.apache.org/licenses/LICENSE-2.0github
- https://github.com/mrog/LittleProxy/issues
+ https://github.com/LittleProxy/LittleProxy/issues
- scm:git:https://github.com/mrog/LittleProxy.git
- scm:git:git@github.com:mrog/LittleProxy.git
- scm:git:https://github.com/mrog/LittleProxy
+ scm:git:https://github.com/LittleProxy/LittleProxy.git
+ scm:git:git@github.com:LittleProxy/LittleProxy.git
+ scm:git:https://github.com/LittleProxy/LittleProxyHEAD
-
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
-
- ossrh
- https://oss.sonatype.org/service/local/staging/deploy/maven2/
-
-
-
2009
+
+ smoke-test
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ slow-test
+ -ea -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=target/smoke-tests.hprof
+
+
+
+
+
+
+ slow-tests
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ slow-test
+ -ea -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=target/slow-tests.hprof
+
+
+
+
+ release
@@ -108,52 +148,47 @@
${gpg.keyname}
- ${gpg.keyname}gpg
-
- --pinentry-mode
- loopback
-
-
- org.sonatype.plugins
- nexus-staging-maven-plugin
- true
-
- ossrh
- https://oss.sonatype.org/
- false
-
-
-
- org.apache.maven.plugins
- maven-release-plugin
-
- true
- false
- release
- deploy
-
-
+
com.google.guavaguava
- 27.1-jre
+ ${guava.version}
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
+
+
+ org.jspecify
+ jspecify
+ 1.0.1
+ provided
+
+
+ com.google.errorprone
+ error_prone_annotations
+ ${error_prone.version}
+ providedcommons-clicommons-cli
- 1.4
+ ${commons.cli.version}true
@@ -161,183 +196,209 @@
org.apache.commonscommons-lang3
- 3.8.1
+ ${commons.lang3.version}
+
- junit
- junit
- 4.12
- test
+ io.netty
+ netty-buffer
-
- org.hamcrest
- hamcrest-core
- 2.1
- test
+ io.netty
+ netty-codec
-
- org.hamcrest
- hamcrest-library
- 2.1
- test
+ io.netty
+ netty-codec-http
-
- org.eclipse.jetty
- jetty-server
- 9.4.20.v20190813
- test
+ io.netty
+ netty-codec-haproxy
+
+
+ io.netty
+ netty-codec-socks
+
+
+ io.netty
+ netty-common
+
+
+ io.netty
+ netty-handler
+
+
+ io.netty
+ netty-handler-proxy
+
+
+ io.netty
+ netty-resolver
+
+
+ io.netty
+ netty-transport
+
- org.mockito
- mockito-core
- 2.25.1
- test
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ ${log4j.version}
+ true
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
+ true
+
+
+
+ com.lmax
+ disruptor
+ ${disruptor.version}
+ true
+
- org.mock-server
- mockserver-netty
- 5.6.1
- test
+ org.littleshoot
+ dnssec4j
+ ${dnssec4j.version}
+ true
- ch.qos.logback
- logback-classic
+ org.littleshoot
+ dnsjava
+
+
+ log4j
+ log4j
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ org.slf4j
+ slf4j-api
- org.seleniumhq.selenium
- selenium-java
- 3.141.59
- test
+ dnsjava
+ dnsjava
+ ${dnsjava.version}
+ true
- io.netty
- netty
+ org.slf4j
+ slf4j-api
+
- org.apache.logging.log4j
- log4j-core
- 2.11.2
- true
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
- org.apache.httpcomponents
- httpclient
- 4.5.8
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}test
- io.netty
- netty-all
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
- io.netty
- netty-example
+ org.wiremock
+ wiremock-standalone
+ ${wiremock.version}test
- com.barchart.udt
- barchart-udt-bundle
- 2.3.0
+ commons-io
+ commons-io
+ ${commons.io.version}
+ test
- org.littleshoot
- dnssec4j
- 0.1.6
- true
+ org.seleniumhq.selenium
+ selenium-java
+ ${selenium.version}
+ test
- org.littleshoot
- dnsjava
+ io.netty
+ netty
- dnsjava
- dnsjava
- 2.1.8
- true
-
-
-
- org.slf4j
- slf4j-log4j12
- ${slf4j.version}
- true
+ org.apache.httpcomponents
+ httpclient
+ ${httpclient.version}
+ test
- org.slf4j
- slf4j-api
- ${slf4j.version}
+ io.netty
+ netty-example
+ ${netty.version}
+ test
-
org.apache.commonscommons-exec
- 1.3
+ ${commons.exec.version}test
-
-
-
- io.netty
- netty-all
- ${netty.version}
-
-
- io.netty
- netty-buffer
- ${netty.version}
-
-
- io.netty
- netty-codec
- ${netty.version}
-
-
- io.netty
- netty-codec-haproxy
- ${netty.version}
-
-
- io.netty
- netty-codec-http
- ${netty.version}
-
-
- io.netty
- netty-codec-socks
- ${netty.version}
-
+
io.netty
- netty-common
+ netty-bom${netty.version}
+ import
+ pom
+
+
io.nettynetty-example
@@ -362,43 +423,14 @@
-
- io.netty
- netty-handler
- ${netty.version}
-
-
- io.netty
- netty-handler-proxy
- ${netty.version}
-
-
- io.netty
- netty-transport
- ${netty.version}
-
-
- io.netty
- netty-transport-rxtx
- ${netty.version}
-
-
- io.netty
- netty-transport-sctp
- ${netty.version}
-
-
- io.netty
- netty-transport-udp
- ${netty.version}
-
+
-
-
- com.fasterxml.jackson.core
- jackson-databind
- 2.9.9.3
+ com.fasterxml.jackson
+ jackson-bom
+ ${jackson.bom.version}
+ import
+ pom
@@ -409,80 +441,110 @@
org.apache.maven.pluginsmaven-enforcer-plugin
- 3.0.0-M2
+ 3.6.3org.apache.maven.pluginsmaven-site-plugin
- 3.8.2
+ 3.22.0org.apache.maven.pluginsmaven-release-plugin
- 2.5.3
+ 3.3.1org.apache.maven.pluginsmaven-dependency-plugin
- 3.1.1
+ 3.11.0org.apache.maven.pluginsmaven-clean-plugin
- 3.1.0
+ 3.5.0org.apache.maven.pluginsmaven-deploy-plugin
- 3.0.0-M1
+ 3.2.0org.apache.maven.pluginsmaven-compiler-plugin
- 3.8.0
+ 3.16.0
- ${java.version}
- ${java.version}
+ ${java.version}UTF-8
+
+
+ -XDcompilePolicy=simple
+ -Xplugin:ErrorProne -Xep:MissingSummary:OFF -Xep:JdkObsolete:OFF -Xep:ReferenceEquality:OFF -Xep:OperatorPrecedence:OFF
+
+ --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+ --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+ --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+ --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+ --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+ --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+
+
+
+ com.google.errorprone
+ error_prone_core
+ ${error_prone.version}
+
+
+
+
+ default-testCompile
+ test-compile
+
+ testCompile
+
+
+ ${java.version.tests}
+
+
+ org.apache.maven.pluginsmaven-install-plugin
- 3.0.0-M1
+ 3.2.0org.apache.maven.pluginsmaven-jar-plugin
- 3.1.1
+ 3.5.1org.apache.maven.pluginsmaven-resources-plugin
- 3.1.0
+ 3.5.0org.apache.maven.pluginsmaven-source-plugin
- 3.0.1
+ 3.4.0org.apache.maven.pluginsmaven-javadoc-plugin
- 3.1.1
+ 3.12.0
- all,-missing
+ all,-missing,-referenceprivate${java.version}
@@ -494,21 +556,14 @@
org.apache.maven.pluginsmaven-surefire-plugin
- 2.22.1
+ 3.6.0org.apache.maven.pluginsmaven-gpg-plugin
- 1.6
-
-
-
- org.sonatype.plugins
- nexus-staging-maven-plugin
- 1.6.8
+ 3.2.8
-
@@ -517,7 +572,7 @@
org.apache.maven.pluginsmaven-surefire-plugin
- -Xmx1g -XX:MaxPermSize=256m
+ -Xmx1g
@@ -534,7 +589,7 @@
org.apache.maven.pluginsmaven-shade-plugin
- 3.2.1
+ 3.6.2package
@@ -542,6 +597,7 @@
shade
+ falsetruelittleproxy-shade
@@ -549,7 +605,11 @@
org.bouncycastle:*
-
+
+ org.apache.logging.log4j:log4j-core:${log4j.version}
+ org.apache.logging.log4j:log4j-slf4j2-impl:${log4j.version}
+ com.lmax:disruptor:${disruptor.version}
+ *:*
@@ -565,11 +625,16 @@
org.littleshoot.proxy.Launcher
+ true
-
- log4j.xml
- src/main/config/log4j.xml
+
+
+
+ META-INF/log4j2-plugin.dat
+
+
+ META-INF/maven/org.apache.logging.log4j/log4j-core/pom.properties
@@ -591,12 +656,55 @@
3.0.4
+
+
+ junit:junit
+ org.hamcrest:hamcrest-core
+
+
-
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+ true
+
+ central
+ true
+ published
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 3.10.2
+
+
+
+ src/main/java/**/*.java
+ src/test/java/**/*.java
+
+
+ 1.28.0
+
+
+
+
+
+
+
+
+
+ apply
+
+ compile
+
+
+
@@ -621,7 +729,7 @@
org.apache.maven.pluginsmaven-project-info-reports-plugin
- 3.0.0
+ 3.9.0
@@ -632,7 +740,7 @@
org.apache.maven.pluginsmaven-surefire-report-plugin
- 3.0.0-M3
+ 3.6.0false
@@ -641,7 +749,7 @@
org.apache.maven.pluginsmaven-checkstyle-plugin
- 3.0.0
+ 3.6.0
@@ -675,13 +783,13 @@
org.apache.maven.pluginsmaven-jxr-plugin
- 3.0.0
+ 3.6.0org.apache.maven.pluginsmaven-pmd-plugin
- 3.11.0
+ 3.28.0trueutf-8
@@ -693,7 +801,7 @@
org.codehaus.mojoversions-maven-plugin
- 2.7
+ 2.22.0
@@ -709,7 +817,7 @@
org.codehaus.mojotaglist-maven-plugin
- 2.4
+ 3.2.3true
@@ -724,7 +832,6 @@
-
mrogers
@@ -735,5 +842,14 @@
Developer-7
+
+ asolntsev
+ Andrei Solntsev
+ andrei.solntsev+littleproxy@gmail.com
+ LittleProxy
+ https://github.com/LittleProxy
+ Maintainer
+ +3
+
diff --git a/public-gpg.key b/public-gpg.key
new file mode 100644
index 00000000..ce63b313
--- /dev/null
+++ b/public-gpg.key
@@ -0,0 +1,31 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFx5TsYBCAC5MFtvCeSvvt60CAY3P2TAoPxIdcOowB4yg/jMzw7bGOwBX5xm
+e7cfvIeuSvgR3UuMLbvcJU3R4q6WRCm8OsISFJjGduKO89FNS3EvetJwIXjPgla+
+v9LjmEVl4wzaBRSH6vzYQl3EbIHMVaW138HQLCbaULf6CxPp1+vBeAVHEmXa9qit
+Ly3sPgOlCm9g/x8o25k4wdj4xwpSFlUe/yl+yPqK9xjVNt0V9oR4NBm9bhsxOeo6
+LH/nQcaCKNRK5wK3ytVN1FgNEYv9cARuN6X7Qvi3Oi0P1+lVVpKJu0Ux0Gx0UB3O
+imnN3V61Pcs/yzHnyyW2OD0PUbX3JvcC4xdhABEBAAG0I01hcmsgUm9nZXJzIDxt
+YXJrLnJvZ2Vyc0BnbWFpbC5jb20+iQFUBBMBCAA+AhsDBQsJCAcCBhUKCQgLAgQW
+AgMBAh4BAheAFiEEzqtrzoRv2mNdpZZkFNFpBtlIKpIFAmDKNJ4FCQn0gFgACgkQ
+FNFpBtlIKpI2LwgAiYRIzB5ISp/Ie90TztYvFjByShxjYSIBE25pWhGOez9wgaEM
+g2uDIhQflVkY7U062Aqecnk686NsMA/McfgP0d2mr9fUxpxQoDHqOqtlmcWkt1k3
+sVZEPLM61OCyXUbU6cITcyDk9v1CLWfba/LKi0TteH+HQRa+/xrctHtuwpvPB2E4
+HCeVZT54DeyW1/YaHyJ1npo4AnGo+x1JGFOIVfJ6X74YHxvi0axcfdjJZVIP4qIZ
+yWWu5YcvhDFjddpDfd/Hh9be+Ym3msVA764anPKHJS00Eg6PQSoesqluh0c0ZUN3
+VxiNVk0+xBx68IgcSYP6kSP5JxuW8CP3detE2rkBDQRceU7GAQgAwaQT351vLvTU
+5kq0/SJxN1S41o4MYShLlI60InjHOTybjE9CdVeEkCivhcwtJVxXmNrAIw8T1vK2
+UxbmEPaii5fe7VSBShndIfwC4KCtCxFg2T7VcV8dftpbHW3E22a0zLbhpkgLcDzO
+EOFSRNl01k4j/Pjda+wHztVHcJdS5I1HzeEa20Sv9Agnsf4UGvRheLtj9h0aWb/o
+XZEhaEYAUDwHSBMd8B9OoG/ZL7sdNbLRwNKZYgDZ+K2k/Hg59+s5UoM+EVo0/ppf
+K1e5NFhiI7qsEUk2mh74+R8cjtdT5mARb4nZZKf1/cS7z1oS3st7Qv1wVkzLMnhA
+Q+fpmo/TuQARAQABiQE8BBgBCAAmAhsMFiEEzqtrzoRv2mNdpZZkFNFpBtlIKpIF
+AmDKNMIFCQn0gHwACgkQFNFpBtlIKpLoTAf/ZlPg4c4BfV5cZ6u3KanJsx8OpENn
+raPEEnyOnJhZdQHmxKUokMgtMwZLheA50jOh80pTPdQjkKStIrcWygE26iBLODKc
++dzjdHZUazU/P6671VNnIZbSVk2mNuJgPUafrGGgyD+hWRxv7rJ12cV4xKvlntf9
+M4gu1aKiXKqaOYMnVXn3eU4lrcfJqVW8tqiHLX3xuWn7IS3JLzQ5PmN7zHKNZO8o
+/beP/hjsW6ceO8AMkY0vMCeQMNRMovxdQ1VU5KcPJjoAHkpJQoA0rvSLrRYq6Vvk
+Bm6EikIxjJMPg9o6hoeg1+lwd0rdJSh9mPC4qlaQLUq0hPOZ5gTVnPtWzQ==
+=lK6I
+-----END PGP PUBLIC KEY BLOCK-----
+
diff --git a/run.bash b/run.bash
index a69d457f..9cd2e049 100755
--- a/run.bash
+++ b/run.bash
@@ -4,12 +4,74 @@ function die() {
exit 1
}
+# Show help if requested
+if [[ "$1" == "--help" || "$1" == "-h" || "$1" == "help" ]]; then
+ echo "LittleProxy Run Script"
+ echo "Usage: $0 [options]"
+ echo ""
+ echo "Options:"
+ echo " --server Run as server"
+ echo " --config Configuration file path"
+ echo " --port Port to listen on"
+ echo " --log_config Log4j2 configuration file path"
+ echo " --activity_log_format Activity log format (CLF, JSON, etc.)"
+ echo " --async_logging_default Use asynchronous logging with default config"
+ echo " --help, -h, help Show this help message"
+ echo ""
+ echo "Example:"
+ echo " $0 --server --config ./config/littleproxy.properties --port 9092 --async_logging_default"
+ exit 0
+fi
+
mvn package -Dmaven.test.skip=true || die "Could not package"
fullPath=`dirname $0`
jar=`find $fullPath/target/littleproxy*-littleproxy-shade.jar`
cp=`echo $jar | sed 's,./,'$fullPath'/,'`
-javaArgs="-server -XX:+HeapDumpOnOutOfMemoryError -Xmx800m -jar "$cp" $*"
+
+# Initialize Java arguments
+javaArgs="-server -XX:+HeapDumpOnOutOfMemoryError -Xmx800m"
+
+# Parse arguments to handle --async_logging_default flag and build proper argument list
+async_logging_default=false
+remaining_args=()
+log_config_set=false
+custom_log_config=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --async_logging_default)
+ async_logging_default=true
+ shift
+ ;;
+ --log_config)
+ log_config_set=true
+ custom_log_config="$2"
+ remaining_args+=("--log_config" "$2")
+ shift 2
+ ;;
+ --log_config=*)
+ log_config_set=true
+ custom_log_config="${1#*=}"
+ remaining_args+=("$1")
+ shift
+ ;;
+ *)
+ remaining_args+=("$1")
+ shift
+ ;;
+ esac
+done
+
+# Add async logging if flag is set AND no custom log config is provided
+if [ "$async_logging_default" = true ] && [ "$log_config_set" = false ]; then
+ echo "Async logging enabled (using default async configuration)"
+ remaining_args+=("--log_config" "./target/classes/littleproxy_async_log4j2.xml")
+elif [ "$async_logging_default" = true ] && [ "$log_config_set" = true ]; then
+ echo "Warning: --async_logging_default flag ignored because custom --log_config is specified: $custom_log_config"
+fi
+
+javaArgs="$javaArgs -jar "$cp" ${remaining_args[@]}"
echo "Running using Java on path at `which java` with args $javaArgs"
java $javaArgs || die "Java process exited abnormally"
diff --git a/src/main/config/log4j.xml b/src/main/config/log4j.xml
deleted file mode 100644
index f284b3fa..00000000
--- a/src/main/config/log4j.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/java/org/littleshoot/proxy/ActivityTracker.java b/src/main/java/org/littleshoot/proxy/ActivityTracker.java
index 28b9cc5e..9b8bb352 100644
--- a/src/main/java/org/littleshoot/proxy/ActivityTracker.java
+++ b/src/main/java/org/littleshoot/proxy/ActivityTracker.java
@@ -2,146 +2,142 @@
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
-
-import javax.net.ssl.SSLSession;
import java.net.InetSocketAddress;
+import javax.net.ssl.SSLSession;
/**
- *
* Interface for receiving information about activity in the proxy.
- *
- *
- *
- * Sub-classes may wish to extend {@link ActivityTrackerAdapter} for sensible
- * defaults.
- *
+ *
+ *
Sub-classes may wish to extend {@link ActivityTrackerAdapter} for sensible defaults.
*/
public interface ActivityTracker {
- /**
- * Record that a client connected.
- */
- void clientConnected(InetSocketAddress clientAddress);
-
- /**
- * Record that a client's SSL handshake completed.
- */
- void clientSSLHandshakeSucceeded(InetSocketAddress clientAddress,
- SSLSession sslSession);
-
- /**
- * Record that a client disconnected.
- */
- void clientDisconnected(InetSocketAddress clientAddress,
- SSLSession sslSession);
-
- /**
- * Record that the proxy received bytes from the client.
- *
- * @param flowContext
- * if full information is available, this will be a
- * {@link FullFlowContext}.
- * @param numberOfBytes
- */
- void bytesReceivedFromClient(FlowContext flowContext,
- int numberOfBytes);
-
- /**
- *
- * Record that proxy received an {@link HttpRequest} from the client.
- *
- *
- *
- * Note - on chunked transfers, this is only called once (for the initial
- * HttpRequest object).
- *
- *
- * @param flowContext
- * if full information is available, this will be a
- * {@link FullFlowContext}.
- * @param httpRequest
- */
- void requestReceivedFromClient(FlowContext flowContext,
- HttpRequest httpRequest);
-
- /**
- * Record that the proxy attempted to send bytes to the server.
- *
- * @param flowContext
- * provides contextual information about the flow
- * @param numberOfBytes
- */
- void bytesSentToServer(FullFlowContext flowContext, int numberOfBytes);
-
- /**
- *
- * Record that proxy attempted to send a request to the server.
- *
- *
- *
- * Note - on chunked transfers, this is only called once (for the initial
- * HttpRequest object).
- *
- *
- * @param flowContext
- * provides contextual information about the flow
- * @param httpRequest
- */
- void requestSentToServer(FullFlowContext flowContext,
- HttpRequest httpRequest);
-
- /**
- * Record that the proxy received bytes from the server.
- *
- * @param flowContext
- * provides contextual information about the flow
- * @param numberOfBytes
- */
- void bytesReceivedFromServer(FullFlowContext flowContext, int numberOfBytes);
-
- /**
- *
- * Record that the proxy received an {@link HttpResponse} from the server.
- *
- *
- *
- * Note - on chunked transfers, this is only called once (for the initial
- * HttpRequest object).
- *
- *
- * @param flowContext
- * provides contextual information about the flow
- * @param httpResponse
- */
- void responseReceivedFromServer(FullFlowContext flowContext,
- HttpResponse httpResponse);
-
- /**
- * Record that the proxy sent bytes to the client.
- *
- * @param flowContext
- * if full information is available, this will be a
- * {@link FullFlowContext}.
- * @param numberOfBytes
- */
- void bytesSentToClient(FlowContext flowContext, int numberOfBytes);
-
- /**
- *
- * Record that the proxy sent a response to the client.
- *
- *
- *
- * Note - on chunked transfers, this is only called once (for the initial
- * HttpRequest object).
- *
- *
- * @param flowContext
- * if full information is available, this will be a
- * {@link FullFlowContext}.
- * @param httpResponse
- */
- void responseSentToClient(FlowContext flowContext,
- HttpResponse httpResponse);
+ /** Record that a client connected. */
+ void clientConnected(FlowContext flowContext);
+
+ /** Record that a client's SSL handshake started. */
+ void clientSSLHandshakeStarted(FlowContext flowContext);
+
+ /** Record that a client's SSL handshake completed. */
+ void clientSSLHandshakeSucceeded(FlowContext flowContext, SSLSession sslSession);
+
+ /** Record that a client disconnected. */
+ void clientDisconnected(FlowContext flowContext, SSLSession sslSession);
+
+ /**
+ * Record that the proxy received bytes from the client.
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ * @param numberOfBytes
+ */
+ void bytesReceivedFromClient(FlowContext flowContext, int numberOfBytes);
+
+ /**
+ * Record that proxy received an {@link HttpRequest} from the client.
+ *
+ *
Note - on chunked transfers, this is only called once (for the initial HttpRequest object).
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ * @param httpRequest
+ */
+ void requestReceivedFromClient(FlowContext flowContext, HttpRequest httpRequest);
+
+ /**
+ * Record that the proxy attempted to send bytes to the server.
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param numberOfBytes
+ */
+ void bytesSentToServer(FullFlowContext flowContext, int numberOfBytes);
+
+ /**
+ * Record that proxy attempted to send a request to the server.
+ *
+ *
Note - on chunked transfers, this is only called once (for the initial HttpRequest object).
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param httpRequest
+ */
+ void requestSentToServer(FullFlowContext flowContext, HttpRequest httpRequest);
+
+ /**
+ * Record that the proxy received bytes from the server.
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param numberOfBytes
+ */
+ void bytesReceivedFromServer(FullFlowContext flowContext, int numberOfBytes);
+
+ /**
+ * Record that the proxy received an {@link HttpResponse} from the server.
+ *
+ *
Note - on chunked transfers, this is only called once (for the initial HttpRequest object).
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param httpResponse
+ */
+ void responseReceivedFromServer(FullFlowContext flowContext, HttpResponse httpResponse);
+
+ /**
+ * Record that the proxy sent bytes to the client.
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ * @param numberOfBytes
+ */
+ void bytesSentToClient(FlowContext flowContext, int numberOfBytes);
+
+ /**
+ * Record that the proxy sent a response to the client.
+ *
+ *
Note - on chunked transfers, this is only called once (for the initial HttpRequest object).
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ * @param httpResponse
+ */
+ void responseSentToClient(FlowContext flowContext, HttpResponse httpResponse);
+
+ /**
+ * Record that the proxy connected to the server.
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param serverAddress the address of the server that was connected
+ */
+ void serverConnected(FullFlowContext flowContext, InetSocketAddress serverAddress);
+
+ /**
+ * Record that the proxy disconnected from the server.
+ *
+ * @param flowContext provides contextual information about the flow
+ * @param serverAddress the address of the server that was disconnected
+ */
+ void serverDisconnected(FullFlowContext flowContext, InetSocketAddress serverAddress);
+
+ /**
+ * Record that a connection became saturated (not writable).
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ */
+ void connectionSaturated(FlowContext flowContext);
+
+ /**
+ * Record that a connection became writable again after being saturated.
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ */
+ void connectionWritable(FlowContext flowContext);
+
+ /**
+ * Record that a connection timed out due to idle timeout.
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ */
+ void connectionTimedOut(FlowContext flowContext);
+ /**
+ * Record that an exception was caught on a connection.
+ *
+ * @param flowContext if full information is available, this will be a {@link FullFlowContext}.
+ * @param cause the exception that was caught
+ */
+ void connectionExceptionCaught(FlowContext flowContext, Throwable cause);
}
diff --git a/src/main/java/org/littleshoot/proxy/ActivityTrackerAdapter.java b/src/main/java/org/littleshoot/proxy/ActivityTrackerAdapter.java
index 9a04838a..f0f052ec 100644
--- a/src/main/java/org/littleshoot/proxy/ActivityTrackerAdapter.java
+++ b/src/main/java/org/littleshoot/proxy/ActivityTrackerAdapter.java
@@ -2,67 +2,66 @@
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
-
-import javax.net.ssl.SSLSession;
import java.net.InetSocketAddress;
+import javax.net.ssl.SSLSession;
/**
- * Adapter of {@link ActivityTracker} interface that provides default no-op
- * implementations of all methods.
+ * Adapter of {@link ActivityTracker} interface that provides default no-op implementations of all
+ * methods.
*/
public class ActivityTrackerAdapter implements ActivityTracker {
- @Override
- public void bytesReceivedFromClient(FlowContext flowContext,
- int numberOfBytes) {
- }
-
- @Override
- public void requestReceivedFromClient(FlowContext flowContext,
- HttpRequest httpRequest) {
- }
-
- @Override
- public void bytesSentToServer(FullFlowContext flowContext, int numberOfBytes) {
- }
-
- @Override
- public void requestSentToServer(FullFlowContext flowContext,
- HttpRequest httpRequest) {
- }
-
- @Override
- public void bytesReceivedFromServer(FullFlowContext flowContext,
- int numberOfBytes) {
- }
-
- @Override
- public void responseReceivedFromServer(FullFlowContext flowContext,
- HttpResponse httpResponse) {
- }
-
- @Override
- public void bytesSentToClient(FlowContext flowContext,
- int numberOfBytes) {
- }
-
- @Override
- public void responseSentToClient(FlowContext flowContext,
- HttpResponse httpResponse) {
- }
-
- @Override
- public void clientConnected(InetSocketAddress clientAddress) {
- }
-
- @Override
- public void clientSSLHandshakeSucceeded(InetSocketAddress clientAddress,
- SSLSession sslSession) {
- }
-
- @Override
- public void clientDisconnected(InetSocketAddress clientAddress,
- SSLSession sslSession) {
- }
+ @Override
+ public void bytesReceivedFromClient(FlowContext flowContext, int numberOfBytes) {}
+
+ @Override
+ public void requestReceivedFromClient(FlowContext flowContext, HttpRequest httpRequest) {}
+
+ @Override
+ public void bytesSentToServer(FullFlowContext flowContext, int numberOfBytes) {}
+
+ @Override
+ public void requestSentToServer(FullFlowContext flowContext, HttpRequest httpRequest) {}
+
+ @Override
+ public void bytesReceivedFromServer(FullFlowContext flowContext, int numberOfBytes) {}
+
+ @Override
+ public void responseReceivedFromServer(FullFlowContext flowContext, HttpResponse httpResponse) {}
+
+ @Override
+ public void bytesSentToClient(FlowContext flowContext, int numberOfBytes) {}
+
+ @Override
+ public void responseSentToClient(FlowContext flowContext, HttpResponse httpResponse) {}
+
+ @Override
+ public void clientConnected(FlowContext flowContext) {}
+
+ @Override
+ public void clientSSLHandshakeStarted(FlowContext flowContext) {}
+
+ @Override
+ public void clientSSLHandshakeSucceeded(FlowContext flowContext, SSLSession sslSession) {}
+
+ @Override
+ public void clientDisconnected(FlowContext flowContext, SSLSession sslSession) {}
+
+ @Override
+ public void serverConnected(FullFlowContext flowContext, InetSocketAddress serverAddress) {}
+
+ @Override
+ public void serverDisconnected(FullFlowContext flowContext, InetSocketAddress serverAddress) {}
+
+ @Override
+ public void connectionSaturated(FlowContext flowContext) {}
+
+ @Override
+ public void connectionWritable(FlowContext flowContext) {}
+
+ @Override
+ public void connectionTimedOut(FlowContext flowContext) {}
+ @Override
+ public void connectionExceptionCaught(FlowContext flowContext, Throwable cause) {}
}
diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxy.java b/src/main/java/org/littleshoot/proxy/ChainedProxy.java
index d39d0f09..2fc960e7 100644
--- a/src/main/java/org/littleshoot/proxy/ChainedProxy.java
+++ b/src/main/java/org/littleshoot/proxy/ChainedProxy.java
@@ -1,92 +1,77 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpObject;
-
import java.net.InetSocketAddress;
/**
- *
* Encapsulates information needed to connect to a chained proxy.
- *
- *
- *
- * Sub-classes may wish to extend {@link ChainedProxyAdapter} for sensible
- * defaults.
- *
+ *
+ *
Sub-classes may wish to extend {@link ChainedProxyAdapter} for sensible defaults.
*/
public interface ChainedProxy extends SslEngineSource {
- /**
- * Return the {@link InetSocketAddress} for connecting to the chained proxy.
- * Returning null indicates that we won't chain.
- *
- * @return The Chain Proxy with Host and Port.
- */
- InetSocketAddress getChainedProxyAddress();
+ /**
+ * Return the {@link InetSocketAddress} for connecting to the chained proxy. Returning null
+ * indicates that we won't chain.
+ *
+ * @return The Chain Proxy with Host and Port.
+ */
+ InetSocketAddress getChainedProxyAddress();
- /**
- * (Optional) ensure that the connection is opened from a specific local
- * address (useful when doing NAT traversal).
- */
- InetSocketAddress getLocalAddress();
+ /**
+ * (Optional) ensure that the connection is opened from a specific local address (useful when
+ * doing NAT traversal).
+ */
+ InetSocketAddress getLocalAddress();
- /**
- * Tell LittleProxy what kind of TransportProtocol to use to communicate
- * with the chained proxy.
- */
- TransportProtocol getTransportProtocol();
+ /**
+ * Tell LittleProxy what kind of TransportProtocol to use to communicate with the chained proxy.
+ */
+ TransportProtocol getTransportProtocol();
- /**
- * Tell LittleProxy the type of chained proxy that it will be
- * connecting to. This setting determines what type of requests
- * LittleProxy will use to communicate with the chained proxy.
- * @return the chained proxy type.
- */
- ChainedProxyType getChainedProxyType();
+ /**
+ * Tell LittleProxy the type of chained proxy that it will be connecting to. This setting
+ * determines what type of requests LittleProxy will use to communicate with the chained proxy.
+ *
+ * @return the chained proxy type.
+ */
+ ChainedProxyType getChainedProxyType();
- /**
- * (Optional) implement this method if the chained proxy requires
- * a username.
- * @return the username to send to the chained proxy.
- */
- String getUsername();
+ /**
+ * (Optional) implement this method if the chained proxy requires a username.
+ *
+ * @return the username to send to the chained proxy.
+ */
+ String getUsername();
- /**
- * (Optional) implement this method if the chained proxy requires
- * a password.
- * @return the password to send to the chained proxy.
- */
- String getPassword();
+ /**
+ * (Optional) implement this method if the chained proxy requires a password.
+ *
+ * @return the password to send to the chained proxy.
+ */
+ String getPassword();
- /**
- * Implement this method to tell LittleProxy whether or not to encrypt
- * connections to the chained proxy for the given request. If true,
- * LittleProxy will call {@link SslEngineSource#newSslEngine()} to obtain an
- * SSLContext used by the downstream proxy.
- *
- * @return true of the connection to the chained proxy should be encrypted
- */
- boolean requiresEncryption();
+ /**
+ * Implement this method to tell LittleProxy whether to encrypt connections to the chained proxy
+ * for the given request. If true, LittleProxy will call {@link SslEngineSource#newSslEngine()} to
+ * obtain an SSLContext used by the downstream proxy.
+ *
+ * @return true of the connection to the chained proxy should be encrypted
+ */
+ boolean requiresEncryption();
- /**
- * Filters requests on their way to the chained proxy.
- */
- void filterRequest(HttpObject httpObject);
+ /** Filters requests on their way to the chained proxy. */
+ void filterRequest(HttpObject httpObject);
- /**
- * Called to let us know that connecting to this proxy succeeded.
- */
- void connectionSucceeded();
+ /** Called to let us know that connecting to this proxy succeeded. */
+ void connectionSucceeded();
- /**
- * Called to let us know that connecting to this proxy failed.
- *
- * @param cause
- * exception that caused this failure (may be null)
- */
- void connectionFailed(Throwable cause);
+ /**
+ * Called to let us know that connecting to this proxy failed.
+ *
+ * @param cause exception that caused this failure (maybe null)
+ */
+ void connectionFailed(Throwable cause);
- /**
- * Called to let us know that we were disconnected.
- */
- void disconnected();
+ /** Called to let us know that we were disconnected. */
+ void disconnected();
}
diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java b/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java
index 947c7486..21fd8c80 100644
--- a/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java
+++ b/src/main/java/org/littleshoot/proxy/ChainedProxyAdapter.java
@@ -1,78 +1,71 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpObject;
-
-import javax.net.ssl.SSLEngine;
import java.net.InetSocketAddress;
+import javax.net.ssl.SSLEngine;
-/**
- * Convenience base class for implementations of {@link ChainedProxy}.
- */
+/** Convenience base class for implementations of {@link ChainedProxy}. */
public class ChainedProxyAdapter implements ChainedProxy {
- /**
- * {@link ChainedProxy} that simply has the downstream proxy make a direct
- * connection to the upstream server.
- */
- public static ChainedProxy FALLBACK_TO_DIRECT_CONNECTION = new ChainedProxyAdapter();
+ /**
+ * {@link ChainedProxy} that simply has the downstream proxy make a direct connection to the
+ * upstream server.
+ */
+ public static final ChainedProxy FALLBACK_TO_DIRECT_CONNECTION = new ChainedProxyAdapter();
+
+ @Override
+ public InetSocketAddress getChainedProxyAddress() {
+ return null;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress() {
+ return null;
+ }
+
+ @Override
+ public TransportProtocol getTransportProtocol() {
+ return TransportProtocol.TCP;
+ }
+
+ @Override
+ public ChainedProxyType getChainedProxyType() {
+ return ChainedProxyType.HTTP;
+ }
+
+ @Override
+ public String getUsername() {
+ return null;
+ }
- @Override
- public InetSocketAddress getChainedProxyAddress() {
- return null;
- }
+ @Override
+ public String getPassword() {
+ return null;
+ }
- @Override
- public InetSocketAddress getLocalAddress() {
- return null;
- }
+ @Override
+ public boolean requiresEncryption() {
+ return false;
+ }
- @Override
- public TransportProtocol getTransportProtocol() {
- return TransportProtocol.TCP;
- }
-
- @Override
- public ChainedProxyType getChainedProxyType() {
- return ChainedProxyType.HTTP;
- }
-
- @Override
- public String getUsername() {
- return null;
- }
-
- @Override
- public String getPassword() {
- return null;
- }
+ @Override
+ public SSLEngine newSslEngine() {
+ return null;
+ }
- @Override
- public boolean requiresEncryption() {
- return false;
- }
+ @Override
+ public void filterRequest(HttpObject httpObject) {}
- @Override
- public SSLEngine newSslEngine() {
- return null;
- }
-
- @Override
- public void filterRequest(HttpObject httpObject) {
- }
-
- @Override
- public void connectionSucceeded() {
- }
+ @Override
+ public void connectionSucceeded() {}
- @Override
- public void connectionFailed(Throwable cause) {
- }
+ @Override
+ public void connectionFailed(Throwable cause) {}
- @Override
- public void disconnected() {
- }
+ @Override
+ public void disconnected() {}
- @Override
- public SSLEngine newSslEngine(String peerHost, int peerPort) {
- return null;
- }
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return null;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java b/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
index 4ebf3f3f..ee2e92ea 100644
--- a/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
+++ b/src/main/java/org/littleshoot/proxy/ChainedProxyManager.java
@@ -1,37 +1,23 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
-import org.littleshoot.proxy.impl.ClientDetails;
-
import java.util.Queue;
+import org.littleshoot.proxy.impl.ClientDetails;
-/**
- *
- * Interface for classes that manage chained proxies.
- *
- */
+/** Interface for classes that manage chained proxies. */
public interface ChainedProxyManager {
- /**
- *
- * Based on the given httpRequest, add any {@link ChainedProxy}s to the list
- * that should be used to process the request. The downstream proxy will
- * attempt to connect to each of these in the order that they appear until
- * it successfully connects to one.
- *
- *
- *
- * To allow the proxy to fall back to a direct connection, you can add
- * {@link ChainedProxyAdapter#FALLBACK_TO_DIRECT_CONNECTION} to the end of
- * the list.
- *
- *
- *
- * To keep the proxy from attempting any connection, leave the list blank.
- * This will cause the proxy to return a 502 response.
- *
- */
- void lookupChainedProxies(HttpRequest httpRequest,
- Queue chainedProxies,
- ClientDetails clientDetails);
-}
\ No newline at end of file
+ /**
+ * Based on the given httpRequest, add any {@link ChainedProxy}s to the list that should be used
+ * to process the request. The downstream proxy will attempt to connect to each of these in the
+ * order that they appear until it successfully connects to one.
+ *
+ *
To allow the proxy to fall back to a direct connection, you can add {@link
+ * ChainedProxyAdapter#FALLBACK_TO_DIRECT_CONNECTION} to the end of the list.
+ *
+ *
To keep the proxy from attempting any connection, leave the list blank. This will cause the
+ * proxy to return a 502 response.
+ */
+ void lookupChainedProxies(
+ HttpRequest httpRequest, Queue chainedProxies, ClientDetails clientDetails);
+}
diff --git a/src/main/java/org/littleshoot/proxy/ChainedProxyType.java b/src/main/java/org/littleshoot/proxy/ChainedProxyType.java
index 1e9a4272..e75c1291 100644
--- a/src/main/java/org/littleshoot/proxy/ChainedProxyType.java
+++ b/src/main/java/org/littleshoot/proxy/ChainedProxyType.java
@@ -1,8 +1,8 @@
package org.littleshoot.proxy;
-/**
- * Enumeration of chained proxy types supported by LittleProxy.
- */
+/** Enumeration of chained proxy types supported by LittleProxy. */
public enum ChainedProxyType {
- HTTP, SOCKS4, SOCKS5
+ HTTP,
+ SOCKS4,
+ SOCKS5
}
diff --git a/src/main/java/org/littleshoot/proxy/DefaultHostResolver.java b/src/main/java/org/littleshoot/proxy/DefaultHostResolver.java
index 3519dcd9..ae274cb0 100644
--- a/src/main/java/org/littleshoot/proxy/DefaultHostResolver.java
+++ b/src/main/java/org/littleshoot/proxy/DefaultHostResolver.java
@@ -5,14 +5,13 @@
import java.net.UnknownHostException;
/**
- * Default implementation of {@link HostResolver} that just uses
- * {@link InetAddress#getByName(String)}.
+ * Default implementation of {@link HostResolver} that just uses {@link
+ * InetAddress#getByName(String)}.
*/
public class DefaultHostResolver implements HostResolver {
- @Override
- public InetSocketAddress resolve(String host, int port)
- throws UnknownHostException {
- InetAddress addr = InetAddress.getByName(host);
- return new InetSocketAddress(addr, port);
- }
+ @Override
+ public InetSocketAddress resolve(String host, int port) throws UnknownHostException {
+ InetAddress address = InetAddress.getByName(host);
+ return new InetSocketAddress(address, port);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/DnsSecServerResolver.java b/src/main/java/org/littleshoot/proxy/DnsSecServerResolver.java
index 90ee9d0d..2c35fa5d 100644
--- a/src/main/java/org/littleshoot/proxy/DnsSecServerResolver.java
+++ b/src/main/java/org/littleshoot/proxy/DnsSecServerResolver.java
@@ -1,14 +1,12 @@
package org.littleshoot.proxy;
-import org.littleshoot.dnssec4j.VerifiedAddressFactory;
-
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
+import org.littleshoot.dnssec4j.VerifiedAddressFactory;
public class DnsSecServerResolver implements HostResolver {
- @Override
- public InetSocketAddress resolve(String host, int port)
- throws UnknownHostException {
- return VerifiedAddressFactory.newInetSocketAddress(host, port, true);
- }
+ @Override
+ public InetSocketAddress resolve(String host, int port) throws UnknownHostException {
+ return VerifiedAddressFactory.newInetSocketAddress(host, port, true);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/FlowContext.java b/src/main/java/org/littleshoot/proxy/FlowContext.java
index ae4230fa..6eaa0f2b 100644
--- a/src/main/java/org/littleshoot/proxy/FlowContext.java
+++ b/src/main/java/org/littleshoot/proxy/FlowContext.java
@@ -1,42 +1,100 @@
package org.littleshoot.proxy;
-import org.littleshoot.proxy.impl.ClientToProxyConnection;
-
+import io.netty.handler.codec.haproxy.HAProxyMessage;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
-import java.net.InetSocketAddress;
+import org.littleshoot.proxy.impl.ClientToProxyConnection;
/**
- *
- * Encapsulates contextual information for flow information that's being
- * reported to a {@link ActivityTracker}.
- *
+ * Encapsulates contextual information for flow information that's being reported to a {@link
+ * ActivityTracker}.
*/
public class FlowContext {
- private final InetSocketAddress clientAddress;
- private final SSLSession clientSslSession;
-
- public FlowContext(ClientToProxyConnection clientConnection) {
- super();
- this.clientAddress = clientConnection.getClientAddress();
- SSLEngine sslEngine = clientConnection.getSslEngine();
- this.clientSslSession = sslEngine != null ? sslEngine.getSession()
- : null;
- }
+ private final ClientToProxyConnection clientConnection;
+ private final long connectionId;
+ private final Map timingData = new ConcurrentHashMap<>();
- /**
- * The address of the client.
- */
- public InetSocketAddress getClientAddress() {
- return clientAddress;
- }
+ /**
+ * Creates a new FlowContext for the given client connection.
+ *
+ * @param clientConnection the client-side connection that owns this flow
+ */
+ public FlowContext(ClientToProxyConnection clientConnection) {
+ this.clientConnection = clientConnection;
+ this.connectionId = clientConnection.getId();
+ }
- /**
- * If using SSL, this returns the {@link SSLSession} on the client
- * connection.
- */
- public SSLSession getClientSslSession() {
- return clientSslSession;
+ /**
+ * The client's address: the PROXY header's source address when PROXY protocol is in use,
+ * otherwise the TCP peer. Resolved lazily so a header received after construction is reflected.
+ *
+ * @return the client's socket address
+ */
+ public InetSocketAddress getClientAddress() {
+ HAProxyMessage haProxyMessage = clientConnection.getHaProxyMessage();
+ if (haProxyMessage != null
+ && haProxyMessage.sourceAddress() != null
+ && !haProxyMessage.sourceAddress().isBlank()) {
+ return new InetSocketAddress(haProxyMessage.sourceAddress(), haProxyMessage.sourcePort());
}
+ return clientConnection.getClientAddress();
+ }
+
+ /**
+ * If using SSL, this returns the {@link SSLSession} on the client connection.
+ *
+ * @return the SSL session, or null if the client connection is not using SSL
+ */
+ public SSLSession getClientSslSession() {
+ SSLEngine sslEngine = clientConnection.getSslEngine();
+ return sslEngine != null ? sslEngine.getSession() : null;
+ }
+
+ /**
+ * Stores timing data for this flow.
+ *
+ * @param key the timing metric key
+ * @param value the timing value in milliseconds
+ */
+ public void setTimingData(String key, Long value) {
+ Objects.requireNonNull(key, "timing key must not be null");
+ Objects.requireNonNull(value, "timing value must not be null");
+ timingData.put(key, value);
+ }
+
+ /**
+ * Retrieves timing data for this flow.
+ *
+ * @param key the timing metric key
+ * @return the timing value in milliseconds, or null if not available
+ */
+ public Long getTimingData(String key) {
+ return timingData.get(key);
+ }
+
+ /**
+ * Gets all timing data for this flow.
+ *
+ * @return map of all timing data
+ */
+ public Map getTimings() {
+ return Map.copyOf(timingData);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FlowContext)) return false;
+ FlowContext that = (FlowContext) o;
+ return connectionId == that.connectionId;
+ }
+ @Override
+ public int hashCode() {
+ return Long.hashCode(connectionId);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/FullFlowContext.java b/src/main/java/org/littleshoot/proxy/FullFlowContext.java
index cb8ac1d7..94ac86a7 100644
--- a/src/main/java/org/littleshoot/proxy/FullFlowContext.java
+++ b/src/main/java/org/littleshoot/proxy/FullFlowContext.java
@@ -1,35 +1,38 @@
package org.littleshoot.proxy;
+import io.netty.channel.ChannelHandlerContext;
import org.littleshoot.proxy.impl.ClientToProxyConnection;
import org.littleshoot.proxy.impl.ProxyToServerConnection;
/**
- * Extension of {@link FlowContext} that provides additional information (which
- * we know after actually processing the request from the client).
+ * Extension of {@link FlowContext} that provides additional information (which we know after
+ * actually processing the request from the client).
*/
public class FullFlowContext extends FlowContext {
- private final String serverHostAndPort;
- private final ChainedProxy chainedProxy;
+ private final String serverHostAndPort;
+ private final ChainedProxy chainedProxy;
+ private final ChannelHandlerContext ctx;
- public FullFlowContext(ClientToProxyConnection clientConnection,
- ProxyToServerConnection serverConnection) {
- super(clientConnection);
- this.serverHostAndPort = serverConnection.getServerHostAndPort();
- this.chainedProxy = serverConnection.getChainedProxy();
- }
+ public FullFlowContext(
+ ClientToProxyConnection clientConnection, ProxyToServerConnection serverConnection) {
+ super(clientConnection);
+ serverHostAndPort = serverConnection.getServerHostAndPort();
+ chainedProxy = serverConnection.getChainedProxy();
+ this.ctx = serverConnection.getContext();
+ }
- /**
- * The host and port for the server (i.e. the ultimate endpoint).
- */
- public String getServerHostAndPort() {
- return serverHostAndPort;
- }
+ /** The host and port for the server (i.e. the ultimate endpoint). */
+ public String getServerHostAndPort() {
+ return serverHostAndPort;
+ }
- /**
- * The chained proxy (if proxy chaining).
- */
- public ChainedProxy getChainedProxy() {
- return chainedProxy;
- }
+ /** The chained proxy (if proxy chaining). */
+ public ChainedProxy getChainedProxy() {
+ return chainedProxy;
+ }
+ /** The proxy to server channel context. */
+ public ChannelHandlerContext getProxyToServerContext() {
+ return ctx;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/HostResolver.java b/src/main/java/org/littleshoot/proxy/HostResolver.java
index 17197c9f..e3fb7eef 100644
--- a/src/main/java/org/littleshoot/proxy/HostResolver.java
+++ b/src/main/java/org/littleshoot/proxy/HostResolver.java
@@ -3,9 +3,7 @@
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
-/**
- * Resolves host and port into an InetSocketAddress.
- */
+/** Resolves host and port into an InetSocketAddress. */
public interface HostResolver {
- InetSocketAddress resolve(String host, int port) throws UnknownHostException;
+ InetSocketAddress resolve(String host, int port) throws UnknownHostException;
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpFilters.java b/src/main/java/org/littleshoot/proxy/HttpFilters.java
index b102d1d4..909919a3 100644
--- a/src/main/java/org/littleshoot/proxy/HttpFilters.java
+++ b/src/main/java/org/littleshoot/proxy/HttpFilters.java
@@ -1,211 +1,210 @@
package org.littleshoot.proxy;
import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.http.*;
-import org.littleshoot.proxy.impl.ProxyUtils;
-
+import io.netty.handler.codec.http.HttpContent;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.LastHttpContent;
import java.net.InetSocketAddress;
+import java.util.function.Supplier;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.impl.ProxyUtils;
/**
- *
- * Interface for objects that filter {@link HttpObject}s, including both
- * requests and responses, and informs of different steps in request/response.
- *
- *
- *
- * Multiple methods are defined, corresponding to different steps in the request
- * processing lifecycle. Some of these methods is given the current object
- * (request, response or chunk) and is allowed to modify it in place. Others
- * provide a notification of when specific operations happen (i.e. connection in
- * queue, DNS resolution, SSL handshaking and so forth).
- *
- *
- *
- * Because HTTP transfers can be chunked, for any given request or response, the
- * filter methods that can modify request/response in place may be called
- * multiple times, once for the initial {@link HttpRequest} or
- * {@link HttpResponse}, and once for each subsequent {@link HttpContent}. The
- * last chunk will always be a {@link LastHttpContent} and can be checked for
- * being last using {@link ProxyUtils#isLastChunk(HttpObject)}.
- *
- *
- *
- * {@link HttpFiltersSource#getMaximumRequestBufferSizeInBytes()} and
- * {@link HttpFiltersSource#getMaximumResponseBufferSizeInBytes()} can be used
- * to instruct the proxy to buffer the {@link HttpObject}s sent to all of its
- * request/response filters, in which case it will buffer up to the specified
- * limit and then send either complete {@link HttpRequest}s or
- * {@link HttpResponse}s to the filter methods. When buffering, if the proxy
- * receives more data than fits in the specified maximum bytes to buffer, the
- * proxy will stop processing the request and respond with a 502 Bad Gateway
- * error.
- *
- *
- *
- * A new instance of {@link HttpFilters} is created for each request, so these
- * objects can be stateful.
- *
- *
- *
- * To monitor (and time measure?) the different steps the request/response goes
- * through, many informative methods are provided. Those steps are reported in
- * the following order:
+ * Interface for objects that filter {@link HttpObject}s, including both requests and responses, and
+ * informs of different steps in request/response.
+ *
+ *
Multiple methods are defined, corresponding to different steps in the request processing
+ * lifecycle. Some of these methods is given the current object (request, response or chunk) and is
+ * allowed to modify it in place. Others provide a notification of when specific operations happen
+ * (i.e. connection in queue, DNS resolution, SSL handshaking and so forth).
+ *
+ *
Because HTTP transfers can be chunked, for any given request or response, the filter methods
+ * that can modify request/response in place may be called multiple times, once for the initial
+ * {@link HttpRequest} or {@link HttpResponse}, and once for each subsequent {@link HttpContent}.
+ * The last chunk will always be a {@link LastHttpContent} and can be checked for being last using
+ * {@link ProxyUtils#isLastChunk(HttpObject)}.
+ *
+ *
{@link HttpFiltersSource#getMaximumRequestBufferSizeInBytes()} and {@link
+ * HttpFiltersSource#getMaximumResponseBufferSizeInBytes()} can be used to instruct the proxy to
+ * buffer the {@link HttpObject}s sent to all of its request/response filters, in which case it will
+ * buffer up to the specified limit and then send either complete {@link HttpRequest}s or {@link
+ * HttpResponse}s to the filter methods. When buffering, if the proxy receives more data than fits
+ * in the specified maximum bytes to buffer, the proxy will stop processing the request and respond
+ * with a 502 Bad Gateway error.
+ *
+ *
A new instance of {@link HttpFilters} is created for each request, so these objects can be
+ * stateful.
+ *
+ *
To monitor (and time measure?) the different steps the request/response goes through, many
+ * informative methods are provided. Those steps are reported in the following order:
+ *
*
- *
clientToProxyRequest
- *
proxyToServerConnectionQueued
- *
proxyToServerResolutionStarted
- *
proxyToServerResolutionSucceeded
- *
proxyToServerRequest (can be multiple if chunked)
- *
proxyToServerConnectionStarted
- *
proxyToServerConnectionFailed (if connection couldn't be established)
- *
proxyToServerConnectionSSLHandshakeStarted (only if HTTPS required)
- *
proxyToServerConnectionSucceeded
- *
proxyToServerRequestSending
- *
proxyToServerRequestSent
- *
serverToProxyResponseReceiving
- *
serverToProxyResponse (can be multiple if chuncked)
- *
serverToProxyResponseReceived
- *
proxyToClientResponse
+ *
clientToProxyRequest
+ *
proxyToServerConnectionQueued
+ *
proxyToServerResolutionStarted
+ *
proxyToServerResolutionSucceeded
+ *
proxyToServerRequest (can be multiple if chunked)
+ *
proxyToServerConnectionStarted
+ *
proxyToServerConnectionFailed (if connection couldn't be established)
+ *
proxyToServerConnectionSSLHandshakeStarted (only if HTTPS required)
+ *
proxyToServerConnectionSucceeded
+ *
proxyToServerRequestSending
+ *
proxyToServerRequestSent
+ *
serverToProxyResponseReceiving
+ *
serverToProxyResponse (can be multiple if chunked)
+ *
serverToProxyResponseReceived
+ *
proxyToClientResponse
*
*/
public interface HttpFilters {
- /**
- * Filters requests on their way from the client to the proxy. To interrupt processing of this request and return a
- * response to the client immediately, return an HttpResponse here. Otherwise, return null to continue processing as
- * usual.
- *
- * Important: When returning a response, you must include a mechanism to allow the client to determine the length
- * of the message (see RFC 7230, section 3.3.3: https://tools.ietf.org/html/rfc7230#section-3.3.3 ). For messages that
- * may contain a body, you may do this by setting the Transfer-Encoding to chunked, setting an appropriate
- * Content-Length, or by adding a "Connection: close" header to the response (which will instruct LittleProxy to close
- * the connection). If the short-circuit response contains body content, it is recommended that you return a
- * FullHttpResponse.
- *
- * @param httpObject Client to Proxy HttpRequest (and HttpContent, if chunked)
- * @return a short-circuit response, or null to continue processing as usual
- */
- HttpResponse clientToProxyRequest(HttpObject httpObject);
-
- /**
- * Filters requests on their way from the proxy to the server. To interrupt processing of this request and return a
- * response to the client immediately, return an HttpResponse here. Otherwise, return null to continue processing as
- * usual.
- *
- * Important: When returning a response, you must include a mechanism to allow the client to determine the length
- * of the message (see RFC 7230, section 3.3.3: https://tools.ietf.org/html/rfc7230#section-3.3.3 ). For messages that
- * may contain a body, you may do this by setting the Transfer-Encoding to chunked, setting an appropriate
- * Content-Length, or by adding a "Connection: close" header to the response. (which will instruct LittleProxy to close
- * the connection). If the short-circuit response contains body content, it is recommended that you return a
- * FullHttpResponse.
- *
- * @param httpObject Proxy to Server HttpRequest (and HttpContent, if chunked)
- * @return a short-circuit response, or null to continue processing as usual
- */
- HttpResponse proxyToServerRequest(HttpObject httpObject);
-
- /**
- * Informs filter that proxy to server request is being sent.
- */
- void proxyToServerRequestSending();
-
- /**
- * Informs filter that the HTTP request, including any content, has been sent.
- */
- void proxyToServerRequestSent();
-
- /**
- * Filters responses on their way from the server to the proxy.
- *
- * @param httpObject
- * Server to Proxy HttpResponse (and HttpContent, if chunked)
- * @return the modified (or unmodified) HttpObject. Returning null will
- * force a disconnect.
- */
- HttpObject serverToProxyResponse(HttpObject httpObject);
-
- /**
- * Informs filter that a timeout occurred before the server response was received by the client. The timeout may have
- * occurred while the client was sending the request, waiting for a response, or after the client started receiving
- * a response (i.e. if the response from the server "stalls").
- *
- * See {@link HttpProxyServerBootstrap#withIdleConnectionTimeout(int)} for information on setting the timeout.
- */
- void serverToProxyResponseTimedOut();
-
- /**
- * Informs filter that server to proxy response is being received.
- */
- void serverToProxyResponseReceiving();
-
- /**
- * Informs filter that server to proxy response has been received.
- */
- void serverToProxyResponseReceived();
-
- /**
- * Filters responses on their way from the proxy to the client.
- *
- * @param httpObject
- * Proxy to Client HttpResponse (and HttpContent, if chunked)
- * @return the modified (or unmodified) HttpObject. Returning null will
- * force a disconnect.
- */
- HttpObject proxyToClientResponse(HttpObject httpObject);
-
- /**
- * Informs filter that proxy to server connection is in queue.
- */
- void proxyToServerConnectionQueued();
-
- /**
- * Filter DNS resolution from proxy to server.
- *
- * @param resolvingServerHostAndPort
- * Server "HOST:PORT"
- * @return alternative address resolution. Returning null will let normal
- * DNS resolution continue.
- */
- InetSocketAddress proxyToServerResolutionStarted(
- String resolvingServerHostAndPort);
-
- /**
- * Informs filter that proxy to server DNS resolution failed for the specified host and port.
- *
- * @param hostAndPort hostname and port the proxy failed to resolve
- */
- void proxyToServerResolutionFailed(String hostAndPort);
-
- /**
- * Informs filter that proxy to server DNS resolution has happened.
- *
- * @param serverHostAndPort
- * Server "HOST:PORT"
- * @param resolvedRemoteAddress
- * Address it was proxyToServerResolutionSucceeded to
- */
- void proxyToServerResolutionSucceeded(String serverHostAndPort,
- InetSocketAddress resolvedRemoteAddress);
-
- /**
- * Informs filter that proxy to server connection is initiating.
- */
- void proxyToServerConnectionStarted();
-
- /**
- * Informs filter that proxy to server ssl handshake is initiating.
- */
- void proxyToServerConnectionSSLHandshakeStarted();
-
- /**
- * Informs filter that proxy to server connection has failed.
- */
- void proxyToServerConnectionFailed();
-
- /**
- * Informs filter that proxy to server connection has succeeded.
- *
- * @param serverCtx the {@link io.netty.channel.ChannelHandlerContext} used to connect to the server
- */
- void proxyToServerConnectionSucceeded(ChannelHandlerContext serverCtx);
-
+ /**
+ * Filters requests on their way from the client to the proxy. To interrupt processing of this
+ * request and return a response to the client immediately, return an HttpResponse here.
+ * Otherwise, return null to continue processing as usual.
+ *
+ *
Important: When returning a response, you must include a mechanism to allow the
+ * client to determine the length of the message (see RFC 7230, section 3.3.3 ).
+ *
+ *
For messages that may contain a body, you may do this by setting the Transfer-Encoding to
+ * chunked, setting an appropriate Content-Length, or by adding a "Connection: close" header to
+ * the response (which will instruct LittleProxy to close the connection). If the short-circuit
+ * response contains body content, it is recommended that you return a FullHttpResponse.
+ *
+ * @param httpObject Client to Proxy HttpRequest (and HttpContent, if chunked)
+ * @return a short-circuit response, or null to continue processing as usual
+ */
+ @Nullable HttpResponse clientToProxyRequest(@NonNull HttpObject httpObject);
+
+ /**
+ * Filters requests on their way from the proxy to the server. To interrupt processing of this
+ * request and return a response to the client immediately, return an HttpResponse here.
+ * Otherwise, return null to continue processing as usual.
+ *
+ *
Important: When returning a response, you must include a mechanism to allow the
+ * client to determine the length of the message (see RFC 7230, section 3.3.3 ). For
+ * messages that may contain a body, you may do this by setting the Transfer-Encoding to chunked,
+ * setting an appropriate Content-Length, or by adding a "Connection: close" header to the
+ * response. (which will instruct LittleProxy to close the connection). If the short-circuit
+ * response contains body content, it is recommended that you return a FullHttpResponse.
+ *
+ * @param httpObject Proxy to Server HttpRequest (and HttpContent, if chunked)
+ * @return a short-circuit response, or null to continue processing as usual
+ */
+ @Nullable HttpResponse proxyToServerRequest(@NonNull HttpObject httpObject);
+
+ /** Informs filter that proxy to server request is being sent. */
+ void proxyToServerRequestSending();
+
+ /** Informs filter that the HTTP request, including any content, has been sent. */
+ void proxyToServerRequestSent();
+
+ /**
+ * Filters responses on their way from the server to the proxy.
+ *
+ * @param httpObject Server to Proxy HttpResponse (and HttpContent, if chunked)
+ * @return the modified (or unmodified) HttpObject. Returning null will force a disconnect.
+ */
+ @Nullable HttpObject serverToProxyResponse(@NonNull HttpObject httpObject);
+
+ /**
+ * Informs filter that a timeout occurred before the server response was received by the client.
+ * The timeout may have occurred while the client was sending the request, waiting for a response,
+ * or after the client started receiving a response (i.e. if the response from the server
+ * "stalls").
+ *
+ *
See {@link HttpProxyServerBootstrap#withIdleConnectionTimeout(int)} for information on
+ * setting the timeout.
+ */
+ void serverToProxyResponseTimedOut();
+
+ /** Informs filter that server to proxy response is being received. */
+ void serverToProxyResponseReceiving();
+
+ /** Informs filter that server to proxy response has been received. */
+ void serverToProxyResponseReceived();
+
+ /**
+ * Filters responses on their way from the proxy to the client.
+ *
+ * @param httpObject Proxy to Client HttpResponse (and HttpContent, if chunked)
+ * @return the modified (or unmodified) HttpObject. Returning null will force a disconnect.
+ */
+ @Nullable HttpObject proxyToClientResponse(@NonNull HttpObject httpObject);
+
+ /** Informs filter that proxy to server connection is in queue. */
+ void proxyToServerConnectionQueued();
+
+ /**
+ * Filter DNS resolution from proxy to server.
+ *
+ * @param resolvingServerHostAndPort Server "HOST:PORT"
+ * @return alternative address resolution. Returning null will let normal DNS resolution continue.
+ */
+ @Nullable InetSocketAddress proxyToServerResolutionStarted(
+ @NonNull String resolvingServerHostAndPort);
+
+ /**
+ * Informs filter that proxy to server DNS resolution failed for the specified host and port.
+ *
+ * @param hostAndPort hostname and port the proxy failed to resolve
+ */
+ void proxyToServerResolutionFailed(@NonNull String hostAndPort);
+
+ /**
+ * Informs filter that proxy to server DNS resolution has happened.
+ *
+ * @param serverHostAndPort Server "HOST:PORT"
+ * @param resolvedRemoteAddress Address it was proxyToServerResolutionSucceeded to
+ */
+ void proxyToServerResolutionSucceeded(
+ @NonNull String serverHostAndPort, @NonNull InetSocketAddress resolvedRemoteAddress);
+
+ /** Informs filter that proxy to server connection is initiating. */
+ void proxyToServerConnectionStarted();
+
+ /** Informs filter that proxy to server ssl handshake is initiating. */
+ void proxyToServerConnectionSSLHandshakeStarted();
+
+ /** Informs filter that proxy to server connection has failed. */
+ void proxyToServerConnectionFailed();
+
+ /**
+ * Informs filter that proxy to server connection has succeeded.
+ *
+ * @param serverCtx the {@link io.netty.channel.ChannelHandlerContext} used to connect to the
+ * server
+ */
+ void proxyToServerConnectionSucceeded(@NonNull ChannelHandlerContext serverCtx);
+
+ /**
+ * Allow this proxy to act as an SSL man in the middle.
+ *
+ *
Has no impact if man in the middle is not enabled.
+ *
+ * @return true to allow mitm, false to not mitm the proxy to server connection.
+ */
+ boolean proxyToServerAllowMitm();
+
+ /**
+ * Notifies the filter that a WebSocket frame has been received and is about to be forwarded.
+ * Called after the HTTP connection has been upgraded to WebSocket.
+ *
+ *
The {@code frameBytes} contain the raw, unmodified WebSocket frame as received from the
+ * network. Client-to-server frames are masked per RFC 6455; server-to-client frames are not.
+ *
+ *
This method is informational — the frame cannot be modified or suppressed here.
+ *
+ *
Important: The {@code frameBytes} supplier must be called synchronously within this
+ * method. Storing the supplier for later invocation will result in undefined behavior as the
+ * underlying buffer is released after this method returns.
+ *
+ * @param frameBytes the raw bytes of the WebSocket frame
+ * @param fromClient true if the frame was sent by the client, false if sent by the server
+ */
+ default void webSocketFrameReceived(Supplier frameBytes, boolean fromClient) {}
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpFiltersAdapter.java b/src/main/java/org/littleshoot/proxy/HttpFiltersAdapter.java
index 2871364a..8d901cf6 100644
--- a/src/main/java/org/littleshoot/proxy/HttpFiltersAdapter.java
+++ b/src/main/java/org/littleshoot/proxy/HttpFiltersAdapter.java
@@ -4,103 +4,98 @@
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
-
import java.net.InetSocketAddress;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
-/**
- * Convenience base class for implementations of {@link HttpFilters}.
- */
+/** Convenience base class for implementations of {@link HttpFilters}. */
+@NullMarked
public class HttpFiltersAdapter implements HttpFilters {
- /**
- * A default, stateless, no-op {@link HttpFilters} instance.
- */
- public static final HttpFiltersAdapter NOOP_FILTER = new HttpFiltersAdapter(null);
-
- protected final HttpRequest originalRequest;
- protected final ChannelHandlerContext ctx;
-
- public HttpFiltersAdapter(HttpRequest originalRequest,
- ChannelHandlerContext ctx) {
- this.originalRequest = originalRequest;
- this.ctx = ctx;
- }
-
- public HttpFiltersAdapter(HttpRequest originalRequest) {
- this(originalRequest, null);
- }
-
- @Override
- public HttpResponse clientToProxyRequest(HttpObject httpObject) {
- return null;
- }
-
- @Override
- public HttpResponse proxyToServerRequest(HttpObject httpObject) {
- return null;
- }
-
- @Override
- public void proxyToServerRequestSending() {
- }
-
- @Override
- public void proxyToServerRequestSent() {
- }
-
- @Override
- public HttpObject serverToProxyResponse(HttpObject httpObject) {
- return httpObject;
- }
-
- @Override
- public void serverToProxyResponseTimedOut() {
- }
-
- @Override
- public void serverToProxyResponseReceiving() {
- }
-
- @Override
- public void serverToProxyResponseReceived() {
- }
-
- @Override
- public HttpObject proxyToClientResponse(HttpObject httpObject) {
- return httpObject;
- }
-
- @Override
- public void proxyToServerConnectionQueued() {
- }
-
- @Override
- public InetSocketAddress proxyToServerResolutionStarted(
- String resolvingServerHostAndPort) {
- return null;
- }
-
- @Override
- public void proxyToServerResolutionFailed(String hostAndPort) {
- }
-
- @Override
- public void proxyToServerResolutionSucceeded(String serverHostAndPort,
- InetSocketAddress resolvedRemoteAddress) {
- }
-
- @Override
- public void proxyToServerConnectionStarted() {
- }
-
- @Override
- public void proxyToServerConnectionSSLHandshakeStarted() {
- }
-
- @Override
- public void proxyToServerConnectionFailed() {
- }
-
- @Override
- public void proxyToServerConnectionSucceeded(ChannelHandlerContext serverCtx) {
- }
+ /** A default, stateless, no-op {@link HttpFilters} instance. */
+ public static final HttpFiltersAdapter NOOP_FILTER = new HttpFiltersAdapter(null);
+
+ @Nullable protected final HttpRequest originalRequest;
+ @Nullable protected final ChannelHandlerContext ctx;
+
+ public HttpFiltersAdapter(
+ @Nullable HttpRequest originalRequest, @Nullable ChannelHandlerContext ctx) {
+ this.originalRequest = originalRequest;
+ this.ctx = ctx;
+ }
+
+ public HttpFiltersAdapter(@Nullable HttpRequest originalRequest) {
+ this(originalRequest, null);
+ }
+
+ @Nullable
+ @Override
+ public HttpResponse clientToProxyRequest(@NonNull HttpObject httpObject) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public HttpResponse proxyToServerRequest(@NonNull HttpObject httpObject) {
+ return null;
+ }
+
+ @Override
+ public void proxyToServerRequestSending() {}
+
+ @Override
+ public void proxyToServerRequestSent() {}
+
+ @Override
+ public HttpObject serverToProxyResponse(HttpObject httpObject) {
+ return httpObject;
+ }
+
+ @Override
+ public void serverToProxyResponseTimedOut() {}
+
+ @Override
+ public void serverToProxyResponseReceiving() {}
+
+ @Override
+ public void serverToProxyResponseReceived() {}
+
+ @Override
+ public HttpObject proxyToClientResponse(HttpObject httpObject) {
+ return httpObject;
+ }
+
+ @Override
+ public void proxyToServerConnectionQueued() {}
+
+ @Nullable
+ @Override
+ public InetSocketAddress proxyToServerResolutionStarted(
+ @NonNull String resolvingServerHostAndPort) {
+ return null;
+ }
+
+ @Override
+ public void proxyToServerResolutionFailed(@NonNull String hostAndPort) {}
+
+ @Override
+ public void proxyToServerResolutionSucceeded(
+ @NonNull String serverHostAndPort, @NonNull InetSocketAddress resolvedRemoteAddress) {}
+
+ @Override
+ public void proxyToServerConnectionStarted() {}
+
+ @Override
+ public void proxyToServerConnectionSSLHandshakeStarted() {}
+
+ @Override
+ public void proxyToServerConnectionFailed() {}
+
+ @Override
+ public void proxyToServerConnectionSucceeded(@NonNull ChannelHandlerContext serverCtx) {}
+
+ @Override
+ public boolean proxyToServerAllowMitm() {
+ return true;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpFiltersSource.java b/src/main/java/org/littleshoot/proxy/HttpFiltersSource.java
index e554e61a..04e01d00 100644
--- a/src/main/java/org/littleshoot/proxy/HttpFiltersSource.java
+++ b/src/main/java/org/littleshoot/proxy/HttpFiltersSource.java
@@ -5,39 +5,37 @@
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
-/**
- * Factory for {@link HttpFilters}.
- */
+/** Factory for {@link HttpFilters}. */
+@NullMarked
public interface HttpFiltersSource {
- /**
- * Return an {@link HttpFilters} object for this request if and only if we
- * want to filter the request and/or its responses.
- */
- HttpFilters filterRequest(HttpRequest originalRequest,
- ChannelHandlerContext ctx);
+ /**
+ * Return an {@link HttpFilters} object for this request if and only if we want to filter the
+ * request and/or its responses.
+ */
+ @Nullable HttpFilters filterRequest(
+ @NonNull HttpRequest originalRequest, @Nullable ChannelHandlerContext ctx);
- /**
- * Indicate how many (if any) bytes to buffer for incoming
- * {@link HttpRequest}s. A value of 0 or less indicates that no buffering
- * should happen and that messages will be passed to the {@link HttpFilters}
- * request filtering methods chunk by chunk. A positive value will cause
- * LittleProxy to try an create a {@link FullHttpRequest} using the data
- * received from the client, with its content already decompressed (in case
- * the client was compressing it). If the request size exceeds the maximum
- * buffer size, the request will fail.
- */
- int getMaximumRequestBufferSizeInBytes();
+ /**
+ * Indicate how many (if any) bytes to buffer for incoming {@link HttpRequest}s. A value of 0 or
+ * less indicates that no buffering should happen and that messages will be passed to the {@link
+ * HttpFilters} request filtering methods chunk by chunk. A positive value will cause LittleProxy
+ * to try to create a {@link FullHttpRequest} using the data received from the client, with its
+ * content already decompressed (in case the client was compressing it). If the request size
+ * exceeds the maximum buffer size, the request will fail.
+ */
+ int getMaximumRequestBufferSizeInBytes();
- /**
- * Indicate how many (if any) bytes to buffer for incoming
- * {@link HttpResponse}s. A value of 0 or less indicates that no buffering
- * should happen and that messages will be passed to the {@link HttpFilters}
- * response filtering methods chunk by chunk. A positive value will cause
- * LittleProxy to try an create a {@link FullHttpResponse} using the data
- * received from the server, with its content already decompressed (in case
- * the server was compressing it). If the response size exceeds the maximum
- * buffer size, the response will fail.
- */
- int getMaximumResponseBufferSizeInBytes();
+ /**
+ * Indicate how many (if any) bytes to buffer for incoming {@link HttpResponse}s. A value of 0 or
+ * less indicates that no buffering should happen and that messages will be passed to the {@link
+ * HttpFilters} response filtering methods chunk by chunk. A positive value will cause LittleProxy
+ * to try to create a {@link FullHttpResponse} using the data received from the server, with its
+ * content already decompressed (in case the server was compressing it). If the response size
+ * exceeds the maximum buffer size, the response will fail.
+ */
+ int getMaximumResponseBufferSizeInBytes();
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpFiltersSourceAdapter.java b/src/main/java/org/littleshoot/proxy/HttpFiltersSourceAdapter.java
index 1b2cecfd..1858d317 100644
--- a/src/main/java/org/littleshoot/proxy/HttpFiltersSourceAdapter.java
+++ b/src/main/java/org/littleshoot/proxy/HttpFiltersSourceAdapter.java
@@ -2,30 +2,33 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpRequest;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
-/**
- * Convenience base class for implementations of {@link HttpFiltersSource}.
- */
+/** Convenience base class for implementations of {@link HttpFiltersSource}. */
+@NullMarked
public class HttpFiltersSourceAdapter implements HttpFiltersSource {
- public HttpFilters filterRequest(HttpRequest originalRequest) {
- return new HttpFiltersAdapter(originalRequest, null);
- }
-
- @Override
- public HttpFilters filterRequest(HttpRequest originalRequest,
- ChannelHandlerContext ctx) {
- return filterRequest(originalRequest);
- }
+ @Nullable
+ public HttpFilters filterRequest(@NonNull HttpRequest originalRequest) {
+ return new HttpFiltersAdapter(originalRequest, null);
+ }
- @Override
- public int getMaximumRequestBufferSizeInBytes() {
- return 0;
- }
+ @Override
+ @Nullable
+ public HttpFilters filterRequest(
+ @NonNull HttpRequest originalRequest, @NonNull ChannelHandlerContext ctx) {
+ return filterRequest(originalRequest);
+ }
- @Override
- public int getMaximumResponseBufferSizeInBytes() {
- return 0;
- }
+ @Override
+ public int getMaximumRequestBufferSizeInBytes() {
+ return 0;
+ }
+ @Override
+ public int getMaximumResponseBufferSizeInBytes() {
+ return 0;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServer.java b/src/main/java/org/littleshoot/proxy/HttpProxyServer.java
index 47e40d16..a2a21a1f 100644
--- a/src/main/java/org/littleshoot/proxy/HttpProxyServer.java
+++ b/src/main/java/org/littleshoot/proxy/HttpProxyServer.java
@@ -1,63 +1,44 @@
package org.littleshoot.proxy;
import java.net.InetSocketAddress;
+import java.time.Duration;
-/**
- * Interface for the top-level proxy server class.
- */
+/** Interface for the top-level proxy server class. */
public interface HttpProxyServer {
- int getIdleConnectionTimeout();
-
- void setIdleConnectionTimeout(int idleConnectionTimeout);
-
- /**
- * Returns the maximum time to wait, in milliseconds, to connect to a server.
- */
- int getConnectTimeout();
-
- /**
- * Sets the maximum time to wait, in milliseconds, to connect to a server.
- */
- void setConnectTimeout(int connectTimeoutMs);
-
- /**
- *
- * Clone the existing server, with a port 1 higher and everything else the
- * same. If the proxy was started with port 0 (JVM-assigned port), the cloned proxy will also use a JVM-assigned
- * port.
- *
- *
- *
- * The new server will share event loops with the original server. The event
- * loops will use whatever name was given to the first server in the clone
- * group. The server group will not terminate until the original server and all clones terminate.
- *
- *
- * @return a bootstrap that allows customizing and starting the cloned
- * server
- */
- HttpProxyServerBootstrap clone();
-
- /**
- * Stops the server and all related clones. Waits for traffic to stop before shutting down.
- */
- void stop();
-
- /**
- * Stops the server and all related clones immediately, without waiting for traffic to stop.
- */
- void abort();
-
- /**
- * Return the address on which this proxy is listening.
- */
- InetSocketAddress getListenAddress();
-
- /**
- *
- * Set the read/write throttle bandwidths (in bytes/second) for this proxy.
- *
- */
- void setThrottle(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond);
+ int getIdleConnectionTimeout();
+
+ void setIdleConnectionTimeout(int idleConnectionTimeoutInSeconds);
+
+ void setIdleConnectionTimeout(Duration idleConnectionTimeout);
+
+ /** Returns the maximum time to wait, in milliseconds, to connect to a server. */
+ int getConnectTimeout();
+
+ /** Sets the maximum time to wait, in milliseconds, to connect to a server. */
+ void setConnectTimeout(int connectTimeoutMs);
+
+ /**
+ * Clone the existing server, with a port 1 higher and everything else the same. If the proxy was
+ * started with port 0 (JVM-assigned port), the cloned proxy will also use a JVM-assigned port.
+ *
+ *
The new server will share event loops with the original server. The event loops will use
+ * whatever name was given to the first server in the clone group. The server group will not
+ * terminate until the original server and all clones terminate.
+ *
+ * @return a bootstrap that allows customizing and starting the cloned server
+ */
+ HttpProxyServerBootstrap clone();
+
+ /** Stops the server and all related clones. Waits for traffic to stop before shutting down. */
+ void stop();
+
+ /** Stops the server and all related clones immediately, without waiting for traffic to stop. */
+ void abort();
+
+ /** Return the address on which this proxy is listening. */
+ InetSocketAddress getListenAddress();
+
+ /** Set the read/write throttle bandwidths (in bytes/second) for this proxy. */
+ void setThrottle(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond);
}
diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
index 5ef9075c..35ce454e 100644
--- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
+++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
@@ -1,306 +1,298 @@
package org.littleshoot.proxy;
-import org.littleshoot.proxy.impl.ThreadPoolConfiguration;
-import org.littleshoot.proxy.impl.ServerGroup;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.net.InetSocketAddress;
+import java.time.Duration;
+import org.jspecify.annotations.NullMarked;
+import org.littleshoot.proxy.impl.ServerGroup;
+import org.littleshoot.proxy.impl.ThreadPoolConfiguration;
/**
- * Configures and starts an {@link HttpProxyServer}. The HttpProxyServer is
- * built using {@link #start()}. Sensible defaults are available for all
- * parameters such that {@link #start()} could be called immediately if you
- * wish.
+ * Configures and starts an {@link HttpProxyServer}. The HttpProxyServer is built using {@link
+ * #start()}. Sensible defaults are available for all parameters such that {@link #start()} could be
+ * called immediately if you wish.
*/
+@NullMarked
public interface HttpProxyServerBootstrap {
- /**
- *
- * Give the server a name (used for naming threads, useful for logging).
- *
- * Specify whether or not to only allow local connections.
- *
- *
- *
- * Default = true
- *
- */
- HttpProxyServerBootstrap withAllowLocalOnly(boolean allowLocalOnly);
-
- /**
- * This method has no effect and will be removed in a future release.
- * @deprecated use {@link #withNetworkInterface(InetSocketAddress)} to avoid listening on all local addresses
- */
- @Deprecated
- HttpProxyServerBootstrap withListenOnAllAddresses(boolean listenOnAllAddresses);
-
- /**
- *
- * Specify an {@link SslEngineSource} to use for encrypting inbound
- * connections. Enabling this will enable SSL client authentication
- * by default (see {@link #withAuthenticateSslClients(boolean)})
- *
- *
- *
- * Default = null
- *
- *
- *
- * Note - This and {@link #withManInTheMiddle(MitmManager)} are
- * mutually exclusive.
- *
- * Specify the timeout for connecting to the upstream server on a new
- * connection, in milliseconds.
- *
- *
- *
- * Default = 40000
- *
- */
- HttpProxyServerBootstrap withConnectTimeout(
- int connectTimeout);
-
- /**
- * Specify a custom {@link HostResolver} for resolving server addresses.
- */
- HttpProxyServerBootstrap withServerResolver(HostResolver serverResolver);
-
- /**
- * Specify a custom {@link ServerGroup} to use for managing this server's resources and such.
- * If one isn't provided, a default one will be created using the {@link ThreadPoolConfiguration} provided
- *
- * @param group A custom server group
- */
- HttpProxyServerBootstrap withServerGroup(ServerGroup group);
-
- /**
- *
- * Add an {@link ActivityTracker} for tracking activity in this proxy.
- *
- * Specify the read and/or write bandwidth throttles for this proxy server. 0 indicates not throttling.
- *
- */
- HttpProxyServerBootstrap withThrottling(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond);
-
- /**
- * All outgoing-communication of the proxy-instance is goin' to be routed via the given network-interface
- *
- * @param inetSocketAddress to be used for outgoing communication
- */
- HttpProxyServerBootstrap withNetworkInterface(InetSocketAddress inetSocketAddress);
-
- HttpProxyServerBootstrap withMaxInitialLineLength(int maxInitialLineLength);
-
- HttpProxyServerBootstrap withMaxHeaderSize(int maxHeaderSize);
-
- HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize);
-
- /**
- * When true, the proxy will accept requests that appear to be directed at an origin server (i.e. the URI in the HTTP
- * request will contain an origin-form, rather than an absolute-form, as specified in RFC 7230, section 5.3).
- * This is useful when the proxy is acting as a gateway/reverse proxy. Note: This feature should not be
- * enabled when running as a forward proxy; doing so may cause an infinite loop if the client requests the URI of the proxy.
- *
- * @param allowRequestToOriginServer when true, the proxy will accept origin-form HTTP requests
- */
- HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer);
-
- /**
- * Sets the alias to use when adding Via headers to incoming and outgoing HTTP messages. The alias may be any
- * pseudonym, or if not specified, defaults to the hostname of the local machine. See RFC 7230, section 5.7.1.
- *
- * @param alias the pseudonym to add to Via headers
- */
- HttpProxyServerBootstrap withProxyAlias(String alias);
-
- /**
- *
- * Build and starts the server.
- *
- *
- * @return the newly built and started server
- */
- HttpProxyServer start();
-
- /**
- * Set the configuration parameters for the proxy's thread pools.
- *
- * @param configuration thread pool configuration
- * @return proxy server bootstrap for chaining
- */
- HttpProxyServerBootstrap withThreadPoolConfiguration(ThreadPoolConfiguration configuration);
-
- /**
- * Specifies if the proxy server should accept a proxy protocol header. Once set it works with request that
- * include a proxy protocol header. The proxy server reads an incoming proxy protocol header from the
- * client.
- * @param allowProxyProtocol when true, the proxy will accept a proxy protocol header
- */
- HttpProxyServerBootstrap withAcceptProxyProtocol(boolean allowProxyProtocol);
-
- /**
- * Specifies if the proxy server should send a proxy protocol header.
- * @param sendProxyProtocol when true, the proxy will send a proxy protocol header
- */
- HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol);
+ /**
+ * Give the server a name (used for naming threads, useful for logging).
+ *
+ *
Default = LittleProxy
+ */
+ HttpProxyServerBootstrap withName(String name);
+
+ /**
+ * Listen for incoming connections on the given address.
+ *
+ *
Default = [bound ip]:8080
+ */
+ HttpProxyServerBootstrap withAddress(InetSocketAddress address);
+
+ /**
+ * Listen for incoming connections on the given port.
+ *
+ *
Default = 8080
+ */
+ HttpProxyServerBootstrap withPort(int port);
+
+ /**
+ * Specify whether or not to only allow local connections.
+ *
+ *
Default = true
+ */
+ HttpProxyServerBootstrap withAllowLocalOnly(boolean allowLocalOnly);
+
+ /**
+ * Specify an {@link SslEngineSource} to use for encrypting inbound connections. Enabling this
+ * will enable SSL client authentication by default (see {@link
+ * #withAuthenticateSslClients(boolean)})
+ *
+ *
Default = null
+ *
+ *
Note - This and {@link #withManInTheMiddle(MitmManager)} are mutually exclusive.
+ */
+ HttpProxyServerBootstrap withSslEngineSource(SslEngineSource sslEngineSource);
+
+ /**
+ * Specify whether or not to authenticate inbound SSL clients (only applies if {@link
+ * #withSslEngineSource(SslEngineSource)} has been set).
+ *
+ *
Default = true
+ */
+ HttpProxyServerBootstrap withAuthenticateSslClients(boolean authenticateSslClients);
+
+ /**
+ * Specify a {@link ProxyAuthenticator} to use for doing basic HTTP authentication of clients.
+ *
+ *
Default = null
+ */
+ HttpProxyServerBootstrap withProxyAuthenticator(ProxyAuthenticator proxyAuthenticator);
+
+ /**
+ * Specify a {@link ChainedProxyManager} to use for chaining requests to another proxy.
+ *
+ *
Default = null
+ */
+ HttpProxyServerBootstrap withChainProxyManager(ChainedProxyManager chainProxyManager);
+
+ /**
+ * Specify an {@link MitmManager} to use for making this proxy act as an SSL man in the middle
+ *
+ *
Default = null
+ *
+ *
Note - This and {@link #withSslEngineSource(SslEngineSource)} are mutually exclusive.
+ */
+ HttpProxyServerBootstrap withManInTheMiddle(MitmManager mitmManager);
+
+ /**
+ * Specify a {@link HttpFiltersSource} to use for filtering requests and/or responses through this
+ * proxy.
+ *
+ *
Default = null
+ */
+ HttpProxyServerBootstrap withFiltersSource(HttpFiltersSource filtersSource);
+
+ /**
+ * Specify whether or not to use secure DNS lookups for outbound connections.
+ *
+ *
Default = false
+ */
+ @CanIgnoreReturnValue
+ HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec);
+
+ /**
+ * Specify whether or not to run this proxy as a transparent proxy.
+ *
+ *
Default = false
+ */
+ HttpProxyServerBootstrap withTransparent(boolean transparent);
+
+ /**
+ * Specify the timeout after which to disconnect idle connections, in seconds.
+ *
+ *
Default = 70
+ */
+ HttpProxyServerBootstrap withIdleConnectionTimeout(int idleConnectionTimeoutInSeconds);
+
+ /**
+ * Specify the timeout after which to disconnect idle connections
+ *
+ *
Default = 70 seconds
+ */
+ HttpProxyServerBootstrap withIdleConnectionTimeout(Duration idleConnectionTimeout);
+
+ /**
+ * Specify the timeout for connecting to the upstream server on a new connection, in milliseconds.
+ *
+ *
Default = 40000
+ */
+ HttpProxyServerBootstrap withConnectTimeout(int connectTimeout);
+
+ /** Specify a custom {@link HostResolver} for resolving server addresses. */
+ HttpProxyServerBootstrap withServerResolver(HostResolver serverResolver);
+
+ /**
+ * Specify a custom {@link ServerGroup} to use for managing this server's resources and such. If
+ * one isn't provided, a default one will be created using the {@link ThreadPoolConfiguration}
+ * provided
+ *
+ * @param group A custom server group
+ */
+ HttpProxyServerBootstrap withServerGroup(ServerGroup group);
+
+ /** Add an {@link ActivityTracker} for tracking activity in this proxy. */
+ HttpProxyServerBootstrap plusActivityTracker(ActivityTracker activityTracker);
+
+ /**
+ * Specify the read and/or write bandwidth throttles for this proxy server. 0 indicates not
+ * throttling.
+ */
+ HttpProxyServerBootstrap withThrottling(
+ long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond);
+
+ /**
+ * All outgoing-communication of the proxy-instance is going to be routed via the given
+ * network-interface
+ *
+ * @param inetSocketAddress to be used for outgoing communication
+ */
+ @CanIgnoreReturnValue
+ @NullMarked
+ HttpProxyServerBootstrap withNetworkInterface(InetSocketAddress inetSocketAddress);
+
+ HttpProxyServerBootstrap withMaxInitialLineLength(int maxInitialLineLength);
+
+ HttpProxyServerBootstrap withMaxHeaderSize(int maxHeaderSize);
+
+ HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize);
+
+ /**
+ * When true, the proxy will accept requests that appear to be directed at an origin server (i.e.
+ * the URI in the HTTP request will contain an origin-form, rather than an absolute-form, as
+ * specified in RFC 7230, section 5.3). This is useful when the proxy is acting as a
+ * gateway/reverse proxy. Note: This feature should not be enabled when running as a
+ * forward proxy; doing so may cause an infinite loop if the client requests the URI of the proxy.
+ *
+ * @param allowRequestToOriginServer when true, the proxy will accept origin-form HTTP requests
+ */
+ HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer);
+
+ /**
+ * Sets the alias to use when adding Via headers to incoming and outgoing HTTP messages. The alias
+ * may be any pseudonym, or if not specified, defaults to the hostname of the local machine. See
+ * RFC 7230, section 5.7.1.
+ *
+ * @param alias the pseudonym to add to Via headers
+ */
+ HttpProxyServerBootstrap withProxyAlias(String alias);
+
+ /**
+ * Build and starts the server.
+ *
+ * @return the newly built and started server
+ */
+ HttpProxyServer start();
+
+ /**
+ * Set the configuration parameters for the proxy's thread pools.
+ *
+ * @param configuration thread pool configuration
+ * @return proxy server bootstrap for chaining
+ */
+ HttpProxyServerBootstrap withThreadPoolConfiguration(ThreadPoolConfiguration configuration);
+
+ /**
+ * Specifies if the proxy server should accept a proxy protocol header. Once set it works with
+ * request that include a proxy protocol header. The proxy server reads an incoming proxy protocol
+ * header from the client.
+ *
+ * @param allowProxyProtocol when true, the proxy will accept a proxy protocol header
+ */
+ HttpProxyServerBootstrap withAcceptProxyProtocol(boolean allowProxyProtocol);
+
+ /**
+ * Specifies if the proxy server should send a proxy protocol header.
+ *
+ * @param sendProxyProtocol when true, the proxy will send a proxy protocol header
+ */
+ HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol);
+
+ /**
+ * Enable or disable the shared server connection pool.
+ *
+ *
When enabled, all client connections share a common pool of server connections, allowing
+ * connections to the same server to be reused across different client connections. This addresses
+ * connection explosion when many clients connect to the same servers.
+ *
+ *
Disabled by default for backwards compatibility.
+ *
+ * @param useSharedServerConnectionPool true to enable the shared pool
+ */
+ HttpProxyServerBootstrap withSharedServerConnectionPool(boolean useSharedServerConnectionPool);
+
+ /**
+ * Selects the server connection pool implementation to use when the shared pool is enabled.
+ *
+ *
Default is {@link ServerConnectionPoolType#CONCURRENT_MAP}.
+ *
+ * @param poolType the pool implementation to use
+ */
+ HttpProxyServerBootstrap withServerConnectionPoolType(ServerConnectionPoolType poolType);
+
+ /**
+ * Sets the maximum number of connections per host:port when using the shared connection pool.
+ *
+ *
Default is 10. This allows multiple connections to the same server for high concurrency
+ * scenarios.
+ *
+ * @param maxConnectionsPerHost the maximum number of connections per host:port
+ */
+ HttpProxyServerBootstrap withMaxConnectionsPerHost(int maxConnectionsPerHost);
+
+ /**
+ * Sets the maximum total number of connections in the shared connection pool.
+ *
+ *
Default is 200.
+ *
+ * @param maxConnections the maximum total number of pooled connections
+ */
+ HttpProxyServerBootstrap withMaxConnections(int maxConnections);
+
+ /**
+ * Sets the idle timeout for pooled connections. Connections that remain idle (available in the
+ * pool) for longer than this duration will be evicted.
+ *
+ *
Default is null (no eviction based on idle time). When set, connections will be evicted
+ * after being idle for the specified duration.
+ *
+ * @param idleTimeout the idle timeout duration, or null to disable idle eviction
+ */
+ HttpProxyServerBootstrap withPoolIdleTimeout(Duration idleTimeout);
+
+ /**
+ * When enabled, MITM connections will use the shared server connection pool. This allows upstream
+ * connections for MITM'd HTTPS traffic to be reused across different client connections.
+ *
+ *
Enabled only takes effect when {@link #withSharedServerConnectionPool(boolean)} is also
+ * enabled.
+ *
+ *
Disabled by default for backwards compatibility.
+ *
+ * @param poolSharedMitmConnections true to allow MITM connections to use the shared pool
+ */
+ HttpProxyServerBootstrap withPoolSharedMitmConnections(boolean poolSharedMitmConnections);
+
+ /**
+ * When enabled, each HTTP request through an MITM tunnel independently acquires and releases a
+ * server connection from the shared pool. This allows multiple clients' MITM requests to share
+ * upstream connections concurrently (one request at a time per connection).
+ *
+ *
Only takes effect when {@link #withPoolSharedMitmConnections(boolean)} is also enabled.
+ *
+ *
Disabled by default for backwards compatibility.
+ *
+ * @param poolPerRequestInMitm true to enable per-request pooling inside MITM tunnels
+ */
+ HttpProxyServerBootstrap withPoolPerRequestInMitm(boolean poolPerRequestInMitm);
}
diff --git a/src/main/java/org/littleshoot/proxy/Launcher.java b/src/main/java/org/littleshoot/proxy/Launcher.java
index b3d7e666..db15db38 100644
--- a/src/main/java/org/littleshoot/proxy/Launcher.java
+++ b/src/main/java/org/littleshoot/proxy/Launcher.java
@@ -1,137 +1,474 @@
package org.littleshoot.proxy;
-import org.apache.commons.cli.*;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.ACCEPTOR_THREADS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.ALLOW_PROXY_PROTOCOL;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.ALLOW_REQUESTS_TO_ORIGIN_SERVER;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.CLIENT_TO_PROXY_WORKER_THREADS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.PROXY_TO_SERVER_WORKER_THREADS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SEND_PROXY_PROTOCOL;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_ALIAS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PASSWORD;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PATH;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SSL_CLIENTS_SEND_CERTS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.SSL_CLIENTS_TRUST_ALL_SERVERS;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.THROTTLE_READ_BYTES_PER_SECOND;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.THROTTLE_WRITE_BYTES_PER_SECOND;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.TRANSPARENT;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.bootstrap;
+import static org.littleshoot.proxy.impl.DefaultHttpProxyServer.bootstrapFromFile;
+
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.util.Arrays;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.extras.ActivityLogger;
+import org.littleshoot.proxy.extras.LogFormat;
import org.littleshoot.proxy.extras.SelfSignedMitmManager;
-import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
+import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;
import org.littleshoot.proxy.impl.ProxyUtils;
+import org.littleshoot.proxy.impl.ThreadPoolConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.net.InetSocketAddress;
-import java.util.Arrays;
-
-/**
- * Launches a new HTTP proxy.
- */
+/** Launches a new HTTP proxy. */
public class Launcher {
- private static final Logger LOG = LoggerFactory.getLogger(Launcher.class);
-
- private static final String OPTION_DNSSEC = "dnssec";
-
- private static final String OPTION_PORT = "port";
-
- private static final String OPTION_HELP = "help";
-
- private static final String OPTION_MITM = "mitm";
-
- private static final String OPTION_NIC = "nic";
-
- /**
- * Starts the proxy from the command line.
- *
- * @param args
- * Any command line arguments.
- */
- public static void main(final String... args) {
- pollLog4JConfigurationFileIfAvailable();
- LOG.info("Running LittleProxy with args: {}", Arrays.asList(args));
- final Options options = new Options();
- options.addOption(null, OPTION_DNSSEC, true,
- "Request and verify DNSSEC signatures.");
- options.addOption(null, OPTION_PORT, true, "Run on the specified port.");
- options.addOption(null, OPTION_NIC, true, "Run on a specified Nic");
- options.addOption(null, OPTION_HELP, false,
- "Display command line help.");
- options.addOption(null, OPTION_MITM, false, "Run as man in the middle.");
-
- final CommandLineParser parser = new DefaultParser();
- final CommandLine cmd;
- try {
- cmd = parser.parse(options, args);
- if (cmd.getArgs().length > 0) {
- throw new UnrecognizedOptionException(
- "Extra arguments were provided in "
- + Arrays.asList(args));
- }
- } catch (final ParseException e) {
- printHelp(options,
- "Could not parse command line: " + Arrays.asList(args));
- return;
- }
- if (cmd.hasOption(OPTION_HELP)) {
- printHelp(options, null);
- return;
- }
- final int defaultPort = 8080;
- int port;
- if (cmd.hasOption(OPTION_PORT)) {
- final String val = cmd.getOptionValue(OPTION_PORT);
- try {
- port = Integer.parseInt(val);
- } catch (final NumberFormatException e) {
- printHelp(options, "Unexpected port " + val);
- return;
- }
+ public static final int DEFAULT_PORT = 8080;
+ private static final Logger LOG = LoggerFactory.getLogger(Launcher.class);
+
+ private static final String OPTION_DNSSEC = "dnssec";
+
+ private static final String OPTION_PORT = "port";
+
+ private static final String OPTION_HELP = "help";
+
+ private static final String OPTION_MITM = "mitm";
+
+ private static final String OPTION_NIC = "nic";
+
+ private static final String OPTION_CONFIG = "config";
+
+ private static final String OPTION_LOG_CONFIG = "log_config";
+ private static final String OPTION_SERVER = "server";
+ private static final String OPTION_NAME = "name";
+ private static final String OPTION_ADDRESS = "address";
+ private static final String OPTION_PROXY_ALIAS = "proxy_alias";
+ private static final String OPTION_ALLOW_LOCAL_ONLY = "allow_local_only";
+ private static final String OPTION_AUTHENTICATE_SSL_CLIENTS = "authenticate_ssl_clients";
+ private static final String OPTION_SSL_CLIENTS_TRUST_ALL_SERVERS = SSL_CLIENTS_TRUST_ALL_SERVERS;
+ private static final String OPTION_SSL_CLIENTS_SEND_CERTS = SSL_CLIENTS_SEND_CERTS;
+ private static final String OPTION_SSL_CLIENTS_KEYSTORE_PATH = SSL_CLIENTS_KEYSTORE_PATH;
+ private static final String OPTION_SSL_CLIENTS_KEYSTORE_ALIAS = SSL_CLIENTS_KEYSTORE_ALIAS;
+ private static final String OPTION_SSL_CLIENTS_KEYSTORE_PASSWORD = SSL_CLIENTS_KEYSTORE_PASSWORD;
+ private static final String OPTION_TRANSPARENT = TRANSPARENT;
+ private static final String OPTION_THROTTLE_READ_BYTES_PER_SECOND =
+ THROTTLE_READ_BYTES_PER_SECOND;
+ private static final String OPTION_THROTTLE_WRITE_BYTES_PER_SECOND =
+ THROTTLE_WRITE_BYTES_PER_SECOND;
+ private static final String OPTION_ALLOW_REQUEST_TO_ORIGIN_SERVER =
+ ALLOW_REQUESTS_TO_ORIGIN_SERVER;
+ private static final String OPTION_ALLOW_PROXY_PROTOCOL = ALLOW_PROXY_PROTOCOL;
+ private static final String OPTION_SEND_PROXY_PROTOCOL = SEND_PROXY_PROTOCOL;
+ private static final String OPTION_CLIENT_TO_PROXY_WORKER_THREADS =
+ CLIENT_TO_PROXY_WORKER_THREADS;
+ private static final String OPTION_PROXY_TO_SERVER_WORKER_THREADS =
+ PROXY_TO_SERVER_WORKER_THREADS;
+ private static final String OPTION_ACCEPTOR_THREADS = ACCEPTOR_THREADS;
+ private static final String OPTION_ACTIVITY_LOG_FORMAT = "activity_log_format";
+ public static final int DELAY_IN_SECONDS_BETWEEN_RELOAD = 15;
+ private static final String DEFAULT_JKS_KEYSTORE_PATH = "littleproxy_keystore.jks";
+
+ @Nullable private volatile HttpProxyServer httpProxyServer;
+
+ /**
+ * Starts the proxy from the command line.
+ *
+ * @param args Any command line arguments.
+ */
+ public static void main(final String... args) {
+ Launcher launcher = new Launcher();
+ launcher.start(args);
+ }
+
+ protected void start(String[] args) {
+ final Options options = buildOptions();
+
+ CommandLine cmd = parseCommandLine(args, options);
+
+ configureLogging(cmd);
+
+ LOG.info("Running LittleProxy with args: {}", Arrays.asList(args));
+
+ if (cmd.hasOption(OPTION_HELP)) {
+ printHelp(options, null);
+ return;
+ }
+
+ HttpProxyServerBootstrap bootstrap;
+ if (cmd.hasOption(OPTION_CONFIG)) {
+ String proxyConfigurationPath = cmd.getOptionValue(OPTION_CONFIG);
+ LOG.info("Using configuration file: {}", proxyConfigurationPath);
+ cmd.getOptionValue(OPTION_CONFIG);
+ bootstrap = bootstrapFromFile(proxyConfigurationPath);
+ } else {
+ bootstrap = bootstrap();
+ }
+
+ int port;
+ if (cmd.hasOption(OPTION_PORT)) {
+ final String val = cmd.getOptionValue(OPTION_PORT);
+ try {
+ port = Integer.parseInt(val);
+ } catch (final NumberFormatException e) {
+ printHelp(options, "Unexpected port " + val);
+ return;
+ }
+ } else {
+ port = DEFAULT_PORT;
+ }
+ bootstrap.withPort(port);
+ LOG.info("About to start server on port: '{}'", port);
+
+ if (cmd.hasOption(OPTION_NIC)) {
+ final String val = cmd.getOptionValue(OPTION_NIC);
+ bootstrap.withNetworkInterface(new InetSocketAddress(val, 0));
+ }
+
+ if (cmd.hasOption(OPTION_MITM)) {
+ LOG.info("Running as Man in the Middle");
+ String keyStorePath = DEFAULT_JKS_KEYSTORE_PATH;
+ if (cmd.hasOption(OPTION_SSL_CLIENTS_KEYSTORE_PATH)) {
+ keyStorePath = cmd.getOptionValue(OPTION_SSL_CLIENTS_KEYSTORE_PATH);
+ }
+ bootstrap.withManInTheMiddle(new SelfSignedMitmManager(keyStorePath, true, true));
+ }
+
+ if (cmd.hasOption(OPTION_DNSSEC)) {
+ final String val = cmd.getOptionValue(OPTION_DNSSEC);
+ if (ProxyUtils.isTrue(val)) {
+ LOG.info("Using DNSSEC");
+ bootstrap.withUseDnsSec(true);
+ } else if (ProxyUtils.isFalse(val)) {
+ LOG.info("Not using DNSSEC");
+ bootstrap.withUseDnsSec(false);
+ } else {
+ printHelp(options, "Unexpected value for " + OPTION_DNSSEC + "=:" + val);
+ return;
+ }
+ }
+
+ if (cmd.hasOption(OPTION_NAME)) {
+ final String val = cmd.getOptionValue(OPTION_NAME);
+ LOG.info("Running with name: '{}'", val);
+ bootstrap.withName(val);
+ }
+
+ if (cmd.hasOption(OPTION_ADDRESS)) {
+ final String val = cmd.getOptionValue(OPTION_ADDRESS);
+ LOG.info("Binding to address: '{}'", val);
+ InetSocketAddress address = ProxyUtils.resolveSocketAddress(val);
+ if (address != null) {
+ bootstrap.withAddress(address);
+ }
+ }
+
+ if (cmd.hasOption(OPTION_PROXY_ALIAS)) {
+ final String val = cmd.getOptionValue(OPTION_PROXY_ALIAS);
+ LOG.info("Using proxy alias: '{}'", val);
+ if (val != null) {
+ bootstrap.withProxyAlias(val);
+ }
+ }
+
+ if (cmd.hasOption(OPTION_ALLOW_LOCAL_ONLY)) {
+ final String val = cmd.getOptionValue(OPTION_ALLOW_LOCAL_ONLY);
+ LOG.info("Setting allow local only to: '{}'", val);
+ if (val != null) {
+ bootstrap.withAllowLocalOnly(Boolean.parseBoolean(val));
+ }
+ }
+
+ if (cmd.hasOption(OPTION_AUTHENTICATE_SSL_CLIENTS)) {
+ final String val = cmd.getOptionValue(OPTION_AUTHENTICATE_SSL_CLIENTS);
+ LOG.info("Setting authenticate SSL clients with a selfSigned cert : '{}'", val);
+ if (val != null) {
+ boolean trustAllServers =
+ Boolean.parseBoolean(cmd.getOptionValue(OPTION_SSL_CLIENTS_TRUST_ALL_SERVERS, "false"));
+ boolean sendCerts =
+ Boolean.parseBoolean(cmd.getOptionValue(OPTION_SSL_CLIENTS_SEND_CERTS, "false"));
+ SelfSignedSslEngineSource sslEngineSource;
+ if (cmd.hasOption(OPTION_SSL_CLIENTS_KEYSTORE_PATH)) {
+ String keyStorePath = cmd.getOptionValue(OPTION_SSL_CLIENTS_KEYSTORE_PATH);
+ if (cmd.hasOption(OPTION_SSL_CLIENTS_KEYSTORE_PASSWORD)) {
+ String keyStoreAlias = cmd.getOptionValue(OPTION_SSL_CLIENTS_KEYSTORE_ALIAS, "");
+ String keyStorePassword = cmd.getOptionValue(OPTION_SSL_CLIENTS_KEYSTORE_PASSWORD);
+ sslEngineSource =
+ new SelfSignedSslEngineSource(
+ keyStorePath, trustAllServers, sendCerts, keyStoreAlias, keyStorePassword);
+ } else {
+ sslEngineSource =
+ new SelfSignedSslEngineSource(keyStorePath, trustAllServers, sendCerts);
+ }
} else {
- port = defaultPort;
+ sslEngineSource =
+ new SelfSignedSslEngineSource(DEFAULT_JKS_KEYSTORE_PATH, trustAllServers, sendCerts);
}
+ bootstrap.withSslEngineSource(sslEngineSource);
+ bootstrap.withAuthenticateSslClients(Boolean.parseBoolean(val));
+ }
+ }
+ if (cmd.hasOption(OPTION_TRANSPARENT)) {
+ String optionValue = cmd.getOptionValue(OPTION_TRANSPARENT);
+ LOG.info("Transparent proxy enabled :'{}'", optionValue);
+ if (optionValue != null) {
+ bootstrap.withTransparent(Boolean.parseBoolean(optionValue));
+ }
+ }
+ long throttlingReadBytesPerSecond = 0;
+ long throttlingWriteBytesPerSecond = 0;
+ if (cmd.hasOption(OPTION_THROTTLE_READ_BYTES_PER_SECOND)) {
+ throttlingReadBytesPerSecond =
+ Long.parseLong(cmd.getOptionValue(OPTION_THROTTLE_READ_BYTES_PER_SECOND));
+ }
+ if (cmd.hasOption(OPTION_THROTTLE_WRITE_BYTES_PER_SECOND)) {
+ throttlingWriteBytesPerSecond =
+ Long.parseLong(cmd.getOptionValue(OPTION_THROTTLE_WRITE_BYTES_PER_SECOND));
+ }
+ if (throttlingReadBytesPerSecond > 0 || throttlingWriteBytesPerSecond > 0) {
+ LOG.info(
+ "Throttling enabled : read {} bytes/s, write {} bytes/s",
+ throttlingReadBytesPerSecond,
+ throttlingWriteBytesPerSecond);
+ bootstrap.withThrottling(throttlingReadBytesPerSecond, throttlingWriteBytesPerSecond);
+ }
- System.out.println("About to start server on port: " + port);
- HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer
- .bootstrapFromFile("./littleproxy.properties")
- .withPort(port)
- .withAllowLocalOnly(false);
+ if (cmd.hasOption(OPTION_ALLOW_REQUEST_TO_ORIGIN_SERVER)) {
+ String optionValue = cmd.getOptionValue(OPTION_ALLOW_REQUEST_TO_ORIGIN_SERVER);
+ LOG.info("Allow request to origin server :'{}'", optionValue);
+ if (optionValue != null) {
+ bootstrap.withAllowRequestToOriginServer(Boolean.parseBoolean(optionValue));
+ }
+ }
- if (cmd.hasOption(OPTION_NIC)) {
- final String val = cmd.getOptionValue(OPTION_NIC);
- bootstrap.withNetworkInterface(new InetSocketAddress(val, 0));
- }
+ if (cmd.hasOption(OPTION_ALLOW_PROXY_PROTOCOL)) {
+ String optionValue = cmd.getOptionValue(OPTION_ALLOW_PROXY_PROTOCOL);
+ LOG.info("Allow proxy protocol :'{}'", optionValue);
+ if (optionValue != null) {
+ bootstrap.withAcceptProxyProtocol(Boolean.parseBoolean(optionValue));
+ }
+ }
- if (cmd.hasOption(OPTION_MITM)) {
- LOG.info("Running as Man in the Middle");
- bootstrap.withManInTheMiddle(new SelfSignedMitmManager());
- }
-
- if (cmd.hasOption(OPTION_DNSSEC)) {
- final String val = cmd.getOptionValue(OPTION_DNSSEC);
- if (ProxyUtils.isTrue(val)) {
- LOG.info("Using DNSSEC");
- bootstrap.withUseDnsSec(true);
- } else if (ProxyUtils.isFalse(val)) {
- LOG.info("Not using DNSSEC");
- bootstrap.withUseDnsSec(false);
- } else {
- printHelp(options, "Unexpected value for " + OPTION_DNSSEC
- + "=:" + val);
- return;
- }
- }
+ if (cmd.hasOption(OPTION_SEND_PROXY_PROTOCOL)) {
+ String optionValue = cmd.getOptionValue(OPTION_SEND_PROXY_PROTOCOL);
+ LOG.info("Send proxy protocol header:'{}'", optionValue);
+ if (optionValue != null) {
+ bootstrap.withSendProxyProtocol(Boolean.parseBoolean(optionValue));
+ }
+ }
- System.out.println("About to start...");
- bootstrap.start();
+ ThreadPoolConfiguration threadPoolConfiguration = new ThreadPoolConfiguration();
+ boolean threadPoolConfigSet =
+ false; // Flag to track if thread pool configuration is set through command line
+ // options
+ if (cmd.hasOption(OPTION_CLIENT_TO_PROXY_WORKER_THREADS)) {
+ String optionValue = cmd.getOptionValue(OPTION_CLIENT_TO_PROXY_WORKER_THREADS);
+ LOG.info("Setting client to proxy worker threads to :'{}'", optionValue);
+ if (optionValue != null) {
+ threadPoolConfiguration.withClientToProxyWorkerThreads(Integer.parseInt(optionValue));
+ threadPoolConfigSet = true;
+ }
+ }
+ if (cmd.hasOption(OPTION_PROXY_TO_SERVER_WORKER_THREADS)) {
+ String optionValue = cmd.getOptionValue(OPTION_PROXY_TO_SERVER_WORKER_THREADS);
+ LOG.info("Setting proxy to server worker threads to :'{}'", optionValue);
+ if (optionValue != null) {
+ threadPoolConfiguration.withProxyToServerWorkerThreads(Integer.parseInt(optionValue));
+ threadPoolConfigSet = true;
+ }
+ }
+ if (cmd.hasOption(OPTION_ACCEPTOR_THREADS)) {
+ String optionValue = cmd.getOptionValue(OPTION_ACCEPTOR_THREADS);
+ LOG.info("Setting acceptor threads to :'{}'", optionValue);
+ if (optionValue != null) {
+ threadPoolConfiguration.withAcceptorThreads(Integer.parseInt(optionValue));
+ threadPoolConfigSet = true;
+ }
+ }
+ if (threadPoolConfigSet) {
+ bootstrap.withThreadPoolConfiguration(threadPoolConfiguration);
}
- private static void printHelp(final Options options,
- final String errorMessage) {
- if (!StringUtils.isBlank(errorMessage)) {
- LOG.error(errorMessage);
- System.err.println(errorMessage);
- }
+ if (cmd.hasOption(OPTION_ACTIVITY_LOG_FORMAT)) {
+ String format = cmd.getOptionValue(OPTION_ACTIVITY_LOG_FORMAT);
+ try {
+ LogFormat logFormat = LogFormat.valueOf(format.toUpperCase());
+ bootstrap.plusActivityTracker(new ActivityLogger(logFormat));
+ LOG.info("Using activity log format: {}", logFormat);
+ } catch (IllegalArgumentException e) {
+ printHelp(options, "Unknown activity log format: " + format);
+ return;
+ }
+ }
- final HelpFormatter formatter = new HelpFormatter();
- formatter.printHelp("littleproxy", options);
+ LOG.info("About to start...");
+ httpProxyServer = bootstrap.start();
+ if (cmd.hasOption(OPTION_SERVER)) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> stop()));
+ try {
+ Thread.currentThread().join();
+ } catch (InterruptedException e) {
+ stop();
+ Thread.currentThread().interrupt();
+ }
}
+ }
- private static void pollLog4JConfigurationFileIfAvailable() {
- File log4jConfigurationFile = new File("src/test/resources/log4j.xml");
- if (log4jConfigurationFile.exists()) {
- DOMConfigurator.configureAndWatch(
- log4jConfigurationFile.getAbsolutePath(), 15);
- }
+ public boolean isRunning() {
+ return httpProxyServer != null;
+ }
+
+ public void stop() {
+ HttpProxyServer server = httpProxyServer;
+ if (server != null) {
+ LOG.info("Shutting down...");
+ server.stop();
+ httpProxyServer = null;
+ LOG.info("Shut down.");
}
+ }
+
+ @SuppressWarnings("java:S106")
+ private void configureLogging(CommandLine cmd) {
+ if (cmd.hasOption(OPTION_LOG_CONFIG)) {
+ String optionValue = cmd.getOptionValue(OPTION_LOG_CONFIG);
+ File logConfigPath = new File(optionValue);
+ if (logConfigPath.exists()) {
+ Configurator.initialize(null, logConfigPath.getAbsolutePath());
+ }
+ } else {
+ // default log4j.xml file shipped with the jar
+ ClassLoader classLoader = Launcher.class.getClassLoader();
+ URL defaultLogConfigUrl = classLoader.getResource("littleproxy_default_log4j2.xml");
+ Configurator.initialize(null, defaultLogConfigUrl.toString());
+ System.out.println("using 'littleproxy_default_log4j2.xml'");
+ }
+ }
+
+ private @NonNull CommandLine parseCommandLine(String[] args, Options options) {
+ final CommandLineParser parser = new DefaultParser();
+ CommandLine cmd;
+ try {
+ cmd = parser.parse(options, args);
+ if (cmd.getArgs().length > 0) {
+ throw new UnrecognizedOptionException(
+ "Extra arguments were provided in " + Arrays.asList(args));
+ }
+ } catch (final ParseException e) {
+ printHelp(options, "Could not parse command line: " + Arrays.asList(args));
+ throw new IllegalArgumentException("Could not parse command line: " + Arrays.asList(args), e);
+ }
+ return cmd;
+ }
+
+ protected @NonNull Options buildOptions() {
+ final Options options = new Options();
+ options.addOption(null, OPTION_DNSSEC, true, "Request and verify DNSSEC signatures.");
+ options.addOption(
+ null, OPTION_CONFIG, true, "Path to proxy configuration file (relative or absolute).");
+ options.addOption(
+ null,
+ OPTION_LOG_CONFIG,
+ true,
+ "Path to log4j configuration file (relative to current directory or absolute).");
+ options.addOption(null, OPTION_PORT, true, "Run on the specified port.");
+ options.addOption(null, OPTION_NIC, true, "Run on a specified Nic");
+ options.addOption(null, OPTION_HELP, false, "Display command line help.");
+ options.addOption(null, OPTION_MITM, false, "Run as man in the middle.");
+ options.addOption(null, OPTION_SERVER, false, "Run proxy as a server.");
+ options.addOption(null, OPTION_NAME, true, "name of the proxy.");
+ options.addOption(null, OPTION_ADDRESS, true, "address to bind the proxy.");
+ options.addOption(null, OPTION_PROXY_ALIAS, true, "alias for the proxy.");
+ options.addOption(
+ null,
+ OPTION_ALLOW_LOCAL_ONLY,
+ true,
+ "Allow only local connections to the proxy (true|false).");
+ options.addOption(
+ null,
+ OPTION_AUTHENTICATE_SSL_CLIENTS,
+ true,
+ "Whether to authenticate SSL clients (true|false).");
+ options.addOption(
+ null,
+ OPTION_SSL_CLIENTS_TRUST_ALL_SERVERS,
+ true,
+ "Whether SSL clients should trust all servers (true|false).");
+ options.addOption(
+ null,
+ OPTION_SSL_CLIENTS_SEND_CERTS,
+ true,
+ "Whether SSL clients should send certificates (true|false).");
+ options.addOption(
+ null, OPTION_SSL_CLIENTS_KEYSTORE_PATH, true, "Path to keystore for SSL clients.");
+ options.addOption(
+ null, OPTION_SSL_CLIENTS_KEYSTORE_ALIAS, true, "Alias for the keystore for SSL clients.");
+ options.addOption(
+ null,
+ OPTION_SSL_CLIENTS_KEYSTORE_PASSWORD,
+ true,
+ "Password for the keystore for SSL clients.");
+ options.addOption(
+ null, OPTION_TRANSPARENT, true, "Whether to run in transparent mode (true|false).");
+ options.addOption(
+ null, OPTION_THROTTLE_READ_BYTES_PER_SECOND, true, "Throttling read bytes per second.");
+ options.addOption(
+ null, OPTION_THROTTLE_WRITE_BYTES_PER_SECOND, true, "Throttling write bytes per second.");
+ options.addOption(
+ null,
+ OPTION_ALLOW_REQUEST_TO_ORIGIN_SERVER,
+ true,
+ "Allow requests to origin server (true|false).");
+ options.addOption(
+ null, OPTION_ALLOW_PROXY_PROTOCOL, true, "Allow Proxy Protocol (true|false).");
+ options.addOption(
+ null, OPTION_SEND_PROXY_PROTOCOL, true, "send Proxy Protocol header (true|false).");
+ options.addOption(
+ null,
+ OPTION_CLIENT_TO_PROXY_WORKER_THREADS,
+ true,
+ "Number of client-to-proxy worker threads.");
+ options.addOption(
+ null,
+ OPTION_PROXY_TO_SERVER_WORKER_THREADS,
+ true,
+ "Number of proxy-to-server worker threads.");
+ options.addOption(null, OPTION_ACCEPTOR_THREADS, true, "Number of acceptor threads.");
+ options.addOption(
+ null, OPTION_ACTIVITY_LOG_FORMAT, true, "Activity log format: CLF, ELF, JSON, SQUID, W3C");
+ return options;
+ }
+
+ @SuppressWarnings("java:S106")
+ private void printHelp(final Options options, final String errorMessage) {
+ if (!StringUtils.isBlank(errorMessage)) {
+ LOG.error(errorMessage);
+ // log4j is not yet loaded at this point in some cases
+ System.err.println(errorMessage);
+ }
+
+ final HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("littleproxy", options);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/MitmManager.java b/src/main/java/org/littleshoot/proxy/MitmManager.java
index 7ba17eb3..bb209b95 100644
--- a/src/main/java/org/littleshoot/proxy/MitmManager.java
+++ b/src/main/java/org/littleshoot/proxy/MitmManager.java
@@ -1,54 +1,45 @@
package org.littleshoot.proxy;
import io.netty.handler.codec.http.HttpRequest;
-
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
/**
- * MITMManagers encapsulate the logic required for letting LittleProxy act as a
- * man in the middle for HTTPS requests.
+ * MITMManagers encapsulate the logic required for letting LittleProxy act as a man in the middle
+ * for HTTPS requests.
*/
public interface MitmManager {
- /**
- * Creates an {@link SSLEngine} for encrypting the server connection. The SSLEngine created by this method
- * may use the given peer information to send SNI information when connecting to the upstream host.
- *
- * @param peerHost to start a client connection to the server.
- * @param peerPort to start a client connection to the server.
- *
- * @return an SSLEngine used to connect to an upstream server
- */
- SSLEngine serverSslEngine(String peerHost, int peerPort);
+ /**
+ * Creates an {@link SSLEngine} for encrypting the server connection. The SSLEngine created by
+ * this method may use the given peer information to send SNI information when connecting to the
+ * upstream host.
+ *
+ * @param peerHost to start a client connection to the server.
+ * @param peerPort to start a client connection to the server.
+ * @return an SSLEngine used to connect to an upstream server
+ */
+ SSLEngine serverSslEngine(String peerHost, int peerPort);
- /**
- * Creates an {@link SSLEngine} for encrypting the server connection.
- *
- * @return an SSLEngine used to connect to an upstream server
- */
- SSLEngine serverSslEngine();
+ /**
+ * Creates an {@link SSLEngine} for encrypting the server connection.
+ *
+ * @return an SSLEngine used to connect to an upstream server
+ */
+ SSLEngine serverSslEngine();
- /**
- *
- * Creates an {@link SSLEngine} for encrypting the client connection based
- * on the given serverSslSession.
- *
- *
- *
- * The serverSslSession is provided in case this method needs to inspect the
- * server's certificates or something else about the encryption on the way
- * to the server.
- *
- *
- *
- * This is the place where one would implement impersonation of the server
- * by issuing replacement certificates signed by the proxy's own
- * certificate.
- *
- *
- * @param httpRequest the HTTP CONNECT request that is being man-in-the-middled
- * @param serverSslSession the {@link SSLSession} that's been established with the server
- * @return the SSLEngine used to connect to the client
- */
- SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession);
+ /**
+ * Creates an {@link SSLEngine} for encrypting the client connection based on the given
+ * serverSslSession.
+ *
+ *
The serverSslSession is provided in case this method needs to inspect the server's
+ * certificates or something else about the encryption on the way to the server.
+ *
+ *
This is the place where one would implement impersonation of the server by issuing
+ * replacement certificates signed by the proxy's own certificate.
+ *
+ * @param httpRequest the HTTP CONNECT request that is being man-in-the-middled
+ * @param serverSslSession the {@link SSLSession} that's been established with the server
+ * @return the SSLEngine used to connect to the client
+ */
+ SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession);
}
diff --git a/src/main/java/org/littleshoot/proxy/ProxyAuthenticator.java b/src/main/java/org/littleshoot/proxy/ProxyAuthenticator.java
index 9f96b689..3b1bb956 100644
--- a/src/main/java/org/littleshoot/proxy/ProxyAuthenticator.java
+++ b/src/main/java/org/littleshoot/proxy/ProxyAuthenticator.java
@@ -1,26 +1,22 @@
package org.littleshoot.proxy;
/**
- * Interface for objects that can authenticate someone for using our Proxy on
- * the basis of a username and password.
+ * Interface for objects that can authenticate someone for using our Proxy on the basis of a
+ * username and password.
*/
public interface ProxyAuthenticator {
- /**
- * Authenticates the user using the specified userName and password.
- *
- * @param userName
- * The user name.
- * @param password
- * The password.
- * @return true if the credentials are acceptable, otherwise
- * false.
- */
- boolean authenticate(String userName, String password);
-
- /**
- * The realm value to be used in the request for proxy authentication
- * ("Proxy-Authenticate" header). Returning null will cause the string
- * "Restricted Files" to be used by default.
- */
- String getRealm();
+ /**
+ * Authenticates the user using the specified userName and password.
+ *
+ * @param userName The username.
+ * @param password The password.
+ * @return true if the credentials are acceptable, otherwise false.
+ */
+ boolean authenticate(String userName, String password);
+
+ /**
+ * The realm value to be used in the request for proxy authentication ("Proxy-Authenticate"
+ * header). Returning null will cause the string "Restricted Files" to be used by default.
+ */
+ String getRealm();
}
diff --git a/src/main/java/org/littleshoot/proxy/ServerConnectionPoolType.java b/src/main/java/org/littleshoot/proxy/ServerConnectionPoolType.java
new file mode 100644
index 00000000..60562995
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ServerConnectionPoolType.java
@@ -0,0 +1,7 @@
+package org.littleshoot.proxy;
+
+/** Defines which implementation backs the shared server connection pool. */
+public enum ServerConnectionPoolType {
+ /** Simple ConcurrentHashMap-based pool. */
+ CONCURRENT_MAP
+}
diff --git a/src/main/java/org/littleshoot/proxy/SslEngineSource.java b/src/main/java/org/littleshoot/proxy/SslEngineSource.java
index c88ba7f0..85cd4007 100644
--- a/src/main/java/org/littleshoot/proxy/SslEngineSource.java
+++ b/src/main/java/org/littleshoot/proxy/SslEngineSource.java
@@ -2,29 +2,21 @@
import javax.net.ssl.SSLEngine;
-/**
- * Source for {@link SSLEngine}s.
- */
+/** Source for {@link SSLEngine}s. */
public interface SslEngineSource {
- /**
- * Returns an {@link SSLEngine} to use for a server connection from
- * LittleProxy to the client.
- */
- SSLEngine newSslEngine();
-
- /**
- * Returns an {@link SSLEngine} to use for a client connection from
- * LittleProxy to the upstream server. *
- *
- * Note: Peer information is needed to send the server_name extension in
- * handshake with Server Name Indication (SNI).
- *
- * @param peerHost
- * to start a client connection to the server.
- * @param peerPort
- * to start a client connection to the server.
- */
- SSLEngine newSslEngine(String peerHost, int peerPort);
+ /** Returns an {@link SSLEngine} to use for a server connection from LittleProxy to the client. */
+ SSLEngine newSslEngine();
+ /**
+ * Returns an {@link SSLEngine} to use for a client connection from LittleProxy to the upstream
+ * server. *
+ *
+ *
Note: Peer information is needed to send the server_name extension in handshake with Server
+ * Name Indication (SNI).
+ *
+ * @param peerHost to start a client connection to the server.
+ * @param peerPort to start a client connection to the server.
+ */
+ SSLEngine newSslEngine(String peerHost, int peerPort);
}
diff --git a/src/main/java/org/littleshoot/proxy/TransportProtocol.java b/src/main/java/org/littleshoot/proxy/TransportProtocol.java
index 49d4fd05..4e09e499 100644
--- a/src/main/java/org/littleshoot/proxy/TransportProtocol.java
+++ b/src/main/java/org/littleshoot/proxy/TransportProtocol.java
@@ -1,10 +1,6 @@
package org.littleshoot.proxy;
-/**
- * Enumeration of transport protocols supported by LittleProxy.
- *
- * UDT support is deprecated in Netty, so it's being deprecated here, too. We'll remove it when Netty removes it.
- */
+/** Enumeration of transport protocols supported by LittleProxy */
public enum TransportProtocol {
- TCP, @Deprecated UDT
-}
\ No newline at end of file
+ TCP
+}
diff --git a/src/main/java/org/littleshoot/proxy/UnknownChainedProxyTypeException.java b/src/main/java/org/littleshoot/proxy/UnknownChainedProxyTypeException.java
index d6176db0..72fb1bcc 100644
--- a/src/main/java/org/littleshoot/proxy/UnknownChainedProxyTypeException.java
+++ b/src/main/java/org/littleshoot/proxy/UnknownChainedProxyTypeException.java
@@ -1,13 +1,14 @@
package org.littleshoot.proxy;
/**
- * This exception indicates that the system was asked to use an
- * {@link ChainedProxyType} that it didn't know how to handle.
+ * This exception indicates that the system was asked to use an {@link ChainedProxyType} that it
+ * didn't know how to handle.
*/
public class UnknownChainedProxyTypeException extends RuntimeException {
- private static final long serialVersionUID = 1L;
-
- public UnknownChainedProxyTypeException(ChainedProxyType chainedProxyType) {
- super(String.format("Unknown %s: %s", ChainedProxyType.class.getSimpleName(), chainedProxyType));
- }
+ private static final long serialVersionUID = 1L;
+
+ public UnknownChainedProxyTypeException(ChainedProxyType chainedProxyType) {
+ super(
+ String.format("Unknown %s: %s", ChainedProxyType.class.getSimpleName(), chainedProxyType));
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/UnknownTransportProtocolException.java b/src/main/java/org/littleshoot/proxy/UnknownTransportProtocolException.java
index b264b818..40477fce 100644
--- a/src/main/java/org/littleshoot/proxy/UnknownTransportProtocolException.java
+++ b/src/main/java/org/littleshoot/proxy/UnknownTransportProtocolException.java
@@ -1,12 +1,13 @@
package org.littleshoot.proxy;
/**
- * This exception indicates that the system was asked to use a TransportProtocol that it didn't know how to handle.
+ * This exception indicates that the system was asked to use a TransportProtocol that it didn't know
+ * how to handle.
*/
public class UnknownTransportProtocolException extends RuntimeException {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- public UnknownTransportProtocolException(TransportProtocol transportProtocol) {
- super(String.format("Unknown TransportProtocol: %1$s", transportProtocol));
- }
+ public UnknownTransportProtocolException(TransportProtocol transportProtocol) {
+ super(String.format("Unknown TransportProtocol: %1$s", transportProtocol));
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/extras/ActivityLogger.java b/src/main/java/org/littleshoot/proxy/extras/ActivityLogger.java
new file mode 100644
index 00000000..f4cc933c
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/ActivityLogger.java
@@ -0,0 +1,300 @@
+package org.littleshoot.proxy.extras;
+
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import java.net.InetSocketAddress;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.littleshoot.proxy.ActivityTrackerAdapter;
+import org.littleshoot.proxy.FlowContext;
+import org.littleshoot.proxy.FullFlowContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** An {@link org.littleshoot.proxy.ActivityTracker} that logs HTTP activity. */
+public class ActivityLogger extends ActivityTrackerAdapter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ActivityLogger.class);
+ private static final String DATE_FORMAT_CLF = "dd/MMM/yyyy:HH:mm:ss Z";
+ public static final String UTC = "UTC";
+ public static final String USER_AGENT = "User-Agent";
+ public static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+
+ private final LogFormat logFormat;
+
+ private static class TimedRequest {
+ final HttpRequest request;
+ final long startTime;
+
+ TimedRequest(HttpRequest request, long startTime) {
+ this.request = request;
+ this.startTime = startTime;
+ }
+ }
+
+ private final Map requestMap = new ConcurrentHashMap<>();
+
+ public ActivityLogger(LogFormat logFormat) {
+ this.logFormat = logFormat;
+ }
+
+ @Override
+ public void requestReceivedFromClient(FlowContext flowContext, HttpRequest httpRequest) {
+ requestMap.put(flowContext, new TimedRequest(httpRequest, System.currentTimeMillis()));
+ }
+
+ @Override
+ public void responseSentToClient(FlowContext flowContext, HttpResponse httpResponse) {
+ TimedRequest timedRequest = requestMap.remove(flowContext);
+ if (timedRequest == null) {
+ return;
+ }
+
+ String logMessage = formatLogEntry(flowContext, timedRequest, httpResponse);
+ if (logMessage != null) {
+ log(logMessage);
+ }
+ }
+
+ protected void log(String message) {
+ LOG.info(message);
+ }
+
+ @Override
+ public void clientDisconnected(FlowContext flowContext, javax.net.ssl.SSLSession sslSession) {
+ requestMap.remove(flowContext);
+ }
+
+ @Override
+ public void connectionTimedOut(FlowContext flowContext) {
+ requestMap.remove(flowContext);
+ }
+
+ @Override
+ public void connectionExceptionCaught(FlowContext flowContext, Throwable cause) {
+ requestMap.remove(flowContext);
+ }
+
+ private String formatLogEntry(
+ FlowContext flowContext, TimedRequest timedInfo, HttpResponse response) {
+ HttpRequest request = timedInfo.request;
+ long duration = System.currentTimeMillis() - timedInfo.startTime;
+
+ StringBuilder sb = new StringBuilder();
+ InetSocketAddress clientAddress = flowContext.getClientAddress();
+ String clientIp = clientAddress != null ? clientAddress.getAddress().getHostAddress() : "-";
+ ZonedDateTime now = ZonedDateTime.now(ZoneId.of(UTC));
+
+ switch (logFormat) {
+ case CLF:
+ // host ident authuser [date] "request" status bytes
+ sb.append(clientIp).append(" ");
+ sb.append("- "); // ident
+ sb.append("- "); // authuser
+ sb.append("[").append(format(now, DATE_FORMAT_CLF)).append("] ");
+ sb.append("\"")
+ .append(request.method())
+ .append(" ")
+ .append(getFullUrl(request))
+ .append(" ")
+ .append(request.protocolVersion())
+ .append("\" ");
+ sb.append(response.status().code()).append(" ");
+ sb.append(getContentLength(response));
+ break;
+
+ case ELF:
+ // Extended Log Format (ELF) - actually NCSA Combined Log Format
+ // host ident authuser [date] "request" status bytes "referer" "user-agent"
+ sb.append(clientIp).append(" ");
+ sb.append("- "); // ident
+ sb.append("- "); // authuser
+ sb.append("[").append(format(now, DATE_FORMAT_CLF)).append("] ");
+ sb.append("\"")
+ .append(request.method())
+ .append(" ")
+ .append(getFullUrl(request))
+ .append(" ")
+ .append(request.protocolVersion())
+ .append("\" ");
+ sb.append(response.status().code()).append(" ");
+ sb.append(getContentLength(response)).append(" ");
+ sb.append("\"").append(getHeader(request, "Referer")).append("\" ");
+ sb.append("\"").append(getHeader(request, USER_AGENT)).append("\"");
+ break;
+
+ case W3C:
+ // W3C Extended Log Format (simplified default)
+ // date time c-ip cs-method cs-uri-stem sc-status sc-bytes
+ // time-taken(optional/unavailable) cs(User-Agent)
+ DateTimeFormatter w3cDateTimeFormatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US);
+ sb.append(now.format(w3cDateTimeFormatter)).append(" ");
+ sb.append(clientIp).append(" ");
+ sb.append(request.method()).append(" ");
+ sb.append(getFullUrl(request)).append(" ");
+ sb.append(response.status().code()).append(" ");
+ sb.append(getContentLength(response)).append(" ");
+ sb.append("\"").append(getHeader(request, USER_AGENT)).append("\"");
+ break;
+
+ case JSON:
+ sb.append("{");
+ sb.append("\"timestamp\":\"").append(format(now, ISO_8601_PATTERN)).append("\",");
+ sb.append("\"client_ip\":\"").append(clientIp).append("\",");
+ sb.append("\"method\":\"").append(request.method()).append("\",");
+ sb.append("\"uri\":\"").append(escapeJson(getFullUrl(request))).append("\",");
+ sb.append("\"protocol\":\"").append(request.protocolVersion()).append("\",");
+ sb.append("\"status\":").append(response.status().code()).append(",");
+ sb.append("\"bytes\":").append(getContentLength(response)).append(",");
+ sb.append("\"duration\":").append(duration).append(",");
+ sb.append("\"user_agent\":\"")
+ .append(escapeJson(getHeader(request, USER_AGENT)))
+ .append("\"");
+ sb.append("}");
+ break;
+
+ case LTSV:
+ // Labeled Tab-Separated Values
+ sb.append("time:").append(format(now, ISO_8601_PATTERN)).append("\t");
+ sb.append("host:").append(clientIp).append("\t");
+ sb.append("method:").append(request.method()).append("\t");
+ sb.append("uri:").append(getFullUrl(request)).append("\t");
+ sb.append("status:").append(response.status().code()).append("\t");
+ sb.append("size:").append(getContentLength(response)).append("\t");
+ sb.append("duration:").append(duration).append("\t");
+ sb.append("ua:").append(getHeader(request, USER_AGENT));
+ break;
+
+ case CSV:
+ // Comma-Separated Values: timestamp,host,method,uri,status,bytes,duration,ua
+ sb.append("\"").append(format(now, ISO_8601_PATTERN)).append("\",");
+ sb.append("\"").append(clientIp).append("\",");
+ sb.append("\"").append(request.method()).append("\",");
+ sb.append("\"").append(escapeJson(getFullUrl(request))).append("\",");
+ sb.append(response.status().code()).append(",");
+ sb.append(getContentLength(response)).append(",");
+ sb.append(duration).append(",");
+ sb.append("\"").append(escapeJson(getHeader(request, USER_AGENT))).append("\"");
+ break;
+
+ case SQUID:
+ // time elapsed remotehost code/status bytes method URL rfc931
+ // peerstatus/peerhost type
+ long timestamp = now.toEpochSecond();
+ sb.append(timestamp / 1000).append(".").append(timestamp % 1000).append(" ");
+ sb.append(duration).append(" "); // elapsed
+ sb.append(clientIp).append(" ");
+ sb.append("TCP_MISS/").append(response.status().code()).append(" ");
+ sb.append(getContentLength(response)).append(" ");
+ sb.append(request.method()).append(" ");
+ sb.append(getFullUrl(request)).append(" ");
+ sb.append("- "); // rfc931
+ sb.append("DIRECT/").append(getServerIp(flowContext)).append(" ");
+ sb.append(getContentType(response));
+ break;
+
+ case HAPROXY:
+ // HAProxy HTTP format approximation
+ // client_ip:port [date] frontend backend/server Tq Tw Tc Tr Tr_tot status bytes
+ // ...
+ // simplified: client_ip [date] method uri status bytes duration
+ sb.append(clientIp).append(" ");
+ sb.append("[").append(format(now, "dd/MMM/yyyy:HH:mm:ss.SSS")).append("] ");
+ sb.append("\"")
+ .append(request.method())
+ .append(" ")
+ .append(getFullUrl(request))
+ .append(" ")
+ .append(request.protocolVersion())
+ .append("\" ");
+ sb.append(response.status().code()).append(" ");
+ sb.append(getContentLength(response)).append(" ");
+ sb.append(duration); // duration in ms
+ break;
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Reconstructs the full URL from the request. If the URI is already absolute (starts with http://
+ * or https://), returns it as-is. Otherwise, prepends the Host header to create a complete URL.
+ *
+ * @param request the HTTP request
+ * @return the full URL
+ */
+ protected String getFullUrl(HttpRequest request) {
+ String uri = request.uri();
+
+ // Check if URI is already absolute (contains scheme)
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
+ return uri;
+ }
+
+ // For CONNECT requests, the URI is just host:port
+ if (request.method().name().equals("CONNECT")) {
+ return uri;
+ }
+
+ // Get host from Host header
+ String host = request.headers().get("Host");
+ if (host == null || host.isEmpty()) {
+ // Fallback: return URI as-is if no Host header
+ return uri;
+ }
+
+ // Determine scheme (default to http)
+ String scheme = "http";
+
+ // Reconstruct full URL
+ if (uri.startsWith("/")) {
+ return scheme + "://" + host + uri;
+ } else {
+ return scheme + "://" + host + "/" + uri;
+ }
+ }
+
+ private String format(ZonedDateTime zonedDateTime, String pattern) {
+ DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern, Locale.US);
+ return zonedDateTime.format(dtf);
+ }
+
+ private String getContentLength(HttpResponse response) {
+ String len = response.headers().get("Content-Length");
+ return len != null ? len : "-";
+ }
+
+ private String getHeader(HttpRequest request, String headerName) {
+ String val = request.headers().get(headerName);
+ return val != null ? val : "-";
+ }
+
+ private String getContentType(HttpResponse response) {
+ String val = response.headers().get("Content-Type");
+ return val != null ? val : "-";
+ }
+
+ private String getServerIp(FlowContext context) {
+ if (context instanceof FullFlowContext) {
+ String hostAndPort = ((FullFlowContext) context).getServerHostAndPort();
+ if (hostAndPort != null) {
+ // Returns "host:port", we want just the host/ip usually, or stick with
+ // host:port?
+ // Squid format usually asks for remotehost or peerhost.
+ // We will return request host.
+ return hostAndPort.split(":")[0];
+ }
+ }
+ return "-";
+ }
+
+ private String escapeJson(String s) {
+ if (s == null) return "";
+ return s.replace("\"", "\\\"").replace("\\", "\\\\");
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java b/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java
index a58fb928..5fe5779a 100644
--- a/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java
+++ b/src/main/java/org/littleshoot/proxy/extras/HAProxyMessageEncoder.java
@@ -7,18 +7,25 @@
/**
* Encodes an HAProxy proxy protocol header
*
- * @see Proxy Protocol Specification
+ * @see Proxy Protocol
+ * Specification
*/
public class HAProxyMessageEncoder extends MessageToByteEncoder {
- @Override
- protected void encode(ChannelHandlerContext ctx, ProxyProtocolMessage msg, ByteBuf out) {
- out.writeBytes(getHaProxyMessage(msg));
- }
-
- private byte [] getHaProxyMessage(ProxyProtocolMessage msg) {
- return String.format("%s %s %s %s %s %s\r\n", msg.getCommand(), msg.getProxiedProtocol(), msg.getSourceAddress(), msg.getDestinationAddress(), msg.getSourcePort(),
- msg.getDestinationPort()).getBytes();
- }
+ @Override
+ protected void encode(ChannelHandlerContext ctx, ProxyProtocolMessage msg, ByteBuf out) {
+ out.writeBytes(getHaProxyMessage(msg));
+ }
+ private byte[] getHaProxyMessage(ProxyProtocolMessage msg) {
+ return String.format(
+ "%s %s %s %s %s %s\r\n",
+ msg.getCommand(),
+ msg.getProxiedProtocol(),
+ msg.getSourceAddress(),
+ msg.getDestinationAddress(),
+ msg.getSourcePort(),
+ msg.getDestinationPort())
+ .getBytes();
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/extras/LogFormat.java b/src/main/java/org/littleshoot/proxy/extras/LogFormat.java
new file mode 100644
index 00000000..0244d88b
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/LogFormat.java
@@ -0,0 +1,34 @@
+package org.littleshoot.proxy.extras;
+
+/** Enumeration of supported log formats for the {@link ActivityLogger}. */
+public enum LogFormat {
+ /** Common Log Format (CLF). host ident authuser [date] "request" status bytes */
+ CLF,
+
+ /**
+ * Extended Log Format (ELF). Similar to w3c but often customizable. We'll use a standard extended
+ * set.
+ */
+ ELF,
+
+ /** JSON Format. Structured log in JSON. */
+ JSON,
+
+ /**
+ * Squid Native access log format. time elapsed remotehost code/status bytes method URL rfc931
+ * peerstatus/peerhost type
+ */
+ SQUID,
+
+ /** W3C Extended Log File Format. */
+ W3C,
+
+ /** Labeled Tab-Separated Values (LTSV). label:value\tlabel2:value2 */
+ LTSV,
+
+ /** Comma-Separated Values (CSV). "timestamp","host","method","uri",... */
+ CSV,
+
+ /** HAProxy HTTP Log Format. Includes detailed timing information. */
+ HAPROXY
+}
diff --git a/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java b/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java
index cee89ad0..227d2386 100644
--- a/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java
+++ b/src/main/java/org/littleshoot/proxy/extras/ProxyProtocolMessage.java
@@ -7,60 +7,66 @@
public class ProxyProtocolMessage {
- private HAProxyProtocolVersion protocolVersion;
- private HAProxyCommand command;
- private HAProxyProxiedProtocol proxiedProtocol;
- private String sourceAddress;
- private String destinationAddress;
- private int sourcePort;
- private int destinationPort;
+ private final HAProxyProtocolVersion protocolVersion;
+ private final HAProxyCommand command;
+ private final HAProxyProxiedProtocol proxiedProtocol;
+ private final String sourceAddress;
+ private final String destinationAddress;
+ private final int sourcePort;
+ private final int destinationPort;
- public ProxyProtocolMessage(HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, String sourceAddress, String destinationAddress
- , int sourcePort, int destinationPort) {
- this.protocolVersion = protocolVersion;
- this.command = command;
- this.proxiedProtocol = proxiedProtocol;
- this.sourceAddress = sourceAddress;
- this.destinationAddress = destinationAddress;
- this.sourcePort = sourcePort;
- this.destinationPort = destinationPort;
- }
+ public ProxyProtocolMessage(
+ HAProxyProtocolVersion protocolVersion,
+ HAProxyCommand command,
+ HAProxyProxiedProtocol proxiedProtocol,
+ String sourceAddress,
+ String destinationAddress,
+ int sourcePort,
+ int destinationPort) {
+ this.protocolVersion = protocolVersion;
+ this.command = command;
+ this.proxiedProtocol = proxiedProtocol;
+ this.sourceAddress = sourceAddress;
+ this.destinationAddress = destinationAddress;
+ this.sourcePort = sourcePort;
+ this.destinationPort = destinationPort;
+ }
- public ProxyProtocolMessage(HAProxyMessage haProxyMessage) {
- this.protocolVersion = haProxyMessage.protocolVersion();
- this.command = haProxyMessage.command();
- this.proxiedProtocol = haProxyMessage.proxiedProtocol();
- this.sourceAddress = haProxyMessage.sourceAddress();
- this.destinationAddress = haProxyMessage.destinationAddress();
- this.sourcePort = haProxyMessage.sourcePort();
- this.destinationPort = haProxyMessage.destinationPort();
- }
+ public ProxyProtocolMessage(HAProxyMessage haProxyMessage) {
+ protocolVersion = haProxyMessage.protocolVersion();
+ command = haProxyMessage.command();
+ proxiedProtocol = haProxyMessage.proxiedProtocol();
+ sourceAddress = haProxyMessage.sourceAddress();
+ destinationAddress = haProxyMessage.destinationAddress();
+ sourcePort = haProxyMessage.sourcePort();
+ destinationPort = haProxyMessage.destinationPort();
+ }
- public HAProxyProtocolVersion getProtocolVersion() {
- return protocolVersion;
- }
+ public HAProxyProtocolVersion getProtocolVersion() {
+ return protocolVersion;
+ }
- public HAProxyCommand getCommand() {
- return command;
- }
+ public HAProxyCommand getCommand() {
+ return command;
+ }
- public HAProxyProxiedProtocol getProxiedProtocol() {
- return proxiedProtocol;
- }
+ public HAProxyProxiedProtocol getProxiedProtocol() {
+ return proxiedProtocol;
+ }
- public String getSourceAddress() {
- return sourceAddress;
- }
+ public String getSourceAddress() {
+ return sourceAddress;
+ }
- public String getDestinationAddress() {
- return destinationAddress;
- }
+ public String getDestinationAddress() {
+ return destinationAddress;
+ }
- public int getSourcePort() {
- return sourcePort;
- }
+ public int getSourcePort() {
+ return sourcePort;
+ }
- public int getDestinationPort() {
- return destinationPort;
- }
+ public int getDestinationPort() {
+ return destinationPort;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
index 190eacaa..2f6b366b 100644
--- a/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
+++ b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManager.java
@@ -1,37 +1,39 @@
package org.littleshoot.proxy.extras;
import io.netty.handler.codec.http.HttpRequest;
-import org.littleshoot.proxy.MitmManager;
-
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
+import org.littleshoot.proxy.MitmManager;
-/**
- * {@link MitmManager} that uses self-signed certs for everything.
- */
+/** {@link MitmManager} that uses self-signed certs for everything. */
public class SelfSignedMitmManager implements MitmManager {
- private final SelfSignedSslEngineSource selfSignedSslEngineSource;
-
- public SelfSignedMitmManager() {
- this.selfSignedSslEngineSource = new SelfSignedSslEngineSource(true);
- }
-
- public SelfSignedMitmManager(SelfSignedSslEngineSource selfSignedSslEngineSource) {
- this.selfSignedSslEngineSource = selfSignedSslEngineSource;
- }
-
- @Override
- public SSLEngine serverSslEngine(String peerHost, int peerPort) {
- return selfSignedSslEngineSource.newSslEngine(peerHost, peerPort);
- }
-
- @Override
- public SSLEngine serverSslEngine() {
- return selfSignedSslEngineSource.newSslEngine();
- }
-
- @Override
- public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) {
- return selfSignedSslEngineSource.newSslEngine();
- }
+ private final SelfSignedSslEngineSource selfSignedSslEngineSource;
+
+ public SelfSignedMitmManager(String keyStorePath) {
+ selfSignedSslEngineSource = new SelfSignedSslEngineSource(keyStorePath, true, true);
+ }
+
+ public SelfSignedMitmManager(String keyStorePath, boolean trustAllServers, boolean sendCerts) {
+ selfSignedSslEngineSource =
+ new SelfSignedSslEngineSource(keyStorePath, trustAllServers, sendCerts);
+ }
+
+ public SelfSignedMitmManager(SelfSignedSslEngineSource selfSignedSslEngineSource) {
+ this.selfSignedSslEngineSource = selfSignedSslEngineSource;
+ }
+
+ @Override
+ public SSLEngine serverSslEngine(String peerHost, int peerPort) {
+ return selfSignedSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
+
+ @Override
+ public SSLEngine serverSslEngine() {
+ return selfSignedSslEngineSource.newSslEngine();
+ }
+
+ @Override
+ public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) {
+ return selfSignedSslEngineSource.newSslEngine();
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java b/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
index 3d435059..0efa224c 100644
--- a/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
+++ b/src/main/java/org/littleshoot/proxy/extras/SelfSignedSslEngineSource.java
@@ -1,191 +1,224 @@
package org.littleshoot.proxy.extras;
-import com.google.common.io.ByteStreams;
-import org.littleshoot.proxy.SslEngineSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import static java.lang.System.nanoTime;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static java.util.Objects.requireNonNullElse;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import javax.net.ssl.*;
+import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Security;
-import java.security.cert.X509Certificate;
-import java.util.Arrays;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import org.littleshoot.proxy.SslEngineSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
- * Basic {@link SslEngineSource} for testing. The {@link SSLContext} uses
- * self-signed certificates that are generated lazily if the given key store
- * file doesn't yet exist.
+ * Basic {@link SslEngineSource} for testing. The {@link SSLContext} uses self-signed certificates
+ * that are generated lazily if the given key store file doesn't yet exist.
*/
public class SelfSignedSslEngineSource implements SslEngineSource {
- private static final Logger LOG = LoggerFactory
- .getLogger(SelfSignedSslEngineSource.class);
-
- private static final String PROTOCOL = "TLS";
-
- private final String alias;
- private final String password;
- private final String keyStoreFile;
- private final boolean trustAllServers;
- private final boolean sendCerts;
-
- private SSLContext sslContext;
-
- public SelfSignedSslEngineSource(String keyStorePath, boolean trustAllServers, boolean sendCerts,
- String alias, String password) {
- this.trustAllServers = trustAllServers;
- this.sendCerts = sendCerts;
- this.keyStoreFile = keyStorePath;
- this.alias = alias;
- this.password = password;
- initializeSSLContext();
+ private static final Logger LOG = LoggerFactory.getLogger(SelfSignedSslEngineSource.class);
+
+ private static final String PROTOCOL = "TLS";
+
+ private final String alias;
+ private final String password;
+ private final String keyStoreFile;
+ private final boolean trustAllServers;
+ private final boolean sendCerts;
+
+ private SSLContext sslContext;
+
+ public SelfSignedSslEngineSource(
+ String keyStorePath,
+ boolean trustAllServers,
+ boolean sendCerts,
+ String alias,
+ String password) {
+ this.trustAllServers = trustAllServers;
+ this.sendCerts = sendCerts;
+ this.keyStoreFile = keyStorePath;
+ this.alias = alias;
+ this.password = password;
+ initializeSSLContext();
+ }
+
+ public SelfSignedSslEngineSource(
+ String keyStorePath, boolean trustAllServers, boolean sendCerts) {
+ this(keyStorePath, trustAllServers, sendCerts, "littleproxy", "Be Your Own Lantern");
+ }
+
+ public SelfSignedSslEngineSource(String keyStorePath) {
+ this(keyStorePath, false, true);
+ }
+
+ public SelfSignedSslEngineSource(boolean trustAllServers) {
+ this(trustAllServers, true);
+ }
+
+ public SelfSignedSslEngineSource(boolean trustAllServers, boolean sendCerts) {
+ this("littleproxy_keystore.jks", trustAllServers, sendCerts);
+ }
+
+ public SelfSignedSslEngineSource() {
+ this(false);
+ }
+
+ @Override
+ public SSLEngine newSslEngine() {
+ return sslContext.createSSLEngine();
+ }
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return sslContext.createSSLEngine(peerHost, peerPort);
+ }
+
+ public SSLContext getSslContext() {
+ return sslContext;
+ }
+
+ private void initializeKeyStore(File keyStoreLocalFile) {
+ initializeKeyStore(keyStoreLocalFile, "littleproxy_cert");
+ }
+
+ private void initializeKeyStore(File keyStoreLocalFile, String certificateFileName) {
+ File keyStoreLocalAbsoluteFile = keyStoreLocalFile.getAbsoluteFile();
+
+ nativeCall(
+ "keytool",
+ "-genkey",
+ "-alias",
+ alias,
+ "-keysize",
+ "4096",
+ "-validity",
+ "36500",
+ "-keyalg",
+ "RSA",
+ "-dname",
+ "CN=littleproxy",
+ "-keypass",
+ password,
+ "-storepass",
+ password,
+ "-keystore",
+ keyStoreLocalAbsoluteFile.getPath());
+
+ LOG.info("Generated LittleProxy keystore in {}", keyStoreLocalAbsoluteFile);
+
+ Path certificateFile = Paths.get(keyStoreLocalAbsoluteFile.getParent(), certificateFileName);
+ nativeCall(
+ "keytool",
+ "-exportcert",
+ "-alias",
+ alias,
+ "-keystore",
+ keyStoreLocalAbsoluteFile.getPath(),
+ "-storepass",
+ password,
+ "-file",
+ certificateFile.toString());
+ LOG.info("Generated LittleProxy certificate in {}", certificateFile);
+ }
+
+ private void initializeSSLContext() {
+ String algorithm =
+ requireNonNullElse(Security.getProperty("ssl.KeyManagerFactory.algorithm"), "SunX509");
+
+ try {
+ final KeyStore ks = loadKeyStore();
+
+ // Set up key manager factory to use our key store
+ final KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
+ kmf.init(ks, password.toCharArray());
+
+ // Set up a trust manager factory to use our key store
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
+ tmf.init(ks);
+
+ TrustManager[] trustManagers = createTrustManagers(tmf);
+ KeyManager[] keyManagers = sendCerts ? kmf.getKeyManagers() : new KeyManager[0];
+
+ // Initialize the SSLContext to work with our key managers.
+ sslContext = SSLContext.getInstance(PROTOCOL);
+ sslContext.init(keyManagers, trustManagers, null);
+ } catch (IOException | GeneralSecurityException e) {
+ throw new RuntimeException("Failed to initialize the server-side SSLContext", e);
}
-
- public SelfSignedSslEngineSource(String keyStorePath, boolean trustAllServers, boolean sendCerts) {
- this(keyStorePath, trustAllServers, sendCerts, "littleproxy", "Be Your Own Lantern");
- }
-
- public SelfSignedSslEngineSource(String keyStorePath) {
- this(keyStorePath, false, true);
- }
-
- public SelfSignedSslEngineSource(boolean trustAllServers) {
- this(trustAllServers, true);
- }
-
- public SelfSignedSslEngineSource(boolean trustAllServers, boolean sendCerts) {
- this("littleproxy_keystore.jks", trustAllServers, sendCerts);
+ }
+
+ private TrustManager[] createTrustManagers(TrustManagerFactory tmf) {
+ return trustAllServers
+ ? new TrustManager[] {new TrustingTrustManager()}
+ : tmf.getTrustManagers();
+ }
+
+ private KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
+ URL resourceUrl = getClass().getResource(keyStoreFile);
+ if (resourceUrl != null) {
+ return loadKeyStore(resourceUrl);
+ } else {
+ File keyStoreLocalFile = new File(keyStoreFile);
+ if (!keyStoreLocalFile.isFile()) {
+ initializeKeyStore(keyStoreLocalFile);
+ }
+ return loadKeyStore(keyStoreLocalFile.toURI().toURL());
}
+ }
- public SelfSignedSslEngineSource() {
- this(false);
+ private KeyStore loadKeyStore(URL url) throws IOException, GeneralSecurityException {
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (InputStream is = url.openStream()) {
+ keyStore.load(is, password.toCharArray());
}
-
- @Override
- public SSLEngine newSslEngine() {
- return sslContext.createSSLEngine();
- }
-
- @Override
- public SSLEngine newSslEngine(String peerHost, int peerPort) {
- return sslContext.createSSLEngine(peerHost, peerPort);
- }
-
- public SSLContext getSslContext() {
- return sslContext;
- }
-
- private void initializeKeyStore(String filename) {
- nativeCall("keytool", "-genkey", "-alias", alias, "-keysize",
- "4096", "-validity", "36500", "-keyalg", "RSA", "-dname",
- "CN=littleproxy", "-keypass", password, "-storepass",
- password, "-keystore", filename);
-
- nativeCall("keytool", "-exportcert", "-alias", alias, "-keystore",
- filename, "-storepass", password, "-file",
- "littleproxy_cert");
- }
-
- private void initializeSSLContext() {
- String algorithm = Security
- .getProperty("ssl.KeyManagerFactory.algorithm");
- if (algorithm == null) {
- algorithm = "SunX509";
- }
-
- try {
- final KeyStore ks = loadKeyStore();
-
- // Set up key manager factory to use our key store
- final KeyManagerFactory kmf =
- KeyManagerFactory.getInstance(algorithm);
- kmf.init(ks, password.toCharArray());
-
- // Set up a trust manager factory to use our key store
- TrustManagerFactory tmf = TrustManagerFactory
- .getInstance(algorithm);
- tmf.init(ks);
-
- TrustManager[] trustManagers;
- if (!trustAllServers) {
- trustManagers = tmf.getTrustManagers();
- } else {
- trustManagers = new TrustManager[] { new X509TrustManager() {
- // TrustManager that trusts all servers
- @Override
- public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return null;
- }
- } };
- }
-
- KeyManager[] keyManagers;
- if (sendCerts) {
- keyManagers = kmf.getKeyManagers();
- } else {
- keyManagers = new KeyManager[0];
- }
-
- // Initialize the SSLContext to work with our key managers.
- sslContext = SSLContext.getInstance(PROTOCOL);
- sslContext.init(keyManagers, trustManagers, null);
- } catch (final Exception e) {
- throw new Error(
- "Failed to initialize the server-side SSLContext", e);
- }
- }
-
- private KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
- final KeyStore keyStore = KeyStore.getInstance("JKS");
- URL resourceUrl = getClass().getResource(keyStoreFile);
- if(resourceUrl != null) {
- loadKeyStore(keyStore, resourceUrl);
- } else {
- File keyStoreLocalFile = new File(keyStoreFile);
- if(!keyStoreLocalFile.isFile()) {
- initializeKeyStore(keyStoreLocalFile.getName());
- }
- loadKeyStore(keyStore, keyStoreLocalFile.toURI().toURL());
- }
- return keyStore;
- }
-
- private void loadKeyStore(KeyStore keyStore, URL url) throws IOException, GeneralSecurityException {
- try(InputStream is = url.openStream()) {
- keyStore.load(is, password.toCharArray());
- }
- }
-
- private String nativeCall(final String... commands) {
- LOG.info("Running '{}'", Arrays.asList(commands));
- final ProcessBuilder pb = new ProcessBuilder(commands);
- try {
- final Process process = pb.start();
- byte[] data;
- try (InputStream is = process.getInputStream()) {
- data = ByteStreams.toByteArray(is);
- }
- String dataAsString = new String(data);
-
- LOG.info("Completed native call: '{}'\nResponse: '" + dataAsString + "'",
- Arrays.asList(commands));
- return dataAsString;
- } catch (final IOException e) {
- LOG.error("Error running commands: " + Arrays.asList(commands), e);
- return "";
- }
+ LOG.debug("Loaded LittleProxy keystore from {}", url);
+ return keyStore;
+ }
+
+ private void nativeCall(final String... commands) {
+ long start = nanoTime();
+ LOG.info("Running '{}'", asList(commands));
+ final ProcessBuilder pb = new ProcessBuilder(commands);
+ // Merge stderr into stdout so we only need to read one stream
+ pb.redirectErrorStream(true);
+ try {
+ final Process process = pb.start();
+ byte[] data;
+ try (InputStream is = process.getInputStream()) {
+ data = ByteStreams.toByteArray(is);
+ }
+ int exitCode = process.waitFor();
+ String dataAsString = new String(data, UTF_8);
+ LOG.info(
+ "Completed native call '{}' in {} ms (exit: {})\nResponse: '{}'",
+ asList(commands),
+ duration(start),
+ exitCode,
+ dataAsString);
+ } catch (IOException e) {
+ LOG.error("Error running commands {} after {} ms", asList(commands), duration(start), e);
+ } catch (InterruptedException e) {
+ LOG.error("Error running commands {} after {} ms", asList(commands), duration(start), e);
+ Thread.currentThread().interrupt();
}
+ }
+ private long duration(long start) {
+ return NANOSECONDS.toMillis(nanoTime() - start);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/extras/TrustingTrustManager.java b/src/main/java/org/littleshoot/proxy/extras/TrustingTrustManager.java
new file mode 100644
index 00000000..e7ff656c
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/TrustingTrustManager.java
@@ -0,0 +1,18 @@
+package org.littleshoot.proxy.extras;
+
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/** TrustManager that trusts all servers */
+class TrustingTrustManager implements X509TrustManager {
+ @Override
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1) {}
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] arg0, String arg1) {}
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/CategorizedThreadFactory.java b/src/main/java/org/littleshoot/proxy/impl/CategorizedThreadFactory.java
index 54c07ab7..70ab09c8 100644
--- a/src/main/java/org/littleshoot/proxy/impl/CategorizedThreadFactory.java
+++ b/src/main/java/org/littleshoot/proxy/impl/CategorizedThreadFactory.java
@@ -1,47 +1,56 @@
package org.littleshoot.proxy.impl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-/**
- * A ThreadFactory that adds LittleProxy-specific information to the threads' names.
- */
+/** A ThreadFactory that adds LittleProxy-specific information to the threads' names. */
public class CategorizedThreadFactory implements ThreadFactory {
- private static final Logger log = LoggerFactory.getLogger(CategorizedThreadFactory.class);
-
- private final String name;
- private final String category;
- private final int uniqueServerGroupId;
-
- private AtomicInteger threadCount = new AtomicInteger(0);
-
- /**
- * Exception handler for proxy threads. Logs the name of the thread and the exception that was caught.
- */
- private static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER = (t, e) -> log.error("Uncaught throwable in thread: {}", t.getName(), e);
-
-
- /**
- * @param name the user-supplied name of this proxy
- * @param category the type of threads this factory is creating (acceptor, client-to-proxy worker, proxy-to-server worker)
- * @param uniqueServerGroupId a unique number for the server group creating this thread factory, to differentiate multiple proxy instances with the same name
- */
- public CategorizedThreadFactory(String name, String category, int uniqueServerGroupId) {
- this.category = category;
- this.name = name;
- this.uniqueServerGroupId = uniqueServerGroupId;
- }
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r, name + "-" + uniqueServerGroupId + "-" + category + "-" + threadCount.getAndIncrement());
-
- t.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
-
- return t;
- }
-
+ private static final Logger log = LoggerFactory.getLogger(CategorizedThreadFactory.class);
+
+ private final String name;
+ private final String category;
+ private final int uniqueServerGroupId;
+
+ private final AtomicInteger threadCount = new AtomicInteger(0);
+
+ /**
+ * Exception handler for proxy threads. Logs the name of the thread and the exception that was
+ * caught.
+ */
+ private static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER =
+ (t, e) -> log.error("Uncaught throwable in thread: {}", t.getName(), e);
+
+ /**
+ * @param name the user-supplied name of this proxy
+ * @param category the type of threads this factory is creating (acceptor, client-to-proxy worker,
+ * proxy-to-server worker)
+ * @param uniqueServerGroupId a unique number for the server group creating this thread factory,
+ * to differentiate multiple proxy instances with the same name
+ */
+ public CategorizedThreadFactory(String name, String category, int uniqueServerGroupId) {
+ this.category = category;
+ this.name = name;
+ this.uniqueServerGroupId = uniqueServerGroupId;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t =
+ new Thread(
+ r,
+ name
+ + "-"
+ + uniqueServerGroupId
+ + "-"
+ + category
+ + "-"
+ + threadCount.getAndIncrement());
+
+ t.setDaemon(true);
+ t.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
+
+ return t;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java b/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java
index aa47c276..dda9aac8 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientDetails.java
@@ -2,34 +2,28 @@
import java.net.InetSocketAddress;
-/**
- * Contains information about the client.
- */
+/** Contains information about the client. */
public class ClientDetails {
- /**
- * The user name that was used for authentication, or null if authentication wasn't performed.
- */
- private volatile String userName;
+ /** The username that was used for authentication, or null if authentication wasn't performed. */
+ private volatile String userName;
- /**
- * The client's address
- */
- private volatile InetSocketAddress clientAddress;
+ /** The client's address */
+ private volatile InetSocketAddress clientAddress;
- public String getUserName() {
- return userName;
- }
+ public String getUserName() {
+ return userName;
+ }
- void setUserName(String userName) {
- this.userName = userName;
- }
+ void setUserName(String userName) {
+ this.userName = userName;
+ }
- public InetSocketAddress getClientAddress() {
- return clientAddress;
- }
+ public InetSocketAddress getClientAddress() {
+ return clientAddress;
+ }
- void setClientAddress(InetSocketAddress clientAddress) {
- this.clientAddress = clientAddress;
- }
+ void setClientAddress(InetSocketAddress clientAddress) {
+ this.clientAddress = clientAddress;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
index 6397e7e9..00a6d29e 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
@@ -1,1453 +1,1867 @@
package org.littleshoot.proxy.impl;
-import com.google.common.io.BaseEncoding;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.format.DateTimeFormatter.ofPattern;
+import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
+import static java.util.Optional.ofNullable;
+import static org.littleshoot.proxy.HttpFiltersAdapter.NOOP_FILTER;
+import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_CHUNK;
+import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_INITIAL;
+import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_PROXY_AUTHENTICATION;
+import static org.littleshoot.proxy.impl.ConnectionState.DISCONNECT_REQUESTED;
+import static org.littleshoot.proxy.impl.ConnectionState.NEGOTIATING_CONNECT;
+
+import com.google.errorprone.annotations.CheckReturnValue;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
-import io.netty.handler.codec.http.*;
+import io.netty.handler.codec.http.DefaultHttpRequest;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpContent;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
-import io.netty.util.concurrent.Future;
import io.netty.util.ReferenceCounted;
-import org.apache.commons.lang3.StringUtils;
-import org.littleshoot.proxy.*;
-
-import javax.net.ssl.SSLSession;
+import io.netty.util.concurrent.Future;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
-import java.nio.charset.Charset;
-import java.util.*;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
-
-import static org.littleshoot.proxy.impl.ConnectionState.*;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+import org.apache.commons.lang3.StringUtils;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.ChainedProxy;
+import org.littleshoot.proxy.ChainedProxyManager;
+import org.littleshoot.proxy.ChainedProxyType;
+import org.littleshoot.proxy.FlowContext;
+import org.littleshoot.proxy.FullFlowContext;
+import org.littleshoot.proxy.HttpFilters;
+import org.littleshoot.proxy.ProxyAuthenticator;
+import org.littleshoot.proxy.SslEngineSource;
/**
- *
- * Represents a connection from a client to our proxy. Each
- * ClientToProxyConnection can have multiple {@link ProxyToServerConnection}s,
- * at most one per outbound host:port.
- *
- *
- *
- * Once a ProxyToServerConnection has been created for a given server, it is
- * continually reused. The ProxyToServerConnection goes through its own
- * lifecycle of connects and disconnects, with different underlying
- * {@link Channel}s, but only a single ProxyToServerConnection object is used
- * per server. The one exception to this is CONNECT tunneling - if a connection
- * has been used for CONNECT tunneling, that connection will never be reused.
- *
- *
- *
- * As the ProxyToServerConnections receive responses from their servers, they
- * feed these back to the client by calling
- * {@link #respond(ProxyToServerConnection, HttpFilters, HttpRequest, HttpResponse, HttpObject)}
- * .
- *
+ * Represents a connection from a client to our proxy. Each ClientToProxyConnection can have
+ * multiple {@link ProxyToServerConnection}s, at most one per outbound host:port.
+ *
+ *
Once a ProxyToServerConnection has been created for a given server, it is continually reused.
+ * The ProxyToServerConnection goes through its own lifecycle of connects and disconnects, with
+ * different underlying {@link Channel}s, but only a single ProxyToServerConnection object is used
+ * per server. The one exception to this is CONNECT tunneling - if a connection has been used for
+ * CONNECT tunneling, that connection will never be reused.
+ *
+ *
As the ProxyToServerConnections receive responses from their servers, they feed these back to
+ * the client by calling {@link #respond(ProxyToServerConnection, HttpFilters, HttpRequest,
+ * HttpResponse, HttpObject)} .
*/
+@NullMarked
public class ClientToProxyConnection extends ProxyConnection {
- private static final HttpResponseStatus CONNECTION_ESTABLISHED = new HttpResponseStatus(
- 200, "Connection established");
+ private static final HttpResponseStatus CONNECTION_ESTABLISHED =
+ new HttpResponseStatus(200, "Connection established");
- /**
- * Used for case-insensitive comparisons when checking direct proxy request.
- */
- private static final Pattern HTTP_SCHEME = Pattern.compile("^http://.*", Pattern.CASE_INSENSITIVE);
+ // Pipeline handler names:
+ private static final String HTTP_ENCODER_NAME = "encoder";
+ private static final String HTTP_DECODER_NAME = "decoder";
+ private static final String HTTP_PROXY_DECODER_NAME = "proxy-protocol-decoder";
+ private static final String HTTP_REQUEST_READ_MONITOR_NAME = "requestReadMonitor";
+ private static final String HTTP_RESPONSE_WRITTEN_MONITOR_NAME = "responseWrittenMonitor";
+ private static final String MAIN_HANDLER_NAME = "handler";
- /**
- * Keep track of all ProxyToServerConnections by host+port.
- */
- private final Map serverConnectionsByHostAndPort = new ConcurrentHashMap<>();
+ /** Used for case-insensitive comparisons when checking direct proxy request. */
+ private static final Pattern ABSOLUTE_URI_PATTERN =
+ Pattern.compile("^(http|ws)://.*", Pattern.CASE_INSENSITIVE);
- /**
- * Keep track of how many servers are currently in the process of
- * connecting.
- */
- private final AtomicInteger numberOfCurrentlyConnectingServers = new AtomicInteger(
- 0);
+ /** Keep track of all ProxyToServerConnections by host+port. */
+ private final Map serverConnectionsByHostAndPort =
+ new ConcurrentHashMap<>();
- /**
- * Keep track of proxy protocol header
- */
- private HAProxyMessage haProxyMessage = null;
+ /** Keep track of how many servers are currently in the process of connecting. */
+ private final AtomicInteger numberOfCurrentlyConnectingServers = new AtomicInteger(0);
- /**
- * Keep track of how many servers are currently connected.
- */
- private final AtomicInteger numberOfCurrentlyConnectedServers = new AtomicInteger(
- 0);
+ /** Keep track of proxy protocol header */
+ @Nullable private volatile HAProxyMessage haProxyMessage;
- /**
- * Keep track of how many times we were able to reuse a connection.
- */
- private final AtomicInteger numberOfReusedServerConnections = new AtomicInteger(
- 0);
+ /** Keep track of how many servers are currently connected. */
+ private final AtomicInteger numberOfCurrentlyConnectedServers = new AtomicInteger(0);
- /**
- * This is the current server connection that we're using while transferring
- * chunked data.
- */
- private volatile ProxyToServerConnection currentServerConnection;
+ /** Keep track of how many times we were able to reuse a connection. */
+ private final AtomicInteger numberOfReusedServerConnections = new AtomicInteger(0);
- /**
- * The current filters to apply to incoming requests/chunks.
- */
- private volatile HttpFilters currentFilters = HttpFiltersAdapter.NOOP_FILTER;
+ /** This is the current server connection that we're using while transferring chunked data. */
+ @Nullable private volatile ProxyToServerConnection currentServerConnection;
- private volatile SSLSession clientSslSession;
+ private final Map serverFlowContexts =
+ new ConcurrentHashMap<>();
- /**
- * Tracks whether or not this ClientToProxyConnection is current doing MITM.
- */
- private volatile boolean mitming = false;
+ /** The current filters to apply to incoming requests/chunks. */
+ private volatile HttpFilters currentFilters = NOOP_FILTER;
- private AtomicBoolean authenticated = new AtomicBoolean();
+ @Nullable private volatile SSLSession clientSslSession;
- private final GlobalTrafficShapingHandler globalTrafficShapingHandler;
+ /** Tracks whether this ClientToProxyConnection is current doing MITM. */
+ private volatile boolean mitming;
- /**
- * The current HTTP request that this connection is currently servicing.
- */
- private volatile HttpRequest currentRequest;
-
- private final ClientDetails clientDetails = new ClientDetails();
-
- ClientToProxyConnection(
- final DefaultHttpProxyServer proxyServer,
- SslEngineSource sslEngineSource,
- boolean authenticateClients,
- ChannelPipeline pipeline,
- GlobalTrafficShapingHandler globalTrafficShapingHandler) {
- super(AWAITING_INITIAL, proxyServer, false);
-
- initChannelPipeline(pipeline);
-
- if (sslEngineSource != null) {
- LOG.debug("Enabling encryption of traffic from client to proxy");
- encrypt(pipeline, sslEngineSource.newSslEngine(),
- authenticateClients)
- .addListener(
- future -> {
- if (future.isSuccess()) {
- clientSslSession = sslEngine.getSession();
- recordClientSSLHandshakeSucceeded();
- }
- });
- }
- this.globalTrafficShapingHandler = globalTrafficShapingHandler;
+ private final AtomicBoolean authenticated = new AtomicBoolean();
- LOG.debug("Created ClientToProxyConnection");
- }
+ /** Ensures {@link #recordClientConnected()} fires at most once per connection. */
+ private static final boolean CLIENT_CONNECTED_NOT_YET_RECORDED = false;
- @Override
- protected void readHAProxyMessage(HAProxyMessage msg) {
- haProxyMessage = msg;
- }
+ private static final boolean CLIENT_CONNECTED_RECORDED = true;
+ private final AtomicBoolean clientConnectedRecorded = new AtomicBoolean();
- /* *************************************************************************
- * Reading
- **************************************************************************/
+ private final GlobalTrafficShapingHandler globalTrafficShapingHandler;
- @Override
- protected ConnectionState readHTTPInitial(HttpRequest httpRequest) {
- LOG.debug("Received raw request: {}", httpRequest);
+ /** Cached FlowContext for consistent timing data across client lifecycle events. */
+ private final FlowContext clientFlowContext;
- // if we cannot parse the request, immediately return a 400 and close the connection, since we do not know what state
- // the client thinks the connection is in
- if (httpRequest.decoderResult().isFailure()) {
- LOG.debug("Could not parse request from client. Decoder result: {}", httpRequest.decoderResult().toString());
+ /** The current HTTP request that this connection is currently servicing. */
+ @Nullable private volatile HttpRequest currentRequest;
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
- HttpResponseStatus.BAD_REQUEST,
- "Unable to parse HTTP request");
- HttpUtil.setKeepAlive(response, false);
+ private final ClientDetails clientDetails = new ClientDetails();
- respondWithShortCircuitResponse(response);
+ ClientToProxyConnection(
+ final DefaultHttpProxyServer proxyServer,
+ @Nullable SslEngineSource sslEngineSource,
+ boolean authenticateClients,
+ ChannelPipeline pipeline,
+ GlobalTrafficShapingHandler globalTrafficShapingHandler) {
+ super(AWAITING_INITIAL, proxyServer, false);
+ this.clientFlowContext = new FlowContext(this);
- return DISCONNECT_REQUESTED;
- }
+ initChannelPipeline(pipeline, sslEngineSource, authenticateClients);
- boolean authenticationRequired = authenticationRequired(httpRequest);
+ this.globalTrafficShapingHandler = globalTrafficShapingHandler;
- if (authenticationRequired) {
- LOG.debug("Not authenticated!!");
- return AWAITING_PROXY_AUTHENTICATION;
- } else {
- return doReadHTTPInitial(httpRequest);
- }
- }
+ LOG.debug("Created ClientToProxyConnection");
+ }
- /**
- *
- * Reads an {@link HttpRequest}.
- *
- *
- *
- * If we don't yet have a {@link ProxyToServerConnection} for the desired
- * server, this takes care of creating it.
- *
- *
- *
- * Note - the "server" could be a chained proxy, not the final endpoint for
- * the request.
- *
- */
- private ConnectionState doReadHTTPInitial(HttpRequest httpRequest) {
- // Make a copy of the original request
- this.currentRequest = copy(httpRequest);
-
- // Set up our filters based on the original request. If the HttpFiltersSource returns null (meaning the request/response
- // should not be filtered), fall back to the default no-op filter source.
- HttpFilters filterInstance = proxyServer.getFiltersSource().filterRequest(currentRequest, ctx);
- if (filterInstance != null) {
- currentFilters = filterInstance;
- } else {
- currentFilters = HttpFiltersAdapter.NOOP_FILTER;
- }
+ @Override
+ protected void readHAProxyMessage(HAProxyMessage msg) {
+ haProxyMessage = msg;
+ // PROXY header available: fire the deferred clientConnected now (guarded).
+ recordClientConnected();
+ }
- // Send the request through the clientToProxyRequest filter, and respond with the short-circuit response if required
- HttpResponse clientToProxyFilterResponse = currentFilters.clientToProxyRequest(httpRequest);
+ /* *************************************************************************
+ * Reading
+ **************************************************************************/
- if (clientToProxyFilterResponse != null) {
- LOG.debug("Responding to client with short-circuit response from filter: {}", clientToProxyFilterResponse);
+ @Override
+ ConnectionState readHTTPInitial(HttpRequest httpRequest) {
+ LOG.debug("Received raw request: {}", httpRequest);
- boolean keepAlive = respondWithShortCircuitResponse(clientToProxyFilterResponse);
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
+ // Earliest point to report connected for connections with no PROXY header (guarded; no-op if
+ // already fired).
+ recordClientConnected();
- // if origin-form requests are not explicitly enabled, short-circuit requests that treat the proxy as the
- // origin server, to avoid infinite loops
- if (!proxyServer.isAllowRequestsToOriginServer() && isRequestToOriginServer(httpRequest)) {
- boolean keepAlive = writeBadRequest(httpRequest);
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
+ // if we cannot parse the request, immediately return a 400 and close the connection, since we
+ // do not know what state
+ // the client thinks the connection is in
+ if (httpRequest.decoderResult().isFailure()) {
+ LOG.debug(
+ "Could not parse request from client. Decoder result: {}",
+ httpRequest.decoderResult().toString());
- // Identify our server and chained proxy
- String serverHostAndPort = identifyHostAndPort(httpRequest);
-
- LOG.debug("Ensuring that hostAndPort are available in {}",
- httpRequest.uri());
- if (serverHostAndPort == null || StringUtils.isBlank(serverHostAndPort)) {
- LOG.warn("No host and port found in {}", httpRequest.uri());
- boolean keepAlive = writeBadGateway(httpRequest);
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
+ FullHttpResponse response =
+ ProxyUtils.createFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, "Unable to parse HTTP request");
+ HttpUtil.setKeepAlive(response, false);
- LOG.debug("Finding ProxyToServerConnection for: {}", serverHostAndPort);
- currentServerConnection = isMitming() || isTunneling() ?
- this.currentServerConnection
- : this.serverConnectionsByHostAndPort.get(serverHostAndPort);
-
- boolean newConnectionRequired = false;
- if (ProxyUtils.isCONNECT(httpRequest)) {
- LOG.debug(
- "Not reusing existing ProxyToServerConnection because request is a CONNECT for: {}",
- serverHostAndPort);
- newConnectionRequired = true;
- } else if (currentServerConnection == null) {
- LOG.debug("Didn't find existing ProxyToServerConnection for: {}",
- serverHostAndPort);
- newConnectionRequired = true;
- }
+ respondWithShortCircuitResponse(response);
- if (newConnectionRequired) {
- try {
- currentServerConnection = ProxyToServerConnection.create(
- proxyServer,
- this,
- serverHostAndPort,
- currentFilters,
- httpRequest,
- globalTrafficShapingHandler);
- if (currentServerConnection == null) {
- LOG.debug("Unable to create server connection, probably no chained proxies available");
- boolean keepAlive = writeBadGateway(httpRequest);
- resumeReading();
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
- // Remember the connection for later
- serverConnectionsByHostAndPort.put(serverHostAndPort,
- currentServerConnection);
- } catch (UnknownHostException uhe) {
- LOG.info("Bad Host {}", httpRequest.uri());
- boolean keepAlive = writeBadGateway(httpRequest);
- resumeReading();
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
- } else {
- LOG.debug("Reusing existing server connection: {}",
- currentServerConnection);
- numberOfReusedServerConnections.incrementAndGet();
- }
+ return DISCONNECT_REQUESTED;
+ }
- modifyRequestHeadersToReflectProxying(httpRequest);
+ boolean authenticationRequired = authenticationRequired(httpRequest);
+
+ if (authenticationRequired) {
+ LOG.debug("Not authenticated!!");
+ return AWAITING_PROXY_AUTHENTICATION;
+ } else {
+ return doReadHTTPInitial(httpRequest);
+ }
+ }
+
+ /**
+ * Reads an {@link HttpRequest}.
+ *
+ *
If we don't yet have a {@link ProxyToServerConnection} for the desired server, this takes
+ * care of creating it.
+ *
+ *
Note - the "server" could be a chained proxy, not the final endpoint for the request.
+ */
+ private ConnectionState doReadHTTPInitial(HttpRequest httpRequest) {
+ resetCurrentRequest();
+ // Make a copy of the original request
+ currentRequest = copy(httpRequest);
+
+ // Set up our filters based on the original request. If the HttpFiltersSource returns null
+ // (meaning the request/response
+ // should not be filtered), fall back to the default no-op filter source.
+ HttpFilters filterInstance =
+ proxyServer.getFiltersSource().filterRequest(requireNonNull(currentRequest), ctx);
+ currentFilters = requireNonNullElse(filterInstance, NOOP_FILTER);
+
+ // Send the request through the clientToProxyRequest filter, and respond with the short-circuit
+ // response if required
+ HttpResponse clientToProxyFilterResponse = currentFilters.clientToProxyRequest(httpRequest);
+
+ if (clientToProxyFilterResponse != null) {
+ LOG.debug(
+ "Responding to client with short-circuit response from filter: {}",
+ clientToProxyFilterResponse);
+
+ boolean keepAlive = respondWithShortCircuitResponse(clientToProxyFilterResponse);
+ if (keepAlive) {
+ return AWAITING_INITIAL;
+ } else {
+ return DISCONNECT_REQUESTED;
+ }
+ }
- HttpResponse proxyToServerFilterResponse = currentFilters.proxyToServerRequest(httpRequest);
- if (proxyToServerFilterResponse != null) {
- LOG.debug("Responding to client with short-circuit response from filter: {}", proxyToServerFilterResponse);
+ // if origin-form requests are not explicitly enabled, short-circuit requests that treat the
+ // proxy as the
+ // origin server, to avoid infinite loops
+ if (!proxyServer.isAllowRequestsToOriginServer() && isRequestToOriginServer(httpRequest)) {
+ boolean keepAlive = writeBadRequest(httpRequest);
+ if (keepAlive) {
+ return AWAITING_INITIAL;
+ } else {
+ return DISCONNECT_REQUESTED;
+ }
+ }
- boolean keepAlive = respondWithShortCircuitResponse(proxyToServerFilterResponse);
- if (keepAlive) {
- return AWAITING_INITIAL;
- } else {
- return DISCONNECT_REQUESTED;
- }
- }
+ // Identify our server and chained proxy
+ String serverHostAndPort = identifyHostAndPort(httpRequest);
- LOG.debug("Writing request to ProxyToServerConnection");
- currentServerConnection.write(httpRequest, currentFilters);
+ LOG.debug("Ensuring that hostAndPort are available in {}", httpRequest.uri());
+ if (StringUtils.isBlank(serverHostAndPort)) {
+ LOG.warn("No host and port found in {}", httpRequest.uri());
+ boolean keepAlive = writeBadGateway(httpRequest);
+ return keepAlive ? AWAITING_INITIAL : DISCONNECT_REQUESTED;
+ }
+
+ LOG.debug("Finding ProxyToServerConnection for: {}", serverHostAndPort);
+
+ // Use the shared connection pool if enabled (disabled by default for backwards compatibility)
+ ServerConnectionPool pool = proxyServer.getServerConnectionPool();
+ boolean usePool = pool != null;
+ boolean isConnect = ProxyUtils.isCONNECT(httpRequest);
+ boolean poolSharedMitm = usePool && proxyServer.isPoolSharedMitmConnections();
+ boolean poolPerRequest = usePool && proxyServer.isPoolPerRequestInMitm();
+ boolean useSharedPool =
+ usePool
+ && !isTunneling()
+ && !ProxyUtils.isSwitchingToWebSocketProtocol(httpRequest)
+ && (poolPerRequest || !isMitming())
+ && (poolSharedMitm || !isConnect);
+
+ boolean newConnectionRequired = false;
+ if (!useSharedPool) {
+ // For non-pooled mode, CONNECT, tunneling (WebSocket), or MITM, use dedicated connections
+ LOG.debug(
+ "Not using shared pool for: {} (pool enabled: {}, is CONNECT: {}, is Tunneling: {}, is MITM: {})",
+ serverHostAndPort,
+ usePool,
+ ProxyUtils.isCONNECT(httpRequest),
+ isTunneling(),
+ isMitming());
+ currentServerConnection = serverConnectionsByHostAndPort.get(serverHostAndPort);
+ if (currentServerConnection == null) {
+ newConnectionRequired = true;
+ }
+ } else {
+ // For regular requests with pool enabled, get from shared pool
+ newConnectionRequired = true;
+ }
- // Figure out our next state
- if (ProxyUtils.isCONNECT(httpRequest)) {
- return NEGOTIATING_CONNECT;
- } else if (ProxyUtils.isChunked(httpRequest)) {
- return AWAITING_CHUNK;
+ if (newConnectionRequired) {
+ try {
+ // Create dedicated connection for non-pooled/CONNECT/tunneling/MITM, or use pool
+ if (!useSharedPool) {
+ currentServerConnection =
+ ProxyToServerConnection.create(
+ proxyServer,
+ this,
+ serverHostAndPort,
+ currentFilters,
+ httpRequest,
+ globalTrafficShapingHandler);
} else {
+ // Use the shared pool for regular requests
+ // Resolve ChainedProxy address before pooling to segregate connections by upstream route
+ ChainedProxy chainedProxy = null;
+ ChainedProxyManager chainedProxyManager = proxyServer.getChainProxyManager();
+ if (chainedProxyManager != null) {
+ Queue chainedProxies = new ConcurrentLinkedQueue<>();
+ chainedProxyManager.lookupChainedProxies(
+ httpRequest, chainedProxies, getClientDetails());
+ if (!chainedProxies.isEmpty()) {
+ chainedProxy = chainedProxies.poll();
+ }
+ }
+ InetSocketAddress chainedProxyAddress =
+ chainedProxy != null ? chainedProxy.getChainedProxyAddress() : null;
+ currentServerConnection =
+ pool.getOrCreateConnection(
+ serverHostAndPort, chainedProxyAddress, this, currentFilters, httpRequest);
+ }
+
+ if (currentServerConnection == null) {
+ LOG.debug("Unable to create server connection, probably no chained proxies available");
+ boolean keepAlive = writeBadGateway(httpRequest);
+ resumeReading();
+ if (keepAlive) {
return AWAITING_INITIAL;
+ } else {
+ return DISCONNECT_REQUESTED;
+ }
}
- }
- /**
- * Returns true if the specified request is a request to an origin server, rather than to a proxy server. If this
- * request is being MITM'd, this method always returns false. The format of requests to a proxy server are defined
- * in RFC 7230, section 5.3.2 (all other requests are considered requests to an origin server):
-
- When making a request to a proxy, other than a CONNECT or server-wide
- OPTIONS request (as detailed below), a client MUST send the target
- URI in absolute-form as the request-target.
- [...]
- An example absolute-form of request-line would be:
- GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
- To allow for transition to the absolute-form for all requests in some
- future version of HTTP, a server MUST accept the absolute-form in
- requests, even though HTTP/1.1 clients will only send them in
- requests to proxies.
-
- *
- * @param httpRequest the request to evaluate
- * @return true if the specified request is a request to an origin server, otherwise false
- */
- private boolean isRequestToOriginServer(HttpRequest httpRequest) {
- // while MITMing, all HTTPS requests are requests to the origin server, since the client does not know
- // the request is being MITM'd by the proxy
- if (httpRequest.method() == HttpMethod.CONNECT || isMitming()) {
- return false;
+ // Remember the connection for tracking (for non-pooled connections or
+ // CONNECT/tunneling/MITM). In per-request MITM mode, CONNECT connections are
+ // released to pool after flow completes, so they don't need session tracking.
+ if (!useSharedPool || (isConnect && !poolPerRequest)) {
+ serverConnectionsByHostAndPort.put(
+ serverHostAndPort, requireNonNull(currentServerConnection));
}
-
- // direct requests to the proxy have the path only without a scheme
- String uri = httpRequest.uri();
- return !HTTP_SCHEME.matcher(uri).matches();
+ } catch (UnknownHostException uhe) {
+ LOG.info("Bad Host {}", httpRequest.uri());
+ boolean keepAlive = writeBadGateway(httpRequest);
+ resumeReading();
+ if (keepAlive) {
+ return AWAITING_INITIAL;
+ } else {
+ return DISCONNECT_REQUESTED;
+ }
+ }
+ } else {
+ LOG.debug("Reusing existing server connection: {}", currentServerConnection);
+ numberOfReusedServerConnections.incrementAndGet();
}
- @Override
- protected void readHTTPChunk(HttpContent chunk) {
- currentFilters.clientToProxyRequest(chunk);
- currentFilters.proxyToServerRequest(chunk);
-
- currentServerConnection.write(chunk);
+ // For pooled connections, set the current client connection before writing
+ // This allows the server connection to know where to send the response
+ // Set for pooled requests including CONNECT and MITM when pool flags are active,
+ // so that reused connections route responses to the correct client
+ if (usePool
+ && !isTunneling()
+ && !ProxyUtils.isSwitchingToWebSocketProtocol(httpRequest)
+ && currentServerConnection != null
+ && (poolPerRequest
+ || poolSharedMitm
+ || (!ProxyUtils.isCONNECT(httpRequest) && !isMitming()))) {
+ currentServerConnection.setCurrentClientConnectionForRequest(this);
}
- @Override
- protected void readRaw(ByteBuf buf) {
- currentServerConnection.write(buf);
+ modifyRequestHeadersToReflectProxying(httpRequest);
+
+ HttpResponse proxyToServerFilterResponse = currentFilters.proxyToServerRequest(httpRequest);
+ if (proxyToServerFilterResponse != null) {
+ LOG.debug(
+ "Responding to client with short-circuit response from filter: {}",
+ proxyToServerFilterResponse);
+
+ if (usePool && currentServerConnection != null) {
+ currentServerConnection.setCurrentClientConnectionForRequest(null);
+ currentServerConnection.releaseToPool();
+ }
+
+ boolean keepAlive = respondWithShortCircuitResponse(proxyToServerFilterResponse);
+ if (keepAlive) {
+ return AWAITING_INITIAL;
+ } else {
+ return DISCONNECT_REQUESTED;
+ }
}
- /* *************************************************************************
- * Writing
- **************************************************************************/
+ LOG.debug("Writing request to ProxyToServerConnection");
+ requireNonNull(currentServerConnection).write(httpRequest, currentFilters);
- /**
- * Send a response to the client.
- *
- * @param serverConnection
- * the ProxyToServerConnection that's responding
- * @param filters
- * the filters to apply to the response
- * @param currentHttpRequest
- * the HttpRequest that prompted this response
- * @param currentHttpResponse
- * the HttpResponse corresponding to this data (when doing
- * chunked transfers, this is the initial HttpResponse object
- * that came in before the other chunks)
- * @param httpObject
- * the data with which to respond
- */
- void respond(ProxyToServerConnection serverConnection, HttpFilters filters,
- HttpRequest currentHttpRequest, HttpResponse currentHttpResponse,
- HttpObject httpObject) {
- // we are sending a response to the client, so we are done handling this request
- if (currentRequest != null && currentRequest instanceof ReferenceCounted) {
- ((ReferenceCounted)currentRequest).release();
- }
- this.currentRequest = null;
-
- httpObject = filters.serverToProxyResponse(httpObject);
- if (httpObject == null) {
- forceDisconnect(serverConnection);
- return;
- }
+ // Figure out our next state
+ if (ProxyUtils.isCONNECT(httpRequest)) {
+ return NEGOTIATING_CONNECT;
+ } else if (ProxyUtils.isChunked(httpRequest)) {
+ return AWAITING_CHUNK;
+ } else {
+ return AWAITING_INITIAL;
+ }
+ }
+
+ /**
+ * Returns true if the specified request is a request to an origin server, rather than to a proxy
+ * server. If this request is being MITM'd, this method always returns false. The format of
+ * requests to a proxy server are defined in RFC 7230, section 5.3.2 (all other requests are
+ * considered requests to an origin server):
+ *
+ *
+ * When making a request to a proxy, other than a CONNECT or server-wide
+ * OPTIONS request (as detailed below), a client MUST send the target
+ * URI in absolute-form as the request-target.
+ * [...]
+ * An example absolute-form of request-line would be:
+ * GET https://www.example.org/pub/WWW/TheProject.html HTTP/1.1
+ * To allow for transition to the absolute-form for all requests in some
+ * future version of HTTP, a server MUST accept the absolute-form in
+ * requests, even though HTTP/1.1 clients will only send them in
+ * requests to proxies.
+ *
+ *
+ * @param httpRequest the request to evaluate
+ * @return true if the specified request is a request to an origin server, otherwise false
+ */
+ private boolean isRequestToOriginServer(HttpRequest httpRequest) {
+ // while MITMing, all HTTPS requests are requests to the origin server, since the client does
+ // not know
+ // the request is being MITM'd by the proxy
+ if (httpRequest.method() == HttpMethod.CONNECT || isMitming()) {
+ return false;
+ }
- if (httpObject instanceof HttpResponse) {
- HttpResponse httpResponse = (HttpResponse) httpObject;
-
- // if this HttpResponse does not have any means of signaling the end of the message body other than closing
- // the connection, convert the message to a "Transfer-Encoding: chunked" HTTP response. This avoids the need
- // to close the client connection to indicate the end of the message. (Responses to HEAD requests "must be" empty.)
- if (!ProxyUtils.isHEAD(currentHttpRequest) && !ProxyUtils.isResponseSelfTerminating(httpResponse)) {
- // if this is not a FullHttpResponse, duplicate the HttpResponse from the server before sending it to
- // the client. this allows us to set the Transfer-Encoding to chunked without interfering with netty's
- // handling of the response from the server. if we modify the original HttpResponse from the server,
- // netty will not generate the appropriate LastHttpContent when it detects the connection closure from
- // the server (see HttpObjectDecoder#decodeLast). (This does not apply to FullHttpResponses, for which
- // netty already generates the empty final chunk when Transfer-Encoding is chunked.)
- if (!(httpResponse instanceof FullHttpResponse)) {
- HttpResponse duplicateResponse = ProxyUtils.duplicateHttpResponse(httpResponse);
-
- // set the httpObject and httpResponse to the duplicated response, to allow all other standard processing
- // (filtering, header modification for proxying, etc.) to be applied.
- httpObject = httpResponse = duplicateResponse;
- }
+ // direct requests to the proxy have the path only without a scheme
+ String uri = httpRequest.uri();
+ return !ABSOLUTE_URI_PATTERN.matcher(uri).matches();
+ }
- HttpUtil.setTransferEncodingChunked(httpResponse, true);
- }
+ @Override
+ protected void readHTTPChunk(HttpContent chunk) {
+ if (currentServerConnection == null) {
+ LOG.warn("Cannot forward HTTP chunk: no server connection");
+ return;
+ }
+ currentFilters.clientToProxyRequest(chunk);
+ currentFilters.proxyToServerRequest(chunk);
- fixHttpVersionHeaderIfNecessary(httpResponse);
- modifyResponseHeadersToReflectProxying(httpResponse);
- }
+ currentServerConnection.write(chunk);
+ }
- httpObject = filters.proxyToClientResponse(httpObject);
- if (httpObject == null) {
- forceDisconnect(serverConnection);
- return;
- }
+ @Override
+ protected void readRaw(ByteBuf buf) {
+ if (currentServerConnection == null) {
+ LOG.warn("Cannot forward raw data: no server connection");
+ return;
+ }
+ currentServerConnection.write(buf);
+ }
+
+ /* *************************************************************************
+ * Writing
+ **************************************************************************/
+
+ /**
+ * Send a response to the client.
+ *
+ * @param serverConnection the ProxyToServerConnection that's responding
+ * @param filters the filters to apply to the response
+ * @param currentHttpRequest the HttpRequest that prompted this response
+ * @param currentHttpResponse the HttpResponse corresponding to this data (when doing chunked
+ * transfers, this is the initial HttpResponse object that came in before the other chunks)
+ * @param httpObject the data with which to respond
+ */
+ void respond(
+ ProxyToServerConnection serverConnection,
+ HttpFilters filters,
+ HttpRequest currentHttpRequest,
+ HttpResponse currentHttpResponse,
+ HttpObject httpObject) {
+ // we are sending a response to the client, so we are done handling this request
+ resetCurrentRequest();
+
+ httpObject = filters.serverToProxyResponse(httpObject);
+ if (httpObject == null) {
+ forceDisconnect(serverConnection);
+ return;
+ }
- write(httpObject);
+ final boolean isSwitchingToWebSocketProtocol;
+ if (httpObject instanceof HttpResponse) {
+ HttpResponse httpResponse = (HttpResponse) httpObject;
+
+ isSwitchingToWebSocketProtocol = ProxyUtils.isSwitchingToWebSocketProtocol(httpResponse);
+
+ // if this HttpResponse does not have any means of signaling the end of the message body other
+ // than closing
+ // the connection, convert the message to a "Transfer-Encoding: chunked" HTTP response. This
+ // avoids the need
+ // to close the client connection to indicate the end of the message. (Responses to HEAD
+ // requests "must be" empty.)
+ if (!ProxyUtils.isHEAD(currentHttpRequest)
+ && !ProxyUtils.isResponseSelfTerminating(httpResponse)) {
+ // if this is not a FullHttpResponse, duplicate the HttpResponse from the server before
+ // sending it to
+ // the client. this allows us to set the Transfer-Encoding to chunked without interfering
+ // with netty's
+ // handling of the response from the server. if we modify the original HttpResponse from the
+ // server,
+ // netty will not generate the appropriate LastHttpContent when it detects the connection
+ // closure from
+ // the server (see HttpObjectDecoder#decodeLast). (This does not apply to FullHttpResponses,
+ // for which
+ // netty already generates the empty final chunk when Transfer-Encoding is chunked.)
+ if (!(httpResponse instanceof FullHttpResponse)) {
+ HttpResponse duplicateResponse = ProxyUtils.duplicateHttpResponse(httpResponse);
+
+ // set the httpObject and httpResponse to the duplicated response, to allow all other
+ // standard processing
+ // (filtering, header modification for proxying, etc.) to be applied.
+ httpObject = httpResponse = duplicateResponse;
+ }
+
+ HttpUtil.setTransferEncodingChunked(httpResponse, true);
+ }
+
+ fixHttpVersionHeaderIfNecessary(httpResponse);
+ modifyResponseHeadersToReflectProxying(httpResponse);
+
+ // modifyResponseHeadersToReflectProxying strips hop-by-hop headers (Upgrade, Connection),
+ // but a WebSocket upgrade response requires both to be present for the client to switch
+ // protocols. Re-add them after the general stripping.
+ if (isSwitchingToWebSocketProtocol) {
+ httpResponse.headers().set(HttpHeaderNames.UPGRADE, "websocket");
+ httpResponse.headers().set(HttpHeaderNames.CONNECTION, "Upgrade");
+ }
+ } else {
+ isSwitchingToWebSocketProtocol = false;
+ }
- if (ProxyUtils.isLastChunk(httpObject)) {
- writeEmptyBuffer();
- }
+ final HttpObject filteredhttpObject = filters.proxyToClientResponse(httpObject);
+ if (filteredhttpObject == null) {
+ forceDisconnect(serverConnection);
+ return;
+ }
- closeConnectionsAfterWriteIfNecessary(serverConnection,
- currentHttpRequest, currentHttpResponse, httpObject);
+ if (isSwitchingToWebSocketProtocol) {
+ serverConnection.switchToWebSocketProtocol();
+ }
+ write(filteredhttpObject)
+ .addListener(
+ l -> {
+ if (isSwitchingToWebSocketProtocol) {
+ switchToWebSocketProtocol(serverConnection);
+ } else if (ProxyUtils.isLastChunk(filteredhttpObject)) {
+ writeEmptyBuffer();
+ }
+
+ closeConnectionsAfterWriteIfNecessary(
+ serverConnection, currentHttpRequest, currentHttpResponse, filteredhttpObject);
+ });
+ }
+
+ private void resetCurrentRequest() {
+ if (currentRequest != null && currentRequest instanceof ReferenceCounted) {
+ ((ReferenceCounted) currentRequest).release();
+ }
+ currentRequest = null;
+ }
+
+ private void switchToWebSocketProtocol(final ProxyToServerConnection serverConnection) {
+ final List orderedHandlersToRemove =
+ Arrays.asList(
+ HTTP_REQUEST_READ_MONITOR_NAME,
+ HTTP_RESPONSE_WRITTEN_MONITOR_NAME,
+ HTTP_PROXY_DECODER_NAME,
+ HTTP_ENCODER_NAME,
+ HTTP_DECODER_NAME);
+ if (channel.pipeline().get(MAIN_HANDLER_NAME) != null) {
+ channel
+ .pipeline()
+ .replace(
+ MAIN_HANDLER_NAME,
+ "pipe-to-server",
+ new WebSocketFramePipeHandler(serverConnection, currentFilters, true));
}
+ orderedHandlersToRemove.forEach(this::removeHandlerIfPresent);
+ }
- /* *************************************************************************
- * Connection Lifecycle
- **************************************************************************/
+ /* *************************************************************************
+ * Connection Lifecycle
+ **************************************************************************/
- /**
- * Tells the Client that its HTTP CONNECT request was successful.
- */
- ConnectionFlowStep RespondCONNECTSuccessful = new ConnectionFlowStep(
- this, NEGOTIATING_CONNECT) {
+ /** Tells the Client that its HTTP CONNECT request was successful. */
+ final ConnectionFlowStep RespondCONNECTSuccessful =
+ new ConnectionFlowStep<>(this, NEGOTIATING_CONNECT) {
@Override
boolean shouldSuppressInitialRequest() {
- return true;
+ return true;
}
protected Future> execute() {
- LOG.debug("Responding with CONNECT successful");
- HttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
- CONNECTION_ESTABLISHED);
- response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
- ProxyUtils.addVia(response, proxyServer.getProxyAlias());
- return writeToChannel(response);
- }
- };
-
- /**
- * On connect of the client, start waiting for an initial
- * {@link HttpRequest}.
- */
- @Override
- protected void connected() {
- super.connected();
- become(AWAITING_INITIAL);
- recordClientConnected();
- }
-
- void timedOut(ProxyToServerConnection serverConnection) {
- if (currentServerConnection == serverConnection && this.lastReadTime > currentServerConnection.lastReadTime) {
- // the idle timeout fired on the active server connection. send a timeout response to the client.
- LOG.warn("Server timed out: {}", currentServerConnection);
- currentFilters.serverToProxyResponseTimedOut();
- writeGatewayTimeout(currentRequest);
- }
+ LOG.debug("Responding with CONNECT successful");
+ HttpResponse response =
+ ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, CONNECTION_ESTABLISHED);
+ ProxyUtils.addVia(response, proxyServer.getProxyAlias());
+ return writeToChannel(response);
+ }
+ };
+
+ /** On connect of the client, start waiting for an initial {@link HttpRequest}. */
+ @Override
+ protected void connected() {
+ super.connected();
+ become(AWAITING_INITIAL);
+ // recordClientConnected() is deferred, not called here: with PROXY protocol it must wait for
+ // the
+ // header so it reports the real client address (readHAProxyMessage); otherwise it fires on the
+ // first request (readHTTPInitial). The header isn't available yet at channel-active time.
+ }
+
+ void timedOut(ProxyToServerConnection serverConnection) {
+ if (currentServerConnection == serverConnection
+ && lastReadTime > currentServerConnection.lastReadTime) {
+ // the idle timeout fired on the active server connection. send a timeout response to the
+ // client.
+ LOG.warn("Server timed out: {}", currentServerConnection);
+ currentFilters.serverToProxyResponseTimedOut();
+ writeGatewayTimeout(currentRequest);
}
-
- @Override
- protected void timedOut() {
- // idle timeout fired on the client channel. if we aren't waiting on a response from a server, hang up
- if (currentServerConnection == null || this.lastReadTime <= currentServerConnection.lastReadTime) {
- super.timedOut();
- }
+ }
+
+ @Override
+ protected void timedOut() {
+ // idle timeout fired on the client channel. if we aren't waiting on a response from a server,
+ // hang up
+ //
+ // The original issue: when server.lastReadTime == 0 (server has never read) and
+ // client.lastReadTime > 0, the comparison lastReadTime <= server.lastReadTime evaluates to
+ // FALSE, preventing timeout even when the client IS idle.
+ //
+ // However, we need to be careful not to close when:
+ // 1. A request has been sent but the server hasn't responded yet (normal)
+ // 2. A request was sent, proxy generated 504 (server didn't respond), and we're waiting for
+ // the next request from client
+ //
+ // We distinguish these by checking if the server connection has an "initialRequest" that has
+ // been written to the server but not yet reset. If initialRequest is not null, a request
+ // has been written and we're waiting for response.
+ boolean requestHasBeenWritten = false;
+ if (currentServerConnection != null) {
+ // Check if a request has been written to the server but not yet completed
+ HttpRequest initialRequest = currentServerConnection.getInitialRequest();
+ requestHasBeenWritten = initialRequest != null;
}
- /**
- * On disconnect of the client, disconnect all server connections.
- */
- @Override
- protected void disconnected() {
- super.disconnected();
- for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort
- .values()) {
- serverConnection.disconnect();
- }
- recordClientDisconnected();
+ // no request has been transmitted to server
+ if (currentServerConnection == null
+ ||
+ // server has never read anything yet
+ (currentServerConnection.lastReadTime == 0
+ // no initial request has been written to the server
+ && !requestHasBeenWritten
+ // there are no current request from the client
+ && currentRequest == null)
+ ||
+ // The client hasn't sent data as recently as the server
+ // - Both sides are idle (no activity on either end)
+ // - After a response is complete and neither client nor server has sent anything new
+ lastReadTime <= currentServerConnection.lastReadTime) {
+ super.timedOut();
+ recordConnectionTimedOut();
}
-
- /**
- * Called when {@link ProxyToServerConnection} starts its connection flow.
- */
- protected void serverConnectionFlowStarted(
- ProxyToServerConnection serverConnection) {
- stopReading();
- this.numberOfCurrentlyConnectingServers.incrementAndGet();
+ }
+
+ /** On disconnect of the client, disconnect all server connections. */
+ @Override
+ protected void disconnected() {
+ super.disconnected();
+ boolean poolSharedMitm = proxyServer.isPoolSharedMitmConnections();
+ boolean poolPerRequest = proxyServer.isPoolPerRequestInMitm();
+ for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort.values()) {
+ // Phase 1: release pooled MITM connections back to the pool
+ // Phase 2: connections are already in pool after each response, no release needed
+ if (poolSharedMitm && !poolPerRequest && serverConnection.isManagedByPool()) {
+ serverConnection.releaseToPool();
+ } else {
+ serverConnection.disconnect();
+ }
}
-
- /**
- * If the {@link ProxyToServerConnection} completes its connection lifecycle
- * successfully, this method is called to let us know about it.
- */
- protected void serverConnectionSucceeded(
- ProxyToServerConnection serverConnection,
- boolean shouldForwardInitialRequest) {
- LOG.debug("Connection to server succeeded: {}",
- serverConnection.getRemoteAddress());
- resumeReadingIfNecessary();
- become(shouldForwardInitialRequest ? getCurrentState()
- : AWAITING_INITIAL);
- numberOfCurrentlyConnectedServers.incrementAndGet();
- }
-
- /**
- * If the {@link ProxyToServerConnection} fails to complete its connection
- * lifecycle successfully, this method is called to let us know about it.
- *
- *
- * After failing to connect to the server, one of two things can happen:
- *
- *
- *
- *
If the server was a chained proxy, we fall back to connecting to the
- * ultimate endpoint directly.
- *
If the server was the ultimate endpoint, we return a 502 Bad Gateway
- * to the client.
- *
- *
- * @param serverConnection
- * @param lastStateBeforeFailure
- * @param cause
- * what caused the failure
- *
- * @return true if we're falling back to a another chained proxy (or direct
- * connection) and trying again
- */
- protected boolean serverConnectionFailed(
- ProxyToServerConnection serverConnection,
- ConnectionState lastStateBeforeFailure,
- Throwable cause) {
- resumeReadingIfNecessary();
- HttpRequest initialRequest = serverConnection.getInitialRequest();
- try {
- boolean retrying = serverConnection.connectionFailed(cause);
- if (retrying) {
- LOG.debug("Failed to connect to upstream server or chained proxy. Retrying connection. Last state before failure: {}",
- lastStateBeforeFailure, cause);
- return true;
- } else {
- LOG.debug(
- "Connection to upstream server or chained proxy failed: {}. Last state before failure: {}",
- serverConnection.getRemoteAddress(),
- lastStateBeforeFailure,
- cause);
- connectionFailedUnrecoverably(initialRequest, serverConnection);
- return false;
- }
- } catch (UnknownHostException uhe) {
- connectionFailedUnrecoverably(initialRequest, serverConnection);
- return false;
- }
+ recordClientDisconnected();
+ }
+
+ /** Called when {@link ProxyToServerConnection} starts its connection flow. */
+ protected void serverConnectionFlowStarted(ProxyToServerConnection serverConnection) {
+ stopReading();
+ numberOfCurrentlyConnectingServers.incrementAndGet();
+ }
+
+ /**
+ * If the {@link ProxyToServerConnection} completes its connection lifecycle successfully, this
+ * method is called to let us know about it.
+ */
+ protected void serverConnectionSucceeded(
+ ProxyToServerConnection serverConnection, boolean shouldForwardInitialRequest) {
+ LOG.debug("Connection to server succeeded: {}", serverConnection.getRemoteAddress());
+ resumeReadingIfNecessary();
+ become(shouldForwardInitialRequest ? getCurrentState() : AWAITING_INITIAL);
+ numberOfCurrentlyConnectedServers.incrementAndGet();
+ }
+
+ /**
+ * If the {@link ProxyToServerConnection} fails to complete its connection lifecycle successfully,
+ * this method is called to let us know about it.
+ *
+ *
After failing to connect to the server, one of two things can happen:
+ *
+ *
+ *
If the server was a chained proxy, we fall back to connecting to the ultimate endpoint
+ * directly.
+ *
If the server was the ultimate endpoint, we return a 502 Bad Gateway to the client.
+ *
+ *
+ * @param serverConnection
+ * @param lastStateBeforeFailure
+ * @param cause what caused the failure
+ * @return true if we're falling back to another chained proxy (or direct connection) and trying
+ * again
+ */
+ protected boolean serverConnectionFailed(
+ ProxyToServerConnection serverConnection,
+ ConnectionState lastStateBeforeFailure,
+ Throwable cause) {
+ resumeReadingIfNecessary();
+ HttpRequest initialRequest = serverConnection.getInitialRequest();
+ try {
+ boolean retrying = serverConnection.connectionFailed(cause);
+ if (retrying) {
+ LOG.debug(
+ "Failed to connect to upstream server or chained proxy. Retrying connection. Last state before failure: {}",
+ lastStateBeforeFailure,
+ cause);
+ return true;
+ } else {
+ LOG.debug(
+ "Connection to upstream server or chained proxy failed: {}. Last state before failure: {}",
+ serverConnection.getRemoteAddress(),
+ lastStateBeforeFailure,
+ cause);
+ connectionFailedUnrecoverably(initialRequest, serverConnection);
+ return false;
+ }
+ } catch (UnknownHostException uhe) {
+ connectionFailedUnrecoverably(initialRequest, serverConnection);
+ return false;
}
-
- private void connectionFailedUnrecoverably(HttpRequest initialRequest, ProxyToServerConnection serverConnection) {
- // the connection to the server failed, so disconnect the server and remove the ProxyToServerConnection from the
- // map of open server connections
- serverConnection.disconnect();
- this.serverConnectionsByHostAndPort.remove(serverConnection.getServerHostAndPort());
-
- boolean keepAlive = writeBadGateway(initialRequest);
- if (keepAlive) {
- become(AWAITING_INITIAL);
- } else {
- become(DISCONNECT_REQUESTED);
- }
+ }
+
+ private void connectionFailedUnrecoverably(
+ HttpRequest initialRequest, ProxyToServerConnection serverConnection) {
+ // the connection to the server failed, so disconnect the server and remove the
+ // ProxyToServerConnection from the
+ // map of open server connections
+ serverConnection.disconnect();
+ serverConnectionsByHostAndPort.remove(serverConnection.getServerHostAndPort());
+
+ boolean keepAlive = writeBadGateway(initialRequest);
+ if (keepAlive) {
+ become(AWAITING_INITIAL);
+ } else {
+ become(DISCONNECT_REQUESTED);
}
+ }
- private void resumeReadingIfNecessary() {
- if (this.numberOfCurrentlyConnectingServers.decrementAndGet() == 0) {
- LOG.debug("All servers have finished attempting to connect, resuming reading from client.");
- resumeReading();
- }
+ private void resumeReadingIfNecessary() {
+ if (numberOfCurrentlyConnectingServers.decrementAndGet() == 0) {
+ LOG.debug("All servers have finished attempting to connect, resuming reading from client.");
+ resumeReading();
}
-
- /* *************************************************************************
- * Other Lifecycle
- **************************************************************************/
-
- /**
- * On disconnect of the server, track that we have one fewer connected
- * servers and then disconnect the client if necessary.
- */
- protected void serverDisconnected(ProxyToServerConnection serverConnection) {
- numberOfCurrentlyConnectedServers.decrementAndGet();
-
- // for non-SSL connections, do not disconnect the client from the proxy, even if this was the last server connection.
- // this allows clients to continue to use the open connection to the proxy to make future requests. for SSL
- // connections, whether we are tunneling or MITMing, we need to disconnect the client because there is always
- // exactly one ClientToProxyConnection per ProxyToServerConnection, and vice versa.
- if (isTunneling() || isMitming()) {
- disconnect();
- }
+ }
+
+ /* *************************************************************************
+ * Other Lifecycle
+ **************************************************************************/
+
+ /**
+ * On disconnect of the server, track that we have one fewer connected servers and then disconnect
+ * the client if necessary.
+ */
+ protected void serverDisconnected(ProxyToServerConnection serverConnection) {
+ numberOfCurrentlyConnectedServers.decrementAndGet();
+
+ // for non-SSL connections, do not disconnect the client from the proxy, even if this was the
+ // last server connection.
+ // this allows clients to continue to use the open connection to the proxy to make future
+ // requests. for SSL
+ // connections, whether we are tunneling or MITMing, we need to disconnect the client because
+ // there is always
+ // exactly one ClientToProxyConnection per ProxyToServerConnection, and vice versa.
+ if (isTunneling() || isMitming()) {
+ disconnect();
}
-
- /**
- * When the ClientToProxyConnection becomes saturated, stop reading on all
- * associated ProxyToServerConnections.
- */
- @Override
- synchronized protected void becameSaturated() {
- super.becameSaturated();
- for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort
- .values()) {
- synchronized (serverConnection) {
- if (this.isSaturated()) {
- serverConnection.stopReading();
- }
- }
- }
+ }
+
+ /**
+ * When the ClientToProxyConnection becomes saturated, stop reading on all associated
+ * ProxyToServerConnections.
+ */
+ @Override
+ protected synchronized void becameSaturated() {
+ super.becameSaturated();
+ recordConnectionSaturated();
+ ProxyToServerConnection current = currentServerConnection;
+ for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort.values()) {
+ synchronized (serverConnection) {
+ if (isSaturated()) {
+ serverConnection.stopReading();
+ }
+ }
}
-
- /**
- * When the ClientToProxyConnection becomes writable, resume reading on all
- * associated ProxyToServerConnections.
- */
- @Override
- synchronized protected void becameWritable() {
- super.becameWritable();
- for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort
- .values()) {
- synchronized (serverConnection) {
- if (!this.isSaturated()) {
- serverConnection.resumeReading();
- }
- }
+ if (current != null) {
+ synchronized (current) {
+ if (isSaturated()) {
+ current.stopReading();
}
+ }
}
-
- /**
- * When a server becomes saturated, we stop reading from the client.
- */
- synchronized protected void serverBecameSaturated(
- ProxyToServerConnection serverConnection) {
- if (serverConnection.isSaturated()) {
- LOG.info("Connection to server became saturated, stopping reading");
- stopReading();
- }
+ }
+
+ /**
+ * When the ClientToProxyConnection becomes writable, resume reading on all associated
+ * ProxyToServerConnections.
+ */
+ @Override
+ protected synchronized void becameWritable() {
+ super.becameWritable();
+ recordConnectionWritable();
+ ProxyToServerConnection current = currentServerConnection;
+ for (ProxyToServerConnection serverConnection : serverConnectionsByHostAndPort.values()) {
+ synchronized (serverConnection) {
+ if (!isSaturated()) {
+ serverConnection.resumeReading();
+ }
+ }
}
-
- /**
- * When a server becomes writeable, we check to see if all servers are
- * writeable and if they are, we resume reading.
- */
- synchronized protected void serverBecameWriteable(
- ProxyToServerConnection serverConnection) {
- boolean anyServersSaturated = false;
- for (ProxyToServerConnection otherServerConnection : serverConnectionsByHostAndPort
- .values()) {
- if (otherServerConnection.isSaturated()) {
- anyServersSaturated = true;
- break;
- }
- }
- if (!anyServersSaturated) {
- LOG.info("All server connections writeable, resuming reading");
- resumeReading();
+ if (current != null) {
+ synchronized (current) {
+ if (!isSaturated()) {
+ current.resumeReading();
}
+ }
}
+ }
- @Override
- protected void exceptionCaught(Throwable cause) {
- try {
- if (cause instanceof IOException) {
- // IOExceptions are expected errors, for example when a browser is killed and aborts a connection.
- // rather than flood the logs with stack traces for these expected exceptions, we log the message at the
- // INFO level and the stack trace at the DEBUG level.
- LOG.info("An IOException occurred on ClientToProxyConnection: " + cause.getMessage());
- LOG.debug("An IOException occurred on ClientToProxyConnection", cause);
- } else if (cause instanceof RejectedExecutionException) {
- LOG.info("An executor rejected a read or write operation on the ClientToProxyConnection (this is normal if the proxy is shutting down). Message: " + cause.getMessage());
- LOG.debug("A RejectedExecutionException occurred on ClientToProxyConnection", cause);
- } else {
- LOG.error("Caught an exception on ClientToProxyConnection", cause);
- }
- } finally {
- // always disconnect the client when an exception occurs on the channel
- disconnect();
- }
+ /** When a server becomes saturated, we stop reading from the client. */
+ protected synchronized void serverBecameSaturated(ProxyToServerConnection serverConnection) {
+ if (serverConnection.isSaturated()) {
+ LOG.info("Connection to server became saturated, stopping reading");
+ stopReading();
+ }
+ }
+
+ /**
+ * When a server becomes writeable, we check to see if all servers are writeable and if they are,
+ * we resume reading.
+ */
+ protected synchronized void serverBecameWriteable(ProxyToServerConnection serverConnection) {
+ boolean anyServersSaturated = false;
+ ProxyToServerConnection current = currentServerConnection;
+ for (ProxyToServerConnection otherServerConnection : serverConnectionsByHostAndPort.values()) {
+ if (otherServerConnection.isSaturated()) {
+ anyServersSaturated = true;
+ break;
+ }
+ }
+ if (!anyServersSaturated
+ && current != null
+ && current != serverConnection
+ && current.isSaturated()) {
+ anyServersSaturated = true;
+ }
+ if (!anyServersSaturated) {
+ LOG.info("All server connections writeable, resuming reading");
+ resumeReading();
+ }
+ }
+
+ @Override
+ protected void exceptionCaught(Throwable cause) {
+ try {
+ recordConnectionExceptionCaught(cause);
+ if (cause instanceof IOException) {
+ // IOExceptions are expected errors, for example when a browser is killed and aborts a
+ // connection.
+ // rather than flood the logs with stack traces for these expected exceptions, we log the
+ // message at the
+ // INFO level and the stack trace at the DEBUG level.
+ LOG.info("An IOException occurred on ClientToProxyConnection: " + cause.getMessage());
+ LOG.debug("An IOException occurred on ClientToProxyConnection", cause);
+ } else if (cause instanceof RejectedExecutionException) {
+ LOG.info(
+ "An executor rejected a read or write operation on the ClientToProxyConnection (this is normal if the proxy is shutting down). Message: "
+ + cause.getMessage());
+ LOG.debug("A RejectedExecutionException occurred on ClientToProxyConnection", cause);
+ } else {
+ LOG.error("Caught an exception on ClientToProxyConnection", cause);
+ }
+ } finally {
+ // always disconnect the client when an exception occurs on the channel
+ disconnect();
+ }
+ }
+
+ /* *************************************************************************
+ * Connection Management
+ **************************************************************************/
+
+ /**
+ * Initialize the {@link ChannelPipeline} for the client to proxy channel. LittleProxy acts like a
+ * server here.
+ *
+ *
A {@link ChannelPipeline} invokes the read (Inbound) handlers in ascending ordering of the
+ * list and then the write (Outbound) handlers in descending ordering.
+ *
+ *
Regarding the Javadoc of {@link HttpObjectAggregator} it's needed to have the {@link
+ * HttpResponseEncoder} or {@link io.netty.handler.codec.http.HttpRequestEncoder} before the
+ * {@link HttpObjectAggregator} in the {@link ChannelPipeline}.
+ *
+ *
If an {@link SslEngineSource} is provided, SSL encryption is enabled on the pipeline. When
+ * the proxy protocol is enabled, the {@link HAProxyMessageDecoder} is added after the SSL handler
+ * setup to ensure it is positioned before the {@link io.netty.handler.ssl.SslHandler} in the
+ * inbound pipeline, so that the PROXY protocol header is decoded before the TLS handshake begins.
+ *
+ * @param pipeline the {@link ChannelPipeline} to configure
+ * @param sslEngineSource the {@link SslEngineSource} for client-to-proxy encryption, or {@code
+ * null} if SSL is not enabled
+ * @param authenticateClients whether to require client certificate authentication
+ */
+ private void initChannelPipeline(
+ ChannelPipeline pipeline,
+ @Nullable SslEngineSource sslEngineSource,
+ boolean authenticateClients) {
+ LOG.debug("Configuring ChannelPipeline");
+
+ pipeline.addLast("bytesReadMonitor", bytesReadMonitor);
+ pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
+
+ pipeline.addLast(HTTP_ENCODER_NAME, new HttpResponseEncoder());
+ // We want to allow longer request lines, headers, and chunks
+ // respectively.
+ pipeline.addLast(
+ HTTP_DECODER_NAME,
+ new HttpRequestDecoder(
+ proxyServer.getMaxInitialLineLength(),
+ proxyServer.getMaxHeaderSize(),
+ proxyServer.getMaxChunkSize()));
+
+ // Enable aggregation for filtering if necessary
+ int numberOfBytesToBuffer = proxyServer.getFiltersSource().getMaximumRequestBufferSizeInBytes();
+ if (numberOfBytesToBuffer > 0) {
+ aggregateContentForFiltering(pipeline, numberOfBytesToBuffer);
}
- /* *************************************************************************
- * Connection Management
- **************************************************************************/
-
- /**
- * Initialize the {@link ChannelPipeline} for the client to proxy channel.
- * LittleProxy acts like a server here.
- *
- * A {@link ChannelPipeline} invokes the read (Inbound) handlers in
- * ascending ordering of the list and then the write (Outbound) handlers in
- * descending ordering.
- *
- * Regarding the Javadoc of {@link HttpObjectAggregator} it's needed to have
- * the {@link HttpResponseEncoder} or {@link io.netty.handler.codec.http.HttpRequestEncoder} before the
- * {@link HttpObjectAggregator} in the {@link ChannelPipeline}.
- */
- private void initChannelPipeline(ChannelPipeline pipeline) {
- LOG.debug("Configuring ChannelPipeline");
-
- pipeline.addLast("bytesReadMonitor", bytesReadMonitor);
- pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
-
- pipeline.addLast("encoder", new HttpResponseEncoder());
- if (isAcceptProxyProtocol()) {
- pipeline.addLast("proxy-protocol-decoder", new HAProxyMessageDecoder());
- }
- // We want to allow longer request lines, headers, and chunks
- // respectively.
- pipeline.addLast("decoder", new HttpRequestDecoder(
- proxyServer.getMaxInitialLineLength(),
- proxyServer.getMaxHeaderSize(),
- proxyServer.getMaxChunkSize()));
-
- // Enable aggregation for filtering if necessary
- int numberOfBytesToBuffer = proxyServer.getFiltersSource()
- .getMaximumRequestBufferSizeInBytes();
- if (numberOfBytesToBuffer > 0) {
- aggregateContentForFiltering(pipeline, numberOfBytesToBuffer);
- }
+ pipeline.addLast(HTTP_REQUEST_READ_MONITOR_NAME, requestReadMonitor);
+ pipeline.addLast(HTTP_RESPONSE_WRITTEN_MONITOR_NAME, responseWrittenMonitor);
- pipeline.addLast("requestReadMonitor", requestReadMonitor);
- pipeline.addLast("responseWrittenMonitor", responseWrittenMonitor);
+ pipeline.addLast("idle", new IdleStateHandler(0, 0, proxyServer.getIdleConnectionTimeout()));
- pipeline.addLast(
- "idle",
- new IdleStateHandler(0, 0, proxyServer
- .getIdleConnectionTimeout()));
+ pipeline.addLast(MAIN_HANDLER_NAME, this);
- pipeline.addLast("handler", this);
+ if (sslEngineSource != null) {
+ LOG.debug("Enabling encryption of traffic from client to proxy");
+ SSLEngine sslEngine = sslEngineSource.newSslEngine();
+ recordClientSSLHandshakeStarted();
+ encrypt(pipeline, sslEngine, authenticateClients)
+ .addListener(
+ future -> {
+ if (future.isSuccess()) {
+ clientSslSession = sslEngine.getSession();
+ recordClientSSLHandshakeSucceeded();
+ }
+ });
}
- /**
- * Is the proxy server set to accept a proxy protocol header
- * @return True if the proxy server set to accept a proxy protocol header. False otherwise
- */
- boolean isAcceptProxyProtocol() {
- return proxyServer.isAcceptProxyProtocol();
+ if (isAcceptProxyProtocol()) {
+ pipeline.addFirst(HTTP_PROXY_DECODER_NAME, new HAProxyMessageDecoder());
}
-
- /**
- * Is the proxy server set to send a proxy protocol header
- * @return True if the proxy server set to send a proxy protocol header. False otherwise
- */
- boolean isSendProxyProtocol() {
- return proxyServer.isSendProxyProtocol();
+ }
+
+ private void removeHandlerIfPresent(String name) {
+ removeHandlerIfPresent(channel.pipeline(), name);
+ }
+
+ /**
+ * Is the proxy server set to accept a proxy protocol header
+ *
+ * @return True if the proxy server set to accept a proxy protocol header. False otherwise
+ */
+ boolean isAcceptProxyProtocol() {
+ return proxyServer.isAcceptProxyProtocol();
+ }
+
+ /**
+ * Is the proxy server set to send a proxy protocol header
+ *
+ * @return True if the proxy server set to send a proxy protocol header. False otherwise
+ */
+ boolean isSendProxyProtocol() {
+ return proxyServer.isSendProxyProtocol();
+ }
+
+ /**
+ * This method takes care of closing client to proxy and/or proxy to server connections after
+ * finishing writing.
+ */
+ private void closeConnectionsAfterWriteIfNecessary(
+ ProxyToServerConnection serverConnection,
+ HttpRequest currentHttpRequest,
+ HttpResponse currentHttpResponse,
+ HttpObject httpObject) {
+ boolean closeServerConnection =
+ shouldCloseServerConnection(currentHttpRequest, currentHttpResponse, httpObject);
+ boolean closeClientConnection =
+ shouldCloseClientConnection(currentHttpRequest, currentHttpResponse, httpObject);
+
+ if (closeServerConnection) {
+ LOG.debug("Closing remote connection after writing to client");
+ serverConnection.disconnect();
}
- /**
- * This method takes care of closing client to proxy and/or proxy to server
- * connections after finishing a write.
- */
- private void closeConnectionsAfterWriteIfNecessary(
- ProxyToServerConnection serverConnection,
- HttpRequest currentHttpRequest, HttpResponse currentHttpResponse,
- HttpObject httpObject) {
- boolean closeServerConnection = shouldCloseServerConnection(
- currentHttpRequest, currentHttpResponse, httpObject);
- boolean closeClientConnection = shouldCloseClientConnection(
- currentHttpRequest, currentHttpResponse, httpObject);
-
- if (closeServerConnection) {
- LOG.debug("Closing remote connection after writing to client");
- serverConnection.disconnect();
- }
-
- if (closeClientConnection) {
- LOG.debug("Closing connection to client after writes");
- disconnect();
+ if (closeClientConnection) {
+ LOG.debug("Closing connection to client after writes");
+ disconnect();
+ }
+ }
+
+ private void forceDisconnect(ProxyToServerConnection serverConnection) {
+ LOG.debug("Forcing disconnect");
+ serverConnection.disconnect();
+ disconnect();
+ }
+
+ /** Determine whether the client connection should be closed. */
+ private boolean shouldCloseClientConnection(
+ HttpRequest req, HttpResponse res, HttpObject httpObject) {
+ if (ProxyUtils.isChunked(res)) {
+ // If the response is chunked, we want to return false unless it's
+ // the last chunk. If it is the last chunk, then we want to pass
+ // through to the same close semantics we'd otherwise use.
+ if (httpObject != null) {
+ if (!ProxyUtils.isLastChunk(httpObject)) {
+ String uri = null;
+ if (req != null) {
+ uri = req.uri();
+ }
+ LOG.debug("Not closing client connection on middle chunk for {}", uri);
+ return false;
+ } else {
+ LOG.debug("Handling last chunk. Using normal client connection closing rules.");
}
+ }
}
- private void forceDisconnect(ProxyToServerConnection serverConnection) {
- LOG.debug("Forcing disconnect");
- serverConnection.disconnect();
- disconnect();
+ if (!HttpUtil.isKeepAlive(req)) {
+ LOG.debug("Closing client connection since request is not keep alive: {}", req);
+ // Here we simply want to close the connection because the
+ // client itself has requested it be closed in the request.
+ return true;
}
- /**
- * Determine whether or not the client connection should be closed.
- */
- private boolean shouldCloseClientConnection(HttpRequest req,
- HttpResponse res, HttpObject httpObject) {
- if (ProxyUtils.isChunked(res)) {
- // If the response is chunked, we want to return false unless it's
- // the last chunk. If it is the last chunk, then we want to pass
- // through to the same close semantics we'd otherwise use.
- if (httpObject != null) {
- if (!ProxyUtils.isLastChunk(httpObject)) {
- String uri = null;
- if (req != null) {
- uri = req.uri();
- }
- LOG.debug("Not closing client connection on middle chunk for {}", uri);
- return false;
- } else {
- LOG.debug("Handling last chunk. Using normal client connection closing rules.");
- }
- }
- }
-
- if (!HttpUtil.isKeepAlive(req)) {
- LOG.debug("Closing client connection since request is not keep alive: {}", req);
- // Here we simply want to close the connection because the
- // client itself has requested it be closed in the request.
- return true;
+ // ignore the response's keep-alive; we can keep this client connection open as long as the
+ // client allows it.
+
+ LOG.debug("Not closing client connection for request: {}", req);
+ return false;
+ }
+
+ /**
+ * Determines if the remote connection should be closed based on the request and response pair. If
+ * the request is HTTP 1.0 with no keep-alive header, for example, the connection should be
+ * closed.
+ *
+ *
This in part determines if we should close the connection. Here's the relevant section of
+ * RFC 2616:
+ *
+ *
"HTTP/1.1 defines the "close" connection option for the sender to signal that the connection
+ * will be closed after completion of the response. For example,
+ *
+ *
Connection: close
+ *
+ *
in either the request or the response header fields indicates that the connection SHOULD NOT
+ * be considered "persistent" (section 8.1) after the current request/response is complete."
+ *
+ * @param req The request.
+ * @param res The response.
+ * @param msg The message.
+ * @return Returns true if the connection should close.
+ */
+ private boolean shouldCloseServerConnection(HttpRequest req, HttpResponse res, HttpObject msg) {
+ if (ProxyUtils.isChunked(res)) {
+ // If the response is chunked, we want to return false unless it's
+ // the last chunk. If it is the last chunk, then we want to pass
+ // through to the same close semantics we'd otherwise use.
+ if (msg != null) {
+ if (!ProxyUtils.isLastChunk(msg)) {
+ String uri = null;
+ if (req != null) {
+ uri = req.uri();
+ }
+ LOG.debug("Not closing server connection on middle chunk for {}", uri);
+ return false;
+ } else {
+ LOG.debug("Handling last chunk. Using normal server connection closing rules.");
}
+ }
+ }
- // ignore the response's keep-alive; we can keep this client connection open as long as the client allows it.
+ // ignore the request's keep-alive; we can keep this server connection open as long as the
+ // server allows it.
- LOG.debug("Not closing client connection for request: {}", req);
- return false;
+ if (!HttpUtil.isKeepAlive(res)) {
+ LOG.debug("Closing server connection since response is not keep alive: {}", res);
+ // In this case, we want to honor the Connection: close header
+ // from the remote server and close that connection. We don't
+ // necessarily want to close the connection to the client, however
+ // as it's possible it has other connections open.
+ return true;
}
- /**
- * Determines if the remote connection should be closed based on the request
- * and response pair. If the request is HTTP 1.0 with no keep-alive header,
- * for example, the connection should be closed.
- *
- * This in part determines if we should close the connection. Here's the
- * relevant section of RFC 2616:
- *
- * "HTTP/1.1 defines the "close" connection option for the sender to signal
- * that the connection will be closed after completion of the response. For
- * example,
- *
- * Connection: close
- *
- * in either the request or the response header fields indicates that the
- * connection SHOULD NOT be considered `persistent' (section 8.1) after the
- * current request/response is complete."
- *
- * @param req
- * The request.
- * @param res
- * The response.
- * @param msg
- * The message.
- * @return Returns true if the connection should close.
- */
- private boolean shouldCloseServerConnection(HttpRequest req,
- HttpResponse res, HttpObject msg) {
- if (ProxyUtils.isChunked(res)) {
- // If the response is chunked, we want to return false unless it's
- // the last chunk. If it is the last chunk, then we want to pass
- // through to the same close semantics we'd otherwise use.
- if (msg != null) {
- if (!ProxyUtils.isLastChunk(msg)) {
- String uri = null;
- if (req != null) {
- uri = req.uri();
- }
- LOG.debug("Not closing server connection on middle chunk for {}", uri);
- return false;
- } else {
- LOG.debug("Handling last chunk. Using normal server connection closing rules.");
- }
- }
- }
+ LOG.debug("Not closing server connection for response: {}", res);
+ return false;
+ }
+
+ /* *************************************************************************
+ * Authentication
+ **************************************************************************/
+
+ /**
+ * Checks whether the given HttpRequest requires authentication.
+ *
+ *
If the request contains credentials, these are checked.
+ *
+ *
If authentication is still required, either because no credentials were provided or the
+ * credentials were wrong, this writes a 407 response to the client.
+ */
+ private boolean authenticationRequired(HttpRequest request) {
+
+ if (authenticated.get()) {
+ return false;
+ }
- // ignore the request's keep-alive; we can keep this server connection open as long as the server allows it.
+ final ProxyAuthenticator authenticator = proxyServer.getProxyAuthenticator();
- if (!HttpUtil.isKeepAlive(res)) {
- LOG.debug("Closing server connection since response is not keep alive: {}", res);
- // In this case, we want to honor the Connection: close header
- // from the remote server and close that connection. We don't
- // necessarily want to close the connection to the client, however
- // as it's possible it has other connections open.
- return true;
- }
+ if (authenticator == null) return false;
- LOG.debug("Not closing server connection for response: {}", res);
- return false;
+ if (!request.headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION)) {
+ writeAuthenticationRequired(authenticator.getRealm());
+ return true;
}
- /* *************************************************************************
- * Authentication
- **************************************************************************/
-
- /**
- *
- * Checks whether the given HttpRequest requires authentication.
- *
- *
- *
- * If the request contains credentials, these are checked.
- *
- *
- *
- * If authentication is still required, either because no credentials were
- * provided or the credentials were wrong, this writes a 407 response to the
- * client.
- *
- */
- private boolean authenticationRequired(HttpRequest request) {
+ List values = request.headers().getAll(HttpHeaderNames.PROXY_AUTHORIZATION);
+ String fullValue = values.iterator().next();
+ String value = StringUtils.substringAfter(fullValue, "Basic ").trim();
- if (authenticated.get()) {
- return false;
- }
+ String decodedValue = new String(Base64.getDecoder().decode(value), UTF_8);
- final ProxyAuthenticator authenticator = proxyServer
- .getProxyAuthenticator();
+ String userName = StringUtils.substringBefore(decodedValue, ":");
+ String password = StringUtils.substringAfter(decodedValue, ":");
+ if (!authenticator.authenticate(userName, password)) {
+ writeAuthenticationRequired(authenticator.getRealm());
+ return true;
+ }
+ clientDetails.setUserName(userName);
+
+ LOG.debug("Got proxy authorization!");
+ // We need to remove the header before sending the request on.
+ String authentication = request.headers().get(HttpHeaderNames.PROXY_AUTHORIZATION);
+ LOG.debug(authentication);
+ request.headers().remove(HttpHeaderNames.PROXY_AUTHORIZATION);
+ authenticated.set(true);
+ return false;
+ }
+
+ private void writeAuthenticationRequired(String realm) {
+ String body =
+ "\n"
+ + "\n"
+ + "407 Proxy Authentication Required\n"
+ + "\n"
+ + "
Proxy Authentication Required
\n"
+ + "
This server could not verify that you\n"
+ + "are authorized to access the document\n"
+ + "requested. Either you supplied the wrong\n"
+ + "credentials (e.g., bad password), or your\n"
+ + "browser doesn't understand how to supply\n"
+ + "the credentials required.
\n"
+ + "\n";
+ FullHttpResponse response =
+ ProxyUtils.createFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED, body);
+ response.headers().set(HttpHeaderNames.DATE, dateHeaderValue());
+ response
+ .headers()
+ .set(
+ HttpHeaderNames.PROXY_AUTHENTICATE,
+ "Basic realm=\"" + (realm == null ? "Restricted Files" : realm) + "\"");
+ write(response);
+ }
+
+ private String dateHeaderValue() {
+ return LocalDateTime.now()
+ .atZone(ZoneId.of("GMT"))
+ .format(ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz"));
+ }
+
+ /* *************************************************************************
+ * Request/Response Rewriting
+ **************************************************************************/
+
+ /** Copy the given {@link HttpRequest} verbatim. */
+ @NonNull
+ @CheckReturnValue
+ private HttpRequest copy(HttpRequest original) {
+ if (original instanceof FullHttpRequest) {
+ return ((FullHttpRequest) original).copy();
+ } else {
+ HttpRequest request =
+ new DefaultHttpRequest(original.protocolVersion(), original.method(), original.uri());
+ request.headers().set(original.headers());
+ return request;
+ }
+ }
+
+ /**
+ * Chunked encoding is an HTTP 1.1 feature, but sometimes we get a chunked response that reports
+ * its HTTP version as 1.0. In this case, we change it to 1.1.
+ */
+ private void fixHttpVersionHeaderIfNecessary(HttpResponse httpResponse) {
+ String te = httpResponse.headers().get(HttpHeaderNames.TRANSFER_ENCODING);
+ if (StringUtils.isNotBlank(te) && te.equalsIgnoreCase(HttpHeaderValues.CHUNKED.toString())) {
+ if (httpResponse.protocolVersion() != HttpVersion.HTTP_1_1) {
+ LOG.debug("Fixing HTTP version.");
+ httpResponse.setProtocolVersion(HttpVersion.HTTP_1_1);
+ }
+ }
+ }
+
+ /**
+ * If and only if our proxy is not running in transparent mode, modify the request headers to
+ * reflect that it was proxied.
+ */
+ private void modifyRequestHeadersToReflectProxying(HttpRequest httpRequest) {
+ if (isNextHopOriginServer()) {
+ /*
+ * We are making the request to the origin server, so must modify
+ * the 'absolute-URI' into the 'origin-form' as per RFC 7230
+ * section 5.3.1.
+ *
+ * This must happen even for 'transparent' mode, otherwise the origin
+ * server could infer that the request came via a proxy server.
+ */
+ LOG.debug("Modifying request for proxy chaining");
+ // Strip host from uri
+ String uri = httpRequest.uri();
+ String adjustedUri = ProxyUtils.stripHost(uri);
+ LOG.debug("Stripped host from uri: {} yielding: {}", uri, adjustedUri);
+ httpRequest.setUri(adjustedUri);
+ }
+ if (!proxyServer.isTransparent()) {
+ LOG.debug("Modifying request headers for proxying");
- if (authenticator == null)
- return false;
+ HttpHeaders headers = httpRequest.headers();
- if (!request.headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION)) {
- writeAuthenticationRequired(authenticator.getRealm());
- return true;
- }
+ // Remove sdch from encodings we accept since we can't decode it.
+ ProxyUtils.removeSdchEncoding(headers);
+ switchProxyConnectionHeader(headers);
+ stripConnectionTokens(headers);
- List values = request.headers().getAll(
- HttpHeaderNames.PROXY_AUTHORIZATION);
- String fullValue = values.iterator().next();
- String value = StringUtils.substringAfter(fullValue, "Basic ").trim();
+ stripHopByHopHeaders(headers);
- byte[] decodedValue = BaseEncoding.base64().decode(value);
+ // If we're forwarding to an upstream proxy that requires authentication, add the credentials
+ if (shouldPreserveProxyAuthorizationForUpstream()) {
+ addUpstreamProxyAuthorization(headers);
+ }
- String decodedString = new String(decodedValue, Charset.forName("UTF-8"));
-
- String userName = StringUtils.substringBefore(decodedString, ":");
- String password = StringUtils.substringAfter(decodedString, ":");
- if (!authenticator.authenticate(userName, password)) {
- writeAuthenticationRequired(authenticator.getRealm());
- return true;
- }
- clientDetails.setUserName(userName);
-
- LOG.debug("Got proxy authorization!");
- // We need to remove the header before sending the request on.
- String authentication = request.headers().get(
- HttpHeaderNames.PROXY_AUTHORIZATION);
- LOG.debug(authentication);
- request.headers().remove(HttpHeaderNames.PROXY_AUTHORIZATION);
- authenticated.set(true);
- return false;
+ ProxyUtils.addVia(httpRequest, proxyServer.getProxyAlias());
}
-
- private void writeAuthenticationRequired(String realm) {
- String body = "\n"
- + "\n"
- + "407 Proxy Authentication Required\n"
- + "\n"
- + "
Proxy Authentication Required
\n"
- + "
This server could not verify that you\n"
- + "are authorized to access the document\n"
- + "requested. Either you supplied the wrong\n"
- + "credentials (e.g., bad password), or your\n"
- + "browser doesn't understand how to supply\n"
- + "the credentials required.
\n" + "\n";
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
- HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED, body);
- response.headers().set(HttpHeaderNames.DATE, new Date());
- response.headers().set(HttpHeaderNames.PROXY_AUTHENTICATE,
- "Basic realm=\"" + (realm == null ? "Restricted Files" : realm) + "\"");
- write(response);
- }
-
- /* *************************************************************************
- * Request/Response Rewriting
- **************************************************************************/
-
- /**
- * Copy the given {@link HttpRequest} verbatim.
- */
- private HttpRequest copy(HttpRequest original) {
- if (original instanceof FullHttpRequest) {
- return ((FullHttpRequest) original).copy();
- } else {
- HttpRequest request = new DefaultHttpRequest(original.protocolVersion(),
- original.method(), original.uri());
- request.headers().set(original.headers());
- return request;
- }
+ }
+
+ /**
+ * Checks if we should preserve Proxy-Authorization headers for upstream proxy authentication.
+ *
+ * @return true if we're forwarding to an upstream proxy that requires authentication
+ */
+ boolean shouldPreserveProxyAuthorizationForUpstream() {
+ if (!currentServerConnection.hasUpstreamChainedProxy()) {
+ return false;
}
- /**
- * Chunked encoding is an HTTP 1.1 feature, but sometimes we get a chunked
- * response that reports its HTTP version as 1.0. In this case, we change it
- * to 1.1.
- */
- private void fixHttpVersionHeaderIfNecessary(HttpResponse httpResponse) {
- String te = httpResponse.headers().get(
- HttpHeaderNames.TRANSFER_ENCODING);
- if (StringUtils.isNotBlank(te)
- && te.equalsIgnoreCase(HttpHeaderValues.CHUNKED.toString())) {
- if (httpResponse.protocolVersion() != HttpVersion.HTTP_1_1) {
- LOG.debug("Fixing HTTP version.");
- httpResponse.setProtocolVersion(HttpVersion.HTTP_1_1);
- }
- }
+ ChainedProxy chainedProxy = currentServerConnection.getChainedProxy();
+ if (chainedProxy == null) {
+ return false;
}
- /**
- * If and only if our proxy is not running in transparent mode, modify the
- * request headers to reflect that it was proxied.
- */
- private void modifyRequestHeadersToReflectProxying(HttpRequest httpRequest) {
- if (isNextHopOriginServer()) {
- /*
- * We are making the request to the origin server, so must modify
- * the 'absolute-URI' into the 'origin-form' as per RFC 7230
- * section 5.3.1.
- *
- * This must happen even for 'transparent' mode, otherwise the origin
- * server could infer that the request came via a proxy server.
- */
- LOG.debug("Modifying request for proxy chaining");
- // Strip host from uri
- String uri = httpRequest.uri();
- String adjustedUri = ProxyUtils.stripHost(uri);
- LOG.debug("Stripped host from uri: {} yielding: {}", uri,
- adjustedUri);
- httpRequest.setUri(adjustedUri);
- }
- if (!proxyServer.isTransparent()) {
- LOG.debug("Modifying request headers for proxying");
-
- HttpHeaders headers = httpRequest.headers();
-
- // Remove sdch from encodings we accept since we can't decode it.
- ProxyUtils.removeSdchEncoding(headers);
- switchProxyConnectionHeader(headers);
- stripConnectionTokens(headers);
- stripHopByHopHeaders(headers);
- ProxyUtils.addVia(httpRequest, proxyServer.getProxyAlias());
- }
+ // Only preserve for HTTP proxies (not SOCKS)
+ if (chainedProxy.getChainedProxyType() != ChainedProxyType.HTTP) {
+ return false;
}
- private boolean isNextHopOriginServer() {
- // If there is no upstream chained proxy, the next hop must be the origin server.
- if (!currentServerConnection.hasUpstreamChainedProxy()) {
- return true;
- }
-
- /*
- * Upstream SOCKS proxies are a special case because they do not
- * parse or modify the HTTP request in any way. If the upstream
- * chained proxy is a SOCKS proxy, we should treat it as if we
- * are connecting directly to the origin server.
- */
- switch (currentServerConnection.getChainedProxyType()) {
- case HTTP:
- return false;
- case SOCKS4:
- case SOCKS5:
- return true;
- default:
- LOG.warn("Assuming upstream chained proxy of unknown type "
- + currentServerConnection.getChainedProxyType()
- + " should not be treated as an origin server");
- return false;
- }
+ // Check if the upstream proxy requires authentication
+ return chainedProxy.getUsername() != null && chainedProxy.getPassword() != null;
+ }
+
+ /**
+ * Handles upstream proxy 407 (Proxy Authentication Required) responses. This method checks if the
+ * response is a 407 from an upstream proxy and handles the authentication challenge
+ * appropriately.
+ *
+ * @param httpResponse the response from the upstream proxy
+ * @return true if this is an upstream proxy 407 that should be handled, false otherwise
+ */
+ boolean handleUpstreamProxyAuthenticationRequired(HttpResponse httpResponse) {
+ // Check if this is a 407 response
+ if (httpResponse.status() != HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED) {
+ return false;
}
- /**
- * If and only if our proxy is not running in transparent mode, modify the
- * response headers to reflect that it was proxied.
- */
- private void modifyResponseHeadersToReflectProxying(
- HttpResponse httpResponse) {
- if (!proxyServer.isTransparent()) {
- HttpHeaders headers = httpResponse.headers();
-
- stripConnectionTokens(headers);
- stripHopByHopHeaders(headers);
- ProxyUtils.addVia(httpResponse, proxyServer.getProxyAlias());
-
- /*
- * RFC2616 Section 14.18
- *
- * A received message that does not have a Date header field MUST be
- * assigned one by the recipient if the message will be cached by
- * that recipient or gatewayed via a protocol which requires a Date.
- */
- if (!headers.contains(HttpHeaderNames.DATE)) {
- headers.set(HttpHeaderNames.DATE, new Date());
- }
- }
+ // Check if we have an upstream chained proxy
+ if (!currentServerConnection.hasUpstreamChainedProxy()) {
+ return false;
}
- /**
- * Switch the de-facto standard "Proxy-Connection" header to "Connection"
- * when we pass it along to the remote host. This is largely undocumented
- * but seems to be what most browsers and servers expect.
- *
- * @param headers
- * The headers to modify
- */
- private void switchProxyConnectionHeader(HttpHeaders headers) {
- String proxyConnectionKey = "Proxy-Connection";
- if (headers.contains(proxyConnectionKey)) {
- String header = headers.get(proxyConnectionKey);
- headers.remove(proxyConnectionKey);
- headers.set(HttpHeaderNames.CONNECTION, header);
- }
+ ChainedProxy chainedProxy = currentServerConnection.getChainedProxy();
+ if (chainedProxy == null) {
+ return false;
}
- /**
- * RFC2616 Section 14.10
- *
- * HTTP/1.1 proxies MUST parse the Connection header field before a message
- * is forwarded and, for each connection-token in this field, remove any
- * header field(s) from the message with the same name as the
- * connection-token.
- *
- * @param headers
- * The headers to modify
- */
- private void stripConnectionTokens(HttpHeaders headers) {
- if (headers.contains(HttpHeaderNames.CONNECTION)) {
- for (String headerValue : headers.getAll(HttpHeaderNames.CONNECTION)) {
- for (String connectionToken : ProxyUtils.splitCommaSeparatedHeaderValues(headerValue)) {
- // do not strip out the Transfer-Encoding header if it is specified in the Connection header, since LittleProxy does not
- // normally modify the Transfer-Encoding of the message.
- if (!HttpHeaderNames.TRANSFER_ENCODING.toString().equals(connectionToken.toLowerCase(Locale.US))) {
- headers.remove(connectionToken);
- }
- }
- }
- }
+ // Only handle for HTTP proxies
+ if (chainedProxy.getChainedProxyType() != ChainedProxyType.HTTP) {
+ return false;
}
- /**
- * Removes all headers that should not be forwarded. See RFC 2616 13.5.1
- * End-to-end and Hop-by-hop Headers.
- *
- * @param headers
- * The headers to modify
- */
- private void stripHopByHopHeaders(HttpHeaders headers) {
- Set headerNames = headers.names();
- for (String headerName : headerNames) {
- if (ProxyUtils.shouldRemoveHopByHopHeader(headerName)) {
- headers.remove(headerName);
- }
- }
+ // Check if the upstream proxy requires authentication
+ if (chainedProxy.getUsername() == null || chainedProxy.getPassword() == null) {
+ // Upstream proxy doesn't have credentials configured, pass the 407 to client
+ return false;
}
- /* *************************************************************************
- * Miscellaneous
- **************************************************************************/
-
- /**
- * Tells the client that something went wrong trying to proxy its request. If the Bad Gateway is a response to
- * an HTTP HEAD request, the response will contain no body, but the Content-Length header will be set to the
- * value it would have been if this 502 Bad Gateway were in response to a GET.
- *
- * @param httpRequest the HttpRequest that is resulting in the Bad Gateway response
- * @return true if the connection will be kept open, or false if it will be disconnected
- */
- private boolean writeBadGateway(HttpRequest httpRequest) {
- String body = "Bad Gateway: " + httpRequest.uri();
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);
-
- if (ProxyUtils.isHEAD(httpRequest)) {
- // don't allow any body content in response to a HEAD request
- response.content().clear();
- }
-
- return respondWithShortCircuitResponse(response);
+ // This is an upstream proxy 407 that we can handle
+ LOG.debug("Received 407 from upstream proxy, will retry with authentication");
+
+ // We need to retry the request with proper authentication
+ // This would require more complex logic to retry the request
+ // For now, we'll pass the 407 to the client as the current architecture
+ // doesn't easily support retrying requests
+
+ return true;
+ }
+
+ /**
+ * Adds Proxy-Authorization header for upstream proxy authentication.
+ *
+ * @param headers the headers to modify
+ */
+ void addUpstreamProxyAuthorization(HttpHeaders headers) {
+ ChainedProxy chainedProxy = currentServerConnection.getChainedProxy();
+ if (chainedProxy != null) {
+ String username = chainedProxy.getUsername();
+ String password = chainedProxy.getPassword();
+
+ if (username != null && password != null) {
+ String credentials = username + ":" + password;
+ String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes(UTF_8));
+ String authHeader = "Basic " + base64Credentials;
+
+ headers.set(HttpHeaderNames.PROXY_AUTHORIZATION, authHeader);
+ }
}
+ }
- /**
- * Tells the client that the request was malformed or erroneous. If the Bad Request is a response to
- * an HTTP HEAD request, the response will contain no body, but the Content-Length header will be set to the
- * value it would have been if this Bad Request were in response to a GET.
- *
- * @return true if the connection will be kept open, or false if it will be disconnected
- */
- private boolean writeBadRequest(HttpRequest httpRequest) {
- String body = "Bad Request to URI: " + httpRequest.uri();
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, body);
-
- if (ProxyUtils.isHEAD(httpRequest)) {
- // don't allow any body content in response to a HEAD request
- response.content().clear();
- }
-
- return respondWithShortCircuitResponse(response);
+ private boolean isNextHopOriginServer() {
+ // If there is no upstream chained proxy, the next hop must be the origin server.
+ if (!currentServerConnection.hasUpstreamChainedProxy()) {
+ return true;
}
- /**
- * Tells the client that the connection to the server, or possibly to some intermediary service (such as DNS), timed out.
- * If the Gateway Timeout is a response to an HTTP HEAD request, the response will contain no body, but the
- * Content-Length header will be set to the value it would have been if this 504 Gateway Timeout were in response to a GET.
- *
- * @param httpRequest the HttpRequest that is resulting in the Gateway Timeout response
- * @return true if the connection will be kept open, or false if it will be disconnected
+ /*
+ * Upstream SOCKS proxies are a special case because they do not
+ * parse or modify the HTTP request in any way. If the upstream
+ * chained proxy is a SOCKS proxy, we should treat it as if we
+ * are connecting directly to the origin server.
*/
- private boolean writeGatewayTimeout(HttpRequest httpRequest) {
- String body = "Gateway Timeout";
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1,
- HttpResponseStatus.GATEWAY_TIMEOUT, body);
-
- if (httpRequest != null && ProxyUtils.isHEAD(httpRequest)) {
- // don't allow any body content in response to a HEAD request
- response.content().clear();
- }
-
- return respondWithShortCircuitResponse(response);
+ switch (currentServerConnection.getChainedProxyType()) {
+ case HTTP:
+ return false;
+ case SOCKS4:
+ case SOCKS5:
+ return true;
+ default:
+ LOG.warn(
+ "Assuming upstream chained proxy of unknown type "
+ + currentServerConnection.getChainedProxyType()
+ + " should not be treated as an origin server");
+ return false;
+ }
+ }
+
+ /**
+ * If and only if our proxy is not running in transparent mode, modify the response headers to
+ * reflect that it was proxied.
+ */
+ private void modifyResponseHeadersToReflectProxying(HttpResponse httpResponse) {
+ if (!proxyServer.isTransparent()) {
+ HttpHeaders headers = httpResponse.headers();
+
+ stripConnectionTokens(headers);
+ stripHopByHopHeaders(headers);
+ ProxyUtils.addVia(httpResponse, proxyServer.getProxyAlias());
+
+ /*
+ * RFC2616 Section 14.18
+ *
+ * A received message that does not have a Date header field MUST be
+ * assigned one by the recipient if the message will be cached by
+ * that recipient or gatewayed via a protocol which requires a Date.
+ */
+ if (!headers.contains(HttpHeaderNames.DATE)) {
+ headers.set(HttpHeaderNames.DATE, dateHeaderValue());
+ }
+ }
+ }
+
+ /**
+ * Switch the de-facto standard "Proxy-Connection" header to "Connection" when we pass it along to
+ * the remote host. This is largely undocumented but seems to be what most browsers and servers
+ * expect.
+ *
+ * @param headers The headers to modify
+ */
+ private void switchProxyConnectionHeader(HttpHeaders headers) {
+ String proxyConnectionKey = "Proxy-Connection";
+ if (headers.contains(proxyConnectionKey)) {
+ String header = headers.get(proxyConnectionKey);
+ headers.remove(proxyConnectionKey);
+ headers.set(HttpHeaderNames.CONNECTION, header);
+ }
+ }
+
+ /**
+ * RFC2616 Section 14.10
+ *
+ *
HTTP/1.1 proxies MUST parse the Connection header field before a message is forwarded and,
+ * for each connection-token in this field, remove any header field(s) from the message with the
+ * same name as the connection-token.
+ *
+ * @param headers The headers to modify
+ */
+ private void stripConnectionTokens(HttpHeaders headers) {
+ if (headers.contains(HttpHeaderNames.CONNECTION)) {
+ for (String headerValue : headers.getAll(HttpHeaderNames.CONNECTION)) {
+ for (String connectionToken : ProxyUtils.splitCommaSeparatedHeaderValues(headerValue)) {
+ // do not strip out the Transfer-Encoding header if it is specified in the Connection
+ // header, since LittleProxy does not
+ // normally modify the Transfer-Encoding of the message.
+ if (!HttpHeaderNames.TRANSFER_ENCODING
+ .toString()
+ .equals(connectionToken.toLowerCase(Locale.US))) {
+ headers.remove(connectionToken);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes all headers that should not be forwarded. See RFC 2616 13.5.1 End-to-end and Hop-by-hop
+ * Headers.
+ *
+ * @param headers The headers to modify
+ */
+ void stripHopByHopHeaders(HttpHeaders headers) {
+ ProxyUtils.stripHopByHopHeaders(headers);
+ }
+
+ /* *************************************************************************
+ * Miscellaneous
+ **************************************************************************/
+
+ /**
+ * Tells the client that something went wrong trying to proxy its request. If the Bad Gateway is a
+ * response to an HTTP HEAD request, the response will contain no body, but the Content-Length
+ * header will be set to the value it would have been if this 502 Bad Gateway were in response to
+ * a GET.
+ *
+ * @param httpRequest the HttpRequest that is resulting in the Bad Gateway response
+ * @return true if the connection will be kept open, or false if it will be disconnected
+ */
+ private boolean writeBadGateway(HttpRequest httpRequest) {
+ String body = "Bad Gateway: " + httpRequest.uri();
+ FullHttpResponse response =
+ ProxyUtils.createFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);
+
+ if (ProxyUtils.isHEAD(httpRequest)) {
+ // don't allow any body content in response to a HEAD request
+ response.content().clear();
}
- /**
- * Responds to the client with the specified "short-circuit" response. The response will be sent through the
- * {@link HttpFilters#proxyToClientResponse(HttpObject)} filter method before writing it to the client. The client
- * will not be disconnected, unless the response includes a "Connection: close" header, or the filter returns
- * a null HttpResponse (in which case no response will be written to the client and the connection will be
- * disconnected immediately). If the response is not a Bad Gateway or Gateway Timeout response, the response's headers
- * will be modified to reflect proxying, including adding a Via header, Date header, etc.
- *
- * @param httpResponse the response to return to the client
- * @return true if the connection will be kept open, or false if it will be disconnected.
- */
- private boolean respondWithShortCircuitResponse(HttpResponse httpResponse) {
- // we are sending a response to the client, so we are done handling this request
- this.currentRequest = null;
-
- HttpResponse filteredResponse = (HttpResponse) currentFilters.proxyToClientResponse(httpResponse);
- if (filteredResponse == null) {
- disconnect();
- return false;
- }
-
- // allow short-circuit messages to close the connection. normally the Connection header would be stripped when modifying
- // the message for proxying, so save the keep-alive status before the modifications are made.
- boolean isKeepAlive = HttpUtil.isKeepAlive(httpResponse);
-
- // if the response is not a Bad Gateway or Gateway Timeout, modify the headers "as if" the short-circuit response were proxied
- int statusCode = httpResponse.status().code();
- if (statusCode != HttpResponseStatus.BAD_GATEWAY.code() && statusCode != HttpResponseStatus.GATEWAY_TIMEOUT.code()) {
- modifyResponseHeadersToReflectProxying(httpResponse);
- }
-
- // restore the keep alive status, if it was overwritten when modifying headers for proxying
- HttpUtil.setKeepAlive(httpResponse, isKeepAlive);
-
- write(httpResponse);
+ return respondWithShortCircuitResponse(response);
+ }
+
+ /**
+ * Tells the client that the request was malformed or erroneous. If the Bad Request is a response
+ * to an HTTP HEAD request, the response will contain no body, but the Content-Length header will
+ * be set to the value it would have been if this Bad Request were in response to a GET.
+ *
+ * @return true if the connection will be kept open, or false if it will be disconnected
+ */
+ private boolean writeBadRequest(HttpRequest httpRequest) {
+ String body = "Bad Request to URI: " + httpRequest.uri();
+ FullHttpResponse response =
+ ProxyUtils.createFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, body);
+
+ if (ProxyUtils.isHEAD(httpRequest)) {
+ // don't allow any body content in response to a HEAD request
+ response.content().clear();
+ }
- if (ProxyUtils.isLastChunk(httpResponse)) {
- writeEmptyBuffer();
- }
+ return respondWithShortCircuitResponse(response);
+ }
+
+ /**
+ * Tells the client that the connection to the server, or possibly to some intermediary service
+ * (such as DNS), timed out. If the Gateway Timeout is a response to an HTTP HEAD request, the
+ * response will contain no body, but the Content-Length header will be set to the value it would
+ * have been if this 504 Gateway Timeout were in response to a GET.
+ *
+ * @param httpRequest the HttpRequest that is resulting in the Gateway Timeout response
+ * @return true if the connection will be kept open, or false if it will be disconnected
+ */
+ private void writeGatewayTimeout(HttpRequest httpRequest) {
+ String body = "Gateway Timeout";
+ FullHttpResponse response =
+ ProxyUtils.createFullHttpResponse(
+ HttpVersion.HTTP_1_1, HttpResponseStatus.GATEWAY_TIMEOUT, body);
+
+ if (ProxyUtils.isHEAD(httpRequest)) {
+ // don't allow any body content in response to a HEAD request
+ response.content().clear();
+ }
- if (!HttpUtil.isKeepAlive(httpResponse)) {
- disconnect();
- return false;
- }
+ respondWithShortCircuitResponse(response);
+ }
+
+ /**
+ * Responds to the client with the specified "short-circuit" response. The response will be sent
+ * through the {@link HttpFilters#proxyToClientResponse(HttpObject)} filter method before writing
+ * it to the client. The client will not be disconnected, unless the response includes a
+ * "Connection: close" header, or the filter returns a null HttpResponse (in which case no
+ * response will be written to the client and the connection will be disconnected immediately). If
+ * the response is not a Bad Gateway or Gateway Timeout response, the response's headers will be
+ * modified to reflect proxying, including adding a Via header, Date header, etc.
+ *
+ * @param httpResponse the response to return to the client
+ * @return true if the connection will be kept open, or false if it will be disconnected.
+ */
+ private boolean respondWithShortCircuitResponse(HttpResponse httpResponse) {
+ // we are sending a response to the client, so we are done handling this request
+ resetCurrentRequest();
+
+ // allow short-circuit messages to close the connection. normally the Connection header would be
+ // stripped when modifying
+ // the message for proxying, so save the keep-alive status before the modifications are made.
+ boolean isKeepAlive = HttpUtil.isKeepAlive(httpResponse);
+
+ HttpResponse filteredResponse =
+ (HttpResponse) currentFilters.proxyToClientResponse(httpResponse);
+ if (filteredResponse == null) {
+ disconnect();
+ return false;
+ }
- return true;
+ // if the response is not a Bad Gateway or Gateway Timeout, modify the headers "as if" the
+ // short-circuit response were proxied
+ int statusCode = filteredResponse.status().code();
+ if (statusCode != HttpResponseStatus.BAD_GATEWAY.code()
+ && statusCode != HttpResponseStatus.GATEWAY_TIMEOUT.code()) {
+
+ // Handle upstream proxy authentication challenges
+ if (handleUpstreamProxyAuthenticationRequired(filteredResponse)) {
+ // This is a 407 from upstream proxy that we can handle
+ // For now, we'll modify the response to indicate we're handling it
+ // In a more complete implementation, we would retry the request with auth
+ LOG.debug("Handling upstream proxy 407 response");
+ }
+
+ modifyResponseHeadersToReflectProxying(filteredResponse);
}
- /**
- * Identify the host and port for a request.
- */
- private String identifyHostAndPort(HttpRequest httpRequest) {
- String hostAndPort = ProxyUtils.parseHostAndPort(httpRequest);
- if (StringUtils.isBlank(hostAndPort)) {
- List hosts = httpRequest.headers().getAll(
- HttpHeaderNames.HOST);
- if (hosts != null && !hosts.isEmpty()) {
- hostAndPort = hosts.get(0);
- }
- }
+ // restore the keep alive status, if it was overwritten when modifying headers for proxying
+ HttpUtil.setKeepAlive(filteredResponse, isKeepAlive);
- return hostAndPort;
- }
-
- /**
- * Write an empty buffer at the end of a chunked transfer. We need to do
- * this to handle the way Netty creates HttpChunks from responses that
- * aren't in fact chunked from the remote server using Transfer-Encoding:
- * chunked. Netty turns these into pseudo-chunked responses in cases where
- * the response would otherwise fill up too much memory or where the length
- * of the response body is unknown. This is handy because it means we can
- * start streaming response bodies back to the client without reading the
- * entire response. The problem is that in these pseudo-cases the last chunk
- * is encoded to null, and this thwarts normal ChannelFutures from
- * propagating operationComplete events on writes to appropriate channel
- * listeners. We work around this by writing an empty buffer in those cases
- * and using the empty buffer's future instead to handle any operations we
- * need to when responses are fully written back to clients.
- */
- private void writeEmptyBuffer() {
- write(Unpooled.EMPTY_BUFFER);
+ write(filteredResponse);
+
+ if (ProxyUtils.isLastChunk(filteredResponse)) {
+ writeEmptyBuffer();
}
- public boolean isMitming() {
- return mitming;
+ if (!HttpUtil.isKeepAlive(filteredResponse)) {
+ disconnect();
+ return false;
}
- protected void setMitming(boolean isMitming) {
- this.mitming = isMitming;
+ return true;
+ }
+
+ /** Identify the host and port for a request. */
+ @NonNull
+ @CheckReturnValue
+ private String identifyHostAndPort(@NonNull HttpRequest httpRequest) {
+ String hostAndPort = ProxyUtils.parseHostAndPort(httpRequest);
+ if (StringUtils.isBlank(hostAndPort)) {
+ List hosts = httpRequest.headers().getAll(HttpHeaderNames.HOST);
+ if (hosts != null && !hosts.isEmpty()) {
+ hostAndPort = hosts.get(0);
+ }
}
- /* *************************************************************************
- * Activity Tracking/Statistics
- *
- * We track statistics on bytes, requests and responses by adding handlers
- * at the appropriate parts of the pipeline (see initChannelPipeline()).
- **************************************************************************/
- private final BytesReadMonitor bytesReadMonitor = new BytesReadMonitor() {
+ return hostAndPort;
+ }
+
+ /**
+ * Write an empty buffer at the end of a chunked transfer. We need to do this to handle the way
+ * Netty creates HttpChunks from responses that aren't in fact chunked from the remote server
+ * using Transfer-Encoding: chunked. Netty turns these into pseudo-chunked responses in cases
+ * where the response would otherwise fill up too much memory or where the length of the response
+ * body is unknown. This is handy because it means we can start streaming response bodies back to
+ * the client without reading the entire response. The problem is that in these pseudo-cases the
+ * last chunk is encoded to null, and this thwarts normal ChannelFutures from propagating
+ * operationComplete events on writes to appropriate channel listeners. We work around this by
+ * writing an empty buffer in those cases and using the empty buffer's future instead to handle
+ * any operations we need to when responses are fully written back to clients.
+ */
+ private void writeEmptyBuffer() {
+ write(Unpooled.EMPTY_BUFFER);
+ }
+
+ public boolean isMitming() {
+ return mitming;
+ }
+
+ protected void setMitming(boolean isMitming) {
+ mitming = isMitming;
+ }
+
+ /* *************************************************************************
+ * Activity Tracking/Statistics
+ *
+ * We track statistics on bytes, requests and responses by adding handlers
+ * at the appropriate parts of the pipeline (see initChannelPipeline()).
+ **************************************************************************/
+ private final BytesReadMonitor bytesReadMonitor =
+ new BytesReadMonitor() {
@Override
protected void bytesRead(int numberOfBytes) {
- FlowContext flowContext = flowContext();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.bytesReceivedFromClient(flowContext, numberOfBytes);
- }
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.bytesReceivedFromClient(flowContext, numberOfBytes);
+ }
}
- };
+ };
- private RequestReadMonitor requestReadMonitor = new RequestReadMonitor() {
+ private final RequestReadMonitor requestReadMonitor =
+ new RequestReadMonitor() {
@Override
protected void requestRead(HttpRequest httpRequest) {
- FlowContext flowContext = flowContext();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.requestReceivedFromClient(flowContext, httpRequest);
- }
+ recordClientConnected();
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.requestReceivedFromClient(flowContext, httpRequest);
+ }
}
- };
+ };
- private BytesWrittenMonitor bytesWrittenMonitor = new BytesWrittenMonitor() {
+ private final BytesWrittenMonitor bytesWrittenMonitor =
+ new BytesWrittenMonitor() {
@Override
protected void bytesWritten(int numberOfBytes) {
- FlowContext flowContext = flowContext();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.bytesSentToClient(flowContext, numberOfBytes);
- }
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.bytesSentToClient(flowContext, numberOfBytes);
+ }
}
- };
+ };
- private ResponseWrittenMonitor responseWrittenMonitor = new ResponseWrittenMonitor() {
+ private final ResponseWrittenMonitor responseWrittenMonitor =
+ new ResponseWrittenMonitor() {
@Override
protected void responseWritten(HttpResponse httpResponse) {
- FlowContext flowContext = flowContext();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.responseSentToClient(flowContext,
- httpResponse);
- }
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.responseSentToClient(flowContext, httpResponse);
+ }
}
- };
-
- private void recordClientConnected() {
- try {
- InetSocketAddress clientAddress = getClientAddress();
- clientDetails.setClientAddress(clientAddress);
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.clientConnected(clientAddress);
- }
- } catch (Exception e) {
- LOG.error("Unable to recordClientConnected", e);
- }
- }
+ };
- private void recordClientSSLHandshakeSucceeded() {
- try {
- InetSocketAddress clientAddress = getClientAddress();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.clientSSLHandshakeSucceeded(
- clientAddress, clientSslSession);
- }
- } catch (Exception e) {
- LOG.error("Unable to recorClientSSLHandshakeSucceeded", e);
- }
+ private void recordClientConnected() {
+ if (!clientConnectedRecorded.compareAndSet(
+ CLIENT_CONNECTED_NOT_YET_RECORDED, CLIENT_CONNECTED_RECORDED)) {
+ return;
}
-
- private void recordClientDisconnected() {
- try {
- InetSocketAddress clientAddress = getClientAddress();
- for (ActivityTracker tracker : proxyServer
- .getActivityTrackers()) {
- tracker.clientDisconnected(
- clientAddress, clientSslSession);
- }
- } catch (Exception e) {
- LOG.error("Unable to recordClientDisconnected", e);
- }
+ try {
+ FlowContext flowContext = flowContext();
+ // Resolve via FlowContext so ClientDetails (used for chained-proxy routing) sees the real
+ // client IP, not the TCP peer.
+ clientDetails.setClientAddress(flowContext.getClientAddress());
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.clientConnected(flowContext);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordClientConnected", e);
}
-
- public InetSocketAddress getClientAddress() {
- if (channel == null) {
- return null;
- }
- return (InetSocketAddress) channel.remoteAddress();
+ }
+
+ private void recordClientSSLHandshakeStarted() {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.clientSSLHandshakeStarted(flowContext);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordClientSSLHandshakeStarted", e);
}
-
- private FlowContext flowContext() {
- if (currentServerConnection != null) {
- return new FullFlowContext(this, currentServerConnection);
- } else {
- return new FlowContext(this);
- }
+ }
+
+ private void recordClientSSLHandshakeSucceeded() {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.clientSSLHandshakeSucceeded(flowContext, clientSslSession);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordClientSSLHandshakeSucceeded", e);
}
-
- public HAProxyMessage getHaProxyMessage() {
- return haProxyMessage;
+ }
+
+ private void recordClientDisconnected() {
+ // Ensure clientConnected was reported before clientDisconnected, even for silent connections
+ // (guarded).
+ recordClientConnected();
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ try {
+ tracker.clientDisconnected(flowContext, clientSslSession);
+ } catch (Exception e) {
+ LOG.error("Unable to recordClientDisconnected", e);
+ }
}
-
- public ClientDetails getClientDetails() {
- return clientDetails;
+ }
+
+ private void recordConnectionSaturated() {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.connectionSaturated(flowContext);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordConnectionSaturated", e);
}
-
+ }
+
+ private void recordConnectionWritable() {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.connectionWritable(flowContext);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordConnectionWritable", e);
+ }
+ }
+
+ private void recordConnectionTimedOut() {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.connectionTimedOut(flowContext);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordConnectionTimedOut", e);
+ }
+ }
+
+ private void recordConnectionExceptionCaught(Throwable cause) {
+ try {
+ FlowContext flowContext = flowContext();
+ for (ActivityTracker tracker : proxyServer.getActivityTrackers()) {
+ tracker.connectionExceptionCaught(flowContext, cause);
+ }
+ } catch (Exception e) {
+ LOG.error("Unable to recordConnectionExceptionCaught", e);
+ }
+ }
+
+ @Nullable
+ public InetSocketAddress getClientAddress() {
+ return ofNullable(channel)
+ .map(c -> c.remoteAddress())
+ .filter(InetSocketAddress.class::isInstance)
+ .map(InetSocketAddress.class::cast)
+ .orElse(null);
+ }
+
+ FlowContext flowContext() {
+ FlowContext cached = clientFlowContext;
+ if (currentServerConnection != null && !(cached instanceof FullFlowContext)) {
+ cached = flowContextForServerConnection(currentServerConnection);
+ }
+ return cached;
+ }
+
+ FullFlowContext flowContextForServerConnection(ProxyToServerConnection serverConnection) {
+ return serverFlowContexts.computeIfAbsent(
+ serverConnection, sc -> new FullFlowContext(this, sc));
+ }
+
+ void clearFlowContextForServerConnection(ProxyToServerConnection serverConnection) {
+ serverFlowContexts.remove(serverConnection);
+ }
+
+ public @Nullable HAProxyMessage getHaProxyMessage() {
+ return haProxyMessage;
+ }
+
+ public ClientDetails getClientDetails() {
+ return clientDetails;
+ }
+
+ /**
+ * Gets the authenticated status of this connection.
+ *
+ * @return the authenticated status
+ */
+ public AtomicBoolean getAuthenticated() {
+ return authenticated;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ConcurrentMapServerConnectionPool.java b/src/main/java/org/littleshoot/proxy/impl/ConcurrentMapServerConnectionPool.java
new file mode 100644
index 00000000..b6ec549f
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/ConcurrentMapServerConnectionPool.java
@@ -0,0 +1,474 @@
+package org.littleshoot.proxy.impl;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.HttpRequest;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.ChainedProxy;
+import org.littleshoot.proxy.ChainedProxyManager;
+import org.littleshoot.proxy.HttpFilters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ConcurrentMapServerConnectionPool implements ServerConnectionPool {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(ConcurrentMapServerConnectionPool.class);
+
+ static final int DEFAULT_MAX_CONNECTIONS_PER_HOST = 10;
+ static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 200;
+
+ private final ConcurrentMap>
+ connectionsByHostAndPort = new ConcurrentHashMap<>();
+ private final ConcurrentMap> availableConnectionsByHostAndPort =
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap connectionCountByHostAndPort =
+ new ConcurrentHashMap<>();
+ private final ConcurrentMap> pendingRequestsByChannel =
+ new ConcurrentHashMap<>();
+ private final AtomicInteger totalConnectionsCreated = new AtomicInteger(0);
+
+ @Nullable private volatile Duration idleTimeout;
+ private volatile boolean connectionValidationEnabled = false;
+ private final ScheduledExecutorService evictionScheduler =
+ Executors.newSingleThreadScheduledExecutor(
+ r -> {
+ Thread t = new Thread(r, "connection-pool-eviction");
+ t.setDaemon(true);
+ return t;
+ });
+ @Nullable private volatile ScheduledFuture> evictionTask;
+
+ private final int maxConnectionsPerHost;
+ private final int maxConnections;
+ private final DefaultHttpProxyServer proxyServer;
+ private final io.netty.handler.traffic.GlobalTrafficShapingHandler globalTrafficShapingHandler;
+
+ private final ConcurrentMap connectionKeys =
+ new ConcurrentHashMap<>();
+
+ private final java.util.concurrent.atomic.AtomicLong borrowCount =
+ new java.util.concurrent.atomic.AtomicLong(0);
+ private final java.util.concurrent.atomic.AtomicLong returnCount =
+ new java.util.concurrent.atomic.AtomicLong(0);
+ private final java.util.concurrent.atomic.AtomicLong evictionCount =
+ new java.util.concurrent.atomic.AtomicLong(0);
+ private final java.util.concurrent.atomic.AtomicLong validationFailureCount =
+ new java.util.concurrent.atomic.AtomicLong(0);
+
+ public ConcurrentMapServerConnectionPool(
+ DefaultHttpProxyServer proxyServer,
+ io.netty.handler.traffic.GlobalTrafficShapingHandler globalTrafficShapingHandler) {
+ this(
+ proxyServer,
+ globalTrafficShapingHandler,
+ DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ DEFAULT_MAX_TOTAL_CONNECTIONS);
+ }
+
+ public ConcurrentMapServerConnectionPool(
+ DefaultHttpProxyServer proxyServer,
+ io.netty.handler.traffic.GlobalTrafficShapingHandler globalTrafficShapingHandler,
+ int maxConnectionsPerHost) {
+ this(
+ proxyServer,
+ globalTrafficShapingHandler,
+ maxConnectionsPerHost,
+ DEFAULT_MAX_TOTAL_CONNECTIONS);
+ }
+
+ public ConcurrentMapServerConnectionPool(
+ DefaultHttpProxyServer proxyServer,
+ io.netty.handler.traffic.GlobalTrafficShapingHandler globalTrafficShapingHandler,
+ int maxConnectionsPerHost,
+ int maxConnections) {
+ this.proxyServer = proxyServer;
+ this.globalTrafficShapingHandler = globalTrafficShapingHandler;
+ this.maxConnectionsPerHost =
+ maxConnectionsPerHost > 0 ? maxConnectionsPerHost : DEFAULT_MAX_CONNECTIONS_PER_HOST;
+ this.maxConnections = maxConnections > 0 ? maxConnections : DEFAULT_MAX_TOTAL_CONNECTIONS;
+ }
+
+ @Override
+ @Nullable
+ public ProxyToServerConnection getOrCreateConnection(
+ String serverHostAndPort,
+ @Nullable InetSocketAddress chainedProxyAddress,
+ ClientToProxyConnection clientConnection,
+ HttpFilters initialFilters,
+ HttpRequest initialHttpRequest) {
+ ChainedProxy chainedProxy = resolveChainedProxy(initialHttpRequest, clientConnection);
+ String poolKey = computePoolKey(serverHostAndPort, chainedProxyAddress);
+ ProxyToServerConnection available = borrowAvailableConnection(poolKey);
+ if (available != null) {
+ borrowCount.incrementAndGet();
+ return available;
+ }
+
+ int currentHostCount =
+ connectionCountByHostAndPort.computeIfAbsent(poolKey, k -> new AtomicInteger(0)).get();
+
+ if (currentHostCount >= maxConnectionsPerHost) {
+ LOG.warn(
+ "Per-host connection limit reached for {}: {} connections, max is {}",
+ poolKey,
+ currentHostCount,
+ maxConnectionsPerHost);
+ return null;
+ }
+
+ if (totalConnectionsCreated.get() >= maxConnections) {
+ LOG.warn(
+ "Pool exhausted: {} connections created, max is {}",
+ totalConnectionsCreated.get(),
+ maxConnections);
+ return null;
+ }
+
+ synchronized (this) {
+ ProxyToServerConnection existingAvailable = borrowAvailableConnection(poolKey);
+ if (existingAvailable != null) {
+ return existingAvailable;
+ }
+
+ int hostCount =
+ connectionCountByHostAndPort.computeIfAbsent(poolKey, k -> new AtomicInteger(0)).get();
+
+ if (hostCount >= maxConnectionsPerHost) {
+ LOG.warn(
+ "Per-host limit reached after sync for {}: {} connections, max is {}",
+ poolKey,
+ hostCount,
+ maxConnectionsPerHost);
+ return null;
+ }
+
+ if (totalConnectionsCreated.get() >= maxConnections) {
+ LOG.warn(
+ "Pool exhausted after sync: {} connections created, max is {}",
+ totalConnectionsCreated.get(),
+ maxConnections);
+ return null;
+ }
+
+ try {
+ ProxyToServerConnection newConnection =
+ ProxyToServerConnection.createForPool(
+ proxyServer,
+ this,
+ clientConnection,
+ serverHostAndPort,
+ chainedProxy,
+ initialFilters,
+ initialHttpRequest,
+ globalTrafficShapingHandler);
+
+ if (newConnection != null) {
+ connectionsByHostAndPort
+ .computeIfAbsent(poolKey, k -> new ConcurrentHashMap<>())
+ .put(newConnection, Boolean.TRUE);
+ connectionCountByHostAndPort
+ .computeIfAbsent(poolKey, k -> new AtomicInteger(0))
+ .incrementAndGet();
+ connectionKeys.put(newConnection, poolKey);
+ totalConnectionsCreated.incrementAndGet();
+ borrowCount.incrementAndGet();
+ return newConnection;
+ }
+ } catch (java.net.UnknownHostException e) {
+ LOG.warn("Failed to resolve host for {}", serverHostAndPort, e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void releaseConnection(ProxyToServerConnection connection) {
+ if (connection == null) {
+ return;
+ }
+ String poolKey = connectionKeys.get(connection);
+ if (poolKey == null) {
+ return;
+ }
+ ConcurrentMap connections =
+ connectionsByHostAndPort.get(poolKey);
+ if (connections == null || !connections.containsKey(connection)) {
+ return;
+ }
+ if (!connection.isConnected()) {
+ removeConnection(connection);
+ return;
+ }
+ returnCount.incrementAndGet();
+ availableConnectionsByHostAndPort
+ .computeIfAbsent(poolKey, k -> new ConcurrentLinkedQueue<>())
+ .add(new PooledConnection(connection, System.currentTimeMillis()));
+ }
+
+ @Override
+ public void registerPendingRequest(
+ Channel channel,
+ ClientToProxyConnection clientConnection,
+ HttpRequest request,
+ HttpFilters filters) {
+ pendingRequestsByChannel
+ .computeIfAbsent(channel, k -> new ConcurrentLinkedQueue<>())
+ .add(new PendingRequest(clientConnection, request, filters));
+ }
+
+ @Override
+ @Nullable
+ public PendingRequest removePendingRequest(Channel channel) {
+ final PendingRequest[] result = new PendingRequest[1];
+ pendingRequestsByChannel.computeIfPresent(
+ channel,
+ (k, queue) -> {
+ result[0] = queue.poll();
+ return queue.isEmpty() ? null : queue;
+ });
+ return result[0];
+ }
+
+ @Override
+ @Nullable
+ public PendingRequest peekPendingRequest(Channel channel) {
+ Queue queue = pendingRequestsByChannel.get(channel);
+ if (queue == null || queue.isEmpty()) {
+ return null;
+ }
+ return queue.peek();
+ }
+
+ @Override
+ public void drainPendingRequests(Channel channel) {
+ pendingRequestsByChannel.remove(channel);
+ }
+
+ @Override
+ public void removeConnection(ProxyToServerConnection connection) {
+ if (connection == null) {
+ return;
+ }
+ String poolKey = connectionKeys.remove(connection);
+ if (poolKey == null) {
+ return;
+ }
+ ConcurrentMap connections =
+ connectionsByHostAndPort.get(poolKey);
+ Queue available = availableConnectionsByHostAndPort.get(poolKey);
+ if (connections != null) {
+ if (connections.remove(connection) != null) {
+ AtomicInteger count = connectionCountByHostAndPort.get(poolKey);
+ if (count != null) {
+ int newCount = count.decrementAndGet();
+ if (newCount <= 0) {
+ connectionCountByHostAndPort.remove(poolKey, count);
+ connectionsByHostAndPort.remove(poolKey, connections);
+ if (available != null) {
+ availableConnectionsByHostAndPort.remove(poolKey, available);
+ } else {
+ availableConnectionsByHostAndPort.remove(poolKey);
+ }
+ }
+ }
+ totalConnectionsCreated.decrementAndGet();
+ }
+ }
+ if (available != null) {
+ available.removeIf(p -> p.connection == connection);
+ }
+ }
+
+ @Override
+ public void closeAll() {
+ stopEvictionTask();
+ evictionScheduler.shutdown();
+ for (ConcurrentMap connections :
+ connectionsByHostAndPort.values()) {
+ for (ProxyToServerConnection connection : connections.keySet()) {
+ connection.close();
+ }
+ }
+ connectionsByHostAndPort.clear();
+ availableConnectionsByHostAndPort.clear();
+ connectionCountByHostAndPort.clear();
+ connectionKeys.clear();
+ pendingRequestsByChannel.clear();
+ totalConnectionsCreated.set(0);
+ }
+
+ @Override
+ public int getMaxConnectionsPerHost() {
+ return maxConnectionsPerHost;
+ }
+
+ @Override
+ public int getMaxConnections() {
+ return maxConnections;
+ }
+
+ @Override
+ public void setIdleTimeout(@Nullable Duration idleTimeout) {
+ this.idleTimeout = idleTimeout;
+ if (idleTimeout != null && idleTimeout.toMillis() > 0) {
+ startEvictionTask();
+ } else {
+ stopEvictionTask();
+ }
+ }
+
+ @Override
+ @Nullable
+ public Duration getIdleTimeout() {
+ return idleTimeout;
+ }
+
+ @Override
+ public void setConnectionValidationEnabled(boolean validationEnabled) {
+ this.connectionValidationEnabled = validationEnabled;
+ LOG.info("Connection validation enabled: {}", validationEnabled);
+ }
+
+ @Override
+ public boolean isConnectionValidationEnabled() {
+ return connectionValidationEnabled;
+ }
+
+ @Override
+ public PoolMetrics getMetrics() {
+ int total = totalConnectionsCreated.get();
+ int idle = availableConnectionsByHostAndPort.values().stream().mapToInt(q -> q.size()).sum();
+ return new PoolMetrics(
+ total,
+ total - idle,
+ idle,
+ borrowCount.get(),
+ returnCount.get(),
+ evictionCount.get(),
+ validationFailureCount.get());
+ }
+
+ private void startEvictionTask() {
+ if (evictionTask != null && !evictionTask.isCancelled()) {
+ return;
+ }
+ long intervalMillis = idleTimeout != null ? idleTimeout.toMillis() / 2 : 30_000;
+ evictionTask =
+ evictionScheduler.scheduleAtFixedRate(
+ this::evictIdleConnections, intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);
+ LOG.info("Started idle connection eviction task with interval {}ms", intervalMillis);
+ }
+
+ private void stopEvictionTask() {
+ if (evictionTask != null) {
+ evictionTask.cancel(false);
+ evictionTask = null;
+ LOG.info("Stopped idle connection eviction task");
+ }
+ }
+
+ private void evictIdleConnections() {
+ if (idleTimeout == null || idleTimeout.toMillis() <= 0) {
+ return;
+ }
+ long now = System.currentTimeMillis();
+ long idleThreshold = now - idleTimeout.toMillis();
+ int evicted = 0;
+
+ for (String serverHostAndPort : availableConnectionsByHostAndPort.keySet()) {
+ Queue queue = availableConnectionsByHostAndPort.get(serverHostAndPort);
+ if (queue == null) {
+ continue;
+ }
+ Queue toRemove = new ConcurrentLinkedQueue<>();
+ for (PooledConnection pooled : queue) {
+ if (pooled.releasedAt < idleThreshold) {
+ toRemove.add(pooled);
+ evicted++;
+ }
+ }
+ for (PooledConnection pooled : toRemove) {
+ queue.remove(pooled);
+ removeConnection(pooled.connection);
+ pooled.connection.close();
+ }
+ }
+ if (evicted > 0) {
+ evictionCount.addAndGet(evicted);
+ LOG.debug("Evicted {} idle connections", evicted);
+ }
+ }
+
+ @Nullable
+ private ProxyToServerConnection borrowAvailableConnection(String poolKey) {
+ Queue queue = availableConnectionsByHostAndPort.get(poolKey);
+ if (queue == null || queue.isEmpty()) {
+ return null;
+ }
+ int checked = 0;
+ int size = queue.size();
+ while (checked < size) {
+ PooledConnection pooled = queue.poll();
+ if (pooled == null) {
+ return null;
+ }
+ checked++;
+ if (!pooled.connection.isConnected()) {
+ removeConnection(pooled.connection);
+ continue;
+ }
+ if (connectionValidationEnabled && !isConnectionValid(pooled.connection)) {
+ validationFailureCount.incrementAndGet();
+ removeConnection(pooled.connection);
+ pooled.connection.close();
+ LOG.debug("Connection validation failed, removing connection to {}", poolKey);
+ continue;
+ }
+ if (pooled.connection.isAvailableForNewRequest()) {
+ return pooled.connection;
+ }
+ queue.add(pooled);
+ }
+ return null;
+ }
+
+ private boolean isConnectionValid(ProxyToServerConnection connection) {
+ return connection.isConnected() && connection.isAvailableForNewRequest();
+ }
+
+ @Nullable
+ private ChainedProxy resolveChainedProxy(
+ HttpRequest httpRequest, ClientToProxyConnection clientConnection) {
+ ChainedProxyManager chainedProxyManager = proxyServer.getChainProxyManager();
+ if (chainedProxyManager == null) {
+ return null;
+ }
+ Queue chainedProxies = new ConcurrentLinkedQueue<>();
+ chainedProxyManager.lookupChainedProxies(
+ httpRequest, chainedProxies, clientConnection.getClientDetails());
+ if (chainedProxies.isEmpty()) {
+ return null;
+ }
+ return chainedProxies.poll();
+ }
+
+ private static class PooledConnection {
+ final ProxyToServerConnection connection;
+ final long releasedAt;
+
+ PooledConnection(ProxyToServerConnection connection, long releasedAt) {
+ this.connection = connection;
+ this.releasedAt = releasedAt;
+ }
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
index b13295d5..efbe8807 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
@@ -1,231 +1,182 @@
package org.littleshoot.proxy.impl;
-import io.netty.handler.codec.haproxy.HAProxyCommand;
-import io.netty.handler.codec.haproxy.HAProxyMessage;
-import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
-import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.util.concurrent.Future;
-import io.netty.util.concurrent.GenericFutureListener;
-import org.littleshoot.proxy.extras.ProxyProtocolMessage;
-
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
-import java.net.InetSocketAddress;
/**
- * Coordinates the various steps involved in establishing a connection, such as
- * establishing a socket connection, SSL handshaking, HTTP CONNECT request
- * processing, and so on.
+ * Coordinates the various steps involved in establishing a connection, such as establishing a
+ * socket connection, SSL handshaking, HTTP CONNECT request processing, and so on.
*/
class ConnectionFlow {
- private Deque steps = new ConcurrentLinkedDeque();
-
- private final ClientToProxyConnection clientConnection;
- private final ProxyToServerConnection serverConnection;
- private volatile ConnectionFlowStep currentStep;
- private volatile boolean suppressInitialRequest = false;
- private final Object connectLock;
-
- /**
- * Construct a new {@link ConnectionFlow} for the given client and server
- * connections.
- *
- * @param clientConnection
- * @param serverConnection
- * @param connectLock
- * an object that's shared by {@link ConnectionFlow} and
- * {@link ProxyToServerConnection} and that is used for
- * synchronizing the reader and writer threads that are both
- * involved during the establishing of a connection.
- */
- ConnectionFlow(
- ClientToProxyConnection clientConnection,
- ProxyToServerConnection serverConnection,
- Object connectLock) {
- super();
- this.clientConnection = clientConnection;
- this.serverConnection = serverConnection;
- this.connectLock = connectLock;
+ private final Deque> steps = new ConcurrentLinkedDeque<>();
+
+ private final ClientToProxyConnection clientConnection;
+ private final ProxyToServerConnection serverConnection;
+ private volatile ConnectionFlowStep> currentStep;
+ private volatile boolean suppressInitialRequest;
+ private final Object connectLock;
+
+ /**
+ * Construct a new {@link ConnectionFlow} for the given client and server connections.
+ *
+ * @param clientConnection
+ * @param serverConnection
+ * @param connectLock an object that's shared by {@link ConnectionFlow} and {@link
+ * ProxyToServerConnection} and that is used for synchronizing the reader and writer threads
+ * that are both involved during the establishing of a connection.
+ */
+ ConnectionFlow(
+ ClientToProxyConnection clientConnection,
+ ProxyToServerConnection serverConnection,
+ Object connectLock) {
+ super();
+ this.clientConnection = clientConnection;
+ this.serverConnection = serverConnection;
+ this.connectLock = connectLock;
+ }
+
+ /** Add a {@link ConnectionFlowStep} to the beginning of this flow. */
+ ConnectionFlow first(ConnectionFlowStep> step) {
+ steps.addFirst(step);
+ return this;
+ }
+
+ /** Add a {@link ConnectionFlowStep} to the end of this flow. */
+ ConnectionFlow then(ConnectionFlowStep> step) {
+ steps.addLast(step);
+ return this;
+ }
+
+ /**
+ * While we're in the process of connecting, any messages read by the {@link
+ * ProxyToServerConnection} are passed to this method, which passes it on to {@link
+ * ConnectionFlowStep#read(ConnectionFlow, Object)} for the current {@link ConnectionFlowStep}.
+ */
+ void read(Object msg) {
+ if (currentStep != null) {
+ currentStep.read(this, msg);
}
-
- /**
- * Add a {@link ConnectionFlowStep} to the beginning of this flow.
- */
- ConnectionFlow first(ConnectionFlowStep step) {
- steps.addFirst(step);
- return this;
+ }
+
+ /**
+ * Starts the connection flow, notifying the {@link ClientToProxyConnection} that we've started.
+ */
+ void start() {
+ clientConnection.serverConnectionFlowStarted(serverConnection);
+ advance();
+ }
+
+ /**
+ * Advances the flow. {@link #advance()} will be called until we're either out of steps, or a step
+ * has failed.
+ */
+ void advance() {
+ currentStep = steps.poll();
+ if (currentStep == null) {
+ succeed();
+ } else {
+ processCurrentStep();
}
-
- /**
- * Add a {@link ConnectionFlowStep} to the end of this flow.
- */
- ConnectionFlow then(ConnectionFlowStep step) {
- steps.addLast(step);
- return this;
+ }
+
+ /**
+ * Process the current {@link ConnectionFlowStep}. With each step, we:
+ *
+ *
+ *
Change the state of the associated {@link ProxyConnection} to the value of {@link
+ * ConnectionFlowStep#getState()}
+ *
Call {@link ConnectionFlowStep#execute()}
+ *
On completion of the {@link Future} returned by {@link ConnectionFlowStep#execute()},
+ * check the success.
+ *
If successful, we call back into {@link ConnectionFlowStep#onSuccess(ConnectionFlow)}.
+ *
If unsuccessful, we call {@link #fail()}, stopping the connection flow
+ *
+ */
+ private void processCurrentStep() {
+ final ProxyConnection> connection = currentStep.getConnection();
+ final ProxyConnectionLogger LOG = connection.getLOG();
+
+ LOG.debug("Processing connection flow step: {}", currentStep);
+ connection.become(currentStep.getState());
+ suppressInitialRequest = suppressInitialRequest || currentStep.shouldSuppressInitialRequest();
+
+ if (currentStep.shouldExecuteOnEventLoop()) {
+ connection.ctx.executor().submit(() -> doProcessCurrentStep(LOG));
+ } else {
+ doProcessCurrentStep(LOG);
}
-
- /**
- * While we're in the process of connecting, any messages read by the
- * {@link ProxyToServerConnection} are passed to this method, which passes
- * it on to {@link ConnectionFlowStep#read(ConnectionFlow, Object)} for the
- * current {@link ConnectionFlowStep}.
- */
- void read(Object msg) {
- if (this.currentStep != null) {
- this.currentStep.read(this, msg);
- }
+ }
+
+ /**
+ * Does the work of processing the current step, checking the result and handling success/failure.
+ */
+ private void doProcessCurrentStep(final ProxyConnectionLogger LOG) {
+ currentStep
+ .execute()
+ .addListener(
+ future -> {
+ synchronized (connectLock) {
+ if (future.isSuccess()) {
+ LOG.debug("ConnectionFlowStep succeeded");
+ currentStep.onSuccess(ConnectionFlow.this);
+ } else {
+ LOG.debug("ConnectionFlowStep failed", future.cause());
+ fail(future.cause());
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when the flow is complete and successful. Notifies the {@link ProxyToServerConnection}
+ * that we succeeded.
+ */
+ void succeed() {
+ synchronized (connectLock) {
+ serverConnection.getLOG().debug("Connection flow completed successfully: {}", currentStep);
+ serverConnection.connectionSucceeded(!suppressInitialRequest);
+ notifyThreadsWaitingForConnection();
}
-
- /**
- * Starts the connection flow, notifying the {@link ClientToProxyConnection}
- * that we've started.
- */
- void start() {
- clientConnection.serverConnectionFlowStarted(serverConnection);
- advance();
- }
-
- /**
- *
- * Advances the flow. {@link #advance()} will be called until we're either
- * out of steps, or a step has failed.
- *
- * Process the current {@link ConnectionFlowStep}. With each step, we:
- *
- *
- *
- *
Change the state of the associated {@link ProxyConnection} to the
- * value of {@link ConnectionFlowStep#getState()}
- *
Call {@link ConnectionFlowStep#execute()}
- *
On completion of the {@link Future} returned by
- * {@link ConnectionFlowStep#execute()}, check the success.
- *
If successful, we call back into
- * {@link ConnectionFlowStep#onSuccess(ConnectionFlow)}.
- *
If unsuccessful, we call {@link #fail()}, stopping the connection
- * flow
- *
- */
- private void processCurrentStep() {
- final ProxyConnection connection = currentStep.getConnection();
- final ProxyConnectionLogger LOG = connection.getLOG();
-
- LOG.debug("Processing connection flow step: {}", currentStep);
- connection.become(currentStep.getState());
- suppressInitialRequest = suppressInitialRequest
- || currentStep.shouldSuppressInitialRequest();
-
- if (currentStep.shouldExecuteOnEventLoop()) {
- connection.ctx.executor().submit(() -> doProcessCurrentStep(LOG));
- } else {
- doProcessCurrentStep(LOG);
- }
- }
-
- /**
- * Does the work of processing the current step, checking the result and
- * handling success/failure.
- */
- @SuppressWarnings("unchecked")
- private void doProcessCurrentStep(final ProxyConnectionLogger LOG) {
- currentStep.execute().addListener(
- future -> {
- synchronized (connectLock) {
- if (future.isSuccess()) {
- LOG.debug("ConnectionFlowStep succeeded");
- currentStep.onSuccess(ConnectionFlow.this);
- } else {
- LOG.debug("ConnectionFlowStep failed",
- future.cause());
- fail(future.cause());
- }
- }
- });
- }
-
- /**
- * Called when the flow is complete and successful. Notifies the
- * {@link ProxyToServerConnection} that we succeeded.
- */
- void succeed() {
- synchronized (connectLock) {
- serverConnection.getLOG().debug(
- "Connection flow completed successfully: {}", currentStep);
- serverConnection.connectionSucceeded(!suppressInitialRequest);
- relayProxyInformation();
- notifyThreadsWaitingForConnection();
- }
- }
-
- private void relayProxyInformation() {
- if (clientConnection.isSendProxyProtocol()) {
- ProxyProtocolMessage proxyProtocolMessage = getHAProxyMessage(clientConnection.getClientAddress(), serverConnection.getRemoteAddress());
- if ( proxyProtocolMessage != null ){
- serverConnection.writeToChannel(proxyProtocolMessage);
- }
- }
- }
-
- private ProxyProtocolMessage getHAProxyMessage(InetSocketAddress clientAddress, InetSocketAddress remoteAddress) {
- HAProxyMessage haProxyMessage = clientConnection.getHaProxyMessage();
- if ( haProxyMessage != null ){
- return new ProxyProtocolMessage(haProxyMessage);
- }
- return new ProxyProtocolMessage(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, HAProxyProxiedProtocol.TCP4, clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(), clientAddress.getPort(), remoteAddress.getPort());
- }
-
- /**
- * Called when the flow fails at some {@link ConnectionFlowStep}.
- * Disconnects the {@link ProxyToServerConnection} and informs the
- * {@link ClientToProxyConnection} that our connection failed.
- */
- @SuppressWarnings("unchecked")
- void fail(final Throwable cause) {
- final ConnectionState lastStateBeforeFailure = serverConnection
- .getCurrentState();
- serverConnection.disconnect().addListener(
- (GenericFutureListener) future -> {
- synchronized (connectLock) {
- if (!clientConnection.serverConnectionFailed(
- serverConnection,
- lastStateBeforeFailure,
- cause)) {
- // the connection to the server failed and we are not retrying, so transition to the
- // DISCONNECTED state
- serverConnection.become(ConnectionState.DISCONNECTED);
-
- // We are not retrying our connection, let anyone waiting for a connection know that we're done
- notifyThreadsWaitingForConnection();
- }
- }
- });
- }
-
- /**
- * Like {@link #fail(Throwable)} but with no cause.
- */
- void fail() {
- fail(null);
- }
-
- /**
- * Once we've finished recording our connection and written our initial
- * request, we can notify anyone who is waiting on the connection that it's
- * okay to proceed.
- */
- private void notifyThreadsWaitingForConnection() {
- connectLock.notifyAll();
- }
-
+ }
+
+ /**
+ * Called when the flow fails at some {@link ConnectionFlowStep}. Disconnects the {@link
+ * ProxyToServerConnection} and informs the {@link ClientToProxyConnection} that our connection
+ * failed.
+ */
+ void fail(final Throwable cause) {
+ final ConnectionState lastStateBeforeFailure = serverConnection.getCurrentState();
+ serverConnection
+ .disconnect()
+ .addListener(
+ future -> {
+ synchronized (connectLock) {
+ if (!clientConnection.serverConnectionFailed(
+ serverConnection, lastStateBeforeFailure, cause)) {
+ // the connection to the server failed, and we are not retrying, so transition to
+ // the
+ // DISCONNECTED state
+ serverConnection.become(ConnectionState.DISCONNECTED);
+
+ // We are not retrying our connection, let anyone waiting for a connection know
+ // that we're done
+ notifyThreadsWaitingForConnection();
+ }
+ }
+ });
+ }
+
+ /** Like {@link #fail(Throwable)} but with no cause. */
+ void fail() {
+ fail(null);
+ }
+
+ /**
+ * Once we've finished recording our connection and written our initial request, we can notify
+ * anyone who is waiting on the connection that it's okay to proceed.
+ */
+ private void notifyThreadsWaitingForConnection() {
+ connectLock.notifyAll();
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java
index a112727f..2226be7b 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlowStep.java
@@ -1,107 +1,81 @@
package org.littleshoot.proxy.impl;
+import io.netty.handler.codec.http.HttpObject;
import io.netty.util.concurrent.Future;
-/**
- * Represents a phase in a {@link ConnectionFlow}.
- */
-abstract class ConnectionFlowStep {
- private final ProxyConnectionLogger LOG;
- private final ProxyConnection connection;
- private final ConnectionState state;
+/** Represents a phase in a {@link ConnectionFlow}. */
+abstract class ConnectionFlowStep {
+ private final ProxyConnectionLogger LOG;
+ private final ProxyConnection connection;
+ private final ConnectionState state;
- /**
- * Construct a new step in a connection flow.
- *
- * @param connection
- * the connection that we're working on
- * @param state
- * the state that the connection will show while we're processing
- * this step
- */
- ConnectionFlowStep(ProxyConnection connection,
- ConnectionState state) {
- super();
- this.connection = connection;
- this.state = state;
- this.LOG = connection.getLOG();
- }
+ /**
+ * Construct a new step in a connection flow.
+ *
+ * @param connection the connection that we're working on
+ * @param state the state that the connection will show while we're processing this step
+ */
+ ConnectionFlowStep(ProxyConnection connection, ConnectionState state) {
+ this.connection = connection;
+ this.state = state;
+ LOG = connection.getLOG();
+ }
- ProxyConnection getConnection() {
- return connection;
- }
+ ProxyConnection getConnection() {
+ return connection;
+ }
- ConnectionState getState() {
- return state;
- }
+ ConnectionState getState() {
+ return state;
+ }
- /**
- * Indicates whether or not to suppress the initial request. Defaults to
- * false, can be overridden.
- */
- boolean shouldSuppressInitialRequest() {
- return false;
- }
+ /** Indicates whether to suppress the initial request. Defaults to false, can be overridden. */
+ boolean shouldSuppressInitialRequest() {
+ return false;
+ }
- /**
- *
- * Indicates whether or not this step should be executed on the channel's
- * event loop. Defaults to true, can be overridden.
- *
- *
- *
- * If this step modifies the pipeline, for example by adding/removing
- * handlers, it's best to make it execute on the event loop.
- *
- */
- boolean shouldExecuteOnEventLoop() {
- return true;
- }
+ /**
+ * Indicates whether this step should be executed on the channel's event loop. Defaults to true,
+ * can be overridden.
+ *
+ *
If this step modifies the pipeline, for example by adding/removing handlers, it's best to
+ * make it execute on the event loop.
+ */
+ boolean shouldExecuteOnEventLoop() {
+ return true;
+ }
- /**
- * Implement this method to actually do the work involved in this step of
- * the flow.
- */
- protected abstract Future execute();
+ /** Implement this method to actually do the work involved in this step of the flow. */
+ protected abstract Future> execute();
- /**
- * When the flow determines that this step was successful, it calls into
- * this method. The default implementation simply continues with the flow.
- * Other implementations may choose to not continue and instead wait for a
- * message or something like that.
- */
- void onSuccess(ConnectionFlow flow) {
- flow.advance();
- }
+ /**
+ * When the flow determines that this step was successful, it calls into this method. The default
+ * implementation simply continues with the flow. Other implementations may choose to not continue
+ * and instead wait for a message or something like that.
+ */
+ void onSuccess(ConnectionFlow flow) {
+ flow.advance();
+ }
- /**
- *
- * Any messages that are read from the underlying connection while we're at
- * this step of the connection flow are passed to this method.
- *
- *
- *
- * The default implementation ignores the message and logs this, since we
- * weren't really expecting a message here.
- *
- *
- *
- * Some {@link ConnectionFlowStep}s do need to read the messages, so they
- * override this method as appropriate.
- *
- *
- * @param flow
- * our {@link ConnectionFlow}
- * @param msg
- * the message read from the underlying connection
- */
- void read(ConnectionFlow flow, Object msg) {
- LOG.debug("Received message while in the middle of connecting: {}", msg);
- }
-
- @Override
- public String toString() {
- return state.toString();
- }
+ /**
+ * Any messages that are read from the underlying connection while we're at this step of the
+ * connection flow are passed to this method.
+ *
+ *
The default implementation ignores the message and logs this, since we weren't really
+ * expecting a message here.
+ *
+ *
Some {@link ConnectionFlowStep}s do need to read the messages, so they override this method
+ * as appropriate.
+ *
+ * @param flow our {@link ConnectionFlow}
+ * @param msg the message read from the underlying connection
+ */
+ void read(ConnectionFlow flow, Object msg) {
+ LOG.debug("Received message while in the middle of connecting: {}", msg);
+ }
+ @Override
+ public String toString() {
+ return state.toString();
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java
index 371fcd58..ef658b0a 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionState.java
@@ -1,81 +1,65 @@
package org.littleshoot.proxy.impl;
enum ConnectionState {
- /**
- * Connection attempting to connect.
- */
- CONNECTING(true),
+ /** Connection attempting to connect. */
+ CONNECTING(true),
- /**
- * In the middle of doing an SSL handshake.
- */
- HANDSHAKING(true),
+ /** In the middle of doing an SSL handshake. */
+ HANDSHAKING(true),
- /**
- * In the process of negotiating an HTTP CONNECT from the client.
- */
- NEGOTIATING_CONNECT(true),
+ /** In the process of negotiating an HTTP CONNECT from the client. */
+ NEGOTIATING_CONNECT(true),
- /**
- * When forwarding a CONNECT to a chained proxy, we await the CONNECTION_OK
- * message from the proxy.
- */
- AWAITING_CONNECT_OK(true),
+ /**
+ * When forwarding a CONNECT to a chained proxy, we await the CONNECTION_OK message from the
+ * proxy.
+ */
+ AWAITING_CONNECT_OK(true),
- /**
- * Connected but waiting for proxy authentication.
- */
- AWAITING_PROXY_AUTHENTICATION,
+ /** Connected but waiting for proxy authentication. */
+ AWAITING_PROXY_AUTHENTICATION,
- /**
- * Connected and awaiting initial message (e.g. HttpRequest or
- * HttpResponse).
- */
- AWAITING_INITIAL,
+ /** Connected and awaiting initial message (e.g. HttpRequest or HttpResponse). */
+ AWAITING_INITIAL,
- /**
- * Connected and awaiting HttpContent chunk.
- */
- AWAITING_CHUNK,
+ /** Connected and awaiting HttpContent chunk. */
+ AWAITING_CHUNK,
- /**
- * We've asked the client to disconnect, but it hasn't yet.
- */
- DISCONNECT_REQUESTED(),
+ /** We've asked the client to disconnect, but it hasn't yet. */
+ DISCONNECT_REQUESTED(),
- /**
- * Disconnected
- */
- DISCONNECTED();
+ /** Disconnected */
+ DISCONNECTED();
- private final boolean partOfConnectionFlow;
+ private final boolean partOfConnectionFlow;
- ConnectionState(boolean partOfConnectionFlow) {
- this.partOfConnectionFlow = partOfConnectionFlow;
- }
+ ConnectionState(boolean partOfConnectionFlow) {
+ this.partOfConnectionFlow = partOfConnectionFlow;
+ }
- ConnectionState() {
- this(false);
- }
+ ConnectionState() {
+ this(false);
+ }
- /**
- * Indicates whether this ConnectionState corresponds to a step in a
- * {@link ConnectionFlow}. This is useful to distinguish so that we know
- * whether or not we're in the process of establishing a connection.
- *
- * @return true if part of connection flow, otherwise false
- */
- public boolean isPartOfConnectionFlow() {
- return partOfConnectionFlow;
- }
+ /**
+ * Indicates whether this ConnectionState corresponds to a step in a {@link ConnectionFlow}. This
+ * is useful to distinguish so that we know whether we're in the process of establishing a
+ * connection.
+ *
+ * @return true if part of connection flow, otherwise false
+ */
+ public boolean isPartOfConnectionFlow() {
+ return partOfConnectionFlow;
+ }
- /**
- * Indicates whether this ConnectionState is no longer waiting for messages and is either in the process of disconnecting
- * or is already disconnected.
- *
- * @return true if the connection state is {@link #DISCONNECT_REQUESTED} or {@link #DISCONNECTED}, otherwise false
- */
- public boolean isDisconnectingOrDisconnected() {
- return this == DISCONNECT_REQUESTED || this == DISCONNECTED;
- }
+ /**
+ * Indicates whether this ConnectionState is no longer waiting for messages and is either in the
+ * process of disconnecting or is already disconnected.
+ *
+ * @return true if the connection state is {@link #DISCONNECT_REQUESTED} or {@link #DISCONNECTED},
+ * otherwise false
+ */
+ public boolean isDisconnectingOrDisconnected() {
+ return this == DISCONNECT_REQUESTED || this == DISCONNECTED;
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
index 16253ef5..0dfcf4dd 100644
--- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
@@ -1,912 +1,665 @@
package org.littleshoot.proxy.impl;
import io.netty.bootstrap.ServerBootstrap;
-import io.netty.channel.*;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.ChannelGroupFuture;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
-import io.netty.channel.udt.nio.NioUdtProvider;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
-import org.littleshoot.proxy.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ssl.SSLEngine;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
+import java.time.Duration;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
- *
* Primary implementation of an {@link HttpProxyServer}.
- *
*
- *
- * {@link DefaultHttpProxyServer} is bootstrapped by calling
- * {@link #bootstrap()} or {@link #bootstrapFromFile(String)}, and then calling
- * {@link DefaultHttpProxyServerBootstrap#start()}. For example:
- *
+ *
{@link DefaultHttpProxyServer} is bootstrapped by calling {@link #bootstrap()} or {@link
+ * #bootstrapFromFile(String)}, and then calling {@link
+ * org.littleshoot.proxy.impl.DefaultHttpProxyServerBootstrap#start()}. For example:
*
*
- *
*/
public class DefaultHttpProxyServer implements HttpProxyServer {
- private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpProxyServer.class);
-
- /**
- * The interval in ms at which the GlobalTrafficShapingHandler will run to compute and throttle the
- * proxy-to-server bandwidth.
- */
- private static final long TRAFFIC_SHAPING_CHECK_INTERVAL_MS = 250L;
-
- private static final int MAX_INITIAL_LINE_LENGTH_DEFAULT = 8192;
- private static final int MAX_HEADER_SIZE_DEFAULT = 8192*2;
- private static final int MAX_CHUNK_SIZE_DEFAULT = 8192*2;
-
- /**
- * The proxy alias to use in the Via header if no explicit proxy alias is specified and the hostname of the local
- * machine cannot be resolved.
- */
- private static final String FALLBACK_PROXY_ALIAS = "littleproxy";
-
- /**
- * Our {@link ServerGroup}. Multiple proxy servers can share the same
- * ServerGroup in order to reuse threads and other such resources.
- */
- private final ServerGroup serverGroup;
-
- private final TransportProtocol transportProtocol;
- /*
- * The address that the server will attempt to bind to.
- */
- private final InetSocketAddress requestedAddress;
- /*
- * The actual address to which the server is bound. May be different from the requestedAddress in some circumstances,
- * for example when the requested port is 0.
- */
- private volatile InetSocketAddress localAddress;
- private volatile InetSocketAddress boundAddress;
- private final SslEngineSource sslEngineSource;
- private final boolean authenticateSslClients;
- private final ProxyAuthenticator proxyAuthenticator;
- private final ChainedProxyManager chainProxyManager;
- private final MitmManager mitmManager;
- private final HttpFiltersSource filtersSource;
- private final boolean transparent;
- private volatile int connectTimeout;
- private volatile int idleConnectionTimeout;
- private final HostResolver serverResolver;
- private volatile GlobalTrafficShapingHandler globalTrafficShapingHandler;
- private final int maxInitialLineLength;
- private final int maxHeaderSize;
- private final int maxChunkSize;
- private final boolean allowRequestsToOriginServer;
- private final boolean acceptProxyProtocol;
- private final boolean sendProxyProtocol;
-
- /**
- * The alias or pseudonym for this proxy, used when adding the Via header.
- */
- private final String proxyAlias;
-
- /**
- * True when the proxy has already been stopped by calling {@link #stop()} or {@link #abort()}.
- */
- private final AtomicBoolean stopped = new AtomicBoolean(false);
-
- /**
- * Track all ActivityTrackers for tracking proxying activity.
- */
- private final Collection activityTrackers = new ConcurrentLinkedQueue<>();
-
- /**
- * Keep track of all channels created by this proxy server for later shutdown when the proxy is stopped.
- */
- private final ChannelGroup allChannels = new DefaultChannelGroup("HTTP-Proxy-Server", GlobalEventExecutor.INSTANCE);
-
- /**
- * JVM shutdown hook to shutdown this proxy server. Declared as a class-level variable to allow removing the shutdown hook when the
- * proxy server is stopped normally.
- */
- private final Thread jvmShutdownHook = new Thread(this::abort, "LittleProxy-JVM-shutdown-hook");
-
- /**
- * Bootstrap a new {@link DefaultHttpProxyServer} starting from scratch.
- */
- public static HttpProxyServerBootstrap bootstrap() {
- return new DefaultHttpProxyServerBootstrap();
- }
-
- /**
- * Bootstrap a new {@link DefaultHttpProxyServer} using defaults from the
- * given file.
- */
- public static HttpProxyServerBootstrap bootstrapFromFile(String path) {
- final File propsFile = new File(path);
- Properties props = new Properties();
-
- if (propsFile.isFile()) {
- try (InputStream is = new FileInputStream(propsFile)) {
- props.load(is);
- } catch (final IOException e) {
- LOG.warn("Could not load props file?", e);
- }
- }
-
- return new DefaultHttpProxyServerBootstrap(props);
- }
-
- /**
- * Creates a new proxy server.
- *
- * @param serverGroup
- * our ServerGroup for shared thread pools and such
- * @param transportProtocol
- * The protocol to use for data transport
- * @param requestedAddress
- * The address on which this server will listen
- * @param sslEngineSource
- * (optional) if specified, this Proxy will encrypt inbound
- * connections from clients using an {@link SSLEngine} obtained
- * from this {@link SslEngineSource}.
- * @param authenticateSslClients
- * Indicate whether or not to authenticate clients when using SSL
- * @param proxyAuthenticator
- * (optional) If specified, requests to the proxy will be
- * authenticated using HTTP BASIC authentication per the provided
- * {@link ProxyAuthenticator}
- * @param chainProxyManager
- * The proxy to send requests to if chaining proxies. Typically
- * null.
- * @param mitmManager
- * The {@link MitmManager} to use for man in the middle'ing
- * CONNECT requests
- * @param filtersSource
- * Source for {@link HttpFilters}
- * @param transparent
- * If true, this proxy will run as a transparent proxy. This will
- * not modify the response, and will only modify the request to
- * amend the URI if the target is the origin server (to comply
- * with RFC 7230 section 5.3.1).
- * @param idleConnectionTimeout
- * The timeout (in seconds) for auto-closing idle connections.
- * @param activityTrackers
- * for tracking activity on this proxy
- * @param connectTimeout
- * number of milliseconds to wait to connect to the upstream
- * server
- * @param serverResolver
- * the {@link HostResolver} to use for resolving server addresses
- * @param readThrottleBytesPerSecond
- * read throttle bandwidth
- * @param writeThrottleBytesPerSecond
- * write throttle bandwidth
- * @param maxInitialLineLength
- * @param maxHeaderSize
- * @param maxChunkSize
- * @param allowRequestsToOriginServer
- * when true, allow the proxy to handle requests that contain an origin-form URI, as defined in RFC 7230 5.3.1
- * @param acceptProxyProtocol when true, the proxy will accept a proxy protocol header from client
- * @param sendProxyProtocol when true, the proxy will send a proxy protocol header to the server
- */
- private DefaultHttpProxyServer(ServerGroup serverGroup,
- TransportProtocol transportProtocol,
- InetSocketAddress requestedAddress,
- SslEngineSource sslEngineSource,
- boolean authenticateSslClients,
- ProxyAuthenticator proxyAuthenticator,
- ChainedProxyManager chainProxyManager,
- MitmManager mitmManager,
- HttpFiltersSource filtersSource,
- boolean transparent,
- int idleConnectionTimeout,
- Collection activityTrackers,
- int connectTimeout,
- HostResolver serverResolver,
- long readThrottleBytesPerSecond,
- long writeThrottleBytesPerSecond,
- InetSocketAddress localAddress,
- String proxyAlias,
- int maxInitialLineLength,
- int maxHeaderSize,
- int maxChunkSize,
- boolean allowRequestsToOriginServer,
- boolean acceptProxyProtocol,
- boolean sendProxyProtocol) {
- this.serverGroup = serverGroup;
- this.transportProtocol = transportProtocol;
- this.requestedAddress = requestedAddress;
- this.sslEngineSource = sslEngineSource;
- this.authenticateSslClients = authenticateSslClients;
- this.proxyAuthenticator = proxyAuthenticator;
- this.chainProxyManager = chainProxyManager;
- this.mitmManager = mitmManager;
- this.filtersSource = filtersSource;
- this.transparent = transparent;
- this.idleConnectionTimeout = idleConnectionTimeout;
- if (activityTrackers != null) {
- this.activityTrackers.addAll(activityTrackers);
- }
- this.connectTimeout = connectTimeout;
- this.serverResolver = serverResolver;
-
- if (writeThrottleBytesPerSecond > 0 || readThrottleBytesPerSecond > 0) {
- this.globalTrafficShapingHandler = createGlobalTrafficShapingHandler(transportProtocol, readThrottleBytesPerSecond, writeThrottleBytesPerSecond);
- } else {
- this.globalTrafficShapingHandler = null;
- }
- this.localAddress = localAddress;
-
- if (proxyAlias == null) {
- // attempt to resolve the name of the local machine. if it cannot be resolved, use the fallback name.
- String hostname = ProxyUtils.getHostName();
- if (hostname == null) {
- hostname = FALLBACK_PROXY_ALIAS;
- }
- this.proxyAlias = hostname;
- } else {
- this.proxyAlias = proxyAlias;
- }
- this.maxInitialLineLength = maxInitialLineLength;
- this.maxHeaderSize = maxHeaderSize;
- this.maxChunkSize = maxChunkSize;
- this.allowRequestsToOriginServer = allowRequestsToOriginServer;
- this.acceptProxyProtocol = acceptProxyProtocol;
- this.sendProxyProtocol = sendProxyProtocol;
- }
-
- /**
- * Creates a new GlobalTrafficShapingHandler for this HttpProxyServer, using this proxy's proxyToServerEventLoop.
- */
- private GlobalTrafficShapingHandler createGlobalTrafficShapingHandler(TransportProtocol transportProtocol, long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
- EventLoopGroup proxyToServerEventLoop = this.getProxyToServerWorkerFor(transportProtocol);
- return new GlobalTrafficShapingHandler(proxyToServerEventLoop,
- writeThrottleBytesPerSecond,
- readThrottleBytesPerSecond,
- TRAFFIC_SHAPING_CHECK_INTERVAL_MS,
- Long.MAX_VALUE);
- }
-
- boolean isTransparent() {
- return transparent;
- }
-
- @Override
- public int getIdleConnectionTimeout() {
- return idleConnectionTimeout;
- }
-
- @Override
- public void setIdleConnectionTimeout(int idleConnectionTimeout) {
- this.idleConnectionTimeout = idleConnectionTimeout;
- }
-
- @Override
- public int getConnectTimeout() {
- return connectTimeout;
- }
-
- @Override
- public void setConnectTimeout(int connectTimeoutMs) {
- this.connectTimeout = connectTimeoutMs;
- }
-
- public HostResolver getServerResolver() {
- return serverResolver;
- }
-
- public InetSocketAddress getLocalAddress() {
- return localAddress;
- }
-
- @Override
- public InetSocketAddress getListenAddress() {
- return boundAddress;
- }
-
- @Override
- public void setThrottle(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
- if (globalTrafficShapingHandler != null) {
- globalTrafficShapingHandler.configure(writeThrottleBytesPerSecond, readThrottleBytesPerSecond);
- } else {
- // don't create a GlobalTrafficShapingHandler if throttling was not enabled and is still not enabled
- if (readThrottleBytesPerSecond > 0 || writeThrottleBytesPerSecond > 0) {
- globalTrafficShapingHandler = createGlobalTrafficShapingHandler(transportProtocol, readThrottleBytesPerSecond, writeThrottleBytesPerSecond);
- }
- }
- }
-
- public long getReadThrottle() {
- return globalTrafficShapingHandler.getReadLimit();
- }
-
- public long getWriteThrottle() {
- return globalTrafficShapingHandler.getWriteLimit();
- }
-
- public int getMaxInitialLineLength() {
- return maxInitialLineLength;
- }
-
- public int getMaxHeaderSize() {
- return maxHeaderSize;
- }
-
- public int getMaxChunkSize() {
- return maxChunkSize;
- }
-
- public boolean isAllowRequestsToOriginServer() {
- return allowRequestsToOriginServer;
- }
-
- public boolean isAcceptProxyProtocol() {
- return acceptProxyProtocol;
- }
-
- public boolean isSendProxyProtocol() {
- return sendProxyProtocol;
- }
-
- @Override
- public HttpProxyServerBootstrap clone() {
- return new DefaultHttpProxyServerBootstrap(serverGroup,
- transportProtocol,
- new InetSocketAddress(requestedAddress.getAddress(),
- requestedAddress.getPort() == 0 ? 0 : requestedAddress.getPort() + 1),
- sslEngineSource,
- authenticateSslClients,
- proxyAuthenticator,
- chainProxyManager,
- mitmManager,
- filtersSource,
- transparent,
- idleConnectionTimeout,
- activityTrackers,
- connectTimeout,
- serverResolver,
- globalTrafficShapingHandler != null ? globalTrafficShapingHandler.getReadLimit() : 0,
- globalTrafficShapingHandler != null ? globalTrafficShapingHandler.getWriteLimit() : 0,
- localAddress,
- proxyAlias,
- maxInitialLineLength,
- maxHeaderSize,
- maxChunkSize,
- allowRequestsToOriginServer);
- }
-
- @Override
- public void stop() {
- doStop(true);
- }
-
- @Override
- public void abort() {
- doStop(false);
- }
-
- /**
- * Performs cleanup necessary to stop the server. Closes all channels opened by the server and unregisters this
- * server from the server group.
- *
- * @param graceful when true, waits for requests to terminate before stopping the server
- */
- protected void doStop(boolean graceful) {
- // only stop the server if it hasn't already been stopped
- if (stopped.compareAndSet(false, true)) {
- if (graceful) {
- LOG.info("Shutting down proxy server gracefully");
- } else {
- LOG.info("Shutting down proxy server immediately (non-graceful)");
- }
-
- closeAllChannels(graceful);
-
- serverGroup.unregisterProxyServer(this, graceful);
-
- // remove the shutdown hook that was added when the proxy was started, since it has now been stopped
- try {
- Runtime.getRuntime().removeShutdownHook(jvmShutdownHook);
- } catch (IllegalStateException e) {
- // ignore -- IllegalStateException means the VM is already shutting down
- }
-
- LOG.info("Done shutting down proxy server");
- }
- }
-
- /**
- * Register a new {@link Channel} with this server, for later closing.
- */
- protected void registerChannel(Channel channel) {
- allChannels.add(channel);
- }
-
- /**
- * Closes all channels opened by this proxy server.
- *
- * @param graceful when false, attempts to shutdown all channels immediately and ignores any channel-closing exceptions
- */
- protected void closeAllChannels(boolean graceful) {
- LOG.info("Closing all channels " + (graceful ? "(graceful)" : "(non-graceful)"));
-
- ChannelGroupFuture future = allChannels.close();
-
- // if this is a graceful shutdown, log any channel closing failures. if this isn't a graceful shutdown, ignore them.
- if (graceful) {
- try {
- future.await(10, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
-
- LOG.warn("Interrupted while waiting for channels to shut down gracefully.");
- }
-
- if (!future.isSuccess()) {
- for (ChannelFuture cf : future) {
- if (!cf.isSuccess()) {
- LOG.info("Unable to close channel. Cause of failure for {} is {}", cf.channel(), cf.cause());
- }
- }
- }
- }
- }
-
- private HttpProxyServer start() {
- if (!serverGroup.isStopped()) {
- LOG.info("Starting proxy at address: " + this.requestedAddress);
-
- serverGroup.registerProxyServer(this);
-
- doStart();
- } else {
- throw new IllegalStateException("Attempted to start proxy, but proxy's server group is already stopped");
- }
-
- return this;
- }
-
- private void doStart() {
- ServerBootstrap serverBootstrap = new ServerBootstrap().group(
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpProxyServer.class);
+
+ /**
+ * The interval in ms at which the GlobalTrafficShapingHandler will run to compute and throttle
+ * the proxy-to-server bandwidth.
+ */
+ private static final long TRAFFIC_SHAPING_CHECK_INTERVAL_MS = 250L;
+
+ private static final int MAX_INITIAL_LINE_LENGTH_DEFAULT = 8192;
+ private static final int MAX_HEADER_SIZE_DEFAULT = 8192 * 2;
+ private static final int MAX_CHUNK_SIZE_DEFAULT = 8192 * 2;
+
+ /**
+ * The proxy alias to use in the Via header if no explicit proxy alias is specified and the
+ * hostname of the local machine cannot be resolved.
+ */
+ private static final String FALLBACK_PROXY_ALIAS = "littleproxy";
+
+ private static final String DEFAULT_LITTLE_PROXY_NAME = "LittleProxy";
+ public static final String LOCAL_ADDRESS = "127.0.0.1";
+ public static final int DEFAULT_PORT = 8080;
+ public static final String DEFAULT_NIC_VALUE = "0.0.0.0";
+ public static final String CLIENT_TO_PROXY_WORKER_THREADS = "client_to_proxy_worker_threads";
+ public static final String PROXY_TO_SERVER_WORKER_THREADS = "proxy_to_server_worker_threads";
+ public static final String ACTIVITY_LOG_FORMAT = "activity_log_format";
+ public static final String ACCEPTOR_THREADS = "acceptor_threads";
+ public static final String SEND_PROXY_PROTOCOL = "send_proxy_protocol";
+ public static final String ALLOW_PROXY_PROTOCOL = "allow_proxy_protocol";
+ public static final String SERVER_CONNECTION_POOL_TYPE = "server_connection_pool_type";
+ public static final String USE_SHARED_SERVER_CONNECTION_POOL =
+ "use_shared_server_connection_pool";
+ public static final String MAX_TOTAL_CONNECTIONS = "max_total_connections";
+ public static final String MAX_CONNECTIONS_PER_HOST = "max_connections_per_host";
+ public static final String POOL_SHARED_MITM_CONNECTIONS = "pool_shared_mitm_connections";
+ public static final String POOL_PER_REQUEST_IN_MITM = "pool_per_request_in_mitm";
+ public static final String ALLOW_REQUESTS_TO_ORIGIN_SERVER = "allow_requests_to_origin_server";
+ public static final String THROTTLE_WRITE_BYTES_PER_SECOND = "throttle_write_bytes_per_second";
+ public static final String THROTTLE_READ_BYTES_PER_SECOND = "throttle_read_bytes_per_second";
+ public static final String TRANSPARENT = "transparent";
+ public static final String SSL_CLIENTS_KEYSTORE_PATH = "ssl_clients_keystore_path";
+ public static final String SSL_CLIENTS_KEYSTORE_PASSWORD = "ssl_clients_keystore_password";
+ public static final String SSL_CLIENTS_KEYSTORE_ALIAS = "ssl_clients_keystore_alias";
+ public static final String SSL_CLIENTS_SEND_CERTS = "ssl_clients_send_certs";
+ public static final String AUTHENTICATE_SSL_CLIENTS = "authenticate_ssl_clients";
+ public static final String SSL_CLIENTS_TRUST_ALL_SERVERS = "ssl_clients_trust_all_servers";
+ public static final String ALLOW_LOCAL_ONLY = "allow_local_only";
+ public static final String PROXY_ALIAS = "proxy_alias";
+ public static final String NIC = "nic";
+ public static final String PORT = "port";
+ public static final String ADDRESS = "address";
+ public static final String NAME = "name";
+ private static final String DEFAULT_JKS_KEYSTORE_PATH = "littleproxy_keystore.jks";
+
+ /**
+ * Our {@link ServerGroup}. Multiple proxy servers can share the same ServerGroup in order to
+ * reuse threads and other such resources.
+ */
+ private final ServerGroup serverGroup;
+
+ private final TransportProtocol transportProtocol;
+ /*
+ * The address that the server will attempt to bind to.
+ */
+ private final InetSocketAddress requestedAddress;
+ /*
+ * The actual address to which the server is bound. May be different from the
+ * requestedAddress in some circumstances,
+ * for example when the requested port is 0.
+ */
+ private final InetSocketAddress localAddress;
+ private volatile InetSocketAddress boundAddress;
+ private final SslEngineSource sslEngineSource;
+ private final boolean authenticateSslClients;
+ private final ProxyAuthenticator proxyAuthenticator;
+ private final ChainedProxyManager chainProxyManager;
+ private final MitmManager mitmManager;
+ private final HttpFiltersSource filtersSource;
+ private final boolean transparent;
+ private volatile int connectTimeout;
+ private volatile Duration idleConnectionTimeout;
+ private final HostResolver serverResolver;
+ private volatile GlobalTrafficShapingHandler globalTrafficShapingHandler;
+ private final int maxInitialLineLength;
+ private final int maxHeaderSize;
+ private final int maxChunkSize;
+ private final boolean allowRequestsToOriginServer;
+ private final boolean acceptProxyProtocol;
+ private final boolean sendProxyProtocol;
+
+ /** The alias or pseudonym for this proxy, used when adding the Via header. */
+ private final String proxyAlias;
+
+ /**
+ * True when the proxy has already been stopped by calling {@link #stop()} or {@link #abort()}.
+ */
+ private final AtomicBoolean stopped = new AtomicBoolean(false);
+
+ /** Track all ActivityTrackers for tracking proxying activity. */
+ private final Collection activityTrackers = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Keep track of all channels created by this proxy server for later shutdown when the proxy is
+ * stopped.
+ */
+ private final ChannelGroup allChannels =
+ new DefaultChannelGroup("HTTP-Proxy-Server", GlobalEventExecutor.INSTANCE, true);
+
+ /**
+ * Shared pool of ProxyToServerConnection instances for all ClientToProxyConnection. This
+ * addresses the connection explosion issue (GitHub issue #83).
+ */
+ private volatile ServerConnectionPool serverConnectionPool;
+
+ /**
+ * Whether to use the shared server connection pool. Disabled by default for backwards
+ * compatibility.
+ */
+ private final boolean useSharedServerConnectionPool;
+
+ /**
+ * Maximum number of connections per host:port when using the shared connection pool. Default is
+ * 10.
+ */
+ private final int maxConnectionsPerHost;
+
+ /** Maximum total connections in the shared pool. */
+ private final int maxConnections;
+
+ /** Selected server connection pool implementation. */
+ private final ServerConnectionPoolType serverConnectionPoolType;
+
+ /** Configuration for the server connection pool. */
+ private final ServerConnectionPoolConfig serverConnectionPoolConfig;
+
+ /**
+ * JVM shutdown hook to shut down this proxy server. Declared as a class-level variable to allow
+ * removing the shutdown hook when the proxy server is stopped normally.
+ */
+ private final Thread jvmShutdownHook = new Thread(this::abort, "LittleProxy-JVM-shutdown-hook");
+
+ /** Bootstrap a new {@link DefaultHttpProxyServer} starting from scratch. */
+ public static HttpProxyServerBootstrap bootstrap() {
+ return new org.littleshoot.proxy.impl.DefaultHttpProxyServerBootstrap();
+ }
+
+ /** Bootstrap a new {@link DefaultHttpProxyServer} using defaults from the given file. */
+ public static HttpProxyServerBootstrap bootstrapFromFile(String path) {
+ final File propsFile = new File(path);
+ Properties props = new Properties();
+
+ if (propsFile.isFile()) {
+ try (InputStream is = new FileInputStream(propsFile)) {
+ props.load(is);
+ } catch (final IOException e) {
+ LOG.error("Could not load props file", e);
+ throw new IllegalArgumentException("Could not load props file." + e.getMessage());
+ }
+ } else {
+ String cause = !propsFile.exists() ? "absent" : "a directory";
+ LOG.error("Could not load props file. file is {}", cause);
+ throw new IllegalArgumentException("Could not load props file. file is " + (cause));
+ }
+
+ return new org.littleshoot.proxy.impl.DefaultHttpProxyServerBootstrap(props);
+ }
+
+ DefaultHttpProxyServer(ServerGroup serverGroup, DefaultHttpProxyServerConfig config) {
+ this.serverGroup = serverGroup;
+ this.transportProtocol = config.getTransportProtocol();
+ this.requestedAddress = config.getRequestedAddress();
+ this.sslEngineSource = config.getSslEngineSource();
+ this.authenticateSslClients = config.isAuthenticateSslClients();
+ this.proxyAuthenticator = config.getProxyAuthenticator();
+ this.chainProxyManager = config.getChainProxyManager();
+ this.mitmManager = config.getMitmManager();
+ this.filtersSource = config.getFiltersSource();
+ this.transparent = config.isTransparent();
+ this.idleConnectionTimeout = config.getIdleConnectionTimeout();
+ this.activityTrackers.addAll(config.getActivityTrackers());
+ this.connectTimeout = config.getConnectTimeout();
+ this.serverResolver = config.getServerResolver();
+
+ long readThrottleBytesPerSecond = config.getReadThrottleBytesPerSecond();
+ long writeThrottleBytesPerSecond = config.getWriteThrottleBytesPerSecond();
+ if (writeThrottleBytesPerSecond > 0 || readThrottleBytesPerSecond > 0) {
+ globalTrafficShapingHandler =
+ createGlobalTrafficShapingHandler(
+ config.getTransportProtocol(),
+ readThrottleBytesPerSecond,
+ writeThrottleBytesPerSecond);
+ } else {
+ globalTrafficShapingHandler = null;
+ }
+ this.localAddress = config.getLocalAddress();
+
+ String proxyAlias = config.getProxyAlias();
+ if (proxyAlias == null) {
+ // attempt to resolve the name of the local machine. if it cannot be resolved,
+ // use the fallback name.
+ String hostname = ProxyUtils.getHostName();
+ if (hostname == null) {
+ hostname = FALLBACK_PROXY_ALIAS;
+ }
+ this.proxyAlias = hostname;
+ } else {
+ this.proxyAlias = proxyAlias;
+ }
+ this.maxInitialLineLength = config.getMaxInitialLineLength();
+ this.maxHeaderSize = config.getMaxHeaderSize();
+ this.maxChunkSize = config.getMaxChunkSize();
+ this.allowRequestsToOriginServer = config.isAllowRequestsToOriginServer();
+ this.acceptProxyProtocol = config.isAcceptProxyProtocol();
+ this.sendProxyProtocol = config.isSendProxyProtocol();
+ this.serverConnectionPoolConfig = config.getServerConnectionPoolConfig();
+ this.useSharedServerConnectionPool = this.serverConnectionPoolConfig.isEnabled();
+ this.maxConnectionsPerHost = this.serverConnectionPoolConfig.getMaxConnectionsPerHost();
+ this.serverConnectionPoolType = this.serverConnectionPoolConfig.getPoolType();
+ this.maxConnections = this.serverConnectionPoolConfig.getMaxConnections();
+ }
+
+ /**
+ * Creates a new GlobalTrafficShapingHandler for this HttpProxyServer, using this proxy's
+ * proxyToServerEventLoop.
+ */
+ private GlobalTrafficShapingHandler createGlobalTrafficShapingHandler(
+ TransportProtocol transportProtocol,
+ long readThrottleBytesPerSecond,
+ long writeThrottleBytesPerSecond) {
+ EventLoopGroup proxyToServerEventLoop = getProxyToServerWorkerFor(transportProtocol);
+ return new GlobalTrafficShapingHandler(
+ proxyToServerEventLoop,
+ writeThrottleBytesPerSecond,
+ readThrottleBytesPerSecond,
+ TRAFFIC_SHAPING_CHECK_INTERVAL_MS,
+ Long.MAX_VALUE);
+ }
+
+ boolean isTransparent() {
+ return transparent;
+ }
+
+ @Override
+ public int getIdleConnectionTimeout() {
+ return (int) idleConnectionTimeout.toSeconds();
+ }
+
+ @Override
+ public void setIdleConnectionTimeout(int idleConnectionTimeoutInSeconds) {
+ this.idleConnectionTimeout = Duration.ofSeconds(idleConnectionTimeoutInSeconds);
+ }
+
+ @Override
+ public void setIdleConnectionTimeout(Duration idleConnectionTimeout) {
+ this.idleConnectionTimeout = idleConnectionTimeout;
+ }
+
+ @Override
+ public int getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ @Override
+ public void setConnectTimeout(int connectTimeoutMs) {
+ connectTimeout = connectTimeoutMs;
+ }
+
+ public HostResolver getServerResolver() {
+ return serverResolver;
+ }
+
+ public InetSocketAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ @Override
+ public InetSocketAddress getListenAddress() {
+ return boundAddress;
+ }
+
+ @Override
+ public void setThrottle(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
+ if (globalTrafficShapingHandler != null) {
+ globalTrafficShapingHandler.configure(
+ writeThrottleBytesPerSecond, readThrottleBytesPerSecond);
+ } else {
+ // don't create a GlobalTrafficShapingHandler if throttling was not enabled and
+ // is still not enabled
+ if (readThrottleBytesPerSecond > 0 || writeThrottleBytesPerSecond > 0) {
+ globalTrafficShapingHandler =
+ createGlobalTrafficShapingHandler(
+ transportProtocol, readThrottleBytesPerSecond, writeThrottleBytesPerSecond);
+ }
+ }
+ }
+
+ public long getReadThrottle() {
+ if (globalTrafficShapingHandler != null) {
+ return globalTrafficShapingHandler.getReadLimit();
+ } else {
+ return 0;
+ }
+ }
+
+ public long getWriteThrottle() {
+ if (globalTrafficShapingHandler != null) {
+ return globalTrafficShapingHandler.getWriteLimit();
+ } else {
+ return 0;
+ }
+ }
+
+ public int getMaxInitialLineLength() {
+ return maxInitialLineLength;
+ }
+
+ public int getMaxHeaderSize() {
+ return maxHeaderSize;
+ }
+
+ public int getMaxChunkSize() {
+ return maxChunkSize;
+ }
+
+ /**
+ * Gets the shared ServerConnectionPool for this server. Creates the pool if it doesn't exist.
+ *
+ * @return the shared pool, or null if the pool is disabled
+ */
+ @Nullable
+ public ServerConnectionPool getServerConnectionPool() {
+ if (!useSharedServerConnectionPool) {
+ return null;
+ }
+ ServerConnectionPool pool = serverConnectionPool;
+ if (pool == null) {
+ synchronized (this) {
+ pool = serverConnectionPool;
+ if (pool == null) {
+ pool = createServerConnectionPool();
+ serverConnectionPool = pool;
+ }
+ }
+ }
+ return pool;
+ }
+
+ private ServerConnectionPool createServerConnectionPool() {
+ ServerConnectionPoolType poolType = serverConnectionPoolConfig.getPoolType();
+ Duration idleTimeout = serverConnectionPoolConfig.getIdleTimeout();
+ int maxConnPerHost = serverConnectionPoolConfig.getMaxConnectionsPerHost();
+ int maxConn = serverConnectionPoolConfig.getMaxConnections();
+
+ switch (poolType) {
+ case CONCURRENT_MAP:
+ default:
+ ConcurrentMapServerConnectionPool concurrentMapPool =
+ new ConcurrentMapServerConnectionPool(
+ this, globalTrafficShapingHandler, maxConnPerHost, maxConn);
+ concurrentMapPool.setIdleTimeout(idleTimeout);
+ return concurrentMapPool;
+ }
+ }
+
+ public boolean isPoolSharedMitmConnections() {
+ return serverConnectionPoolConfig.isPoolSharedMitmConnections();
+ }
+
+ public boolean isPoolPerRequestInMitm() {
+ return serverConnectionPoolConfig.isPoolPerRequestInMitm();
+ }
+
+ public boolean isAllowRequestsToOriginServer() {
+ return allowRequestsToOriginServer;
+ }
+
+ public boolean isAcceptProxyProtocol() {
+ return acceptProxyProtocol;
+ }
+
+ public boolean isSendProxyProtocol() {
+ return sendProxyProtocol;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap clone() {
+ InetSocketAddress clonedAddress =
+ new InetSocketAddress(
+ requestedAddress.getAddress(),
+ requestedAddress.getPort() == 0 ? 0 : requestedAddress.getPort() + 1);
+
+ ServerConnectionPoolConfig poolConfig =
+ new ServerConnectionPoolConfig()
+ .setEnabled(useSharedServerConnectionPool)
+ .setPoolType(serverConnectionPoolType)
+ .setMaxConnectionsPerHost(maxConnectionsPerHost)
+ .setMaxConnections(maxConnections)
+ .setIdleTimeout(serverConnectionPoolConfig.getIdleTimeout())
+ .setPoolSharedMitmConnections(serverConnectionPoolConfig.isPoolSharedMitmConnections())
+ .setPoolPerRequestInMitm(serverConnectionPoolConfig.isPoolPerRequestInMitm());
+
+ DefaultHttpProxyServerConfig serverConfig =
+ new DefaultHttpProxyServerConfig()
+ .setTransportProtocol(transportProtocol)
+ .setRequestedAddress(clonedAddress)
+ .setSslEngineSource(sslEngineSource)
+ .setAuthenticateSslClients(authenticateSslClients)
+ .setProxyAuthenticator(proxyAuthenticator)
+ .setChainProxyManager(chainProxyManager)
+ .setMitmManager(mitmManager)
+ .setFiltersSource(filtersSource)
+ .setTransparent(transparent)
+ .setIdleConnectionTimeout(idleConnectionTimeout)
+ .setActivityTrackers(activityTrackers)
+ .setConnectTimeout(connectTimeout)
+ .setServerResolver(serverResolver)
+ .setReadThrottleBytesPerSecond(
+ globalTrafficShapingHandler != null
+ ? globalTrafficShapingHandler.getReadLimit()
+ : 0)
+ .setWriteThrottleBytesPerSecond(
+ globalTrafficShapingHandler != null
+ ? globalTrafficShapingHandler.getWriteLimit()
+ : 0)
+ .setLocalAddress(localAddress)
+ .setProxyAlias(proxyAlias)
+ .setMaxInitialLineLength(maxInitialLineLength)
+ .setMaxHeaderSize(maxHeaderSize)
+ .setMaxChunkSize(maxChunkSize)
+ .setAllowRequestsToOriginServer(allowRequestsToOriginServer)
+ .setAcceptProxyProtocol(acceptProxyProtocol)
+ .setSendProxyProtocol(sendProxyProtocol)
+ .setServerConnectionPoolConfig(poolConfig);
+
+ return new org.littleshoot.proxy.impl.DefaultHttpProxyServerBootstrap(
+ serverGroup, serverConfig);
+ }
+
+ @Override
+ public void stop() {
+ doStop(true);
+ }
+
+ @Override
+ public void abort() {
+ doStop(false);
+ }
+
+ /**
+ * Performs cleanup necessary to stop the server. Closes all channels opened by the server and
+ * unregisters this server from the server group.
+ *
+ * @param graceful when true, waits for requests to terminate before stopping the server
+ */
+ protected void doStop(boolean graceful) {
+ // only stop the server if it hasn't already been stopped
+ if (stopped.compareAndSet(false, true)) {
+ if (graceful) {
+ LOG.info("Shutting down proxy server gracefully");
+ } else {
+ LOG.info("Shutting down proxy server immediately (non-graceful)");
+ }
+
+ // Close the shared server connection pool
+ if (serverConnectionPool != null) {
+ serverConnectionPool.closeAll();
+ }
+
+ closeAllChannels(graceful);
+
+ serverGroup.unregisterProxyServer(this, graceful);
+
+ // remove the shutdown hook that was added when the proxy was started, since it
+ // has now been stopped
+ try {
+ Runtime.getRuntime().removeShutdownHook(jvmShutdownHook);
+ } catch (IllegalStateException e) {
+ // ignore -- IllegalStateException means the VM is already shutting down
+ }
+
+ LOG.info("Done shutting down proxy server");
+ }
+ }
+
+ /** Register a new {@link Channel} with this server, for later closing. */
+ protected void registerChannel(Channel channel) {
+ allChannels.add(channel);
+ }
+
+ protected void unregisterChannel(Channel channel) {
+ if (channel.isOpen()) {
+ // Unlikely to happen, but just in case...
+ channel.close();
+ }
+ allChannels.remove(channel);
+ }
+
+ /**
+ * Closes all channels opened by this proxy server.
+ *
+ * @param graceful when false, attempts to shut down all channels immediately and ignores any
+ * channel-closing exceptions
+ */
+ protected void closeAllChannels(boolean graceful) {
+ LOG.info("Closing all channels {}", graceful ? "(graceful)" : "(non-graceful)");
+
+ ChannelGroupFuture future = allChannels.close();
+
+ // if this is a graceful shutdown, log any channel closing failures. if this
+ // isn't a graceful shutdown, ignore them.
+ if (graceful) {
+ try {
+ future.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+
+ LOG.warn("Interrupted while waiting for channels to shut down gracefully.");
+ }
+
+ if (!future.isSuccess()) {
+ for (ChannelFuture cf : future) {
+ if (!cf.isSuccess()) {
+ LOG.info(
+ "Unable to close channel. Cause of failure for {} is {}",
+ cf.channel(),
+ String.valueOf(cf.cause()));
+ }
+ }
+ }
+ }
+ }
+
+ HttpProxyServer start() {
+ if (!serverGroup.isStopped()) {
+ LOG.info("Starting proxy at address: {}", requestedAddress);
+
+ serverGroup.registerProxyServer(this);
+
+ doStart();
+ } else {
+ throw new IllegalStateException(
+ "Attempted to start proxy, but proxy's server group is already stopped");
+ }
+
+ return this;
+ }
+
+ private void doStart() {
+ ServerBootstrap serverBootstrap =
+ new ServerBootstrap()
+ .group(
serverGroup.getClientToProxyAcceptorPoolForTransport(transportProtocol),
serverGroup.getClientToProxyWorkerPoolForTransport(transportProtocol));
- ChannelInitializer initializer = new ChannelInitializer() {
- protected void initChannel(Channel ch) {
- new ClientToProxyConnection(
- DefaultHttpProxyServer.this,
- sslEngineSource,
- authenticateSslClients,
- ch.pipeline(),
- globalTrafficShapingHandler);
- }
+ ChannelInitializer initializer =
+ new ChannelInitializer<>() {
+ protected void initChannel(Channel ch) {
+ new ClientToProxyConnection(
+ DefaultHttpProxyServer.this,
+ sslEngineSource,
+ authenticateSslClients,
+ ch.pipeline(),
+ globalTrafficShapingHandler);
+ }
};
- switch (transportProtocol) {
- case TCP:
- LOG.info("Proxy listening with TCP transport");
- serverBootstrap.channelFactory(NioServerSocketChannel::new);
- break;
- case UDT:
- LOG.info("Proxy listening with UDT transport");
- serverBootstrap.channelFactory(NioUdtProvider.BYTE_ACCEPTOR)
- .option(ChannelOption.SO_BACKLOG, 10)
- .option(ChannelOption.SO_REUSEADDR, true);
- break;
- default:
- throw new UnknownTransportProtocolException(transportProtocol);
- }
- serverBootstrap.childHandler(initializer);
- ChannelFuture future = serverBootstrap.bind(requestedAddress)
- .addListener((ChannelFutureListener) future1 -> {
- if (future1.isSuccess()) {
- registerChannel(future1.channel());
- }
- }).awaitUninterruptibly();
-
- Throwable cause = future.cause();
- if (cause != null) {
- throw new RuntimeException(cause);
- }
-
- this.boundAddress = ((InetSocketAddress) future.channel().localAddress());
- LOG.info("Proxy started at address: " + this.boundAddress);
-
- Runtime.getRuntime().addShutdownHook(jvmShutdownHook);
- }
-
- protected ChainedProxyManager getChainProxyManager() {
- return chainProxyManager;
- }
-
- protected MitmManager getMitmManager() {
- return mitmManager;
- }
-
- protected SslEngineSource getSslEngineSource() {
- return sslEngineSource;
- }
-
- protected ProxyAuthenticator getProxyAuthenticator() {
- return proxyAuthenticator;
- }
-
- public HttpFiltersSource getFiltersSource() {
- return filtersSource;
- }
-
- protected Collection getActivityTrackers() {
- return activityTrackers;
- }
-
- public String getProxyAlias() {
- return proxyAlias;
- }
-
-
- protected EventLoopGroup getProxyToServerWorkerFor(TransportProtocol transportProtocol) {
- return serverGroup.getProxyToServerWorkerPoolForTransport(transportProtocol);
- }
-
- // TODO: refactor bootstrap into a separate class
- private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerBootstrap {
- private String name = "LittleProxy";
- private ServerGroup serverGroup = null;
- private TransportProtocol transportProtocol = TransportProtocol.TCP;
- private InetSocketAddress requestedAddress;
- private int port = 8080;
- private boolean allowLocalOnly = true;
- private SslEngineSource sslEngineSource = null;
- private boolean authenticateSslClients = true;
- private ProxyAuthenticator proxyAuthenticator = null;
- private ChainedProxyManager chainProxyManager = null;
- private MitmManager mitmManager = null;
- private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
- private boolean transparent = false;
- private int idleConnectionTimeout = 70;
- private Collection activityTrackers = new ConcurrentLinkedQueue<>();
- private int connectTimeout = 40000;
- private HostResolver serverResolver = new DefaultHostResolver();
- private long readThrottleBytesPerSecond;
- private long writeThrottleBytesPerSecond;
- private InetSocketAddress localAddress;
- private String proxyAlias;
- private int clientToProxyAcceptorThreads = ServerGroup.DEFAULT_INCOMING_ACCEPTOR_THREADS;
- private int clientToProxyWorkerThreads = ServerGroup.DEFAULT_INCOMING_WORKER_THREADS;
- private int proxyToServerWorkerThreads = ServerGroup.DEFAULT_OUTGOING_WORKER_THREADS;
- private int maxInitialLineLength = MAX_INITIAL_LINE_LENGTH_DEFAULT;
- private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT;
- private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT;
- private boolean allowRequestToOriginServer = false;
- private boolean acceptProxyProtocol = false;
- private boolean sendProxyProtocol = false;
-
- private DefaultHttpProxyServerBootstrap() {
- }
-
- private DefaultHttpProxyServerBootstrap(
- ServerGroup serverGroup,
- TransportProtocol transportProtocol,
- InetSocketAddress requestedAddress,
- SslEngineSource sslEngineSource,
- boolean authenticateSslClients,
- ProxyAuthenticator proxyAuthenticator,
- ChainedProxyManager chainProxyManager,
- MitmManager mitmManager,
- HttpFiltersSource filtersSource,
- boolean transparent, int idleConnectionTimeout,
- Collection activityTrackers,
- int connectTimeout, HostResolver serverResolver,
- long readThrottleBytesPerSecond,
- long writeThrottleBytesPerSecond,
- InetSocketAddress localAddress,
- String proxyAlias,
- int maxInitialLineLength,
- int maxHeaderSize,
- int maxChunkSize,
- boolean allowRequestToOriginServer) {
- this.serverGroup = serverGroup;
- this.transportProtocol = transportProtocol;
- this.requestedAddress = requestedAddress;
- this.port = requestedAddress.getPort();
- this.sslEngineSource = sslEngineSource;
- this.authenticateSslClients = authenticateSslClients;
- this.proxyAuthenticator = proxyAuthenticator;
- this.chainProxyManager = chainProxyManager;
- this.mitmManager = mitmManager;
- this.filtersSource = filtersSource;
- this.transparent = transparent;
- this.idleConnectionTimeout = idleConnectionTimeout;
- if (activityTrackers != null) {
- this.activityTrackers.addAll(activityTrackers);
- }
- this.connectTimeout = connectTimeout;
- this.serverResolver = serverResolver;
- this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
- this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
- this.localAddress = localAddress;
- this.proxyAlias = proxyAlias;
- this.maxInitialLineLength = maxInitialLineLength;
- this.maxHeaderSize = maxHeaderSize;
- this.maxChunkSize = maxChunkSize;
- this.allowRequestToOriginServer = allowRequestToOriginServer;
- }
-
- private DefaultHttpProxyServerBootstrap(Properties props) {
- this.withUseDnsSec(ProxyUtils.extractBooleanDefaultFalse(
- props, "dnssec"));
- this.transparent = ProxyUtils.extractBooleanDefaultFalse(
- props, "transparent");
- this.idleConnectionTimeout = ProxyUtils.extractInt(props,
- "idle_connection_timeout");
- this.connectTimeout = ProxyUtils.extractInt(props,
- "connect_timeout", 0);
- this.maxInitialLineLength = ProxyUtils.extractInt(props,
- "max_initial_line_length", MAX_INITIAL_LINE_LENGTH_DEFAULT);
- this.maxHeaderSize = ProxyUtils.extractInt(props,
- "max_header_size", MAX_HEADER_SIZE_DEFAULT);
- this.maxChunkSize = ProxyUtils.extractInt(props,
- "max_chunk_size", MAX_CHUNK_SIZE_DEFAULT);
- }
-
- @Override
- public HttpProxyServerBootstrap withName(String name) {
- this.name = name;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withTransportProtocol(
- TransportProtocol transportProtocol) {
- this.transportProtocol = transportProtocol;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withAddress(InetSocketAddress address) {
- this.requestedAddress = address;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withPort(int port) {
- this.requestedAddress = null;
- this.port = port;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withNetworkInterface(InetSocketAddress inetSocketAddress) {
- this.localAddress = inetSocketAddress;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withProxyAlias(String alias) {
- this.proxyAlias = alias;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withAllowLocalOnly(
- boolean allowLocalOnly) {
- this.allowLocalOnly = allowLocalOnly;
- return this;
- }
-
- @Override
- @Deprecated
- public HttpProxyServerBootstrap withListenOnAllAddresses(boolean listenOnAllAddresses) {
- LOG.warn("withListenOnAllAddresses() is deprecated and will be removed in a future release. Use withNetworkInterface().");
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withSslEngineSource(
- SslEngineSource sslEngineSource) {
- this.sslEngineSource = sslEngineSource;
- if (this.mitmManager != null) {
- LOG.warn("Enabled encrypted inbound connections with man in the middle. "
- + "These are mutually exclusive - man in the middle will be disabled.");
- this.mitmManager = null;
- }
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withAuthenticateSslClients(
- boolean authenticateSslClients) {
- this.authenticateSslClients = authenticateSslClients;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withProxyAuthenticator(
- ProxyAuthenticator proxyAuthenticator) {
- this.proxyAuthenticator = proxyAuthenticator;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withChainProxyManager(
- ChainedProxyManager chainProxyManager) {
- this.chainProxyManager = chainProxyManager;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withManInTheMiddle(
- MitmManager mitmManager) {
- this.mitmManager = mitmManager;
- if (this.sslEngineSource != null) {
- LOG.warn("Enabled man in the middle with encrypted inbound connections. "
- + "These are mutually exclusive - encrypted inbound connections will be disabled.");
- this.sslEngineSource = null;
- }
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withFiltersSource(
- HttpFiltersSource filtersSource) {
- this.filtersSource = filtersSource;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
- if (useDnsSec) {
- this.serverResolver = new DnsSecServerResolver();
- } else {
- this.serverResolver = new DefaultHostResolver();
- }
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withTransparent(
- boolean transparent) {
- this.transparent = transparent;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withIdleConnectionTimeout(
- int idleConnectionTimeout) {
- this.idleConnectionTimeout = idleConnectionTimeout;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withConnectTimeout(
- int connectTimeout) {
- this.connectTimeout = connectTimeout;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withServerResolver(
- HostResolver serverResolver) {
- this.serverResolver = serverResolver;
- return this;
- }
- @Override
- public HttpProxyServerBootstrap withServerGroup(
- ServerGroup group) {
- this.serverGroup = group;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap plusActivityTracker(
- ActivityTracker activityTracker) {
- activityTrackers.add(activityTracker);
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withThrottling(long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
- this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
- this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withMaxInitialLineLength(int maxInitialLineLength){
- this.maxInitialLineLength = maxInitialLineLength;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withMaxHeaderSize(int maxHeaderSize){
- this.maxHeaderSize = maxHeaderSize;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize){
- this.maxChunkSize = maxChunkSize;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer) {
- this.allowRequestToOriginServer = allowRequestToOriginServer;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withAcceptProxyProtocol(boolean acceptProxyProtocol) {
- this.acceptProxyProtocol = acceptProxyProtocol;
- return this;
- }
-
- @Override
- public HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol) {
- this.sendProxyProtocol = sendProxyProtocol;
- return this;
- }
-
- @Override
- public HttpProxyServer start() {
- return build().start();
- }
-
- @Override
- public HttpProxyServerBootstrap withThreadPoolConfiguration(ThreadPoolConfiguration configuration) {
- this.clientToProxyAcceptorThreads = configuration.getAcceptorThreads();
- this.clientToProxyWorkerThreads = configuration.getClientToProxyWorkerThreads();
- this.proxyToServerWorkerThreads = configuration.getProxyToServerWorkerThreads();
- return this;
- }
-
- private DefaultHttpProxyServer build() {
- final ServerGroup serverGroup;
-
- if (this.serverGroup != null) {
- serverGroup = this.serverGroup;
- }
- else {
- serverGroup = new ServerGroup(name, clientToProxyAcceptorThreads, clientToProxyWorkerThreads, proxyToServerWorkerThreads);
- }
-
- return new DefaultHttpProxyServer(serverGroup,
- transportProtocol, determineListenAddress(),
- sslEngineSource, authenticateSslClients,
- proxyAuthenticator, chainProxyManager, mitmManager,
- filtersSource, transparent,
- idleConnectionTimeout, activityTrackers, connectTimeout,
- serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
- localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
- allowRequestToOriginServer, acceptProxyProtocol, sendProxyProtocol);
- }
-
- private InetSocketAddress determineListenAddress() {
- if (requestedAddress != null) {
- return requestedAddress;
- } else {
- // Binding only to localhost can significantly improve the
- // security of the proxy.
- if (allowLocalOnly) {
- return new InetSocketAddress("127.0.0.1", port);
- } else {
- return new InetSocketAddress(port);
- }
- }
- }
- }
+ switch (transportProtocol) {
+ case TCP:
+ LOG.info("Proxy listening with TCP transport");
+ serverBootstrap.channelFactory(NioServerSocketChannel::new);
+ break;
+ default:
+ throw new UnknownTransportProtocolException(transportProtocol);
+ }
+ serverBootstrap.childHandler(initializer);
+ ChannelFuture future = serverBootstrap.bind(requestedAddress).awaitUninterruptibly();
+
+ Throwable cause = future.cause();
+ if (cause != null) {
+ abort();
+ throw new RuntimeException(cause);
+ }
+
+ Channel serverChannel = future.channel();
+ registerChannel(serverChannel);
+ boundAddress = (InetSocketAddress) serverChannel.localAddress();
+ LOG.info("Proxy started at address: {}", boundAddress);
+
+ Runtime.getRuntime().addShutdownHook(jvmShutdownHook);
+ }
+
+ protected ChainedProxyManager getChainProxyManager() {
+ return chainProxyManager;
+ }
+
+ protected MitmManager getMitmManager() {
+ return mitmManager;
+ }
+
+ protected SslEngineSource getSslEngineSource() {
+ return sslEngineSource;
+ }
+
+ protected ProxyAuthenticator getProxyAuthenticator() {
+ return proxyAuthenticator;
+ }
+
+ public HttpFiltersSource getFiltersSource() {
+ return filtersSource;
+ }
+
+ protected Collection getActivityTrackers() {
+ return activityTrackers;
+ }
+
+ public String getProxyAlias() {
+ return proxyAlias;
+ }
+
+ protected EventLoopGroup getProxyToServerWorkerFor(TransportProtocol transportProtocol) {
+ return serverGroup.getProxyToServerWorkerPoolForTransport(transportProtocol);
+ }
}
diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerBootstrap.java
new file mode 100644
index 00000000..3a505ddc
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerBootstrap.java
@@ -0,0 +1,567 @@
+package org.littleshoot.proxy.impl;
+
+import static java.util.Objects.requireNonNullElseGet;
+
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.ChainedProxyManager;
+import org.littleshoot.proxy.DefaultHostResolver;
+import org.littleshoot.proxy.DnsSecServerResolver;
+import org.littleshoot.proxy.HostResolver;
+import org.littleshoot.proxy.HttpFiltersSource;
+import org.littleshoot.proxy.HttpFiltersSourceAdapter;
+import org.littleshoot.proxy.HttpProxyServer;
+import org.littleshoot.proxy.HttpProxyServerBootstrap;
+import org.littleshoot.proxy.Launcher;
+import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.ProxyAuthenticator;
+import org.littleshoot.proxy.ServerConnectionPoolType;
+import org.littleshoot.proxy.SslEngineSource;
+import org.littleshoot.proxy.TransportProtocol;
+import org.littleshoot.proxy.extras.ActivityLogger;
+import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class DefaultHttpProxyServerBootstrap implements HttpProxyServerBootstrap {
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpProxyServerBootstrap.class);
+
+ private static final String DEFAULT_LITTLE_PROXY_NAME = "LittleProxy";
+ private static final int MAX_INITIAL_LINE_LENGTH_DEFAULT = 8192;
+ private static final int MAX_HEADER_SIZE_DEFAULT = 8192 * 2;
+ private static final int MAX_CHUNK_SIZE_DEFAULT = 8192 * 2;
+ private static final String DEFAULT_JKS_KEYSTORE_PATH = "littleproxy_keystore.jks";
+
+ private String name = DEFAULT_LITTLE_PROXY_NAME;
+ @Nullable private ServerGroup serverGroup;
+ private TransportProtocol transportProtocol = TransportProtocol.TCP;
+ @Nullable private InetSocketAddress requestedAddress;
+ private int port = DefaultHttpProxyServer.DEFAULT_PORT;
+ private boolean allowLocalOnly = true;
+ @Nullable private SslEngineSource sslEngineSource;
+ private boolean authenticateSslClients = true;
+ @Nullable private ProxyAuthenticator proxyAuthenticator;
+ @Nullable private ChainedProxyManager chainProxyManager;
+ @Nullable private MitmManager mitmManager;
+ private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
+ private boolean transparent;
+ private Duration idleConnectionTimeout = Duration.ofSeconds(70);
+ private final Collection activityTrackers = new ConcurrentLinkedQueue<>();
+ private int connectTimeout = 40000;
+ private HostResolver serverResolver = new DefaultHostResolver();
+ private long readThrottleBytesPerSecond;
+ private long writeThrottleBytesPerSecond;
+ @Nullable private InetSocketAddress localAddress;
+ @Nullable private String proxyAlias;
+ private int clientToProxyAcceptorThreads = ServerGroup.DEFAULT_INCOMING_ACCEPTOR_THREADS;
+ private int clientToProxyWorkerThreads = ServerGroup.DEFAULT_INCOMING_WORKER_THREADS;
+ private int proxyToServerWorkerThreads = ServerGroup.DEFAULT_OUTGOING_WORKER_THREADS;
+ private int maxInitialLineLength = MAX_INITIAL_LINE_LENGTH_DEFAULT;
+ private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT;
+ private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT;
+ private boolean allowRequestToOriginServer;
+ private boolean acceptProxyProtocol;
+ private boolean sendProxyProtocol;
+ private boolean useSharedServerConnectionPool = false;
+ private int maxConnectionsPerHost = 10;
+ private int maxConnections = ConcurrentMapServerConnectionPool.DEFAULT_MAX_TOTAL_CONNECTIONS;
+ private ServerConnectionPoolType serverConnectionPoolType =
+ ServerConnectionPoolType.CONCURRENT_MAP;
+ @Nullable private Duration poolIdleTimeout;
+ private boolean poolSharedMitmConnections = false;
+ private boolean poolPerRequestInMitm = false;
+
+ DefaultHttpProxyServerBootstrap() {}
+
+ DefaultHttpProxyServerBootstrap(Properties props) {
+ withUseDnsSec(ProxyUtils.extractBooleanDefaultFalse(props, "dnssec"));
+ transparent = ProxyUtils.extractBooleanDefaultFalse(props, DefaultHttpProxyServer.TRANSPARENT);
+ idleConnectionTimeout =
+ Duration.ofSeconds(ProxyUtils.extractInt(props, "idle_connection_timeout"));
+ connectTimeout = ProxyUtils.extractInt(props, "connect_timeout", 0);
+ maxInitialLineLength =
+ ProxyUtils.extractInt(props, "max_initial_line_length", MAX_INITIAL_LINE_LENGTH_DEFAULT);
+ maxHeaderSize = ProxyUtils.extractInt(props, "max_header_size", MAX_HEADER_SIZE_DEFAULT);
+ maxChunkSize = ProxyUtils.extractInt(props, "max_chunk_size", MAX_CHUNK_SIZE_DEFAULT);
+ if (props.containsKey(DefaultHttpProxyServer.NAME)) {
+ name = props.getProperty(DefaultHttpProxyServer.NAME, DEFAULT_LITTLE_PROXY_NAME);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.ADDRESS)) {
+ requestedAddress =
+ ProxyUtils.resolveSocketAddress(props.getProperty(DefaultHttpProxyServer.ADDRESS));
+ }
+ if (props.containsKey(DefaultHttpProxyServer.PORT)) {
+ port = ProxyUtils.extractInt(props, DefaultHttpProxyServer.PORT, Launcher.DEFAULT_PORT);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.NIC)) {
+ localAddress =
+ new InetSocketAddress(
+ props.getProperty(
+ DefaultHttpProxyServer.NIC, DefaultHttpProxyServer.DEFAULT_NIC_VALUE),
+ 0);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.PROXY_ALIAS)) {
+ proxyAlias = props.getProperty(DefaultHttpProxyServer.PROXY_ALIAS);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.ALLOW_LOCAL_ONLY)) {
+ allowLocalOnly =
+ ProxyUtils.extractBooleanDefaultFalse(props, DefaultHttpProxyServer.ALLOW_LOCAL_ONLY);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.AUTHENTICATE_SSL_CLIENTS)) {
+ authenticateSslClients =
+ ProxyUtils.extractBooleanDefaultFalse(
+ props, DefaultHttpProxyServer.AUTHENTICATE_SSL_CLIENTS);
+ boolean trustAllServers =
+ ProxyUtils.extractBooleanDefaultFalse(
+ props, DefaultHttpProxyServer.SSL_CLIENTS_TRUST_ALL_SERVERS);
+ boolean sendCerts =
+ ProxyUtils.extractBooleanDefaultFalse(
+ props, DefaultHttpProxyServer.SSL_CLIENTS_SEND_CERTS);
+
+ if (authenticateSslClients
+ && props.containsKey(DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PATH)) {
+ String keyStorePath = props.getProperty(DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PATH);
+ if (props.containsKey(DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PASSWORD)) {
+ String keyStoreAlias =
+ props.getProperty(DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_ALIAS, "");
+ String keyStorePassword =
+ props.getProperty(DefaultHttpProxyServer.SSL_CLIENTS_KEYSTORE_PASSWORD, "");
+ sslEngineSource =
+ new SelfSignedSslEngineSource(
+ keyStorePath, trustAllServers, sendCerts, keyStoreAlias, keyStorePassword);
+ } else {
+ sslEngineSource = new SelfSignedSslEngineSource(keyStorePath, trustAllServers, sendCerts);
+ }
+ } else {
+ sslEngineSource =
+ new SelfSignedSslEngineSource(DEFAULT_JKS_KEYSTORE_PATH, trustAllServers, sendCerts);
+ }
+ }
+ if (props.containsKey(DefaultHttpProxyServer.TRANSPARENT)) {
+ transparent =
+ ProxyUtils.extractBooleanDefaultFalse(props, DefaultHttpProxyServer.TRANSPARENT);
+ }
+
+ if (props.containsKey(DefaultHttpProxyServer.THROTTLE_READ_BYTES_PER_SECOND)) {
+ readThrottleBytesPerSecond =
+ ProxyUtils.extractLong(props, DefaultHttpProxyServer.THROTTLE_READ_BYTES_PER_SECOND, 0L);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.THROTTLE_WRITE_BYTES_PER_SECOND)) {
+ writeThrottleBytesPerSecond =
+ ProxyUtils.extractLong(props, DefaultHttpProxyServer.THROTTLE_WRITE_BYTES_PER_SECOND, 0L);
+ }
+
+ if (props.containsKey(DefaultHttpProxyServer.ALLOW_REQUESTS_TO_ORIGIN_SERVER)) {
+ allowRequestToOriginServer =
+ ProxyUtils.extractBooleanDefaultFalse(
+ props, DefaultHttpProxyServer.ALLOW_REQUESTS_TO_ORIGIN_SERVER);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.ALLOW_PROXY_PROTOCOL)) {
+ acceptProxyProtocol =
+ ProxyUtils.extractBooleanDefaultFalse(props, DefaultHttpProxyServer.ALLOW_PROXY_PROTOCOL);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.SEND_PROXY_PROTOCOL)) {
+ sendProxyProtocol =
+ ProxyUtils.extractBooleanDefaultFalse(props, DefaultHttpProxyServer.SEND_PROXY_PROTOCOL);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.SERVER_CONNECTION_POOL_TYPE)) {
+ String poolTypeValue =
+ props.getProperty(DefaultHttpProxyServer.SERVER_CONNECTION_POOL_TYPE, "CONCURRENT_MAP");
+ try {
+ serverConnectionPoolType =
+ ServerConnectionPoolType.valueOf(poolTypeValue.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Unknown server connection pool type: {}", poolTypeValue);
+ }
+ }
+ if (props.containsKey(DefaultHttpProxyServer.USE_SHARED_SERVER_CONNECTION_POOL)) {
+ useSharedServerConnectionPool =
+ Boolean.parseBoolean(
+ props.getProperty(DefaultHttpProxyServer.USE_SHARED_SERVER_CONNECTION_POOL).trim());
+ }
+ if (props.containsKey(DefaultHttpProxyServer.MAX_CONNECTIONS_PER_HOST)) {
+ maxConnectionsPerHost =
+ ProxyUtils.extractInt(
+ props, DefaultHttpProxyServer.MAX_CONNECTIONS_PER_HOST, maxConnectionsPerHost);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.MAX_TOTAL_CONNECTIONS)) {
+ maxConnections =
+ ProxyUtils.extractInt(
+ props, DefaultHttpProxyServer.MAX_TOTAL_CONNECTIONS, maxConnections);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.POOL_SHARED_MITM_CONNECTIONS)) {
+ poolSharedMitmConnections =
+ Boolean.parseBoolean(
+ props.getProperty(DefaultHttpProxyServer.POOL_SHARED_MITM_CONNECTIONS).trim());
+ }
+ if (props.containsKey(DefaultHttpProxyServer.POOL_PER_REQUEST_IN_MITM)) {
+ poolPerRequestInMitm =
+ Boolean.parseBoolean(
+ props.getProperty(DefaultHttpProxyServer.POOL_PER_REQUEST_IN_MITM).trim());
+ }
+ if (props.containsKey(DefaultHttpProxyServer.CLIENT_TO_PROXY_WORKER_THREADS)) {
+ clientToProxyWorkerThreads =
+ ProxyUtils.extractInt(props, DefaultHttpProxyServer.CLIENT_TO_PROXY_WORKER_THREADS, 0);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.PROXY_TO_SERVER_WORKER_THREADS)) {
+ proxyToServerWorkerThreads =
+ ProxyUtils.extractInt(props, DefaultHttpProxyServer.PROXY_TO_SERVER_WORKER_THREADS, 0);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.ACCEPTOR_THREADS)) {
+ clientToProxyAcceptorThreads =
+ ProxyUtils.extractInt(props, DefaultHttpProxyServer.ACCEPTOR_THREADS, 0);
+ }
+ if (props.containsKey(DefaultHttpProxyServer.ACTIVITY_LOG_FORMAT)) {
+ String format = props.getProperty(DefaultHttpProxyServer.ACTIVITY_LOG_FORMAT);
+ try {
+ org.littleshoot.proxy.extras.LogFormat logFormat =
+ org.littleshoot.proxy.extras.LogFormat.valueOf(format.toUpperCase());
+ plusActivityTracker(new ActivityLogger(logFormat));
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Unknown activity log format requested in properties: {}", format);
+ }
+ }
+ }
+
+ DefaultHttpProxyServerBootstrap(ServerGroup serverGroup, DefaultHttpProxyServerConfig config) {
+ this.serverGroup = serverGroup;
+ this.transportProtocol = config.getTransportProtocol();
+ this.requestedAddress = config.getRequestedAddress();
+ this.port = config.getRequestedAddress().getPort();
+ this.sslEngineSource = config.getSslEngineSource();
+ this.authenticateSslClients = config.isAuthenticateSslClients();
+ this.proxyAuthenticator = config.getProxyAuthenticator();
+ this.chainProxyManager = config.getChainProxyManager();
+ this.mitmManager = config.getMitmManager();
+ this.filtersSource = config.getFiltersSource();
+ this.transparent = config.isTransparent();
+ this.idleConnectionTimeout = config.getIdleConnectionTimeout();
+ this.activityTrackers.addAll(config.getActivityTrackers());
+ this.connectTimeout = config.getConnectTimeout();
+ this.serverResolver = config.getServerResolver();
+ this.readThrottleBytesPerSecond = config.getReadThrottleBytesPerSecond();
+ this.writeThrottleBytesPerSecond = config.getWriteThrottleBytesPerSecond();
+ this.localAddress = config.getLocalAddress();
+ this.proxyAlias = config.getProxyAlias();
+ this.maxInitialLineLength = config.getMaxInitialLineLength();
+ this.maxHeaderSize = config.getMaxHeaderSize();
+ this.maxChunkSize = config.getMaxChunkSize();
+ this.allowRequestToOriginServer = config.isAllowRequestsToOriginServer();
+ this.acceptProxyProtocol = config.isAcceptProxyProtocol();
+ this.sendProxyProtocol = config.isSendProxyProtocol();
+ ServerConnectionPoolConfig poolConfig = config.getServerConnectionPoolConfig();
+ this.useSharedServerConnectionPool = poolConfig.isEnabled();
+ this.maxConnectionsPerHost = poolConfig.getMaxConnectionsPerHost();
+ this.serverConnectionPoolType = poolConfig.getPoolType();
+ this.maxConnections = poolConfig.getMaxConnections();
+ this.poolIdleTimeout = poolConfig.getIdleTimeout();
+ this.poolSharedMitmConnections = poolConfig.isPoolSharedMitmConnections();
+ this.poolPerRequestInMitm = poolConfig.isPoolPerRequestInMitm();
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withAddress(InetSocketAddress address) {
+ requestedAddress = address;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withPort(int port) {
+ requestedAddress = null;
+ this.port = port;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withNetworkInterface(InetSocketAddress inetSocketAddress) {
+ localAddress = inetSocketAddress;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withProxyAlias(String alias) {
+ proxyAlias = alias;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withAllowLocalOnly(boolean allowLocalOnly) {
+ this.allowLocalOnly = allowLocalOnly;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withSslEngineSource(SslEngineSource sslEngineSource) {
+ this.sslEngineSource = sslEngineSource;
+ if (mitmManager != null) {
+ LOG.warn(
+ "Enabled encrypted inbound connections with man in the middle. "
+ + "These are mutually exclusive - man in the middle will be disabled.");
+ mitmManager = null;
+ }
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withAuthenticateSslClients(boolean authenticateSslClients) {
+ this.authenticateSslClients = authenticateSslClients;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withProxyAuthenticator(ProxyAuthenticator proxyAuthenticator) {
+ this.proxyAuthenticator = proxyAuthenticator;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withChainProxyManager(ChainedProxyManager chainProxyManager) {
+ this.chainProxyManager = chainProxyManager;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withManInTheMiddle(MitmManager mitmManager) {
+ this.mitmManager = mitmManager;
+ if (sslEngineSource != null) {
+ LOG.warn(
+ "Enabled man in the middle with encrypted inbound connections. "
+ + "These are mutually exclusive - encrypted inbound connections will be disabled.");
+ sslEngineSource = null;
+ }
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withFiltersSource(HttpFiltersSource filtersSource) {
+ this.filtersSource = filtersSource;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
+ if (useDnsSec) {
+ serverResolver = new DnsSecServerResolver();
+ } else {
+ serverResolver = new DefaultHostResolver();
+ }
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withTransparent(boolean transparent) {
+ this.transparent = transparent;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withIdleConnectionTimeout(int idleConnectionTimeoutInSeconds) {
+ this.idleConnectionTimeout = Duration.ofSeconds(idleConnectionTimeoutInSeconds);
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withIdleConnectionTimeout(Duration idleConnectionTimeout) {
+ this.idleConnectionTimeout = idleConnectionTimeout;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withConnectTimeout(int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withServerResolver(HostResolver serverResolver) {
+ this.serverResolver = serverResolver;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withServerGroup(ServerGroup group) {
+ serverGroup = group;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap plusActivityTracker(ActivityTracker activityTracker) {
+ activityTrackers.add(activityTracker);
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withThrottling(
+ long readThrottleBytesPerSecond, long writeThrottleBytesPerSecond) {
+ this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
+ this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withMaxInitialLineLength(int maxInitialLineLength) {
+ this.maxInitialLineLength = maxInitialLineLength;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withMaxHeaderSize(int maxHeaderSize) {
+ this.maxHeaderSize = maxHeaderSize;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize) {
+ this.maxChunkSize = maxChunkSize;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withAllowRequestToOriginServer(
+ boolean allowRequestToOriginServer) {
+ this.allowRequestToOriginServer = allowRequestToOriginServer;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withAcceptProxyProtocol(boolean acceptProxyProtocol) {
+ this.acceptProxyProtocol = acceptProxyProtocol;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withSendProxyProtocol(boolean sendProxyProtocol) {
+ this.sendProxyProtocol = sendProxyProtocol;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withServerConnectionPoolType(ServerConnectionPoolType poolType) {
+ this.serverConnectionPoolType =
+ poolType != null ? poolType : ServerConnectionPoolType.CONCURRENT_MAP;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withSharedServerConnectionPool(
+ boolean useSharedServerConnectionPool) {
+ this.useSharedServerConnectionPool = useSharedServerConnectionPool;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withMaxConnectionsPerHost(int maxConnectionsPerHost) {
+ this.maxConnectionsPerHost = maxConnectionsPerHost;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withMaxConnections(int maxConnections) {
+ this.maxConnections = maxConnections;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withPoolIdleTimeout(Duration idleTimeout) {
+ this.poolIdleTimeout = idleTimeout;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withPoolSharedMitmConnections(boolean poolSharedMitmConnections) {
+ this.poolSharedMitmConnections = poolSharedMitmConnections;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withPoolPerRequestInMitm(boolean poolPerRequestInMitm) {
+ this.poolPerRequestInMitm = poolPerRequestInMitm;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServer start() {
+ return build().start();
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withThreadPoolConfiguration(
+ ThreadPoolConfiguration configuration) {
+ clientToProxyAcceptorThreads = configuration.getAcceptorThreads();
+ clientToProxyWorkerThreads = configuration.getClientToProxyWorkerThreads();
+ proxyToServerWorkerThreads = configuration.getProxyToServerWorkerThreads();
+ return this;
+ }
+
+ private DefaultHttpProxyServer build() {
+ final ServerGroup selectedServerGroup =
+ requireNonNullElseGet(
+ this.serverGroup,
+ () ->
+ new ServerGroup(
+ name,
+ clientToProxyAcceptorThreads,
+ clientToProxyWorkerThreads,
+ proxyToServerWorkerThreads));
+
+ ServerConnectionPoolConfig poolConfig =
+ new ServerConnectionPoolConfig()
+ .setEnabled(useSharedServerConnectionPool)
+ .setPoolType(serverConnectionPoolType)
+ .setMaxConnectionsPerHost(maxConnectionsPerHost)
+ .setMaxConnections(maxConnections)
+ .setIdleTimeout(poolIdleTimeout)
+ .setPoolSharedMitmConnections(poolSharedMitmConnections)
+ .setPoolPerRequestInMitm(poolPerRequestInMitm);
+
+ DefaultHttpProxyServerConfig serverConfig =
+ new DefaultHttpProxyServerConfig()
+ .setTransportProtocol(transportProtocol)
+ .setRequestedAddress(determineListenAddress())
+ .setSslEngineSource(sslEngineSource)
+ .setAuthenticateSslClients(authenticateSslClients)
+ .setProxyAuthenticator(proxyAuthenticator)
+ .setChainProxyManager(chainProxyManager)
+ .setMitmManager(mitmManager)
+ .setFiltersSource(filtersSource)
+ .setTransparent(transparent)
+ .setIdleConnectionTimeout(idleConnectionTimeout)
+ .setActivityTrackers(activityTrackers)
+ .setConnectTimeout(connectTimeout)
+ .setServerResolver(serverResolver)
+ .setReadThrottleBytesPerSecond(readThrottleBytesPerSecond)
+ .setWriteThrottleBytesPerSecond(writeThrottleBytesPerSecond)
+ .setLocalAddress(localAddress)
+ .setProxyAlias(proxyAlias)
+ .setMaxInitialLineLength(maxInitialLineLength)
+ .setMaxHeaderSize(maxHeaderSize)
+ .setMaxChunkSize(maxChunkSize)
+ .setAllowRequestsToOriginServer(allowRequestToOriginServer)
+ .setAcceptProxyProtocol(acceptProxyProtocol)
+ .setSendProxyProtocol(sendProxyProtocol)
+ .setServerConnectionPoolConfig(poolConfig);
+
+ return new DefaultHttpProxyServer(selectedServerGroup, serverConfig);
+ }
+
+ private InetSocketAddress determineListenAddress() {
+ if (requestedAddress != null) {
+ return requestedAddress;
+ }
+ if (allowLocalOnly) {
+ return new InetSocketAddress(DefaultHttpProxyServer.LOCAL_ADDRESS, port);
+ }
+ return new InetSocketAddress(port);
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerConfig.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerConfig.java
new file mode 100644
index 00000000..b34d6aa8
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerConfig.java
@@ -0,0 +1,276 @@
+package org.littleshoot.proxy.impl;
+
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.ChainedProxyManager;
+import org.littleshoot.proxy.HostResolver;
+import org.littleshoot.proxy.HttpFiltersSource;
+import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.ProxyAuthenticator;
+import org.littleshoot.proxy.SslEngineSource;
+import org.littleshoot.proxy.TransportProtocol;
+
+public class DefaultHttpProxyServerConfig {
+ private TransportProtocol transportProtocol;
+ private InetSocketAddress requestedAddress;
+ @Nullable private SslEngineSource sslEngineSource;
+ private boolean authenticateSslClients;
+ @Nullable private ProxyAuthenticator proxyAuthenticator;
+ @Nullable private ChainedProxyManager chainProxyManager;
+ @Nullable private MitmManager mitmManager;
+ private HttpFiltersSource filtersSource;
+ private boolean transparent;
+ private Duration idleConnectionTimeout;
+ private final Collection activityTrackers = new ConcurrentLinkedQueue<>();
+ private int connectTimeout;
+ private HostResolver serverResolver;
+ private long readThrottleBytesPerSecond;
+ private long writeThrottleBytesPerSecond;
+ @Nullable private InetSocketAddress localAddress;
+ @Nullable private String proxyAlias;
+ private int maxInitialLineLength;
+ private int maxHeaderSize;
+ private int maxChunkSize;
+ private boolean allowRequestsToOriginServer;
+ private boolean acceptProxyProtocol;
+ private boolean sendProxyProtocol;
+ private ServerConnectionPoolConfig serverConnectionPoolConfig = new ServerConnectionPoolConfig();
+
+ public TransportProtocol getTransportProtocol() {
+ return transportProtocol;
+ }
+
+ public DefaultHttpProxyServerConfig setTransportProtocol(TransportProtocol transportProtocol) {
+ this.transportProtocol = transportProtocol;
+ return this;
+ }
+
+ public InetSocketAddress getRequestedAddress() {
+ return requestedAddress;
+ }
+
+ public DefaultHttpProxyServerConfig setRequestedAddress(InetSocketAddress requestedAddress) {
+ this.requestedAddress = requestedAddress;
+ return this;
+ }
+
+ @Nullable
+ public SslEngineSource getSslEngineSource() {
+ return sslEngineSource;
+ }
+
+ public DefaultHttpProxyServerConfig setSslEngineSource(
+ @Nullable SslEngineSource sslEngineSource) {
+ this.sslEngineSource = sslEngineSource;
+ return this;
+ }
+
+ public boolean isAuthenticateSslClients() {
+ return authenticateSslClients;
+ }
+
+ public DefaultHttpProxyServerConfig setAuthenticateSslClients(boolean authenticateSslClients) {
+ this.authenticateSslClients = authenticateSslClients;
+ return this;
+ }
+
+ @Nullable
+ public ProxyAuthenticator getProxyAuthenticator() {
+ return proxyAuthenticator;
+ }
+
+ public DefaultHttpProxyServerConfig setProxyAuthenticator(
+ @Nullable ProxyAuthenticator proxyAuthenticator) {
+ this.proxyAuthenticator = proxyAuthenticator;
+ return this;
+ }
+
+ @Nullable
+ public ChainedProxyManager getChainProxyManager() {
+ return chainProxyManager;
+ }
+
+ public DefaultHttpProxyServerConfig setChainProxyManager(
+ @Nullable ChainedProxyManager chainProxyManager) {
+ this.chainProxyManager = chainProxyManager;
+ return this;
+ }
+
+ @Nullable
+ public MitmManager getMitmManager() {
+ return mitmManager;
+ }
+
+ public DefaultHttpProxyServerConfig setMitmManager(@Nullable MitmManager mitmManager) {
+ this.mitmManager = mitmManager;
+ return this;
+ }
+
+ public HttpFiltersSource getFiltersSource() {
+ return filtersSource;
+ }
+
+ public DefaultHttpProxyServerConfig setFiltersSource(HttpFiltersSource filtersSource) {
+ this.filtersSource = filtersSource;
+ return this;
+ }
+
+ public boolean isTransparent() {
+ return transparent;
+ }
+
+ public DefaultHttpProxyServerConfig setTransparent(boolean transparent) {
+ this.transparent = transparent;
+ return this;
+ }
+
+ public Duration getIdleConnectionTimeout() {
+ return idleConnectionTimeout;
+ }
+
+ public DefaultHttpProxyServerConfig setIdleConnectionTimeout(Duration idleConnectionTimeout) {
+ this.idleConnectionTimeout = idleConnectionTimeout;
+ return this;
+ }
+
+ public Collection getActivityTrackers() {
+ return activityTrackers;
+ }
+
+ public DefaultHttpProxyServerConfig setActivityTrackers(
+ Collection activityTrackers) {
+ this.activityTrackers.clear();
+ this.activityTrackers.addAll(activityTrackers);
+ return this;
+ }
+
+ public int getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ public DefaultHttpProxyServerConfig setConnectTimeout(int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ public HostResolver getServerResolver() {
+ return serverResolver;
+ }
+
+ public DefaultHttpProxyServerConfig setServerResolver(HostResolver serverResolver) {
+ this.serverResolver = serverResolver;
+ return this;
+ }
+
+ public long getReadThrottleBytesPerSecond() {
+ return readThrottleBytesPerSecond;
+ }
+
+ public DefaultHttpProxyServerConfig setReadThrottleBytesPerSecond(
+ long readThrottleBytesPerSecond) {
+ this.readThrottleBytesPerSecond = readThrottleBytesPerSecond;
+ return this;
+ }
+
+ public long getWriteThrottleBytesPerSecond() {
+ return writeThrottleBytesPerSecond;
+ }
+
+ public DefaultHttpProxyServerConfig setWriteThrottleBytesPerSecond(
+ long writeThrottleBytesPerSecond) {
+ this.writeThrottleBytesPerSecond = writeThrottleBytesPerSecond;
+ return this;
+ }
+
+ @Nullable
+ public InetSocketAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ public DefaultHttpProxyServerConfig setLocalAddress(@Nullable InetSocketAddress localAddress) {
+ this.localAddress = localAddress;
+ return this;
+ }
+
+ @Nullable
+ public String getProxyAlias() {
+ return proxyAlias;
+ }
+
+ public DefaultHttpProxyServerConfig setProxyAlias(@Nullable String proxyAlias) {
+ this.proxyAlias = proxyAlias;
+ return this;
+ }
+
+ public int getMaxInitialLineLength() {
+ return maxInitialLineLength;
+ }
+
+ public DefaultHttpProxyServerConfig setMaxInitialLineLength(int maxInitialLineLength) {
+ this.maxInitialLineLength = maxInitialLineLength;
+ return this;
+ }
+
+ public int getMaxHeaderSize() {
+ return maxHeaderSize;
+ }
+
+ public DefaultHttpProxyServerConfig setMaxHeaderSize(int maxHeaderSize) {
+ this.maxHeaderSize = maxHeaderSize;
+ return this;
+ }
+
+ public int getMaxChunkSize() {
+ return maxChunkSize;
+ }
+
+ public DefaultHttpProxyServerConfig setMaxChunkSize(int maxChunkSize) {
+ this.maxChunkSize = maxChunkSize;
+ return this;
+ }
+
+ public boolean isAllowRequestsToOriginServer() {
+ return allowRequestsToOriginServer;
+ }
+
+ public DefaultHttpProxyServerConfig setAllowRequestsToOriginServer(
+ boolean allowRequestsToOriginServer) {
+ this.allowRequestsToOriginServer = allowRequestsToOriginServer;
+ return this;
+ }
+
+ public boolean isAcceptProxyProtocol() {
+ return acceptProxyProtocol;
+ }
+
+ public DefaultHttpProxyServerConfig setAcceptProxyProtocol(boolean acceptProxyProtocol) {
+ this.acceptProxyProtocol = acceptProxyProtocol;
+ return this;
+ }
+
+ public boolean isSendProxyProtocol() {
+ return sendProxyProtocol;
+ }
+
+ public DefaultHttpProxyServerConfig setSendProxyProtocol(boolean sendProxyProtocol) {
+ this.sendProxyProtocol = sendProxyProtocol;
+ return this;
+ }
+
+ public ServerConnectionPoolConfig getServerConnectionPoolConfig() {
+ return serverConnectionPoolConfig;
+ }
+
+ public DefaultHttpProxyServerConfig setServerConnectionPoolConfig(
+ ServerConnectionPoolConfig serverConnectionPoolConfig) {
+ this.serverConnectionPoolConfig =
+ serverConnectionPoolConfig != null
+ ? serverConnectionPoolConfig
+ : new ServerConnectionPoolConfig();
+ return this;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/Hostname.java b/src/main/java/org/littleshoot/proxy/impl/Hostname.java
new file mode 100644
index 00000000..ab8d8f52
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/Hostname.java
@@ -0,0 +1,87 @@
+package org.littleshoot.proxy.impl;
+
+import static java.lang.System.nanoTime;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.stream.Stream;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class Hostname {
+ private static final Logger LOG = LoggerFactory.getLogger(Hostname.class);
+
+ private static volatile String hostname;
+
+ @Nullable
+ static String getHostName() {
+ if (hostname == null) {
+ hostname = resolveHostName();
+ }
+ return hostname;
+ }
+
+ @Nullable
+ private static String resolveHostName() {
+ long startTime = nanoTime();
+ String hostName =
+ byAllMeans(
+ env("HOSTNAME"), // Most OSs
+ env("COMPUTERNAME"), // Windows
+ Hostname::executeHostname,
+ Hostname::getLocalHost);
+ long duration = NANOSECONDS.toMillis(nanoTime() - startTime);
+ LOG.info("Resolved local machine's hostname \"{}\" in {} ms.", hostName, duration);
+ return hostName;
+ }
+
+ @Nullable
+ @SafeVarargs
+ private static String byAllMeans(SupplierEx... means) {
+ return Stream.of(means)
+ .map(mean -> getOrNull(mean))
+ .filter(host -> host != null)
+ .findFirst()
+ .orElse(null);
+ }
+
+ @Nullable
+ private static String getOrNull(SupplierEx s) {
+ try {
+ return s.get();
+ } catch (Exception e) {
+ LOG.info("Failed to resolve local machine's hostname", e);
+ return null;
+ }
+ }
+
+ private static SupplierEx env(String name) {
+ return () -> System.getenv(name);
+ }
+
+ /**
+ * "hostname" command works on Windows, Mac, and Linux. Usually much faster than {@link
+ * InetAddress#getLocalHost()}.
+ */
+ private static String executeHostname() throws IOException, InterruptedException {
+ Process p = new ProcessBuilder("hostname").start();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+ String line = reader.readLine();
+ if (p.waitFor(5, SECONDS) && line != null) {
+ return line.trim();
+ }
+ }
+ return null;
+ }
+
+ private static String getLocalHost() throws UnknownHostException {
+ return InetAddress.getLocalHost().getHostName();
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/NetworkUtils.java b/src/main/java/org/littleshoot/proxy/impl/NetworkUtils.java
deleted file mode 100644
index 811dbabf..00000000
--- a/src/main/java/org/littleshoot/proxy/impl/NetworkUtils.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.littleshoot.proxy.impl;
-
-import java.net.*;
-import java.util.Enumeration;
-
-/**
- * @deprecated This class is no longer used by LittleProxy and may be removed in a future release.
- */
-@Deprecated
-public class NetworkUtils {
- /**
- * @deprecated This method is no longer used by LittleProxy and may be removed in a future release.
- */
- @Deprecated
- public static InetAddress getLocalHost() throws UnknownHostException {
- return InetAddress.getLocalHost();
- }
-
- /**
- * @deprecated This method is no longer used by LittleProxy and may be removed in a future release.
- */
- @Deprecated
- public static InetAddress firstLocalNonLoopbackIpv4Address() {
- try {
- Enumeration networkInterfaces = NetworkInterface
- .getNetworkInterfaces();
- while (networkInterfaces.hasMoreElements()) {
- NetworkInterface networkInterface = networkInterfaces
- .nextElement();
- if (networkInterface.isUp()) {
- for (InterfaceAddress ifAddress : networkInterface
- .getInterfaceAddresses()) {
- if (ifAddress.getNetworkPrefixLength() > 0
- && ifAddress.getNetworkPrefixLength() <= 32
- && !ifAddress.getAddress().isLoopbackAddress()) {
- return ifAddress.getAddress();
- }
- }
- }
- }
- return null;
- } catch (SocketException se) {
- return null;
- }
- }
-
-}
diff --git a/src/main/java/org/littleshoot/proxy/impl/PendingRequest.java b/src/main/java/org/littleshoot/proxy/impl/PendingRequest.java
new file mode 100644
index 00000000..9ddfdb87
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/PendingRequest.java
@@ -0,0 +1,38 @@
+package org.littleshoot.proxy.impl;
+
+import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.HttpFilters;
+
+/**
+ * Tracks a pending request and its associated client connection and filters for HTTP pipelining.
+ */
+public class PendingRequest {
+ private final ClientToProxyConnection clientConnection;
+ private final HttpRequest request;
+ private final HttpFilters filters;
+ private final long timestamp;
+
+ public PendingRequest(
+ ClientToProxyConnection clientConnection, HttpRequest request, HttpFilters filters) {
+ this.clientConnection = clientConnection;
+ this.request = request;
+ this.filters = filters;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public ClientToProxyConnection getClientConnection() {
+ return clientConnection;
+ }
+
+ public HttpRequest getRequest() {
+ return request;
+ }
+
+ public HttpFilters getFilters() {
+ return filters;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/PoolMetrics.java b/src/main/java/org/littleshoot/proxy/impl/PoolMetrics.java
new file mode 100644
index 00000000..9ebbb314
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/impl/PoolMetrics.java
@@ -0,0 +1,77 @@
+package org.littleshoot.proxy.impl;
+
+/** Pool metrics statistics. */
+public class PoolMetrics {
+ private final int totalConnections;
+ private final int activeConnections;
+ private final int idleConnections;
+ private final long borrowCount;
+ private final long returnCount;
+ private final long evictionCount;
+ private final long validationFailureCount;
+
+ public PoolMetrics(
+ int totalConnections,
+ int activeConnections,
+ int idleConnections,
+ long borrowCount,
+ long returnCount,
+ long evictionCount,
+ long validationFailureCount) {
+ this.totalConnections = totalConnections;
+ this.activeConnections = activeConnections;
+ this.idleConnections = idleConnections;
+ this.borrowCount = borrowCount;
+ this.returnCount = returnCount;
+ this.evictionCount = evictionCount;
+ this.validationFailureCount = validationFailureCount;
+ }
+
+ public int getTotalConnections() {
+ return totalConnections;
+ }
+
+ public int getActiveConnections() {
+ return activeConnections;
+ }
+
+ public int getIdleConnections() {
+ return idleConnections;
+ }
+
+ public long getBorrowCount() {
+ return borrowCount;
+ }
+
+ public long getReturnCount() {
+ return returnCount;
+ }
+
+ public long getEvictionCount() {
+ return evictionCount;
+ }
+
+ public long getValidationFailureCount() {
+ return validationFailureCount;
+ }
+
+ @Override
+ public String toString() {
+ return "PoolMetrics{"
+ + "total="
+ + totalConnections
+ + ", active="
+ + activeConnections
+ + ", idle="
+ + idleConnections
+ + ", borrowCount="
+ + borrowCount
+ + ", returnCount="
+ + returnCount
+ + ", evictionCount="
+ + evictionCount
+ + ", validationFailureCount="
+ + validationFailureCount
+ + '}';
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
index 782537dc..f924f830 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
@@ -1,5 +1,7 @@
package org.littleshoot.proxy.impl;
+import static org.littleshoot.proxy.impl.ConnectionState.*;
+
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
@@ -10,792 +12,743 @@
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
-import org.littleshoot.proxy.HttpFilters;
-
+import java.util.concurrent.atomic.AtomicLong;
import javax.net.ssl.SSLEngine;
-
-import static org.littleshoot.proxy.impl.ConnectionState.*;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.littleshoot.proxy.HttpFilters;
/**
- *
* Base class for objects that represent a connection to/from our proxy.
- *
- *
- * A ProxyConnection models a bidirectional message flow on top of a Netty
- * {@link Channel}.
- *
- *
- * The {@link #read(Object)} method is called whenever a new message arrives on
- * the underlying socket.
- *
- *
- * The {@link #write(Object)} method can be called by anyone wanting to write
- * data out of the connection.
- *
- *
- * ProxyConnection has a lifecycle and its current state within that lifecycle
- * is recorded as a {@link ConnectionState}. The allowed states and transitions
- * vary a little depending on the concrete implementation of ProxyConnection.
- * However, all ProxyConnections share the following lifecycle events:
- *
- *
+ *
+ *
A ProxyConnection models a bidirectional message flow on top of a Netty {@link Channel}.
+ *
+ *
The {@link #read(Object)} method is called whenever a new message arrives on the underlying
+ * socket.
+ *
+ *
The {@link #write(Object)} method can be called by anyone wanting to write data out of the
+ * connection.
+ *
+ *
ProxyConnection has a lifecycle and its current state within that lifecycle is recorded as a
+ * {@link ConnectionState}. The allowed states and transitions vary a little depending on the
+ * concrete implementation of ProxyConnection. However, all ProxyConnections share the following
+ * lifecycle events:
+ *
*
- *
{@link #connected()} - Once the underlying channel is active, the
- * ProxyConnection is considered connected and moves into
- * {@link ConnectionState#AWAITING_INITIAL}. The Channel is recorded at this
- * time for later referencing.
- *
{@link #disconnected()} - When the underlying channel goes inactive, the
- * ProxyConnection moves into {@link ConnectionState#DISCONNECTED}
- *
{@link #becameWritable()} - When the underlying channel becomes
- * writeable, this callback is invoked.
+ *
{@link #connected()} - Once the underlying channel is active, the ProxyConnection is
+ * considered connected and moves into {@link ConnectionState#AWAITING_INITIAL}. The Channel
+ * is recorded at this time for later referencing.
+ *
{@link #disconnected()} - When the underlying channel goes inactive, the ProxyConnection
+ * moves into {@link ConnectionState#DISCONNECTED}
+ *
{@link #becameWritable()} - When the underlying channel becomes writeable, this callback is
+ * invoked.
*
- *
- *
- * By default, incoming data on the underlying channel is automatically read and
- * passed to the {@link #read(Object)} method. Reading can be stopped and
- * resumed using {@link #stopReading()} and {@link #resumeReading()}.
- *
- *
- * @param
- * the type of "initial" message. This will be either
- * {@link HttpResponse} or {@link HttpRequest}.
+ *
+ *
By default, incoming data on the underlying channel is automatically read and passed to the
+ * {@link #read(Object)} method. Reading can be stopped and resumed using {@link #stopReading()} and
+ * {@link #resumeReading()}.
+ *
+ * @param the type of "initial" message. This will be either {@link HttpResponse} or {@link
+ * HttpRequest}.
*/
-abstract class ProxyConnection extends
- SimpleChannelInboundHandler