Search code examples
httpgotcpforwardforwarding

http forward with domain blocking


I'm trying to implement a http forwarding server that supports domain blocking. I've tried

go io.Copy(dst, src)
go io.Copy(src, dst)

and it works like a charm on tcp forwarding. Then I've tried to do request line parsing with something similar to

go func(){
    reader := io.TeeReader(src, dst)
    textReader := textproto.NewReader(bufio.NewReader(reader))
    requestLine, _ = textReader.ReadLine()
    // ...
    ioutil.ReadAll(reader)
}

It works fine, but I was getting worried about bad performance(with ioutil.ReadAll). So I've written the code below.

func (f *Forwarder) handle(src, dst net.Conn) {
    defer dst.Close()
    defer src.Close()
    done := make(chan struct{})
    go func() {
        textReader := bufio.NewReader(src)
        requestLine, _ = textReader.ReadString('\n')
        // parse request line and apply domain blocking
        dst.Write([]byte(requestLine))
        io.Copy(dst, src)
        done <- struct{}{}
    }()
    go func() {
        textReader := bufio.NewReader(dst)
        s.statusLine, _ = textReader.ReadString('\n')
        src.Write([]byte(s.statusLine))
        io.Copy(src, dst)
        done <- struct{}{}
    }()
    <-done
    <-done
}

Unfortunately, it doesn't work at all. Requests get to print out, but not for responses. I've stuck here and don't know what's wrong.


Solution

  • TCP forwarding is to realize that the tunnel proxy does not need to parse data. The reverse proxy can use the standard library.

    The tunnel proxy is implemented to separate the http and https protocols. The client generally uses the tunnel to send https and sends the Connect method. Sending http is the Get method. For the https request service, only dail creates the connection tcp conversion, and the http request is implemented using a reverse proxy.

    func(w http.ResponseWriter, r *http.Request) {
        // check url host
        if r.URL.Host != "" {
            if r.Method == eudore.MethodConnect {
                // tunnel proxy
                conn, err := net.Dial("tcp", r.URL.Host)
                if err != nil {
                    w.WriteHeader(502)
                    return
                }
    
                client, _, err := w.Hijack()
                if err != nil {
                    w.WriteHeader(502)
                    conn.Close()
                    return
                }
                client.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
                go func() {
                    io.Copy(client, conn)
                    client.Close()
                    conn.Close()
                }()
                go func() {
                    io.Copy(conn, client)
                    client.Close()
                    conn.Close()
                }()
            } else {
                // reverse proxy
                httputil.NewSingleHostReverseProxy(r.URL).ServeHTTP(w, r)
            }
        }
    }
    
    

    Implementing a reverse proxy will parse the client request, and the proxy will send the request to the target server.

    Reverse proxy conversion request, not tested :

    func(w http.ResponseWriter, r *http.Request) {
        // set host
        r.URL.Scheme = "http"
        r.URL.Path = "example.com"
        // send
        resp,err := http.DefaultClient.Do(r)
        if err != nil {
            w.WriteHeader(502)
            return
        }
    
        // write respsonse
        defer resp.Body.Close()
        w.WriteHeader(resp.StatusCode)
        h := w.Header()
        for k,v := range resp.Header {
            h[k]=v
        }
        io.Copy(w, resp.Body)
    }
    

    However, the direct forwarding request does not process the hop-to-hop header. The hop-to-hop header is clearly stated in the rfc. The hop-to-hop header is the transmission information between two connections. For example, the client to the proxy and the proxy to the server are two. And the client to the server is end-to-end.

    Please use the standard library directly for the reverse proxy, it has already handled the hop-to-hop header and Upgrade for you.

    exmample NewSingleHostReverseProxy with filter:

    package main
    
    import (
        "net/http"
        "strings"
        "net/http/httputil"
        "net/url"
    )
    
    func main() {
        addr, _ := url.Parse("http://localhost:8088")
        proxy := httputil.NewSingleHostReverseProxy(addr)
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            if strings.HasPrefix(r.URL.Path, "/api/") {
                proxy.ServeHTTP(w, r)
            } else {
                w.WriteHeader(404)
            }
        })
        // Listen Server
    }