├── .gcloudignore ├── .gitignore ├── .nowignore ├── Makefile ├── README.md ├── gohttp.go ├── lambda.go ├── now.json └── serverless.yml /.gcloudignore: -------------------------------------------------------------------------------- 1 | /[!.]* 2 | /.?* 3 | !/gohttp.go 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .serverless/ 3 | -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | * 2 | !gohttp.go 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean aws gcf upload 2 | 3 | build: 4 | env GOOS=linux go build -ldflags="-s -w" -o bin/p lambda.go gohttp.go 5 | 6 | clean: 7 | rm -rf ./bin 8 | 9 | upload: 10 | sls deploy --verbose 11 | 12 | aws: clean build upload 13 | 14 | gcf: 15 | sed -i '' "1s/main/proxlet/" gohttp.go 16 | gcloud functions deploy proxlet --runtime go111 --entry-point Handler --trigger-http 17 | sed -i '' "1s/proxlet/main/" gohttp.go 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxlet 2 | 3 | [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/Contextualist/proxlet) 4 | 5 | Proxlet is a POC, naïve implemetation of serverless reverse proxy for any website. 6 | 7 | ## Supported Providers 8 | 9 | * [Now.sh](https://zeit.co/now) 10 | * [AWS Lambda](https://aws.amazon.com/lambda) 11 | * [Google Cloud Functions](https://cloud.google.com/functions) 12 | 13 | ### Now.sh 14 | 15 | dev dependency: [now-cli](https://github.com/zeit/now-cli#usage) 16 | 17 | ```bash 18 | now 19 | 20 | curl https://{app_name}.now.sh/https://httpbin.org/get 21 | ``` 22 | 23 | ### AWS Lambda 24 | 25 | dev dependency: [Serverless Framework](https://serverless.com/framework/docs/getting-started/) 26 | 27 | ```bash 28 | # First customize serverless.yml's fields: provider.role, provider.region, etc. 29 | make aws 30 | 31 | # Then go to AWS API Gateway console, Setting, add Binary Media Type "*/*". 32 | # (All contents are base64-encoded and will be decoded by the API Gateway.) 33 | 34 | curl https://{id}.execute-api.{region}.amazonaws.com/dev/https://httpbin.org/get 35 | ``` 36 | 37 | CAVEAT: All subsequent requests fail because of they are lack of the stage path `/dev` 38 | 39 | ### Google Cloud Functions 40 | 41 | dev dependency: [gcloud](https://cloud.google.com/sdk/docs/quickstarts) 42 | 43 | ```bash 44 | make gcf 45 | 46 | curl https://{region}-{project-id}.cloudfunctions.net/proxlet/https://httpbin.org/get 47 | ``` 48 | 49 | CAVEAT: All subsequent requests fail because of they are lack of the function path `/proxlet` 50 | 51 | ## About the Cookie 52 | 53 | To get the real host of subsequent request of relative path, Proxlet sets a session cookie `proxlet-host` with the value of real host. The server does not store the cookie. 54 | 55 | ## Known Issues 56 | 57 | * Cross-site requests are not proxied. 58 | * Now.sh and AWS Lambda have an HTTP response size limit of 6MB. For GCF, it's 10MB. 59 | * AWS Lambda's stage path / GCF's function name path makes all subsequent requests impossible *(unless using a custom domain)*. 60 | -------------------------------------------------------------------------------- /gohttp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | func Handler(w http.ResponseWriter, r *http.Request) { 12 | rawurl := r.URL.String()[1:] 13 | u, err := url.Parse(rawurl) 14 | if checkErr(err) { 15 | return 16 | } 17 | isMainReq := u.Scheme != "" 18 | if !isMainReq { // subsequent requests 19 | var referer string 20 | cookie, err := r.Cookie("proxlet-host") 21 | if err == nil { 22 | referer = cookie.Value 23 | } else if refHeader := r.Header.Get("Referer"); refHeader != "" { 24 | // Fallback to Referer header, which does not always work. 25 | tmp, err := url.Parse(refHeader) 26 | if checkErr(err) { 27 | return 28 | } 29 | referer = tmp.Path[1:] 30 | r.Header.Set("Referer", referer) 31 | } else { 32 | return 33 | } 34 | ru, err := url.Parse(referer) 35 | if checkErr(err) { 36 | return 37 | } 38 | u, err = url.Parse(ru.Scheme + "://" + ru.Host + "/" + rawurl) 39 | if checkErr(err) { 40 | return 41 | } 42 | } 43 | //log.Println(u.String()) 44 | r.URL = u 45 | r.Host = u.Host 46 | if rcookie, ok := r.Header["Cookie"]; ok { 47 | for i, v := range rcookie { 48 | if strings.HasPrefix(v, "proxlet-host=") { 49 | rcookie = append(rcookie[:i], rcookie[i+1:]...) 50 | break 51 | } 52 | } 53 | } 54 | r.Header.Set("Accept-Encoding", "identity") // disable any encoding 55 | h := httputil.NewSingleHostReverseProxy(&url.URL{ 56 | Scheme: u.Scheme, 57 | Host: u.Host, 58 | }) 59 | h.ModifyResponse = func(rsp *http.Response) (err error) { // server-side redirect 60 | if isMainReq { 61 | defer func() { 62 | http.SetCookie(w, &http.Cookie{ 63 | Name: "proxlet-host", 64 | Value: r.URL.String(), 65 | Path: "/", 66 | }) 67 | }() 68 | } 69 | switch rsp.StatusCode { 70 | case 301, 302, 303, 307, 308: 71 | default: 72 | return nil 73 | } 74 | loc, err := r.URL.Parse(rsp.Header.Get("Location")) 75 | if err != nil { 76 | return 77 | } 78 | //log.Println(loc.String()) 79 | r.URL = loc 80 | r.Host = r.URL.Host 81 | r.RequestURI = "" 82 | rsp_, err := http.DefaultClient.Do(r) 83 | if rsp_ != nil { 84 | *rsp = *rsp_ 85 | } 86 | return 87 | } 88 | h.ServeHTTP(w, r) 89 | } 90 | 91 | func checkErr(err error) bool { 92 | if err != nil { 93 | log.Println(err) 94 | return true 95 | } 96 | return false 97 | } 98 | -------------------------------------------------------------------------------- /lambda.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | "github.com/zeit/now-builders/utils/go/bridge" 7 | "net/http" 8 | ) 9 | 10 | func awshandler(q events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 11 | encoding := "" 12 | if q.IsBase64Encoded { 13 | encoding = "base64" 14 | } 15 | r, err := bridge.Serve(http.HandlerFunc(Handler), &bridge.Request{ 16 | Host: q.Headers["Host"], 17 | Path: q.Path, 18 | Method: q.HTTPMethod, 19 | Headers: q.Headers, 20 | Encoding: encoding, 21 | Body: q.Body, 22 | }) 23 | return events.APIGatewayProxyResponse{ 24 | StatusCode: r.StatusCode, 25 | MultiValueHeaders: r.Headers, 26 | Body: r.Body, 27 | IsBase64Encoded: true, 28 | }, err 29 | } 30 | 31 | func main() { 32 | lambda.Start(awshandler) 33 | } 34 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "proxlet", 4 | "alias": "proxlet.now.sh", 5 | "regions": ["all"], 6 | "builds": [ 7 | { "src": "gohttp.go", "use": "@now/go" } 8 | ], 9 | "routes": [ 10 | { "src": "/.*", "dest": "gohttp.go" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: proxlet 2 | 3 | frameworkVersion: ">=1.28.0 <2.0.0" 4 | 5 | provider: 6 | name: aws 7 | role: # PUT YOUR ROLE HERE 8 | runtime: go1.x 9 | stage: dev 10 | region: ap-northeast-1 11 | 12 | package: 13 | exclude: 14 | - ./** 15 | include: 16 | - ./bin/** 17 | 18 | functions: 19 | p: 20 | handler: bin/p 21 | events: 22 | - http: 23 | path: /{any+} 24 | method: ANY 25 | --------------------------------------------------------------------------------