3

I want to build a http reverse proxy which checks the HTTP body and send HTTP requests to it's upstream servers after that. How can you do that in go?

Initial attempt (follows) fails because ReverseProxy copies the incoming request, modifies it and sends but the body is already read.

func main() {
    backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        b, err := ioutil.ReadAll(r.Body)
        if err != nil {
            http.Error(w, fmt.Sprintf("ioutil.ReadAll: %s", err), 500)
            return
        }
        // expecting to see hoge=fuga
        fmt.Fprintf(w, "this call was relayed by the reverse proxy, body: %s", string(b))
    }))
    defer backendServer.Close()

    rpURL, err := url.Parse(backendServer.URL)
    if err != nil {
        log.Fatal(err)
    }

    proxy := func(u *url.URL) http.Handler {
        p := httputil.NewSingleHostReverseProxy(u)
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if err := r.ParseForm(); err != nil {
                http.Error(w, fmt.Sprintf("ParseForm: %s", err), 500)
                return
            }
            p.ServeHTTP(w, r)
        })
    }(rpURL)
    frontendProxy := httptest.NewServer(proxy)
    defer frontendProxy.Close()

    resp, err := http.Post(frontendProxy.URL, "application/x-www-form-urlencoded", bytes.NewBufferString("hoge=fuga"))
    if err != nil {
        log.Fatalf("http.Post: %s", err)
    }

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("ioutil.ReadAll: %s", err)
    }

    fmt.Printf("%s", b)
}
// shows: "http: proxy error: http: ContentLength=9 with Body length 0"

Then my next attempt would be to read the whole body into bytes.Reader and use that to check the body content, and Seek to the beginning before sending to upstream servers. But then I have to re-implement ReverseProxy which I would like to avoid. Is there any other elegant way?

mash
  • 4,204
  • 4
  • 32
  • 34
  • Possible duplicate of [Golang read request body](https://stackoverflow.com/questions/43021058/golang-read-request-body) – vitr Apr 10 '18 at 08:13
  • @vitr well the core issue is similar but this question has a different context which results into more variety of answers as seen in ymonad 's one, which I think is valuable for other visitors too – mash Apr 10 '18 at 15:53

2 Answers2

6

You can set Director handler to httputil.ReverseProxy Document: https://golang.org/pkg/net/http/httputil/#ReverseProxy

Here's an example code which reads content body from request and proxies from localhost:8080 to localhost:3333

package main

import (
    "bytes"
    "io/ioutil"
    "log"
    "net/http"
    "net/http/httputil"
)

func main() {
    director := func(req *http.Request) {
        if req.Body != nil {
            // read all bytes from content body and create new stream using it.
            bodyBytes, _ := ioutil.ReadAll(req.Body)
            req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

            // create new request for parsing the body
            req2, _ := http.NewRequest(req.Method, req.URL.String(), bytes.NewReader(bodyBytes))
            req2.Header = req.Header
            req2.ParseForm()
            log.Println(req2.Form)
        }

        req.URL.Host = "localhost:3333"
        req.URL.Scheme = "http"
    }
    proxy := &httputil.ReverseProxy{Director: director}
    log.Fatalln(http.ListenAndServe(":8080", proxy))
}
ymonad
  • 11,710
  • 1
  • 38
  • 49
  • What's the point of the second request? Why not call url.ParseQuery directly? – Peter Apr 10 '18 at 07:27
  • It's just because the OP's code is calling `ParseForm()`. besides I want to show that you can call any function which is included in `http.Request` (e.g. : `ParseMultipartForm()` ) – ymonad Apr 10 '18 at 07:37
3

edit:

As commented above, the parsed form will be empty in this case. You will need to manually parse the form from the body.

The request.Body is a io.ReaderCloser, because this describes the rx part of a tcp connection. But in your use case you need to read everything since you are parsing the body into a form. The trick here is to reassign the r.Body with a io.ReaderCloser object derived from the already read data. Here is what I would do:

1. Get a reference of the request body as a byte slice:

  // before calling r.ParseForm(), get the body
  // as a byte slice
  body, err := ioutil.ReadAll(r.Body)

2. Reassign r.Body after parsing form

  // after calling r.ParseForm(), reassign body
  r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

bytes.NewBuffer(body) converts the body byte slice into a io.Reader, and ioutil.NopCloser convertts a io.Reader into a io.ReaderCloser with a nop Close() method.

Putting Everything Together

  package main

  import "net/http"
  import "net/http/httputil"
  import "net/url"
  import "net/http/httptest"
  import "fmt"
  import "log"
  import "bytes"
  import "io/ioutil"

  func main() {
    backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        b, err := ioutil.ReadAll(r.Body)
        if err != nil {
            http.Error(w, fmt.Sprintf("ioutil.ReadAll: %s", err), 500)
            return
        }
        // expecting to see hoge=fuga
        fmt.Fprintf(w, "this call was relayed by the reverse proxy, body: %s", string(b))
    }))
    defer backendServer.Close()

    rpURL, err := url.Parse(backendServer.URL)
    if err != nil {
        log.Fatal(err)
    }

    proxy := func(u *url.URL) http.Handler {
        p := httputil.NewSingleHostReverseProxy(u)
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // read out body into a slice
            body, err := ioutil.ReadAll(r.Body)
            if err != nil {
                http.Error(w, fmt.Sprintf("Error reading body: %s", err), 500)
                return
            }

            // inspect current body here
            if err := r.ParseForm(); err != nil {
                http.Error(w, fmt.Sprintf("ParseForm: %s", err), 500)
                return
            }

            // assign a new body with previous byte slice
            r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
            p.ServeHTTP(w, r)
        })
    }(rpURL)
    frontendProxy := httptest.NewServer(proxy)
    defer frontendProxy.Close()

    resp, err := http.Post(
        frontendProxy.URL,
        "application/x-www-form-urlencoded",
        bytes.NewBufferString("hoge=fuga"))
    if err != nil {
        log.Fatalf("http.Post: %s", err)
    }

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("ioutil.ReadAll: %s", err)
    }

    fmt.Printf("%s", b)
  }
Aaron Qian
  • 4,477
  • 2
  • 24
  • 27
  • 1
    `r.ParseForm` after `ioutil.ReadAll(r.Body)` there cannot parse the post body, because it's already read. https://play.golang.org/p/rT6dKVsZWBB – mash Apr 10 '18 at 16:18