ReverseProxy取决于request.Body在golang

时间:2018-04-10 04:20:26

标签: http go reverse-proxy

我想构建一个http反向代理,它会检查HTTP正文并在此之后向其上游服务器发送HTTP请求。你怎么能这样做?

初始尝试(跟随)失败,因为ReverseProxy复制传入的请求,修改它并发送但主体已经被读取。

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"

然后我的下一次尝试是将整个身体读入bytes.Reader并使用它来检查身体内容,并在发送到上游服务器之前寻找开头。但后来我必须重新实现我想避免的ReverseProxy。 还有其他优雅的方式吗?

2 个答案:

答案 0 :(得分:1)

您可以将Director处理程序设置为httputil.ReverseProxy 文件:https://golang.org/pkg/net/http/httputil/#ReverseProxy

以下是一个示例代码,用于从请求中读取内容正文,代理从localhost:8080读取到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))
}

答案 1 :(得分:1)

修改

如上所述,在这种情况下,解析后的表单将为空。您需要从正文中手动解析表单。

request.Bodyio.ReaderCloser,因为它描述了tcp连接的rx部分。但是在您的用例中,您需要阅读所有内容,因为您正在将主体解析为表单。这里的技巧是将r.Body重新分配一个从已读取数据派生的io.ReaderCloser对象。这就是我要做的事情:

1。获取请求正文的引用作为字节切片:

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

2。在解析表单

后重新分配r.Body
  // after calling r.ParseForm(), reassign body
  r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

bytes.NewBuffer(body)将正文字节切换转换为io.Readerioutil.NopCloserio.Reader转换为io.ReaderCloser nop {{ 1}}方法。

将所有东西放在一起

Close()