github.com/metacubex/mihomo@v1.18.5/listener/http/proxy.go (about) 1 package http 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net" 8 "net/http" 9 "strings" 10 "sync" 11 _ "unsafe" 12 13 "github.com/metacubex/mihomo/adapter/inbound" 14 "github.com/metacubex/mihomo/common/lru" 15 N "github.com/metacubex/mihomo/common/net" 16 C "github.com/metacubex/mihomo/constant" 17 authStore "github.com/metacubex/mihomo/listener/auth" 18 "github.com/metacubex/mihomo/log" 19 ) 20 21 //go:linkname registerOnHitEOF net/http.registerOnHitEOF 22 func registerOnHitEOF(rc io.ReadCloser, fn func()) 23 24 //go:linkname requestBodyRemains net/http.requestBodyRemains 25 func requestBodyRemains(rc io.ReadCloser) bool 26 27 func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { 28 client := newClient(c, tunnel, additions...) 29 defer client.CloseIdleConnections() 30 ctx, cancel := context.WithCancel(context.Background()) 31 defer cancel() 32 peekMutex := sync.Mutex{} 33 34 conn := N.NewBufferedConn(c) 35 36 keepAlive := true 37 trusted := cache == nil // disable authenticate if lru is nil 38 39 for keepAlive { 40 peekMutex.Lock() 41 request, err := ReadRequest(conn.Reader()) 42 peekMutex.Unlock() 43 if err != nil { 44 break 45 } 46 47 request.RemoteAddr = conn.RemoteAddr().String() 48 49 keepAlive = strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive" 50 51 var resp *http.Response 52 53 if !trusted { 54 var user string 55 resp, user = authenticate(request, cache) 56 additions = append(additions, inbound.WithInUser(user)) 57 trusted = resp == nil 58 } 59 60 if trusted { 61 if request.Method == http.MethodConnect { 62 // Manual writing to support CONNECT for http 1.0 (workaround for uplay client) 63 if _, err = fmt.Fprintf(conn, "HTTP/%d.%d %03d %s\r\n\r\n", request.ProtoMajor, request.ProtoMinor, http.StatusOK, "Connection established"); err != nil { 64 break // close connection 65 } 66 67 tunnel.HandleTCPConn(inbound.NewHTTPS(request, conn, additions...)) 68 69 return // hijack connection 70 } 71 72 host := request.Header.Get("Host") 73 if host != "" { 74 request.Host = host 75 } 76 77 request.RequestURI = "" 78 79 if isUpgradeRequest(request) { 80 handleUpgrade(conn, request, tunnel, additions...) 81 82 return // hijack connection 83 } 84 85 removeHopByHopHeaders(request.Header) 86 removeExtraHTTPHostPort(request) 87 88 if request.URL.Scheme == "" || request.URL.Host == "" { 89 resp = responseWith(request, http.StatusBadRequest) 90 } else { 91 request = request.WithContext(ctx) 92 93 startBackgroundRead := func() { 94 go func() { 95 peekMutex.Lock() 96 defer peekMutex.Unlock() 97 _, err := conn.Peek(1) 98 if err != nil { 99 cancel() 100 } 101 }() 102 } 103 if requestBodyRemains(request.Body) { 104 registerOnHitEOF(request.Body, startBackgroundRead) 105 } else { 106 startBackgroundRead() 107 } 108 resp, err = client.Do(request) 109 if err != nil { 110 resp = responseWith(request, http.StatusBadGateway) 111 } 112 } 113 114 removeHopByHopHeaders(resp.Header) 115 } 116 117 if keepAlive { 118 resp.Header.Set("Proxy-Connection", "keep-alive") 119 resp.Header.Set("Connection", "keep-alive") 120 resp.Header.Set("Keep-Alive", "timeout=4") 121 } 122 123 resp.Close = !keepAlive 124 125 err = resp.Write(conn) 126 if err != nil { 127 break // close connection 128 } 129 } 130 131 _ = conn.Close() 132 } 133 134 func authenticate(request *http.Request, cache *lru.LruCache[string, bool]) (resp *http.Response, u string) { 135 authenticator := authStore.Authenticator() 136 if inbound.SkipAuthRemoteAddress(request.RemoteAddr) { 137 authenticator = nil 138 } 139 if authenticator != nil { 140 credential := parseBasicProxyAuthorization(request) 141 if credential == "" { 142 resp := responseWith(request, http.StatusProxyAuthRequired) 143 resp.Header.Set("Proxy-Authenticate", "Basic") 144 return resp, "" 145 } 146 147 authed, exist := cache.Get(credential) 148 if !exist { 149 user, pass, err := decodeBasicProxyAuthorization(credential) 150 authed = err == nil && authenticator.Verify(user, pass) 151 u = user 152 cache.Set(credential, authed) 153 } 154 if !authed { 155 log.Infoln("Auth failed from %s", request.RemoteAddr) 156 157 return responseWith(request, http.StatusForbidden), u 158 } 159 } 160 161 return nil, u 162 } 163 164 func responseWith(request *http.Request, statusCode int) *http.Response { 165 return &http.Response{ 166 StatusCode: statusCode, 167 Status: http.StatusText(statusCode), 168 Proto: request.Proto, 169 ProtoMajor: request.ProtoMajor, 170 ProtoMinor: request.ProtoMinor, 171 Header: http.Header{}, 172 } 173 }