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  }