Class ScriptedProxy

java.lang.Object
org.microproxy.starlark.ScriptedProxy
All Implemented Interfaces:
ChainedProxyManager, HttpFiltersSource, ProxyAuthenticator

public final class ScriptedProxy extends Object implements HttpFiltersSource, ChainedProxyManager, ProxyAuthenticator
Drives the proxy from a Starlark script. Install it as both the filters source and the chained proxy manager:

 ScriptedProxy script = ScriptedProxy.builder(Path.of("proxy.star")).build();
 MicroProxy.bootstrap().withFiltersSource(script).withChainProxyManager(script).start();
 

Install it as the ProxyAuthenticator too when the script defines authenticate (definesAuthenticate()).

The script may define any of these functions; each is optional:

  • on_request(req, ctx): inspect or change a request. Return response(...) to answer it without contacting the server. Also called for CONNECT.
  • on_response(req, res, ctx): inspect or change a response; may return a new response(...) to replace it.
  • upstream(req, ctx): choose the route. Return None for the default, "DIRECT", a proxy URL such as "socks5://host:1080", or a list of these to try in order.
  • allow_mitm(req, ctx): for a CONNECT, whether to intercept it (when a MITM manager is configured).
  • on_client_hello(hello, ctx): once the client of a CONNECT that may be intercepted (or of a transparent TLS connection, or a transparent QUIC flow, whose ClientHello the proxy reads from its Initial packets) has sent its TLS ClientHello, whether to intercept it: False tunnels (or relays) it untouched, True or None intercepts. hello has sni, alpn, versions and cipher_suites; later hooks of the session see it as ctx.client_hello.
  • buffer_request(req, ctx): whether to buffer this request's body so on_request can read req.body. Default: no.
  • buffer_response(req, res, ctx): whether to buffer this response's body. Default: text bodies in a coding the proxy can decode, except event streams.
  • on_websocket_frame(req, frame, ctx): for each frame of an upgraded WebSocket connection, in both directions. Assign frame.text or frame.payload to change it; return False to drop it, or None to forward it.
  • on_websocket_message(msg, ctx): for each whole message (text or binary) of an upgraded WebSocket connection, joined from its frames and inflated, in both directions, after on_websocket_frame has seen its frames. As with on_frame: change msg.text or msg.payload in place and return None (or the message) to send it, False to drop it, another message to replace it, or a list of messages (websocket_message(content, from_client=...)) to send instead, each in the direction its from_client names (an empty list drops it).
  • authenticate(req, ctx): decides whether a client may use the proxy, when this object is also installed as the ProxyAuthenticator ( withProxyAuthenticator(script)). Return the user name (a non-empty string) or True to accept, False or None to reject with the default 407, or response(...) to reject with that answer. Anything else, a failure or a timeout rejects. A global AUTHENTICATE_EVERY_REQUEST = True authenticates every request instead of the first accepted one on each connection.
  • on_failure(req, failure, ctx): when the proxy has to answer the request itself (the server's name did not resolve, the connection was refused, the server timed out, ...). Return response(...) to answer, or None to leave it to the FailureResponder or the default answer.
  • on_frame(frame, ctx): for each HTTP/2 and HTTP/3 frame, in each direction, when this object's frameInterceptor() is installed (withFrameInterceptor): live where the proxy speaks the protocol, and for HTTP/3 frames run through an Http3FramePipeline with it. Change the frame in place and return None (or the frame) to pass it on, False to drop it, another frame to replace it, or a list of frames to replace it with (an empty list drops it). The rules of FrameInterceptor apply; a result that breaks them is logged, and the frame passed on unchanged.
  • on_tcp_start(ctx), on_tcp_message(msg, ctx) and on_tcp_end(ctx): for raw TCP tunnels (see TcpInterceptor), when this object's tcpInterceptor() is installed (withTcpInterceptor). msg is a TcpMessage: data, text, from_client and timestamp. Change it in place and return None (or True, or the message) to pass it on, False to drop it, or bytes, a string or another message to replace it. ctx is a TcpContext: the target, tls, sni, alpn, vars for the tunnel, error in on_tcp_end after a failure, and send(from_client, data) to add bytes. A failing on_tcp_message passes the bytes on unchanged.
  • on_udp_start(ctx), on_udp_message(msg, ctx) and on_udp_end(ctx): for the datagrams of CONNECT-UDP flows (see UdpInterceptor), when this object's udpInterceptor() is installed (withUdpInterceptor). msg is a UdpMessage, one datagram, with the fields and results of a TcpMessage; ctx is a UdpContext: the target, its resolved address, vars for the flow, and send(from_client, data) to add a datagram.
  • on_quic_start(ctx), on_quic_stream_data(msg, ctx), on_quic_datagram(msg, ctx) and on_quic_end(ctx): for QUIC intercepted stream by stream (see QuicInterceptor), when this object's quicInterceptor() is installed (withQuicInterceptor). msg is a QuicMessage, with the fields and results of a TcpMessage: bytes of one read of a stream, or one datagram. ctx is a QuicContext: the server, sni, alpn, masque, the stream (stream_id, bidirectional, client_initiated; None outside on_quic_stream_data), vars for the connection, error in on_quic_end after a failure, send(from_client, data) to add bytes to the stream and send_datagram(from_client, data) to add a datagram.
  • on_quic_message(msg, ctx): for each whole message of a stream that the proxy's QuicFraming frames (DNS over QUIC by default), instead of on_quic_stream_data: msg.data is the payload, without its length prefix or delimiter, and msg.stream_id names the stream. As with on_websocket_message: change msg in place and return None (or the message) to send it, False to drop it, another message, bytes or a string to replace it, or a list of them to send instead, each message in the direction its from_client names (an empty list drops it). Each is framed again, its length prefix computed afresh.
  • quic_message_length(data): a framer of the script's own, which framing rules name script (quicFramer()): given the bytes kept of a stream direction, the length of the message they start with (the message is given to on_quic_message whole), or None until the bytes say.

A failing hook is logged and answered with 500; a failing allow_mitm or on_client_hello declines interception, a failing upstream rejects the request with 502, and a failing on_failure leaves the answer to the responder or the default. When the script is a file it is re-read when it changes (checked at most once a second); a version that does not compile is logged and the previous one stays in use.

  • Method Details

    • builder

      public static ScriptedProxy.Builder builder(Path file)
      A proxy driven by the script in file.
      Parameters:
      file - the UTF-8 script file, reloaded on changes by default
      Returns:
      a builder configured to load the file
    • builder

      public static ScriptedProxy.Builder builder(String source, String name)
      A proxy driven by source, for embedding and tests.
      Parameters:
      source - the Starlark source to compile
      name - the script name used in diagnostics
      Returns:
      a builder configured to compile the supplied source
    • script

      public StarlarkScript script()
      The script in use, re-reading the file first if it changed.
      Returns:
      the current successfully compiled script
    • filterRequest

      public HttpFilters filterRequest(HttpRequest originalRequest, FlowContext flowContext)
      Description copied from interface: HttpFiltersSource
      Returns the filters for a request, or null to proxy it unfiltered.
      Specified by:
      filterRequest in interface HttpFiltersSource
      Parameters:
      originalRequest - a copy of the request as received from the client
      flowContext - the client connection the request arrived on
      Returns:
      the per-request filters, or null to leave the request unfiltered
    • definesFrameHook

      public boolean definesFrameHook()
      Whether the script in use defines on_frame, so frameInterceptor() is worth installing. A proxy without it pays nothing per frame.
      Returns:
      whether the current script provides the frame hook
    • frameInterceptor

      public FrameInterceptor frameInterceptor()
      An interceptor that calls the script's on_frame(frame, ctx), for HttpProxyServerBootstrap.withFrameInterceptor(org.microproxy.frames.FrameInterceptor) (live HTTP/2 and HTTP/3) or Http3FramePipeline.builder(org.microproxy.frames.FrameInterceptor) (HTTP/3 streams relayed otherwise). Frames pass on unchanged when the script in use (after a reload) has no on_frame, and when the call fails, which is logged.
      Returns:
      the interceptor
    • definesUdpHooks

      public boolean definesUdpHooks()
      Whether the script in use defines on_udp_start, on_udp_message or on_udp_end, so udpInterceptor() is worth installing.
      Returns:
      whether the current script provides a UDP hook
    • udpInterceptor

      public UdpInterceptor udpInterceptor()
      An interceptor that calls the script's on_udp_start(ctx), on_udp_message(msg, ctx) and on_udp_end(ctx), for HttpProxyServerBootstrap.withUdpInterceptor(org.microproxy.udp.UdpInterceptor): the datagrams of CONNECT-UDP flows. The calls of one flow, which come from a thread per direction, run one at a time, so they can share ctx.vars. on_udp_message returns what to send on: None or True for the message (as changed), False to drop it, or bytes, a string or a message to send instead. A datagram passes on unchanged when the script in use has no on_udp_message, and when the call fails, which is logged.
      Returns:
      the interceptor
    • definesQuicHooks

      public boolean definesQuicHooks()
      Whether the script in use defines on_quic_start, on_quic_stream_data, on_quic_message, on_quic_datagram or on_quic_end, so quicInterceptor() is worth installing. Installing it makes the proxy intercept QUIC that is not HTTP/3 (see QuicInterceptor).
      Returns:
      whether the current script provides a QUIC hook
    • quicInterceptor

      public QuicInterceptor quicInterceptor()
      An interceptor that calls the script's on_quic_start(ctx), on_quic_stream_data(msg, ctx), on_quic_datagram(msg, ctx) and on_quic_end(ctx), for HttpProxyServerBootstrap.withQuicInterceptor(org.microproxy.quic.QuicInterceptor): QUIC intercepted stream by stream. The calls of one connection, which come from a thread per direction of each stream and of datagrams, run one at a time, so they can share ctx.vars. on_quic_stream_data and on_quic_datagram return what to send on: None or True for the message (as changed), False to drop it, or bytes, a string or a message to send instead. Bytes pass on unchanged when the script in use has no such hook, and when the call fails, which is logged.

      When the script defines on_quic_message as this is called, the interceptor also overrides QuicInterceptor.streamMessage(org.microproxy.quic.QuicMessage, org.microproxy.quic.QuicStreamContext), so the streams that the proxy's QuicFraming frames go to on_quic_message as whole messages instead of to on_quic_stream_data; a later reload that adds the hook takes effect after a restart, and one that removes it passes messages on unchanged.

      Returns:
      the interceptor
    • definesQuicFramer

      public boolean definesQuicFramer()
      Whether the script in use defines quic_message_length, a framer of its own for quicFramer().
      Returns:
      whether the current script provides a framer
    • quicFramer

      public MessageFramer quicFramer()
      A MessageFramer that asks the script's quic_message_length(data) how long the message at the start of a stream direction's bytes is: an int (the whole message's length, which may be more than the bytes so far), or None until the bytes say. The message goes to on_quic_message as it is, and what that returns is sent as it is. A call that fails or returns something else makes the bytes malformed: the proxy resets the stream. Name it in the framing rules with QuicFraming.withFramer(java.lang.String, org.microproxy.quic.MessageFramer) (--script names it script: --quic-framing myproto=script).
      Returns:
      the framer
    • definesTcpHooks

      public boolean definesTcpHooks()
      Whether the script in use defines on_tcp_start, on_tcp_message or on_tcp_end, so tcpInterceptor() is worth installing. A proxy without one copies tunnel bytes as before.
      Returns:
      whether the current script provides a TCP hook
    • tcpInterceptor

      public TcpInterceptor tcpInterceptor()
      An interceptor that calls the script's on_tcp_start(ctx), on_tcp_message(msg, ctx) and on_tcp_end(ctx), for HttpProxyServerBootstrap.withTcpInterceptor(org.microproxy.tcp.TcpInterceptor). The calls of one tunnel, which come from a thread per direction, run one at a time, so they can share ctx.vars. Bytes pass on unchanged when the script in use (after a reload) has no on_tcp_message, and when the call fails, which is logged.
      Returns:
      the interceptor
    • definesAuthenticate

      public boolean definesAuthenticate()
      Whether the script in use defines authenticate, so this can be the authenticator.
      Returns:
      whether the current script provides the authentication hook
    • authenticate

      public boolean authenticate(String userName, String password)
      Not used: authenticate(HttpRequest, FlowContext) asks the script instead.
      Specified by:
      authenticate in interface ProxyAuthenticator
      Parameters:
      userName - the authenticated user name, or null if unknown
      password - the client password
      Returns:
      whether the credentials are accepted
    • authenticate

      public AuthResult authenticate(HttpRequest request, FlowContext flow)
      Calls the script's authenticate(req, ctx). Fails closed: a script without the function (after a reload), a failing or timed-out call, or an unexpected return value rejects the request with the default challenge, and is logged.
      Specified by:
      authenticate in interface ProxyAuthenticator
      Parameters:
      request - the request being handled
      flow - the client connection, with its address and TLS session
      Returns:
      AuthResult.accept(java.lang.String), or AuthResult.reject() with the answer to send
    • authenticateEveryRequest

      public boolean authenticateEveryRequest()
      Whether the script sets AUTHENTICATE_EVERY_REQUEST to a true value.
      Specified by:
      authenticateEveryRequest in interface ProxyAuthenticator
      Returns:
      whether to authenticate every request rather than the first one accepted on each client connection, e.g. because tokens expire
    • lookupChainedProxies

      public void lookupChainedProxies(HttpRequest request, Queue<ChainedProxy> chainedProxies, ClientDetails client)
      Description copied from interface: ChainedProxyManager
      Adds the proxies to try, in order, to chainedProxies. Leave the queue empty to reject the request with 502, or add ChainedProxyAdapter.FALLBACK_TO_DIRECT_CONNECTION to connect directly.
      Specified by:
      lookupChainedProxies in interface ChainedProxyManager
      Parameters:
      request - the request head
      chainedProxies - the queue to populate with proxies to try, in order
      client - the client address and authentication details