All Implemented Interfaces:
Runnable, EventDispatcher
Direct Known Subclasses:
I2PTunnelHTTPBidirProxy

public class I2PTunnelHTTPClientextends I2PTunnelHTTPClientBase implements Runnable
HTTP proxy that tunnels requests through I2P.

Handles various request formats and forwards through I2P:

  $method http://$site[$port]/$path $protocolVersion
  $method $path $protocolVersion\nHost: $site
  $method http://i2p/$b64key/$path $protocolVersion
  $method /$site/$path $protocolVersion
  (deprecated) $method /eepproxy/$site/$path $protocolVersion

CONNECT (HTTPS) supported since 0.9.11. Note: http://i2p/$b64key/... and /eepproxy/$site/... not recommended for browsers due to broken relative links and cookies. http://$b64key/... and http://$b64key.i2p/... unsupported (illegal characters). Rewrite as http://i2p/$b64key/...

Directs I2P-resolved sites to destinations, others to outproxy (typically "squid.i2p"). Supports HTTP and HTTPS only. GET and POST tested; other methods should work.

  • Field Details

    • addressHelpers

      private final ConcurrentHashMap<String,String> addressHelpers
      Map of host name to base64 destination for destinations collected via address helper links.
    • _pageNonce

      private final String _pageNonce
      Used to protect actions via http://proxy.i2p/
    • AUTH_REALM

      public static final String AUTH_REALM
      HTTP authentication realm for HTTP proxy.
      See Also:
    • UA_I2P

      private static final String UA_I2P
      User-Agent header sent to I2P sites.
      See Also:
    • UA_CLEARNET

      private static final String UA_CLEARNET
      User-Agent header sent to clearnet sites.
      See Also:
    • PROP_UA_I2P

      private static final String PROP_UA_I2P
      Config key overriding the I2P user agent.
      See Also:
    • PROP_UA_CLEARNET

      private static final String PROP_UA_CLEARNET
      Config key overriding the clearnet user agent.
      See Also:
    • OPT_KEEPALIVE_BROWSER

      public static final String OPT_KEEPALIVE_BROWSER
      Config key to enable browser-side keepalive.
      See Also:
    • OPT_KEEPALIVE_I2P

      public static final String OPT_KEEPALIVE_I2P
      Config key to enable I2P-side keepalive.
      See Also:
    • OPT_EMPTY_RETRIES

      public static final String OPT_EMPTY_RETRIES
      Config key: max reconnects on a zero-byte (empty) upstream response for GET/HEAD.
      See Also:
    • BROWSER_KEEPALIVE_TIMEOUT

      static final int BROWSER_KEEPALIVE_TIMEOUT
      How long to wait for another request on the same socket.
      See Also:
    • DEFAULT_KEEPALIVE_BROWSER

      private static final boolean DEFAULT_KEEPALIVE_BROWSER
      Default keep-alive setting for browser connections.
      See Also:
    • I2P_CONNECT_MAX_RETRIES

      static final int I2P_CONNECT_MAX_RETRIES
      Total connect attempts before the client stops retrying and fails the request.
      See Also:
    • I2P_CONNECT_RETRY_BASE_DELAY

      static final long I2P_CONNECT_RETRY_BASE_DELAY
      Backoff floor and per-attempt multiply base for I2P connect retries.
      See Also:
    • DEFAULT_KEEPALIVE_I2P

      private static final boolean DEFAULT_KEEPALIVE_I2P
      Default keep-alive setting for I2P connections.
      See Also:
    • DEFAULT_EMPTY_RETRIES

      private static final int DEFAULT_EMPTY_RETRIES
      Default reconnects on an empty upstream response before giving up.
      See Also:
    • NAMING_IN_SESSION_TIMEOUT_MS

      static final long NAMING_IN_SESSION_TIMEOUT_MS
      In-session b32/b33 naming lookup budget, clamped to the shared request connect deadline so naming cannot outlive the request.
      See Also:
    • NAMING_SERVICE_TIMEOUT_MS

      static final long NAMING_SERVICE_TIMEOUT_MS
      Naming-service lookup budget for out-of-session and hostname lookups, clamped to the shared request connect deadline.
      See Also:
    • MAX_CONNS_PER_DEST

      private static final int MAX_CONNS_PER_DEST
      Per-destination concurrent outbound connection limit. Caps the number of simultaneous I2P sockets the HTTP proxy opens to the same destination. Without any gate, a misbehaving client page-load can fire many parallel requests that each independently call createI2PSocket(), creating a SYN storm to the remote server (20+ simultaneous SYNs observed). The server's inbound SYN-burst gate (ConnectionManager.checkSynBurst) records a strike for exceeding tempBanSynBurst SYNs in tempBanSynRate ms and autobans on a second strike within 60 seconds.

      The gate is pure insurance: browsers already self-limit parallel connections per host (typically 6 over HTTP/1.1, fewer under HTTP/2), and keep-alive means steady-state concurrency per destination is low. The cap is therefore set high enough that a legitimate page-load burst can never hit it -- being tied to the same destination means kicking a 5th+ parallel request would make a browser asset silently fail -- while still bounding an out-of-control client (hundreds of connections) far below the network-path burst that trips the remote SYN-burst gate.

      A permit is acquired before createI2PSocket() and released when the I2P socket is closed (after the tunnel-runner completes). With I2P keepalive the socket is reopened per request, so the permit lifecycle matches the actual connection lifetime. A dual-race secondary socket holds an extra permit only until the race settles.

      See Also:
    • _activeConns

      private static final ConcurrentHashMap<Hash, AtomicInteger> _activeConns
      Active outbound I2P socket count per destination hash. AtomicInteger values; incremented before createI2PSocket(), decremented when the socket closes.
    • _raceBudget

      private static final ConcurrentHashMap<Hash,long[]> _raceBudget
      Per-remote-dest race token bucket for empty-response dual-race opens. Values are long[2] = {tokensMilli, lastRefillMs}, shared across all parallel requests to the same dest so the SYN budget is a property of the remote (one source dest from their view), not of a single browser request. See I2PTunnelRunner.tryConsumeRaceBudget(long[], long, int).
    • ERR_REQUEST_DENIED

      private static final String ERR_REQUEST_DENIED
      These are backups if the xxx.ht error page is missing.
      See Also:
    • ERR_BLACKLISTED

      private static final String ERR_BLACKLISTED
      Error response for a blacklisted address.
      See Also:
    • ERR_NO_OUTPROXY

      private static final String ERR_NO_OUTPROXY
      Error response when no outproxy is configured.
      See Also:
    • ERR_AHELPER_CONFLICT

      private static final String ERR_AHELPER_CONFLICT
      Error response for an address helper key conflict.
      See Also:
    • ERR_AHELPER_NOTFOUND

      private static final String ERR_AHELPER_NOTFOUND
      Error response for an unresolvable helper key.
      See Also:
    • ERR_AHELPER_NEW

      private static final String ERR_AHELPER_NEW
      Error response for a new address helper host.
      See Also:
    • ERR_BAD_PROTOCOL

      private static final String ERR_BAD_PROTOCOL
      Error response for a non-HTTP request protocol.
      See Also:
    • ERR_BAD_URI

      private static final String ERR_BAD_URI
      Error response for an invalid request URI.
      See Also:
    • ERR_LOCALHOST

      private static final String ERR_LOCALHOST
      Error response for a localhost request through the proxy.
      See Also:
    • ERR_INTERNAL_SSL

      private static final String ERR_INTERNAL_SSL
      Error response for SSL requested to an I2P address.
      See Also:
    • isr

      private InternalSocketRunner isr
      Socket runner for private mode, null otherwise.
    • HELPER_PARAM

      private static final String HELPER_PARAM
      URL parameter carrying an address helper.
      See Also:
    • LOCAL_SERVER

      public static final String LOCAL_SERVER
      Local HTTP proxy server hostname.
      See Also:
    • DEFAULT_GZIP

      private static final boolean DEFAULT_GZIP
      Default setting for gzipping responses.
      See Also:
    • PROP_REFERER

      public static final String PROP_REFERER
      All default to false.
      See Also:
    • PROP_USER_AGENT

      public static final String PROP_USER_AGENT
      Config key to enable sending User-Agent header.
      See Also:
    • PROP_VIA

      public static final String PROP_VIA
      Config key to enable sending Via header.
      See Also:
    • PROP_JUMP_SERVERS

      public static final String PROP_JUMP_SERVERS
      Config key for jump server list.
      See Also:
    • PROP_DISABLE_HELPER

      public static final String PROP_DISABLE_HELPER
      Config key to disable address helper.
      See Also:
    • PROP_ACCEPT

      public static final String PROP_ACCEPT
      Config key: false, the default, replaces the browser's Accept header with a generic value for the content types it names; true forwards it unchanged.
      See Also:
    • PROP_INTERNAL_SSL

      public static final String PROP_INTERNAL_SSL
      Config key: false denies SSL to I2P destinations, but only once PROP_SSL_SET shows the user configured SSL by hand. Defaults to true (allow) either way.
      See Also:
    • PROP_SSL_SET

      public static final String PROP_SSL_SET
      Config key: set when the user configures SSL by hand. Its presence is the gate that lets PROP_INTERNAL_SSL deny internal SSL.
      See Also:
    • DEFAULT_JUMP_SERVERS

      public static final String DEFAULT_JUMP_SERVERS
      Default jump server URLs.
      See Also:
    • ERR_HELPER_DISABLED

      private static final String ERR_HELPER_DISABLED
      Error response when address helpers are disabled.
      See Also:
  • Constructor Details

    • I2PTunnelHTTPClient

      public I2PTunnelHTTPClient(int localPort, Logging l, I2PSocketManager sockMgr, I2PTunnel tunnel, EventDispatcher notifyThis, long clientId)
      This constructor always starts the tunnel (ignoring the i2cp.delayOpen option). It is used to add a client to an existing socket manager.

      As of 0.9.20 this is fast, and does NOT connect the manager to the router, or open the local socket. You MUST call startRunning() for that.

      Parameters:
      localPort - the local port to bind to
      l - the logger inherited from the parent tunnel
      sockMgr - the existing socket manager
      tunnel - the parent I2PTunnel instance
      notifyThis - event dispatcher for notifications
      clientId - the client identifier
    • I2PTunnelHTTPClient

      public I2PTunnelHTTPClient(int localPort, Logging l, boolean ownDest, String wwwProxy, EventDispatcher notifyThis, I2PTunnel tunnel) throws IllegalArgumentException
      Create an HTTP proxy client on the given local port.

      Since 0.9.20 this is fast, and does NOT connect the manager to the router, or open the local socket. You MUST call startRunning() for that.

      Parameters:
      localPort - the local port to bind to
      l - the logger inherited from the parent tunnel
      ownDest - whether to use our own destination
      wwwProxy - comma-separated list of outproxy URLs to forward through, or null for no outproxying
      notifyThis - event dispatcher for notifications
      tunnel - the parent I2PTunnel instance
      Throws:
      IllegalArgumentException - if the I2PTunnel does not contain valid config to contact the router
  • Method Details

    • tryAcquireConnPermit

      private static boolean tryAcquireConnPermit(Hash dest)
      Try to acquire a concurrent-connection permit for dest. The acquire is a per-key atomic remap on _activeConns: on rejection the map value is left untouched at the limit (never a sentinel), so the count can never be corrupted by writes of -1/0 and over-admission is impossible even under heavy parallel load.
      Parameters:
      dest - the destination to take a permit for, null always acquires
      Returns:
      true if the permit was acquired, false if the destination is at its connection limit
    • releaseConnPermit

      private static void releaseConnPermit(Hash dest)
      Release a concurrent-connection permit for dest. Per-key atomic remap: the entry is removed from _activeConns exactly when the count reaches zero, so the map cannot grow without bound over the lifetime of the process. Safe to call with null or when the count is already zero (underflow-guarded).
      Parameters:
      dest - the destination to release one permit for, null does nothing
    • executeTask

      private void executeTask(Thread task)
      Execute a runnable task inline.
      Parameters:
      task - Thread task to execute
    • getDefaultOptions

      protected I2PSocketOptions getDefaultOptions()
      Create the default options (using the default timeout, etc). Warning, this does not make a copy of I2PTunnel's client options, it modifies them directly.
      Overrides:
      getDefaultOptions in class I2PTunnelClientBase
      Returns:
      the default options
    • getDefaultOptions

      protected I2PSocketOptions getDefaultOptions(Properties overrides)
      Create the default options (using the default timeout, etc). Warning, this does not make a copy of I2PTunnel's client options, it modifies them directly. Do not use overrides for per-socket options.
      Overrides:
      getDefaultOptions in class I2PTunnelClientBase
      Parameters:
      overrides - the properties to override
      Returns:
      the default options
    • startRunning

      public void startRunning()
      Actually start working on incoming connections. Overridden to start an internal socket too.
      Overrides:
      startRunning in class I2PTunnelClientBase
    • close

      public boolean close(boolean forced)
      Overridden to close internal socket too.
      Overrides:
      close in class I2PTunnelClientBase
      Parameters:
      forced - true to interrupt an in-progress build, false to close gracefully
      Returns:
      true if the tunnel is closed, or false if connections were still active and the close was refused
    • getRealm

      protected String getRealm()
      Authentication realm for the proxy.
      Specified by:
      getRealm in class I2PTunnelHTTPClientBase
      Returns:
      realm string
    • writeErrorPage

      private void writeErrorPage(OutputStream out, I2PTunnelHTTPClient.InputReader reader, String page, String body)
      Write an error page and drain any remaining request data.
      Parameters:
      out - the output stream
      reader - reads the browser's request data
      page - the error page type
      body - the error body
    • clientConnectionRun

      protected void clientConnectionRun(Socket s)
      Note: This does not handle RFC 2616 header line splitting, which is obsoleted in RFC 7230.
      Specified by:
      clientConnectionRun in class I2PTunnelClientBase
      Parameters:
      s - the socket to manage
    • writeServiceUnavailable

      static void writeServiceUnavailable(OutputStream out, String message)
      Write a minimal 503 response and close the connection. The socket close in the caller's finally block flushes the response.
      Parameters:
      out - the output stream for the error response
      message - the reason, or null
    • isBrowserPeerClosed

      static boolean isBrowserPeerClosed(Socket s, I2PTunnelHTTPClient.InputReader reader, String method)
      Probe whether the browser peer has closed while we wait on I2P connect. Only meaningful for GET/HEAD (no request body still unread). A timed read returns -1 on FIN (peer closed), times out when still open, or yields a pipelined byte which is pushed back so it is not lost.
      Parameters:
      s - the browser-facing socket, which may be null
      reader - the request reader wrapping a PushbackInputStream
      method - HTTP method, or null
      Returns:
      true if the peer has closed (or the socket is unusable)
    • cancelWarmSlot

      static void cancelWarmSlot(I2PTunnelHTTPClient.WarmSlot slot)
      Retire a request's warm slot: closes an installed socket, releases the permit the slot still holds, and rejects an attempt still in flight (its thread releases the lease when the install is refused). Idempotent and null-safe. Called from per-iteration cleanup and the request's finally block, where either may already have run.
      Parameters:
      slot - the warm slot to retire, or null
    • openSpeculativeWarm

      private void openSpeculativeWarm(Destination dest, int port, int emptyBudget, long requestId, I2PTunnelHTTPClient.WarmSlot slot)
      Open one secondary I2P socket in the background while the primary is healthy. Fail-open: if the slot is not idle, the per-dest permit is unavailable, or the connect fails, no warm socket is installed and resume falls back to a cold open. Skipped when I2PTunnelClientBase.poolIsDefinitivelyDown() so a dead outbound pool does not accumulate speculative connect storms. The attempt is tagged with the slot's generation, so a completion that races teardown is refused and its permit released by this thread instead of leaking.
      Parameters:
      dest - the destination to warm; its hash keys the per-dest connection permit
      port - remote port (0 = default)
      emptyBudget - connect attempt budget for the secondary
      requestId - logging prefix id
      slot - request-scoped warm slot, or null
    • openEmptyReconnect

      private I2PSocket openEmptyReconnect(Destination dest, int port, int emptyBudget, long requestId, boolean quiet)
      Open a single fresh I2P socket for an empty-response / body-resume reconnect, with the standard attempt budget and dead-pool fail-fast. Shared by the reconnect() and reconnectPair() legs of the callback so both paths get identical backoff and stop conditions.
      Parameters:
      dest - the destination to open the fresh socket to
      port - remote port (0 = default)
      emptyBudget - max connect attempts for this request
      requestId - logging prefix id
      quiet - when true, suppress per-attempt Info logs (used for the staggered first race leg so the second leg's log is the pair marker)
      Returns:
      a connected socket, or null if the budget / dead pool stopped us
    • closeQuietly

      private static void closeQuietly(I2PSocket sock)
      Closes a socket, ignoring any error.
      Parameters:
      sock - the socket to close, may be null
    • sleepQuietly

      static boolean sleepQuietly(long delayMs)
      Sleep for the given delay, restoring the interrupt flag if interrupted.
      Parameters:
      delayMs - the delay in ms
      Returns:
      false if the current thread was interrupted during the sleep
    • parseEmptyRetries

      static int parseEmptyRetries(String value)
      Parse the empty-response retry count from a client option value.

      Clamps to >= 0; malformed or negative input falls back to 0. Pure decision — no context access, safe for unit tests.

      Parameters:
      value - the raw property value, may be null
      Returns:
      the retry count: 0 (never retry) on null, empty, or unparseable input
    • scaledEmptyReconnectBudget

      static int scaledEmptyReconnectBudget(int baseBudget, long contentLength)
      Scale the reconnect attempt budget by the entity size already verified from this exchange's Content-Length header.

      Body-resume reconnects run after response headers have arrived, so the size is known and verifiable; the budget gains one attempt per I2PTunnelRunner.RETRY_RAMP_UNIT_BYTES of entity, capped at 9x the configured budget so a large file cannot pin the runner across an unbounded connect-retry marathon. The 9x cap saturates at the same entity size as the original 3x cap at 4MB granularity (8MB per base attempt). Unknown length (empty-response retry with no headers, or a sub-1MB entity) returns the base budget unchanged. Pure decision — no context access, safe for unit tests.

      Parameters:
      baseBudget - configured attempt budget for this request
      contentLength - entity Content-Length in bytes, or -1 if unknown
      Returns:
      the scaled attempt budget; never below 0
    • getConnectRetryDelayMs

      static long getConnectRetryDelayMs(int attempt)
      Exponential backoff delay (in ms) before the given connect retry. The first retry waits I2P_CONNECT_RETRY_BASE_DELAY (1s), doubling per attempt up to a hard cap of 8s. During a tunnel-pool stall the capped backoff gives the pool time to recover instead of hammering it every 2s, and it still fails fast once I2PTunnelClientBase.poolIsDefinitivelyDown() trips. Pure decision — no context access, safe for unit tests.
      Parameters:
      attempt - the 1-based connect failure count (how many failures so far)
      Returns:
      the delay in ms: 0 for attempt <= 0, else 1000 << (attempt-1) bounded to 8000
    • shouldStopEmptyReconnect

      static boolean shouldStopEmptyReconnect(int poolState)
      Whether the empty-response reconnect loop should stop instead of trying again. A connect attempt that just failed should not be re-driven when the client outbound pool is provably dead (poolState == -1): no further attempt can succeed, so retrying only stalls the browser for the full backoff budget (~39s at the default 9 attempts) and adds load to a mesh already failing to build replacement tunnels. Budget exhaustion is handled separately by the loop's exit condition, so this method only answers the "should we bail early on a dead pool" question.

      A poolState == -2 (unknown: standalone client, or the reflective check failed) deliberately does NOT fail fast — there is no live router pool to observe, so the budget is the only sane bound. Mirroring the initial-connect guard's &lt;= -1 test here would make standalone clients (outproxy, no router) abort every empty-response retry because they always read -2.

      Parameters:
      poolState - the I2PTunnelClientBase.poolState() value from the last failed attempt
      Returns:
      true to stop retrying (fail to the normal empty-response error path)
    • writeHelperSaveForm

      private void writeHelperSaveForm(OutputStream outs, String destination, String ahelperKey, String targetRequest, String referer) throws IOException
      Write the HTML form that prompts the user to save an address helper destination.
      Parameters:
      outs - the stream to write the page to, does nothing if null
      destination - the hostname echoed into the hidden form field, the page text, and the IDN-decoded lookup
      ahelperKey - the base64 destination from the naming service, shown and embedded in the save forms
      targetRequest - the original request URL, the action of the continue-without-saving form
      referer - the referring URL for a hidden field, null if none was sent
      Throws:
      IOException - if writing the page to the output stream fails
    • writeB32SaveForm

      private void writeB32SaveForm(OutputStream outs, String destination, int code, String targetRequest) throws IOException
      Write the HTML form that prompts the user to supply decryption/authentication data.
      Parameters:
      outs - the stream to write the page to, does nothing if null
      destination - the base32 address whose lookup needs more data
      code - the lookup result code naming what is required
      targetRequest - the original request URL, the action of the supplied form
      Throws:
      IOException - if writing the page to the output stream fails
    • getHostName

      private String getHostName(String host)
      The hostname of the given host.
      Parameters:
      host - the requested hostname, null yields null
      Returns:
      b32hash.b32.i2p, or "i2p" on lookup failure. Prior to 0.7.12, returned b64 key
    • isSupportedAddress

      private static boolean isSupportedAddress(String protocol)
      Checks whether the tunnel type serves this protocol.
      Parameters:
      protocol - the request protocol
      Returns:
      true if the address type is supported
    • fixupURI

      static URI fixupURI(String request) throws URISyntaxException
      Fix up a URI whose path or query holds characters that RFC 3986 reserves but that browsers send unescaped: '[', ']', '|', '{' and '}'.

      The scheme, host and port are left untouched, so a fixup cannot move the request to a different host.

      Parameters:
      request - the raw URI string from the request line
      Returns:
      the URI, percent-escaped if a fixup was required
      Throws:
      URISyntaxException - if the URI is invalid even after fixup
    • changeURI

      private static URI changeURI(URI uri, String host, int port, String path) throws URISyntaxException
      Change various parts of the URI. String parameters are all non-encoded.

      Scheme always preserved. Userinfo always cleared. Host changed if non-null. Port changed if non-zero. Path changed if non-null. Query always preserved. Fragment always cleared.

      Parameters:
      uri - the URI to rebuild
      host - the new host, null keeps the original
      port - the new port, 0 keeps the original
      path - the new path, null keeps the original
      Returns:
      the rebuilt URI
      Throws:
      URISyntaxException - if the replaced parts do not form a valid URI
    • replaceQuery

      private static URI replaceQuery(URI uri, String query) throws URISyntaxException
      Replace query in the URI. Userinfo cleared if uri contained a query. Fragment cleared if uri contained a query.
      Parameters:
      uri - the URI whose query is replaced
      query - an ENCODED query, removed if null
      Returns:
      the URI carrying the new query, the original if it had none and none is given
      Throws:
      URISyntaxException - if the URI assembled from the parts is not valid
    • removeHelper

      private static String[] removeHelper(String query)
      Remove the address helper from an encoded query.
      Parameters:
      query - an ENCODED query, removed if null
      Returns:
      rv[0] is ENCODED query with helper removed, non-null but possibly empty; rv[1] is DECODED helper value, non-null but possibly empty; rv null if no helper present
    • lookupWithTimeout

      private Destination lookupWithTimeout(String hostname, long timeoutMs)
      Look up a hostname with a timeout to prevent blocking the request handler. Naming service lookups can block on NetDB searches or network fetches for minutes if the destination isn't cached locally.
      Parameters:
      hostname - the name passed to the naming service; may be an IDN or b32 address
      timeoutMs - maximum time to wait
      Returns:
      resolved Destination or null if lookup failed or timed out