Refactor: plain http proxy (#1443)

This commit is contained in:
Kr328
2021-06-15 17:13:40 +08:00
committed by GitHub
parent 70d53fd45a
commit b6ff08074c
11 changed files with 281 additions and 273 deletions

39
listener/http/client.go Normal file
View File

@ -0,0 +1,39 @@
package http
import (
"context"
"errors"
"net"
"net/http"
"time"
"github.com/Dreamacro/clash/adapter/inbound"
C "github.com/Dreamacro/clash/constant"
)
func newClient(source net.Addr, in chan<- C.ConnContext) *http.Client {
return &http.Client{
Transport: &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: func(context context.Context, network, address string) (net.Conn, error) {
if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, errors.New("unsupported network " + network)
}
left, right := net.Pipe()
in <- inbound.NewHTTP(address, source, right)
return left, nil
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}

10
listener/http/hack.go Normal file
View File

@ -0,0 +1,10 @@
package http
import (
"bufio"
"net/http"
_ "unsafe"
)
//go:linkname ReadRequest net/http.readRequest
func ReadRequest(b *bufio.Reader, deleteHostHeader bool) (req *http.Request, err error)

132
listener/http/proxy.go Normal file
View File

@ -0,0 +1,132 @@
package http
import (
"net"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/adapter/inbound"
"github.com/Dreamacro/clash/common/cache"
N "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant"
authStore "github.com/Dreamacro/clash/listener/auth"
"github.com/Dreamacro/clash/log"
)
func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
client := newClient(c.RemoteAddr(), in)
defer client.CloseIdleConnections()
conn := N.NewBufferedConn(c)
keepAlive := true
trusted := cache == nil // disable authenticate if cache is nil
for keepAlive {
request, err := ReadRequest(conn.Reader(), false)
if err != nil {
break
}
request.RemoteAddr = conn.RemoteAddr().String()
keepAlive = strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive"
var resp *http.Response
if !trusted {
resp = authenticate(request, cache)
trusted = resp == nil
}
if trusted {
if request.Method == http.MethodConnect {
resp = responseWith(200)
resp.Status = "Connection established"
if resp.Write(conn) != nil {
break // close connection
}
in <- inbound.NewHTTPS(request, conn)
return // hijack connection
}
host := request.Header.Get("Host")
if host != "" {
request.Host = host
}
request.RequestURI = ""
RemoveHopByHopHeaders(request.Header)
RemoveExtraHTTPHostPort(request)
if request.URL.Scheme == "" || request.URL.Host == "" {
resp = responseWith(http.StatusBadRequest)
} else {
resp, err = client.Do(request)
if err != nil {
resp = responseWith(http.StatusBadGateway)
}
}
}
RemoveHopByHopHeaders(resp.Header)
if keepAlive {
resp.Header.Set("Proxy-Connection", "keep-alive")
resp.Header.Set("Connection", "keep-alive")
resp.Header.Set("Keep-Alive", "timeout=4")
}
resp.Close = !keepAlive
err = resp.Write(conn)
if err != nil {
break // close connection
}
}
conn.Close()
}
func authenticate(request *http.Request, cache *cache.Cache) *http.Response {
authenticator := authStore.Authenticator()
if authenticator != nil {
credential := ParseBasicProxyAuthorization(request)
if credential == "" {
resp := responseWith(http.StatusProxyAuthRequired)
resp.Header.Set("Proxy-Authenticate", "Basic")
return resp
}
var authed interface{}
if authed = cache.Get(credential); authed == nil {
user, pass, err := DecodeBasicProxyAuthorization(credential)
authed = err == nil && authenticator.Verify(user, pass)
cache.Put(credential, authed, time.Minute)
}
if !authed.(bool) {
log.Infoln("Auth failed from %s", request.RemoteAddr)
return responseWith(http.StatusForbidden)
}
}
return nil
}
func responseWith(statusCode int) *http.Response {
return &http.Response{
StatusCode: statusCode,
Status: http.StatusText(statusCode),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
}

View File

@ -1,44 +1,48 @@
package http
import (
"bufio"
"encoding/base64"
"net"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/adapter/inbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/auth"
C "github.com/Dreamacro/clash/constant"
authStore "github.com/Dreamacro/clash/listener/auth"
"github.com/Dreamacro/clash/log"
)
type Listener struct {
listener net.Listener
address string
closed bool
cache *cache.Cache
}
func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
return NewWithAuthenticate(addr, in, true)
}
func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool) (*Listener, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
hl := &Listener{l, addr, false, cache.New(30 * time.Second)}
var c *cache.Cache
if authenticate {
c = cache.New(time.Second * 30)
}
hl := &Listener{
listener: l,
address: addr,
}
go func() {
for {
c, err := hl.listener.Accept()
conn, err := hl.listener.Accept()
if err != nil {
if hl.closed {
break
}
continue
}
go HandleConn(c, in, hl.cache)
go HandleConn(conn, in, c)
}
}()
@ -53,59 +57,3 @@ func (l *Listener) Close() {
func (l *Listener) Address() string {
return l.address
}
func canActivate(loginStr string, authenticator auth.Authenticator, cache *cache.Cache) (ret bool) {
if result := cache.Get(loginStr); result != nil {
ret = result.(bool)
return
}
loginData, err := base64.StdEncoding.DecodeString(loginStr)
login := strings.Split(string(loginData), ":")
ret = err == nil && len(login) == 2 && authenticator.Verify(login[0], login[1])
cache.Put(loginStr, ret, time.Minute)
return
}
func HandleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
br := bufio.NewReader(conn)
keepAlive:
request, err := http.ReadRequest(br)
if err != nil || request.URL.Host == "" {
conn.Close()
return
}
keepAlive := strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive"
authenticator := authStore.Authenticator()
if authenticator != nil {
if authStrings := strings.Split(request.Header.Get("Proxy-Authorization"), " "); len(authStrings) != 2 {
conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n"))
if keepAlive {
goto keepAlive
}
return
} else if !canActivate(authStrings[1], authenticator, cache) {
conn.Write([]byte("HTTP/1.1 403 Forbidden\r\n\r\n"))
log.Infoln("Auth failed from %s", conn.RemoteAddr().String())
if keepAlive {
goto keepAlive
}
conn.Close()
return
}
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
conn.Close()
return
}
in <- inbound.NewHTTPS(request, conn)
return
}
in <- inbound.NewHTTP(request, conn)
}

74
listener/http/utils.go Normal file
View File

@ -0,0 +1,74 @@
package http
import (
"encoding/base64"
"errors"
"net"
"net/http"
"strings"
)
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
func RemoveExtraHTTPHostPort(req *http.Request) {
host := req.Host
if host == "" {
host = req.URL.Host
}
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
host = pHost
}
req.Host = host
req.URL.Host = host
}
// ParseBasicProxyAuthorization parse header Proxy-Authorization and return base64-encoded credential
func ParseBasicProxyAuthorization(request *http.Request) string {
value := request.Header.Get("Proxy-Authorization")
if !strings.HasPrefix(value, "Basic ") {
return ""
}
return value[6:] // value[len("Basic "):]
}
// DecodeBasicProxyAuthorization decode base64-encoded credential
func DecodeBasicProxyAuthorization(credential string) (string, string, error) {
plain, err := base64.StdEncoding.DecodeString(credential)
if err != nil {
return "", "", err
}
login := strings.Split(string(plain), ":")
if len(login) != 2 {
return "", "", errors.New("invalid login")
}
return login[0], login[1], nil
}

View File

@ -1,41 +0,0 @@
package mixed
import (
"bufio"
"net"
)
type BufferedConn struct {
r *bufio.Reader
net.Conn
}
func NewBufferedConn(c net.Conn) *BufferedConn {
return &BufferedConn{bufio.NewReader(c), c}
}
// Reader returns the internal bufio.Reader.
func (c *BufferedConn) Reader() *bufio.Reader {
return c.r
}
// Peek returns the next n bytes without advancing the reader.
func (c *BufferedConn) Peek(n int) ([]byte, error) {
return c.r.Peek(n)
}
func (c *BufferedConn) Read(p []byte) (int, error) {
return c.r.Read(p)
}
func (c *BufferedConn) ReadByte() (byte, error) {
return c.r.ReadByte()
}
func (c *BufferedConn) UnreadByte() error {
return c.r.UnreadByte()
}
func (c *BufferedConn) Buffered() int {
return c.r.Buffered()
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/Dreamacro/clash/common/cache"
N "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/http"
"github.com/Dreamacro/clash/listener/socks"
@ -51,7 +52,7 @@ func (l *Listener) Address() string {
}
func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
bufConn := NewBufferedConn(conn)
bufConn := N.NewBufferedConn(conn)
head, err := bufConn.Peek(1)
if err != nil {
return