~nhanb/gae-proxy

0855005fbf7bc0a22f62fb319b08161ecf39da64 — Bùi Thành Nhân 1 year, 3 months ago 6b97510
easier to use signature
7 files changed, 82 insertions(+), 88 deletions(-)

M .gcloudignore
M README.md
A app.py
M app.yaml
D main.py
M requirements.txt
D send.py
M .gcloudignore => .gcloudignore +0 -1
@@ 17,4 17,3 @@
__pycache__/
# Ignored by the build system
/setup.cfg
/send.py

M README.md => README.md +3 -4
@@ 7,8 7,7 @@ while plain HTTP is easy, HTTPS requires actual tcp tunnelling which I'm not
really up for at the moment (is that even possible on GAE standard
evironment?).

So I devised my own scheme where every param is defined in a POST json body.
Dumbest thing that works right?
So I devised my own "scheme". Dumbest thing that works right?

# Server



@@ 16,11 15,11 @@ Create an `envars.yaml` file to store secret password:

```yaml
env_variables:
  GAEPROXY_PASSWORD: "long long string"
  GAEPROXY_KEY: "long long string"
```

Then just `gcloud app deploy`.

# Use

See **send.py**. It's pretty straightforward.
See comments in **app.py**.

A app.py => app.py +75 -0
@@ 0,0 1,75 @@
"""
Dumb (bespoke "protocol" & grossly inefficient) http(s) proxy on Google Cloud Functions.
It also attempts to solve CloudFlare's javascript challenge where necessary.

To use, simply send request to proxy just as you would to the target, but provide some
extra http headers:

- X-Proxy-Key: For authentication.
- X-Proxy-Target-Host: So the proxy knows what hostname to proxy to.
- X-Proxy-Target-Scheme: Optional. Defaults to https.

To deploy:

    $ export GAEPROXY_KEY='my-secret-proxy-key'
    $ gcloud app deploy
"""
import os

import cloudscraper
from flask import Flask, Response, request

http = cloudscraper.create_scraper(
    browser={"browser": "firefox", "platform": "windows", "mobile": False}
)

PROXY_KEY = os.environ["GAEPROXY_KEY"]


app = Flask(__name__)


@app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE"])
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE"])
def hello_world(path):
    auth_key = request.headers.get("X-Proxy-Key")
    if auth_key != PROXY_KEY:
        return "Go away", 403

    target_headers = {
        key: val
        for key, val in dict(request.headers).items()
        if not (
            key.startswith("X-Appengine-")
            or key
            in (
                "Accept-Encoding",
                "Content-Length",
                "Transfer-Encoding",
                "X-Amzn-Trace-Id",
                "X-Cloud-Trace-Context",
                "X-Forwarded-For",
                "X-Forwarded-Proto",
            )
        )
    }
    target_headers.pop("X-Proxy-Key")
    target_headers.pop("Host")

    target_host = target_headers.pop("X-Proxy-Target-Host")
    target_scheme = target_headers.pop("X-Proxy-Target-Scheme", "https")
    target_path = "/".join(request.full_path.split("/")[1:])
    target_url = f"{target_scheme}://{target_host}/{target_path}"

    send = getattr(http, request.method.lower())
    target_resp = send(target_url, headers=target_headers)

    resp_headers = dict(target_resp.headers)
    resp_headers.pop("Content-Encoding", None)
    resp_headers.pop("Transfer-Encoding", None)

    return Response(
        response=target_resp.content,
        status=target_resp.status_code,
        headers=resp_headers,
    )

M app.yaml => app.yaml +2 -2
@@ 1,5 1,5 @@
runtime: python37
entrypoint: gunicorn -b :$PORT --workers=5 main:app
runtime: python39
entrypoint: gunicorn -b :$PORT --workers=5 app:app

handlers:
  - url: /.*

D main.py => main.py +0 -47
@@ 1,47 0,0 @@
import os

import requests
from bottle import HTTPResponse, default_app, request, route, run

# GAE recommended
PORT = os.environ.get("PORT", 8080)
PASSWORD = os.environ.get("GAEPROXY_PASSWORD", "")

proxiable_methods = {
    "get": requests.get,
    "post": requests.post,
}


@route("/proxy", method="POST")
def proxy():
    req = request.json

    fields = ("method", "password", "url", "body")
    missing_fields = [f for f in fields if f not in req]
    if missing_fields:
        return HTTPResponse(status=400, body=f"Missing fields: {missing_fields}")

    if req["method"] not in proxiable_methods:
        return HTTPResponse(status=400, body="We serve get/post only!")

    if req.get("password") != PASSWORD:
        return HTTPResponse(status=400, body="Get off my lawn!")

    requests_func = proxiable_methods[req["method"]]
    requests_kwargs = {"url": req["url"], "headers": req["headers"]}
    if req["method"] == "post":
        requests_kwargs["body"] = req["body"]

    try:
        resp = requests_func(**requests_kwargs, timeout=10)
    except Exception as e:
        return HTTPResponse(status=500, body=f"Unexpected error:\n{e}")

    return HTTPResponse(body=resp.text, status=resp.status_code)


app = default_app()

if __name__ == "__main__":
    run(host="localhost", port=PORT)

M requirements.txt => requirements.txt +2 -2
@@ 1,3 1,3 @@
requests
bottle
cloudscraper
flask
gunicorn

D send.py => send.py +0 -32
@@ 1,32 0,0 @@
import os
from datetime import datetime as dt

import requests

"""
Lazy quick test script
"""

PASSWORD = os.environ["GAEPROXY_PASSWORD"]

while True:
    start = dt.now()
    resp = requests.post(
        "https://nhansproxy.df.r.appspot.com/proxy",
        json={
            "url": "https://httpbin.org/ip",
            "method": "get",
            "body": None,
            "headers": {"Foo-Bar": "ehhhh"},
            "password": PASSWORD,
        },
    )

    """
    print(resp.status_code)
    for hkey, hval in resp.headers.items():
        print(hkey, ":", hval)
    """
    end = dt.now()
    print(resp.status_code)
    print((end - start).total_seconds())