Skip to content

bauerd/jqrp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jqrp -- jq Reverse Proxy

CI status Go Reference

⚠️ This is a weekend hack and not production-grade software!

HTTP reverse proxy that mutates JSON responses according to client-supplied jq queries.

Assuming a JSON backend that responds with:

GET /path
Accept: application/json

[
  {
    "id": 1,
    "name": "alpha",
    "active": true
  },
  {
    "id": 2,
    "name": "beta",
    "active": false
  }
]

With jqrp in front, the backend response can be mutated by suppling a query in the JQ request header:

GET /path
Accept: application/json
JQ: .[] | select(.active) | {results: .name}

{ "results": ["alpha"] }

Consult the jq manual for query syntax and available filters.

Installation

Pre-built binaries and Docker images are available.

Building manually

$ git clone https://github.com/bauerd/jqrp .
$ make # Compiles to ./bin/

Usage

$ jqrp https://example.com

jqrp's default port is 9898.

Docker Image

Alternatively, run a container:

$ docker run -p 9898:9898 bauerd/jqrp https://example.com

Behaviour

  • The X-Forwarded-For header gets set on every proxied request.

  • The X-Request-ID header is propagated to the backend. If missing, requests are assigned UUIDs.

  • jqrp attempts to mutate upstream responses only if all of the following conditions hold:

    1. Both Accept: application/json and JQ: <QUERY> headers are set on the request.
    2. The upstream response has a 2xx status code.
    3. The upstream response has the Content-Type: application/json header set.
  • With the above conditions met, jqrp attempts to transform responses to all request methods.

  • Otherwise, jqrp proxies transparently, and applies no transformation other than setting the X-{Forwarded-For, Request-ID} headers.

Status Codes

The status code of jqrp indicates the operations performed and their outcomes:

Status Code Description
203 Non-Authoritative Information The upstream response body was successfully transformed by the query.
400 Bad Request The query provided in the JQ header is malformed.
408 Request Timeout Applying the query to the upstream response exceeded the transformation timeout.
422 Unprocessable Entity The query evaluates to a primitive type that has no valid JSON representation.
500 Internal Server Error An unhandled error occured when proxying the upstream response.
502 Bad Gateway The upstream response body contains invalid JSON, or its Content-Type is not application/json.
504 Gateway Timeout The upstream host exceeded the proxy timeout.
Other The upstream response was not transformed, and its original status code preserved.

Configuration

jqrp can be configured via environment variables.

  • All timeout values use milliseconds
  • Setting a timeout to 0 disables it
  • Setting the CACHE_SIZE to 0 disables query caching
Environment Variable Default Description Reference
PORT 9898 Port to bind to
LOG_LEVEL debug Log level. Either debug, info or error
CACHE_SIZE 512 Size of the LRU query cache. Setting the size to 0 disables query caching
EVAL_TIMEOUT 0 Maximum time spent evaluating jq queries
READ_TIMEOUT 0 Maxium time from when the client connection is accepted to when the request body is fully read Server.ReadTimeout
WRITE_TIMEOUT 0 Maximum time from the end of the client request header read to the end of the response write Server.WriteTimeout
DIAL_TIMEOUT 0 Maximum time spent establishing a backend TCP connection Dialer.Timeout
DIAL_KEEPALIVE 0 Interval between keep-alive probes for an active backend network connection Dialer.KeepAlive
TLS_HANDSHAKE_TIMEOUT 0 Maximum time spent performing backend TLS handshake Transport.TLSHandshakeTimeout
RESPONSE_HEADER_TIMEOUT 0 Maxium time spent reading the headers of the backend response Transport.ResponseHeaderTimeout
EXPECT_CONTINUE_TIMEOUT 0 Maximum time to wait between sending the backend request headers when including an Expect: 100-continue and receiving the go-ahead to send the body Transport.ExpectContinueTimeout

Security Considerations

  • jq is Turing-complete, i.e. evaluation of user-supplied queries may loop indefinitely. jqrp affords setting an evaluation timeout, configurable with the EVAL_TIMEOUT environment variable. Requests with queries exceeding the evaluation timeout get closed with status code 408.

  • jqrp uses gojq, a re-implementation of jq. Because jqrp feeds user input untouched to gojq, its security properties depend mainly on gojq.

  • If gojq panics on query evaluation, the jqrp process exits.

  • Consider stripping the JQ header for unauthenticated/unauthorized requests in front of jqrp.

Performance Considerations

  • jqrp stores compiled queries in an LRU (last-recently-used) cache. Compiled queries retrieved from the cache can be applied immediately to upstream response bodies. The cache has a static size which can be configured with the environment variable CACHE_SIZE.

  • Consider running jqrp behind a caching reverse proxy, that factors in the JQ header when computing cache keys. Note that if clients supply dynamically generated queries, this strategy is not viable.

Edge Cases/Noteworthy

  • If a query results in a single primitive result (i.e. a boolean, number, string or null), the response body is empty and the status code 422. If a query results in multiple primitive results, they are contained in an array.

  • If a query's result set is empty, the status code is 203, and the body depends on the backend's JSON response:

    • If the top-level type was an object, the response body is the empty object {}.
    • If the top-level type was an array, the response body is the empty array [].
  • jqrp does not support HTTP content negotiation and only attempts to transform requests that solely Accept: application/json.

  • jqrp logs only requests applicable to transformation. Requests proxied transparently are not logged.