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.
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.
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.")
}