mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-03 19:23:51 +08:00
295 lines
7.2 KiB
Go
295 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// dynamicForwardProxy is a credential-free, stable local proxy. Each request
|
|
// resolves the current upstream, so pool switches do not rebuild agents or MCP.
|
|
type dynamicForwardProxy struct {
|
|
ln net.Listener
|
|
addr string
|
|
current func() string
|
|
direct func() bool
|
|
server *http.Server
|
|
once sync.Once
|
|
mu sync.Mutex
|
|
conns map[net.Conn]struct{}
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func newDynamicForwardProxy(current func() string) (*dynamicForwardProxy, error) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
p := &dynamicForwardProxy{ln: ln, addr: "http://" + ln.Addr().String(), current: current, direct: func() bool { return true }, conns: map[net.Conn]struct{}{}, ctx: ctx, cancel: cancel}
|
|
p.server = &http.Server{Handler: http.HandlerFunc(p.serveHTTP), BaseContext: func(net.Listener) context.Context { return ctx }}
|
|
go p.server.Serve(ln)
|
|
return p, nil
|
|
}
|
|
|
|
func (p *dynamicForwardProxy) Addr() string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return p.addr
|
|
}
|
|
func (p *dynamicForwardProxy) Close() error {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
p.once.Do(func() {
|
|
p.cancel()
|
|
p.mu.Lock()
|
|
for c := range p.conns {
|
|
_ = c.Close()
|
|
}
|
|
p.conns = nil
|
|
p.mu.Unlock()
|
|
_ = p.server.Close()
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (p *dynamicForwardProxy) track(c net.Conn) func() {
|
|
if c == nil {
|
|
return func() {}
|
|
}
|
|
p.mu.Lock()
|
|
if p.conns != nil {
|
|
p.conns[c] = struct{}{}
|
|
}
|
|
p.mu.Unlock()
|
|
return func() { p.mu.Lock(); delete(p.conns, c); p.mu.Unlock() }
|
|
}
|
|
|
|
func (p *dynamicForwardProxy) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodConnect {
|
|
p.serveConnect(w, r)
|
|
return
|
|
}
|
|
up := strings.TrimSpace(p.current())
|
|
if up == "" && p.direct != nil && !p.direct() {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
tr := &http.Transport{Proxy: nil}
|
|
if up != "" {
|
|
u, err := url.Parse(up)
|
|
if err != nil {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(u.Scheme), "socks5") {
|
|
d, err := socksDialer(u)
|
|
if err != nil {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return dialProxyContext(ctx, d, network, addr)
|
|
}
|
|
} else {
|
|
tr.Proxy = http.ProxyURL(u)
|
|
}
|
|
}
|
|
tr.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
|
defer tr.CloseIdleConnections()
|
|
rr := r.Clone(r.Context())
|
|
if rr.URL.IsAbs() == false {
|
|
rr.URL.Scheme = "http"
|
|
rr.URL.Host = r.Host
|
|
}
|
|
rr.RequestURI = ""
|
|
for k := range rr.Header {
|
|
if strings.EqualFold(k, "Proxy-Authorization") {
|
|
rr.Header.Del(k)
|
|
}
|
|
}
|
|
resp, err := tr.RoundTrip(rr)
|
|
if err != nil {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
for k, vv := range resp.Header {
|
|
for _, v := range vv {
|
|
w.Header().Add(k, v)
|
|
}
|
|
}
|
|
w.WriteHeader(resp.StatusCode)
|
|
_, _ = io.Copy(w, resp.Body)
|
|
}
|
|
|
|
func (p *dynamicForwardProxy) serveConnect(w http.ResponseWriter, r *http.Request) {
|
|
up := strings.TrimSpace(p.current())
|
|
if up == "" && p.direct != nil && !p.direct() {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
target := r.Host
|
|
var conn net.Conn
|
|
var err error
|
|
if up == "" {
|
|
conn, err = (&net.Dialer{Timeout: 10 * time.Second}).DialContext(r.Context(), "tcp", target)
|
|
} else {
|
|
u, e := url.Parse(up)
|
|
if e != nil {
|
|
err = e
|
|
} else if strings.HasPrefix(strings.ToLower(u.Scheme), "socks5") {
|
|
d, e := socksDialer(u)
|
|
if e != nil {
|
|
err = e
|
|
} else {
|
|
conn, err = dialProxyContext(r.Context(), d, "tcp", target)
|
|
}
|
|
} else {
|
|
conn, err = dialHTTPConnect(r.Context(), u, target)
|
|
}
|
|
}
|
|
if err != nil {
|
|
http.Error(w, "proxy unavailable", 502)
|
|
return
|
|
}
|
|
h, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
http.Error(w, "proxy unavailable", 500)
|
|
return
|
|
}
|
|
client, rw, err := h.Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
removeClient := p.track(client)
|
|
defer removeClient()
|
|
removeUpstream := p.track(conn)
|
|
defer removeUpstream()
|
|
defer client.Close()
|
|
defer conn.Close()
|
|
_, _ = rw.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
_ = rw.Flush()
|
|
clientReader := io.Reader(client)
|
|
if rw.Reader != nil && rw.Reader.Buffered() > 0 {
|
|
clientReader = io.MultiReader(rw.Reader, client)
|
|
}
|
|
done := make(chan struct{}, 2)
|
|
go func() { _, _ = io.Copy(conn, clientReader); _ = conn.Close(); done <- struct{}{} }()
|
|
go func() { _, _ = io.Copy(client, conn); _ = client.Close(); done <- struct{}{} }()
|
|
<-done
|
|
}
|
|
|
|
func socksDialer(u *url.URL) (proxy.Dialer, error) {
|
|
var auth *proxy.Auth
|
|
if u.User != nil {
|
|
pw, _ := u.User.Password()
|
|
auth = &proxy.Auth{User: u.User.Username(), Password: pw}
|
|
}
|
|
return proxy.SOCKS5("tcp", u.Host, auth, &net.Dialer{Timeout: 10 * time.Second})
|
|
}
|
|
|
|
func dialProxyContext(ctx context.Context, d proxy.Dialer, network, addr string) (net.Conn, error) {
|
|
if cd, ok := d.(proxy.ContextDialer); ok {
|
|
return cd.DialContext(ctx, network, addr)
|
|
}
|
|
type result struct {
|
|
c net.Conn
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
go func() { c, err := d.Dial(network, addr); ch <- result{c, err} }()
|
|
select {
|
|
case r := <-ch:
|
|
return r.c, r.err
|
|
case <-ctx.Done():
|
|
go func() {
|
|
r := <-ch
|
|
if r.c != nil {
|
|
_ = r.c.Close()
|
|
}
|
|
}()
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
type bufferedConn struct {
|
|
net.Conn
|
|
reader *bufio.Reader
|
|
}
|
|
|
|
func (c *bufferedConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
|
|
|
func dialHTTPConnect(ctx context.Context, u *url.URL, target string) (net.Conn, error) {
|
|
d := &net.Dialer{Timeout: 10 * time.Second}
|
|
addr := u.Host
|
|
if !strings.Contains(addr, ":") {
|
|
addr += ":80"
|
|
}
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.EqualFold(u.Scheme, "https") {
|
|
conn = tls.Client(conn, &tls.Config{ServerName: u.Hostname(), MinVersion: tls.VersionTLS12})
|
|
if err := conn.(*tls.Conn).HandshakeContext(ctx); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
br := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
|
fmt.Fprintf(br, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", target, target)
|
|
if u.User != nil {
|
|
req := &http.Request{Header: http.Header{}}
|
|
req.SetBasicAuth(u.User.Username(), func() string { p, _ := u.User.Password(); return p }())
|
|
fmt.Fprintf(br, "Proxy-Authorization: %s\r\n", req.Header.Get("Authorization"))
|
|
}
|
|
fmt.Fprint(br, "\r\n")
|
|
if err := br.Flush(); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
readCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
type responseResult struct {
|
|
resp *http.Response
|
|
err error
|
|
}
|
|
readDone := make(chan responseResult, 1)
|
|
go func() {
|
|
resp, err := http.ReadResponse(br.Reader, &http.Request{Method: http.MethodConnect})
|
|
readDone <- responseResult{resp: resp, err: err}
|
|
}()
|
|
var rr responseResult
|
|
select {
|
|
case rr = <-readDone:
|
|
case <-readCtx.Done():
|
|
_ = conn.Close()
|
|
<-readDone
|
|
return nil, readCtx.Err()
|
|
}
|
|
resp, err := rr.resp, rr.err
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("upstream CONNECT: %s", resp.Status)
|
|
}
|
|
return &bufferedConn{Conn: conn, reader: br.Reader}, nil
|
|
}
|