ReverseProxy 取决于 golang 中的 request.Body

Posted

技术标签:

【中文标题】ReverseProxy 取决于 golang 中的 request.Body【英文标题】:ReverseProxy depending on the request.Body in golang 【发布时间】:2018-09-19 13:53:13 【问题描述】:

我想构建一个 http 反向代理来检查 HTTP 正文,然后将 HTTP 请求发送到它的上游服务器。你怎么能在 go 中做到这一点?

初始尝试(跟随)失败,因为 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。 还有其他优雅的方式吗?

【问题讨论】:

Golang read request body的可能重复 @vitr 核心问题是相似的,但这个问题有不同的背景,这会导致更多种类的答案,如 ymonad 的答案,我认为这对其他访问者也很有价值 【参考方案1】:

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

这是一个示例代码,它从请求和代理中读取内容主体,从localhost:8080localhost: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.ReverseProxyDirector: director
    log.Fatalln(http.ListenAndServe(":8080", proxy))

【讨论】:

第二个请求有什么意义?为什么不直接调用 url.ParseQuery 呢? 这只是因为 OP 的代码调用了ParseForm()。此外,我想表明您可以调用 http.Request 中包含的任何函数(例如:ParseMultipartForm()【参考方案2】:

编辑:

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

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

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.NopCloser 使用 nop Close() 方法将 io.Reader 转换为 io.ReaderCloser

把所有东西放在一起

  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)
  

【讨论】:

r.ParseFormioutil.ReadAll(r.Body) 之后无法解析帖子正文,因为它已经被读取。 play.golang.org/p/rT6dKVsZWBB

以上是关于ReverseProxy 取决于 golang 中的 request.Body的主要内容,如果未能解决你的问题,请参考以下文章

golang 反向代理reverseproxy源码分析

golang复用http.request.body

golang——常用内建函数

reverseProxy:如何更改嵌入的 JavaScript 文件中的内容

golang maps 预留多少内存?

Golang中的init函数