### ----------------------------------------------------
### -- cro
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Worthington <jnthn@jnthn.net>
### -- File: docs/reference/cro-http-reverseproxy.md
### ----------------------------------------------------
# Cro::HTTP::ReverseProxy

A reverse proxy accepts incoming requests and delegates them to another HTTP
server, passing that server's response back to the client. A reverse proxy may
modify the request and/or response, and route different requests to different
target servers.

Potential applications of a HTTP reverse proxy include:

* Providing a common endpoint that delegates to multiple backend services
* Inserting extra headers (for example, mapping a session token from the
  browser into a JSON Web Token expected by backend services)
* Load-balancing requests over multiple backend services
* Aiding A/B testing by routing requests to partiuclar versions of a service
* Providing authenticated access to a non-authenticated service (assuming the
  non-authenticated service is not exposed directly)
* SSL termination
* Caching
* Content transformation

## Proxying all incoming requests

The class `Cro::HTTP::ReverseProxy` is a `Cro::Transform` that consumes a
`Cro::HTTP::Request` and produces a `Cro::HTTP::Response`. Therefore, it may
be used as the `application` parameter to `Cro::HTTP::Server`. This setup is
ideal if *all* the incoming requests should be proxied, since it avoids the
overhead of `Cro::HTTP::Router`.

First, set up the reverse proxy, specifying `to`, which is the target base URL
(that is, the incoming target will be appended to it):

```
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/';
```

Then host it:

```
my $server = Cro::HTTP::Server.new:
    port => 10101,
    application => $proxy;
$server.start;
react whenever signal(SIGINT) {
    $server.stop;
}
```

## Proxying just some routes

Since `Cro::HTTP::ReverseProxy` is a `Cro::Transform` from HTTP requests to
HTTP responses, it may be used with `Cro::HTTP::Router`'s `delegate` function.
This is useful when wanting to directly handle *some* requests, while proxying
others.

```
my $app = route {
    get -> {
        content 'text/plain', 'Unproxied URL';
    }

    # /user/foo proxied to http://user-service/foo
    delegate <user *> => Cro::HTTP::ReverseProxy.new:
        to => 'http://user-service/';

    # /product/foo proxied to http://product-service/foo
    delegate <product *> => Cro::HTTP::ReverseProxy.new:
        to => 'http://product-service/';
}
```

## Proxying without appending the target URL

Sometimes, one might wish to proxy all requests to a single target URL, without
appending the target of the request. This is primarily useful in conjunction
with other features (such as manipulating the request). Instead of `to`, pass
`to-absolute`:

```
my $app = route {
    # Everything under /images/ proxied to a really cute cat picture
    # (provide your own, or there's just a few on the internet...)
    delegate <images *> => Cro::HTTP::ReverseProxy.new:
        to-absolute => 'http://really-cute.cat/picture.gif';
}
```

## Controlling the target URL

Sometimes, it's desirable to proxy different requests to different URLs. This
can be achieved by passing a `Code` object to `to` or `to-absolute`.

For example, picking two different servers to proxy to at random could be
achieved with:

```
my @servers = 'http://replica-a/', 'http://replica-b/';
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => { @servers.pick };
```

The request object is passed in `to`, allowing the target to be determined
based upon request properties:

```
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => {
        .has-header('authorization')
            ?? 'http://private-service/'
            !! 'http://public-service/'
    };
```

If doing significant amounts of work in the code block, for example doing a
database query, and if the proxy is to support HTTP/2.0 requests, then return
an `Awaitable` object (for example, a `Promise`) that will be completed with
the target URL. This avoids blocking processing of other HTTP/2.0 requests
multiplexed on the same client connection.

Everything works in the same way for `to-absolute`, except that the result
will be used as the absolute target URL.

## Transforming the request

To transform the request before it is proxied to the target, pass the code to
transform it using the `request` named argument. The code will be called with
the `Cro::HTTP::Request` object.

```
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/',
    request => {
        .add-header('X-Experiment', <A B>.pick);
    };
```

If wishing to do something involving the network or some other time-consuming
operation, write this asynchronously to avoid blocking processing of HTTP/2.0
multiplexed requests:

```
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/',
    request => -> $req {
        start {
            my $jwt = await jwt-lookup($req.cookie-value('session-id'));
            $req.add-header('Authorization', "Bearer $jwt");
        }
    };
```

It is also possible to obtain the request body and transform that, using
`set-body` to put in place the new request body.

```
my $asciify-proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/',
    request => -> $req {
        start {
            if $req.content-type.type-and-subtype eq 'application/json' {
                my $body = await $req.body-text;
                $req.set-body:
                    $body.subst(/<-[\x00..\x7F]>/, '?', :g);
            }
        }
    };
```

## Transforming the response

To transform the response from the target before it is returned to the client,
pass the code to transform it using the `response` named argument. The
`Cro::HTTP::Response` object will be passed to the code as an argument.

```
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/',
    response => {
        .remove-header('server');
    };
```

If doing a long-running operation or transforming the body, then it is better
that the transform code works asynchronously. Any `Awaitable` returned will be
automatically awaited.

```
my $analytics-insertion-proxy = Cro::HTTP::ReverseProxy.new:
    to => 'http://the.target.base/url/',
    response => -> $res {
        start {
            if $res.content-type.type-and-subtype eq 'text/html' {
                my $body = await $res.body-text;
                $res.set-body:
                    $body.subst(/<?before '</body>'>/, $analytics-code);
            }
        }
    };
```

## Per-request state

Sometimes it's desirable to keep some state for each request that is being
proxied, and refer to it in the request, response, or dynamic `to` callbacks.
The `$*PROXY-STATE` dynamic variable is set up when each of the callbacks are
made, and can be assigned to and accessed as needed.

For example, an A/B testing proxy could be set up as follows:

```
my %experiments =>
    A => 'http://service-version-a/',
    B => 'http://service-version-b/';
my class ProxyState {
    has Str $.experiment is required;
}
my $proxy = Cro::HTTP::ReverseProxy.new:
    to => sub ($request) {
        my $exp;
        with $request.cookie-value('ab-experiment') {
            $exp = $_ if %experiments{$_}:exists;
        }
        $exp //= %experiments.keys.pick;
        $*PROXY-STATE = ProxyState.new(experiment => $exp);
        return %experiments{$exp};
    },
    response => {
        .set-cookie('ab-experiment', $*PROXY-STATE.experiment);
    };
```

## Thread safety

The callbacks passed to `Cro::HTTP::ReverseProxy` may be called on multiple
threads simultaneously. However, they are called one at a time for a given
request (so the `to` and `request` callbacks can never run at the same time
for a particular request), meaning that use of the per-request `$*PROXY-STATE`
is always safe.

If the proxy needs to deal with other state, then that state should be given
suitable protection (for example, by using `OO::Monitors` and placing it in a
`monitor`).


