Description

  • [HTTP CONNECT] is the most common form of HTTP tunneling is the standard. In this mechanism, the client asks an HTTP proxy server to forward the TCP connection to the desired destination.
  • In this case, the HTTP request looks like CONNECT google.com:443\r\n, this kind of method and path cannot be captured by the gin router, resulting in 404 not found.

How to reproduce

// cannot route
router.Handle("CONNECT", "/", ...)

// cannot route
router.Handle("CONNECT", "*target", ...)

// not allowed
router.Handle("CONNECT", "*", ...)
  • Request: Firefox -> Settings -> General -> Network Settings -> HTTP(s) Proxy

Environment

  • gin version: 1.9.1

Comment From: crazy-dragon

Hi bro, The CONNECT http is not the http web method, so gin do not support it. So, your connect route always to 404.

Gin GIN cannot handle CONNECT where rpath is an empty string

BUT, 天无绝人之路,I have an idea that can make you work around it. If you meet 404 that mean gin has been get your request, so we can process it. But, it remains a problem, when you proxy https requests, then you need to creat a TCP tunnel, but the connection is HTTP connection. But, I find a way, you can hijack the connection then transformer it to TCP connection. (This is how websocker is implemented).

So, let's try it.

Gin GIN cannot handle CONNECT where rpath is an empty string

package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
)

const (
    TUNNEL_PACKET = `HTTP/1.1 200 Connection Established\r\nProxy-agent: CrazyDragonHttpProxy\r\n\r\n`
)

func main() {
    r := gin.Default()
    r.NoRoute(routeProxy) // NO Route is every Route!!!
    r.Run(":8000")        // listen and serve on 0.0.0.0:8080
}

// Then I can process all routes
func routeProxy(c *gin.Context) {
    req := c.Request
    go resolveReq(req) // just print basic info. Remember you can't proxy youself.

    if req.Method == http.MethodConnect {
        // create http tunnel process https
        httpsProxy(c, req)
    } else {
        // process plain http
        httpProxy(c, req)
    }
}

func resolveReq(req *http.Request) {
    fmt.Printf("Method: %s, Host: %s, URL: %s, Version: %s\n", req.Method, req.Host, req.URL.String(), req.Proto)
}

func httpProxy(c *gin.Context, req *http.Request) {
    newReq, _ := http.NewRequest(req.Method, req.URL.String(), req.Body)
    resp, err := http.DefaultClient.Do(newReq)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    code := resp.StatusCode
    c.Status(code) // change the status code, default is 404 !!!
    for k, v := range resp.Header {
        c.Header(k, strings.Join(v, ","))
    }
    c.Header("Server", "CrazyDragonHttpProxy") // just change it, this is yours gin proxy.
    c.Writer.Write(data)
}

func httpsProxy(c *gin.Context, req *http.Request) {
    // established connect tunnel
    c.Status(200)
    c.Header("Server", "CrazyDragonHttpProxy")
    c.Writer.Write([]byte(TUNNEL_PACKET))
    // c.Writer.Flush() // this may cause proble, but I don't know.

    address := req.URL.Host // it contains the port
    tunnelConn, err := net.Dial("tcp", address)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("try to established Connect Tunnel to: %s has been successfully.\n", address)

    // But next is a TCP communication, but the tcp conn is a non-export variable,
    // so I can't get it, in the same, client's data is binary, so gin can't parse it.
    // so I can't do anything!!!
    // LOL, I find HTTP hijacker, it can make me take over the connection!!!
    hj, ok := c.Writer.(http.Hijacker)
    if !ok {
        http.Error(c.Writer, "webserver doesn't support hijacking", http.StatusInternalServerError)
        return
    }
    conn, bufrw, err := hj.Hijack()
    if err != nil {
        http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
        return
    }
    defer conn.Close()

    // first read request, then write response.
    // data flow direction:
    // client <---> proxy <---> server

    done := make(chan struct{})
    go func() {

        io.Copy(tunnelConn, bufrw)

                // this is my first write, but then I find other use io.Copy
        // log.Println("client --> proxy --> server")
        // data := make([]byte, 1024)
        // for {
        //  log.Println("client --> proxy")
        //  n, err := bufrw.Read(data)
        //  if err != nil {
        //      log.Printf("%v\n", err)
        //      done <- struct{}{}
        //  }
        //  log.Println("proxy --> server")
        //  tunnelConn.Write(data[:n])
        // }
    }()

    go func() {

        io.Copy(bufrw, tunnelConn)

        //  log.Println("server --> proxy --> client")
        //  data := make([]byte, 1024)
        //  for {
        //      log.Println("server --> proxy")
        //      for {
        //          n, err := tunnelConn.Read(data)
        //          if err != nil {
        //              log.Printf("%v\n", err)
        //              done <- struct{}{}
        //          }
        //          log.Println("proxy --> client")
        //          bufrw.Write(data[:n])
        //          if n < 1024 {
        //              break
        //          }
        //      }
        //      bufrw.Flush()
        //  }
    }()

    <-done
    fmt.Println("The Tunnel has closed.")
}