Go-repro: a rewriting reverse proxy for testing multi-domain setups

Avatar von Christian Speckner

Web applications that span multiple domains come with their very own set of challenges. As requesting resources that cross domain boundaries is a pattern common to many different attacks that threaten the security of websites, browsers restrict such requests to conform to with the same-origin policy: most resources (in particular if requested from Javascript via XHR) may only be requested from the domain that served the requesting web page.

However, as there are also completely legitimate reasons for such requests, various techniques exist for circumventing the same-origin policy, for example JsonP or the modern HTML5 CORS standard (Cross Origin Resource Sharing). While these techniques enable cross-domain web applications, the same-origin policy remains a source for subtle and hard-to-debug issues. Therefore, reproducing this setup in development and testing environments is vital to make sure that potential issues can be caught and fixed early before they hit production.

Example setup

Assume, for example, that a web shop is served from http://world-of-cheese.acme.com . This application accesses an API served from https://api.acme.com that is shared with various subsidiary companies of ACME Cheese Inc. For each of the fine cheeses sold by ACME, the API will not only supply pricing and shipping information, but also links to pictures and recipes hosted on a media asset server http://media.acme.com (not surprisingly, the media server is also shared by ACME’s subsidiaries).

In order to display detailed information for a single product, the shop running on http://world-of-cheese.acme.com will issue a request to https://api.acme.com/products/7563201. To comply with the browsers same-origin policy, the developers toiling on the project chose to use CORS, and so the request sent by the browser includes a Origin: http://world-of-cheese.acme.com header. The API server acknowledges the legimitacy of the request by sending a reply that includes an Acces-control-allow-origin: http://world-of-cheese.acme.com header. The JSON body of the response looks like this:

{
    "name": "Skunk Delight",
    "description": "Sickly yellow, delightfully acrid stench. Price per pound.",
    "price": 9.99,
    "image": {
        "url": "http://media.acme.com/images/e14f9994a6.jpg",
        "width": 640,
        "height": 480
    }
}

Note that the response contains a link to a product image located on the media server — all three domains listed above are involved.

Test environment

In such a setup, the cross-domain aspect of client server communication should also be part of the test environment. Not quite coincidentially, the above example is a slightly simplified version of the real world setup our team is facing in our customer project. There, we solved this the following way:

  • The development environment runs locally on developer’s machine in a vagrant-managed virtual machine
  • The VM runs a web server that listens on several domains configured as vhosts: http://world-of-cheese.acme.dev , https://api.acme.de and http://media.acme.dev
  • The SSL connection the API is secured by a self-signed cert that has to be imported into the browser
  • In order to provide name resolution, these domain names are mapped to the VM IP in the hosts file on the host system (we use the vagrant-hostmanager plugin for that)

With this setup, we can fully test and debug the cross-domain communication between frontend, API and media server.

Problem solved?

Unfortunately, not quite. Our project also includes a mobile webpage and a mobile app. The mobile app is based on the mobile web page and build with Apache Cordova, and both web page and app must be tested in the Android and iOS emulators and on actual devices during development. This is where things start to become messy.

Managing name resolution within the Android emulator requires editing the hosts file within the emulator and the change will not persist over restarts. Worse, adapting the hosts file on an actual device requires root and thus is impractical. And, in addition to the name resolution issue, we also have the self-signed cert that needs to be imported on both emulated and real devices.

In order to work around the issues with name resolution, we have to either set up an actual DNS server or to find a way of accessing the services on different IPs and / or ports.

Introducing go-repro: a rewriting reverse proxy

Go-repro is a reverse proxy built specifically for mapping several upstream hosts on different domains to a set of ports on the local machine. In the process of forwarding, go-repro transparently rewrites requests and responses in order to account for the mapping. While the proxy started as an experiment during slacktime, it has since become an important part of our development and testing workflow.

Installation

In order to install go-repro, head over to the github repository. Go-repro is written in Go, and you can either build it yourself or download one of the precompiled binaries from the releases page. The binaries are fully static and can be run without any additional dependencies.

How does it work?

In order to setup go-repro for the testcase described above, create a YAML config file

mappings:
  - local: 127.0.0.1:8080
    remote: http://world-of-cheese.acme.dev
  - local: 127.0.0.1:8081
    remote: https://api.acme.dev
  - local: 127.0.0.1:8082
    remote: http://media.acme.dev
rewrites:
  - :8080/.*\.html
  - :8081
allow-insecure: true

This config instructs the reverse proxy to:

  • Map requests to the local ports 8080, 8081 and 8082 to the web, API and media servers running on the local VM
  • Rewrite response bodies for requests with URLs that match one of the two rewrite regexes. While headers are always rewritten, response bodies are rewritten only if one of the rewrite regexes matches.
  • Ignore certificate validation errors. This accounts for our self-signed cert.

The reverse proxy can now be launched with

go-repro --config config.yaml

After launching go-repro, the ACME shop website can be accessed on http://localhost:8080. Redirects will work as expected as the proxy will transparently rewrite the location headers. In addition, by virtue of body rewriting, any absolute resources in HTML pages will be rewritten to point to local ports instead of the shop domain.

If the shop requests product details from http://localhost:8081/products/7563201, the proxy will forward the request to the upstream API server running in the VM. According to CORS, the browser will now send a Origin: http://localhost:8080 header. This will be transparently rewritten to Origin: https://api.acme.dev and, as the resulting Access-control-allow-origin header will be changed back to localhost:8080, CORS will work as expected. Because of body rewriting, the response will now be

{
    "name": "Skunk Delight",
    "description": "Sickly yellow, delightfully acrid stench. Price per pound.",
    "price": 9.99,
    "image": {
        "url": "http://127.0.0.1:8082/images/e14f9994a6.jpg",
        "width": 640,
        "height": 480
    }
}

The application will then proceed to load the product image from the local port.

In order for this setup to work, the application must access the API on the local port. In our project, the web shop is a single page javascript application, and the API domain is encoded in the index document, so this is automatically taken care of by rewriting the response body.

Accessing the application on different local IPs and in the Android emulator

In the example above, we have mapped all upstream hosts to ports on 127.0.0.1. By changing the IP to 0.0.0.0, we can instruct go-repro to listen on all IPs. In this mode, the proxy will determine the IP used for rewriting from the Host header. We can now access the proxy both on 127.0.0.1 and (for example) 192.168.0.120, and proxy will insert the correct IP during rewriting.

This behavior is particularily useful in conjunction with the Android emulator. The emulator forwards all connections to the special IP 10.0.2.2 to the host loopback interface, and we can thus access the application on 10.0.2.2 without any further configuration.

The upshot?

Go-repro has greatly simplified our testing workflow where mobile devices are concerned. Instead of messing with name resolution and / or setting up a DNS server, we can now just test on a set of local ports without changing a single line of our application. Of course, this is a invasive change to our setup, so every now and then we also have to test against real domains. However, we can use our staging system for this where we are working with public domains, so name resultion is not an issue during these tests.

All in all, go-repro has been a great help for us and, if you are struggling with similar problems, it might help you too.

Avatar von Christian Speckner

Kommentare

Eine Antwort zu „Go-repro: a rewriting reverse proxy for testing multi-domain setups“

  1. Frisch im Blog: Go-repro: a rewriting reverse proxy for testing multi-domain setups https://t.co/pUiB1LSUPm

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.