diff --git a/.gitignore b/.gitignore index e646f8e..8398929 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # Log files *.log *.stderr +*.pprof # Local benchmark outputs benchmark/socks5_rudp/out/ diff --git a/README.md b/README.md index 2fac0e2..2eec339 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ SPP is a versatile, high-performance network proxy and traffic-forwarding tool w * Reverse Proxy (NAT traversal / intranet penetration) * SOCKS5 Forward Proxy (supports both TCP and UDP / UDP ASSOCIATE, with optional username/password auth) * SOCKS5 Reverse Proxy (supports both TCP and UDP / UDP ASSOCIATE) + * HTTP/HTTPS Forward Proxy (supports CONNECT tunneling and standard HTTP, with optional Basic auth) + * HTTP/HTTPS Reverse Proxy (supports CONNECT tunneling and standard HTTP, with optional Basic auth) * Shadowsocks SIP003 Plugin support ([spp-shadowsocks-plugin](https://github.com/esrrhs/spp-shadowsocks-plugin)) * **Protocol Multiplexing & Conversion**: Proxy traffic from one protocol (e.g. TCP) over another internal transit protocol (e.g. QUIC, KCP, RUDP, or RICMP). Multiple `-fromaddr`/`-proxyproto` pairs each get an Inputer↔Outputer pair, all sharing one logical session to the server. * **Multi-Path Underlay**: Client can open multiple main pipes (e.g. `-proto tcp -server host:8888 -proto rudp -server host:8889`). Traffic prefers the highest-throughput path; unhealthy pipes are greyed out, probed, and re-enabled when they recover. @@ -71,6 +73,13 @@ Both sides must use the **same** `-key` (auth) and `-encrypt` (wire crypto). Cho -key 'your-auth-key' -encrypt 'your-encrypt-key' ``` +* **Start HTTP/HTTPS Proxy** (open HTTP/HTTPS proxy on local port 8080, supports CONNECT tunneling and standard HTTP, with optional username/password auth): + ```bash + ./spp -type http_client -server www.server.com:8888 \ + -fromaddr :8080 -proxyproto tcp \ + -key 'your-auth-key' -encrypt 'your-encrypt-key' + ``` + Optional: `-name` is only a log tag (not used for auth). Encryption off: omit `-encrypt` or set it empty. Auth (`-key`) is always required. @@ -90,7 +99,7 @@ Encryption off: omit `-encrypt` or set it empty. Auth (`-key`) is always require ### 3. Using Configuration Files -One-shot generate a multi-path server config plus one client config per mode (forward / reverse / socks5 / reverse socks5), with shared random keys: +One-shot generate a multi-path server config plus one client config per mode (forward / reverse / socks5 / reverse socks5 / http / reverse http), with shared random keys: ```bash ./spp -genconfig @@ -103,6 +112,8 @@ Files written: - `config_reverse_proxy_client.json` — reverse proxy - `config_socks5_client.json` — SOCKS5 - `config_reverse_socks5_client.json` — reverse SOCKS5 +- `config_http_client.json` — HTTP/HTTPS proxy +- `config_reverse_http_client.json` — reverse HTTP/HTTPS proxy Each client also dials **all** main channels. Then start the pair you need: diff --git a/USAGE.md b/USAGE.md index a4c2b32..2fb0e6d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -15,7 +15,9 @@ This document provides a comprehensive guide on configuring, running, and deploy - [2. Reverse Proxy](#2-reverse-proxy) - [3. SOCKS5 Forward Proxy](#3-socks5-forward-proxy) - [4. SOCKS5 Reverse Proxy](#4-socks5-reverse-proxy) - - [5. Shadowsocks Plugin](#5-shadowsocks-plugin) + - [5. HTTP/HTTPS Forward Proxy](#5-httphttps-forward-proxy) + - [6. HTTP/HTTPS Reverse Proxy](#6-httphttps-reverse-proxy) + - [7. Shadowsocks Plugin](#7-shadowsocks-plugin) - [Protocol Multiplexing and Conversion](#protocol-multiplexing-and-conversion) - [Configuration File](#configuration-file) - [Configuration File Schema](#configuration-file-schema) @@ -32,7 +34,7 @@ This document provides a comprehensive guide on configuring, running, and deploy SPP is designed to route and forward network traffic across diverse network environments and protocol boundaries. It supports: - **Proxy Protocols**: TCP, UDP - **Transit Protocols**: TCP, UDP, RUDP (Reliable UDP), RICMP (Reliable ICMP), RHTTP (Reliable HTTP), KCP, QUIC -- **Proxy Types**: Forward Proxy, Reverse Proxy, SOCKS5 Forward Proxy, SOCKS5 Reverse Proxy, Shadowsocks plugin mode +- **Proxy Types**: Forward Proxy, Reverse Proxy, SOCKS5 Forward Proxy, SOCKS5 Reverse Proxy, HTTP/HTTPS Forward Proxy, HTTP/HTTPS Reverse Proxy, Shadowsocks plugin mode --- @@ -128,7 +130,35 @@ Opens a SOCKS5 proxy server on the remote SPP server's port `8080`. Both TCP (`C ./spp -name "rev_socks5" -type reverse_socks5_client -server www.server.com:8888 -fromaddr :8080 -proxyproto tcp ``` -### 5. Shadowsocks Plugin +### 5. HTTP/HTTPS Forward Proxy + +Starts an HTTP/HTTPS proxy server on the local machine on port `8080`. Supports standard HTTP methods (`GET`, `POST`, etc.) and HTTPS tunneling (`CONNECT`). + +```bash +./spp -name "http" -type http_client -server www.server.com:8888 -fromaddr :8080 -proxyproto tcp +``` + +With optional Basic authentication (returns `407 Proxy Authentication Required` if unauthenticated): + +```bash +./spp -name "http_auth" -type http_client -server www.server.com:8888 -fromaddr :8080 -proxyproto tcp -username myuser -password mypass +``` + +### 6. HTTP/HTTPS Reverse Proxy + +Opens an HTTP/HTTPS proxy server on the remote SPP server's port `8080`. Traffic sent to the remote server's HTTP proxy port is proxied through the client network. + +```bash +./spp -name "rev_http" -type reverse_http_client -server www.server.com:8888 -fromaddr :8080 -proxyproto tcp +``` + +With optional Basic authentication: + +```bash +./spp -name "rev_http_auth" -type reverse_http_client -server www.server.com:8888 -fromaddr :8080 -proxyproto tcp -username myuser -password mypass +``` + +### 7. Shadowsocks Plugin SPP can function as a SIP003 plugin for Shadowsocks: - [spp-shadowsocks-plugin](https://github.com/esrrhs/spp-shadowsocks-plugin) @@ -191,6 +221,8 @@ Writes these files (shared random auth/encrypt keys): | `config_reverse_proxy_client.json` | Reverse proxy | | `config_socks5_client.json` | SOCKS5 on `:1080` | | `config_reverse_socks5_client.json` | Reverse SOCKS5 on `:1080` | +| `config_http_client.json` | HTTP/HTTPS proxy on `:8081` | +| `config_reverse_http_client.json` | Reverse HTTP/HTTPS proxy on `:8081` | Client configs dial the same full set of underlay addresses. Defaults: AEAD `chacha20`, compression `zstd` / threshold `128`. @@ -203,14 +235,14 @@ Then: ```bash ./spp -config config_server.json -./spp -config config_proxy_client.json # or reverse / socks5 / reverse_socks5 +./spp -config config_proxy_client.json # or reverse / socks5 / reverse_socks5 / http / reverse_http ``` ### Configuration File Schema | Field | Type | Description | | :--- | :--- | :--- | -| `type` | string | `server`, `proxy_client`, `reverse_proxy_client`, `socks5_client`, `reverse_socks5_client` | +| `type` | string | `server`, `proxy_client`, `reverse_proxy_client`, `socks5_client`, `reverse_socks5_client`, `http_client`, `reverse_http_client` | | `proto` | array of string | Internal transit protocols (e.g. `["tcp"]`, `["kcp"]`, `["quic"]`) | | `proxyproto` | array of string | Proxy protocols (e.g. `["tcp"]`, `["udp"]`) | | `listen` | array of string | Server listening addresses (e.g. `[":8888"]`) | @@ -280,7 +312,7 @@ Run: ```text Usage of spp: -type string - Role type: server, proxy_client, reverse_proxy_client, socks5_client, reverse_socks5_client + Role type: server, proxy_client, reverse_proxy_client, socks5_client, reverse_socks5_client, http_client, reverse_http_client -config string Path to json configuration file -proto value diff --git a/configgen.go b/configgen.go index 2c7a733..9373768 100644 --- a/configgen.go +++ b/configgen.go @@ -28,6 +28,8 @@ var clientModes = []struct { {Type: "reverse_proxy_client", File: "config_reverse_proxy_client.json", Name: "reverse_proxy_client"}, {Type: "socks5_client", File: "config_socks5_client.json", Name: "socks5_client"}, {Type: "reverse_socks5_client", File: "config_reverse_socks5_client.json", Name: "reverse_socks5_client"}, + {Type: "http_client", File: "config_http_client.json", Name: "http_client"}, + {Type: "reverse_http_client", File: "config_reverse_http_client.json", Name: "reverse_http_client"}, } func randomSecret() (string, error) { @@ -157,6 +159,14 @@ func defaultClientConfig(clientType, key, encrypt string) (ConfigFile, error) { cfg.Name = "reverse_socks5_client" cfg.ProxyProto = []string{"tcp"} cfg.FromAddr = []string{":1080"} + case "http_client": + cfg.Name = "http_client" + cfg.ProxyProto = []string{"tcp"} + cfg.FromAddr = []string{":8081"} + case "reverse_http_client": + cfg.Name = "reverse_http_client" + cfg.ProxyProto = []string{"tcp"} + cfg.FromAddr = []string{":8081"} default: return ConfigFile{}, fmt.Errorf("unsupported client type: %s", clientType) } @@ -230,6 +240,8 @@ func generateConfigs(outdir string, force bool) error { fmt.Printf("start reverse proxy: ./spp -config %s\n", filepath.Join(outdir, "config_reverse_proxy_client.json")) fmt.Printf("start socks5 proxy: ./spp -config %s\n", filepath.Join(outdir, "config_socks5_client.json")) fmt.Printf("start reverse socks5: ./spp -config %s\n", filepath.Join(outdir, "config_reverse_socks5_client.json")) + fmt.Printf("start http proxy: ./spp -config %s\n", filepath.Join(outdir, "config_http_client.json")) + fmt.Printf("start reverse http proxy: ./spp -config %s\n", filepath.Join(outdir, "config_reverse_http_client.json")) _ = paths return nil } diff --git a/main.go b/main.go index 74bb6e8..7808caf 100644 --- a/main.go +++ b/main.go @@ -140,7 +140,7 @@ func main() { outdir := flag.String("outdir", ".", "output directory for -genconfig") force := flag.Bool("force", false, "overwrite existing files when using -genconfig") - t := flag.String("type", "", "type: server/proxy_client/reverse_proxy_client/socks5_client/reverse_socks5_client") + t := flag.String("type", "", "type: server/proxy_client/reverse_proxy_client/socks5_client/reverse_socks5_client/http_client/reverse_http_client") var protos protoFlags flag.Var(&protos, "proto", "main proto type: "+fmt.Sprintf("%v", network.SupportReliableProtos())) var proxyproto proxyprotoFlags @@ -291,8 +291,10 @@ func main() { *t != "reverse_proxy_client" && *t != "socks5_client" && *t != "reverse_socks5_client" && + *t != "http_client" && + *t != "reverse_http_client" && *t != "server" { - fmt.Println("[type] must be server/proxy_client/reverse_proxy_client/socks5_client/reverse_socks5_client") + fmt.Println("[type] must be server/proxy_client/reverse_proxy_client/socks5_client/reverse_socks5_client/http_client/reverse_http_client") fmt.Println() flag.Usage() return @@ -322,7 +324,9 @@ func main() { } if *t == "socks5_client" || - *t == "reverse_socks5_client" { + *t == "reverse_socks5_client" || + *t == "http_client" || + *t == "reverse_http_client" { if !(len(fromaddr) == len(proxyproto)) { fmt.Println("[fromaddr] [proxyproto] len must be equal") fmt.Println() @@ -332,7 +336,7 @@ func main() { for i := range proxyproto { if len(fromaddr[i]) == 0 || len(servers) == 0 { - fmt.Println("[socks5_client] or [reverse_socks5_client] need [server] [fromaddr] [proxyproto]") + fmt.Println("[socks5_client] or [reverse_socks5_client] or [http_client] or [reverse_http_client] need [server] [fromaddr] [proxyproto]") fmt.Println() flag.Usage() return diff --git a/proxy/client.go b/proxy/client.go index 51ba42f..530bc67 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -601,13 +601,15 @@ func (c *Client) processSession(wg *thread.Group, sess *ServerConn) error { func (c *Client) iniService(wg *thread.Group, serverConn *ServerConn) error { services := c.buildLoginServices() switch c.clienttype { - case CLIENT_TYPE_PROXY, CLIENT_TYPE_SOCKS5, CLIENT_TYPE_SS_PROXY: + case CLIENT_TYPE_PROXY, CLIENT_TYPE_SOCKS5, CLIENT_TYPE_SS_PROXY, CLIENT_TYPE_HTTP: for i, svc := range services { var input *Inputer var err error switch c.clienttype { case CLIENT_TYPE_SOCKS5: input, err = NewSocks5Inputer(wg, svc.Proxyproto.String(), svc.Fromaddr, c.clienttype, c.config, &serverConn.ProxyConn, i) + case CLIENT_TYPE_HTTP: + input, err = NewHttpInputer(wg, svc.Proxyproto.String(), svc.Fromaddr, c.clienttype, c.config, &serverConn.ProxyConn, i) default: input, err = NewInputer(wg, svc.Proxyproto.String(), svc.Fromaddr, c.clienttype, c.config, &serverConn.ProxyConn, svc.Toaddr, i) } @@ -618,7 +620,7 @@ func (c *Client) iniService(wg *thread.Group, serverConn *ServerConn) error { serverConn.appendInput(input) loggo.Info("iniService client input[%d] %s %s -> %s", i, svc.Proxyproto.String(), svc.Fromaddr, svc.Toaddr) } - case CLIENT_TYPE_REVERSE_PROXY, CLIENT_TYPE_REVERSE_SOCKS5: + case CLIENT_TYPE_REVERSE_PROXY, CLIENT_TYPE_REVERSE_SOCKS5, CLIENT_TYPE_REVERSE_HTTP: for i, svc := range services { output, err := NewOutputer(wg, svc.Proxyproto.String(), c.clienttype, c.config, &serverConn.ProxyConn, i) if err != nil { diff --git a/proxy/http.go b/proxy/http.go new file mode 100644 index 0000000..0dd8dc2 --- /dev/null +++ b/proxy/http.go @@ -0,0 +1,274 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "net" + "net/url" + "strings" + "sync/atomic" + "time" + + "github.com/esrrhs/gohome/loggo" + "github.com/esrrhs/gohome/network" + "github.com/esrrhs/gohome/thread" +) + +type bufferedConn struct { + network.Conn + br *bufio.Reader + buf []byte +} + +func newPrefixedConn(c network.Conn, prefix []byte, br *bufio.Reader) network.Conn { + return &bufferedConn{ + Conn: c, + buf: prefix, + br: br, + } +} + +func (b *bufferedConn) Read(p []byte) (int, error) { + if len(b.buf) > 0 { + n := copy(p, b.buf) + b.buf = b.buf[n:] + return n, nil + } + if b.br != nil { + if b.br.Buffered() > 0 { + return b.br.Read(p) + } + return b.Conn.Read(p) + } + return b.Conn.Read(p) +} + +func checkProxyAuth(authHeader, username, password string) bool { + if username == "" && password == "" { + return true + } + if authHeader == "" { + return false + } + const prefix = "Basic " + if len(authHeader) < len(prefix) || !strings.EqualFold(authHeader[:len(prefix)], prefix) { + return false + } + raw := strings.TrimSpace(authHeader[len(prefix):]) + decoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return false + } + expected := username + ":" + password + return string(decoded) == expected +} + +func parseHost(rawHost, defaultPort string) string { + rawHost = strings.TrimSpace(rawHost) + if host, port, err := net.SplitHostPort(rawHost); err == nil { + return net.JoinHostPort(host, port) + } + return net.JoinHostPort(rawHost, defaultPort) +} + +func NewHttpInputer(wg *thread.Group, proto string, addr string, clienttype CLIENT_TYPE, config *Config, father *ProxyConn, serviceIndex int) (*Inputer, error) { + conn, err := network.NewConn(proto) + if conn == nil { + return nil, err + } + + listenconn, err := conn.Listen(addr) + if err != nil { + return nil, err + } + + input := &Inputer{ + clienttype: clienttype, + config: config, + proto: proto, + addr: addr, + father: father, + fwg: wg, + serviceIndex: int32(serviceIndex), + listenconn: listenconn, + } + + wg.Go("Inputer listenHttp "+addr, func() error { + return input.listenHttp() + }) + + loggo.Info("NewHttpInputer ok %s service=%d", addr, serviceIndex) + + return input, nil +} + +func (i *Inputer) listenHttp() error { + loggo.Info("Inputer start listenHttp %s", i.addr) + for !isExit(i.fwg) { + conn, err := i.listenconn.Accept() + if err != nil { + loggo.Error("Inputer listenHttp accept fail %s %s", i.addr, err.Error()) + time.Sleep(10 * time.Millisecond) + continue + } + + size := i.sonnySize() + if size >= i.config.MaxSonny { + loggo.Info("Inputer listen max sonny %s %d", conn.Info(), size) + conn.Close() + continue + } + + proxyconn := &ProxyConn{conn: conn} + i.fwg.Go("Inputer processHttpConn "+conn.Info(), func() error { + atomic.AddInt32(&gStateThreadNum.InputerSonnyThread, 1) + defer atomic.AddInt32(&gStateThreadNum.InputerSonnyThread, -1) + return i.processHttpConn(proxyconn) + }) + } + loggo.Info("Inputer end listenHttp %s", i.addr) + return nil +} + +func (i *Inputer) processHttpConn(proxyConn *ProxyConn) error { + loggo.Debug("processHttpConn start %s", proxyConn.conn.Info()) + + if proxyConn.conn.Name() != "tcp" { + loggo.Error("processHttpConn not tcp %s %s", proxyConn.conn.Info(), proxyConn.conn.Name()) + proxyConn.closeConn() + return errors.New("http proxy not tcp") + } + + br := bufio.NewReader(proxyConn.conn) + + reqLine, err := br.ReadString('\n') + if err != nil { + loggo.Debug("processHttpConn ReadString reqLine fail %s %v", proxyConn.conn.Info(), err) + proxyConn.closeConn() + return err + } + + reqLineTrimmed := strings.TrimRight(reqLine, "\r\n") + parts := strings.Split(reqLineTrimmed, " ") + if len(parts) < 3 { + loggo.Error("processHttpConn invalid reqLine %s: %s", proxyConn.conn.Info(), reqLineTrimmed) + proxyConn.closeConn() + return errors.New("invalid http request line") + } + + method := strings.ToUpper(parts[0]) + rawURI := parts[1] + protoVer := parts[2] + + var rawHeaders []string + var hostHeader string + var proxyAuthHeader string + + for { + headerLine, err := br.ReadString('\n') + if err != nil { + loggo.Error("processHttpConn ReadString header fail %s %v", proxyConn.conn.Info(), err) + proxyConn.closeConn() + return err + } + trimmed := strings.TrimRight(headerLine, "\r\n") + if trimmed == "" { + break + } + + colonIdx := strings.IndexByte(trimmed, ':') + if colonIdx > 0 { + k := strings.TrimSpace(trimmed[:colonIdx]) + v := strings.TrimSpace(trimmed[colonIdx+1:]) + if strings.EqualFold(k, "Host") { + hostHeader = v + } else if strings.EqualFold(k, "Proxy-Authorization") { + proxyAuthHeader = v + continue + } else if strings.EqualFold(k, "Proxy-Connection") { + rawHeaders = append(rawHeaders, "Connection: "+v+"\r\n") + continue + } + } + rawHeaders = append(rawHeaders, trimmed+"\r\n") + } + + if !checkProxyAuth(proxyAuthHeader, i.config.Username, i.config.Password) { + loggo.Warn("processHttpConn proxy auth fail %s", proxyConn.conn.Info()) + const http407 = "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Basic realm=\"SPP Proxy\"\r\n" + + "Content-Length: 0\r\n\r\n" + _, _ = proxyConn.conn.Write([]byte(http407)) + proxyConn.closeConn() + return nil + } + + if method == "CONNECT" { + targetAddr := parseHost(rawURI, "443") + const resp200 = "HTTP/1.1 200 Connection Established\r\n\r\n" + if _, err := proxyConn.conn.Write([]byte(resp200)); err != nil { + loggo.Error("processHttpConn write 200 fail %s %v", proxyConn.conn.Info(), err) + proxyConn.closeConn() + return nil + } + + if br.Buffered() > 0 { + proxyConn.conn = newPrefixedConn(proxyConn.conn, nil, br) + } + + loggo.Debug("processHttpConn CONNECT ok %s -> %s", proxyConn.conn.Info(), targetAddr) + i.fwg.Go("Inputer processProxyConn "+proxyConn.conn.Info(), func() error { + return i.processProxyConn(proxyConn, targetAddr) + }) + return nil + } + + targetAddr := "" + newURI := rawURI + + if strings.HasPrefix(strings.ToLower(rawURI), "http://") || strings.HasPrefix(strings.ToLower(rawURI), "https://") { + u, err := url.Parse(rawURI) + if err == nil && u.Host != "" { + defaultPort := "80" + if strings.EqualFold(u.Scheme, "https") { + defaultPort = "443" + } + targetAddr = parseHost(u.Host, defaultPort) + newURI = u.RequestURI() + if newURI == "" { + newURI = "/" + } + } + } + + if targetAddr == "" { + if hostHeader != "" { + targetAddr = parseHost(hostHeader, "80") + } else { + loggo.Error("processHttpConn missing host %s: %s", proxyConn.conn.Info(), reqLineTrimmed) + proxyConn.closeConn() + return errors.New("missing host in http request") + } + } + + if hostHeader == "" && targetAddr != "" { + rawHeaders = append(rawHeaders, "Host: "+targetAddr+"\r\n") + } + + var prefixBuf bytes.Buffer + prefixBuf.WriteString(method + " " + newURI + " " + protoVer + "\r\n") + for _, h := range rawHeaders { + prefixBuf.WriteString(h) + } + prefixBuf.WriteString("\r\n") + + proxyConn.conn = newPrefixedConn(proxyConn.conn, prefixBuf.Bytes(), br) + + loggo.Debug("processHttpConn HTTP ok %s -> %s", proxyConn.conn.Info(), targetAddr) + i.fwg.Go("Inputer processProxyConn "+proxyConn.conn.Info(), func() error { + return i.processProxyConn(proxyConn, targetAddr) + }) + return nil +} diff --git a/proxy/http_test.go b/proxy/http_test.go new file mode 100644 index 0000000..01b992f --- /dev/null +++ b/proxy/http_test.go @@ -0,0 +1,363 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "strings" + "testing" + "time" +) + +func TestHttp_AuthCheck(t *testing.T) { + // No auth required + if !checkProxyAuth("", "", "") { + t.Fatalf("expected true when user/pass are empty") + } + if !checkProxyAuth("Basic invalid", "", "") { + t.Fatalf("expected true when user/pass are empty even if header present") + } + + // Auth required + user := "admin" + pass := "secret123" + if checkProxyAuth("", user, pass) { + t.Fatalf("expected false when auth header is missing") + } + + badHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("admin:wrongpass")) + if checkProxyAuth(badHeader, user, pass) { + t.Fatalf("expected false for wrong password") + } + + goodHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("admin:secret123")) + if !checkProxyAuth(goodHeader, user, pass) { + t.Fatalf("expected true for valid credentials") + } + + // Case-insensitive "basic " prefix + lowerGoodHeader := "basic " + base64.StdEncoding.EncodeToString([]byte("admin:secret123")) + if !checkProxyAuth(lowerGoodHeader, user, pass) { + t.Fatalf("expected true for lowercase basic prefix") + } +} + +func startHTTPTestBackend(t *testing.T) (string, func()) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to start http backend: %v", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { + // Ensure Proxy-Authorization header was stripped + if r.Header.Get("Proxy-Authorization") != "" { + http.Error(w, "Proxy-Authorization leaked!", http.StatusBadRequest) + return + } + w.Header().Set("X-Custom-Echo", "spp-http-proxy") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello from spp http backend")) + }) + + server := &http.Server{Handler: mux} + go func() { + _ = server.Serve(ln) + }() + + addr := ln.Addr().String() + cleanup := func() { + _ = server.Close() + _ = ln.Close() + } + return addr, cleanup +} + +func TestE2E_HTTPProxy_Forward(t *testing.T) { + backendAddr, stopBackend := startHTTPTestBackend(t) + defer stopBackend() + + echoAddr, stopEcho := startTCPEchoServer(t) + defer stopEcho() + + serverPort := getFreePort(t) + serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) + + httpPort := getFreePort(t) + httpAddr := fmt.Sprintf("127.0.0.1:%d", httpPort) + + user := "proxyuser" + pass := "proxypass" + + cfg := DefaultConfig() + cfg.Key = "test-http-secret" + cfg.Username = user + cfg.Password = pass + + server, err := NewServer(cfg, []string{"tcp"}, []string{serverAddr}) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + defer server.Close() + + client, err := NewClient(cfg, []string{"tcp"}, []string{serverAddr}, "http_client", "HTTP", []string{"tcp"}, []string{httpAddr}, nil) + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + defer client.Close() + + // 1. Test 407 Authentication Required when no auth provided + c1, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to http proxy: %v", err) + } + defer c1.Close() + + c1.SetDeadline(time.Now().Add(3 * time.Second)) + reqNoAuth := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", echoAddr, echoAddr) + if _, err := c1.Write([]byte(reqNoAuth)); err != nil { + t.Fatalf("Write CONNECT failed: %v", err) + } + + reader1 := bufio.NewReader(c1) + respLine, err := reader1.ReadString('\n') + if err != nil { + t.Fatalf("Read response failed: %v", err) + } + if !strings.Contains(respLine, "407") { + t.Fatalf("Expected 407 response without auth, got: %s", respLine) + } + c1.Close() + + // 2. Test CONNECT method with valid auth + c2, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to http proxy for CONNECT: %v", err) + } + defer c2.Close() + + c2.SetDeadline(time.Now().Add(5 * time.Second)) + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)) + reqConnect := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: %s\r\n\r\n", echoAddr, echoAddr, authHeader) + if _, err := c2.Write([]byte(reqConnect)); err != nil { + t.Fatalf("Write CONNECT with auth failed: %v", err) + } + + reader2 := bufio.NewReader(c2) + connectResp, err := reader2.ReadString('\n') + if err != nil { + t.Fatalf("Read CONNECT response failed: %v", err) + } + if !strings.Contains(connectResp, "200") { + t.Fatalf("Expected 200 Connection Established, got: %s", connectResp) + } + // Consume empty line of CONNECT response + _, _ = reader2.ReadString('\n') + + // Echo test over tunnel + echoData := []byte("hello http connect tunnel") + if _, err := c2.Write(echoData); err != nil { + t.Fatalf("Write echoData failed: %v", err) + } + recvBuf := make([]byte, len(echoData)) + if _, err := io.ReadFull(reader2, recvBuf); err != nil { + t.Fatalf("Read echoData failed: %v", err) + } + if !bytes.Equal(recvBuf, echoData) { + t.Fatalf("Echo mismatch: got %s, want %s", string(recvBuf), string(echoData)) + } + c2.Close() + + // 3. Test plain GET request with auth + c3, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to http proxy for GET: %v", err) + } + defer c3.Close() + + c3.SetDeadline(time.Now().Add(5 * time.Second)) + reqGet := fmt.Sprintf("GET http://%s/hello HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: %s\r\n\r\n", backendAddr, backendAddr, authHeader) + if _, err := c3.Write([]byte(reqGet)); err != nil { + t.Fatalf("Write GET failed: %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(c3), nil) + if err != nil { + t.Fatalf("ReadResponse failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Expected 200 OK from backend, got %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("ReadAll body failed: %v", err) + } + if string(body) != "hello from spp http backend" { + t.Fatalf("Unexpected body: %s", string(body)) + } +} + +func TestE2E_HTTPProxy_Reverse(t *testing.T) { + backendAddr, stopBackend := startHTTPTestBackend(t) + defer stopBackend() + + serverPort := getFreePort(t) + serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) + + httpPort := getFreePort(t) + httpAddr := fmt.Sprintf("127.0.0.1:%d", httpPort) + + user := "revuser" + pass := "revpass" + + cfg := DefaultConfig() + cfg.Key = "test-reverse-http-secret" + cfg.Username = user + cfg.Password = pass + + server, err := NewServer(cfg, []string{"tcp"}, []string{serverAddr}) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + defer server.Close() + + // Reverse HTTP client: connects to server, asks server to listen on httpAddr + client, err := NewClient(cfg, []string{"tcp"}, []string{serverAddr}, "rev_http_client", "REVERSE_HTTP", []string{"tcp"}, []string{httpAddr}, nil) + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + defer client.Close() + + // Wait for server to open httpAddr + c, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to reverse http proxy: %v", err) + } + defer c.Close() + + c.SetDeadline(time.Now().Add(5 * time.Second)) + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)) + reqGet := fmt.Sprintf("GET http://%s/hello HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: %s\r\n\r\n", backendAddr, backendAddr, authHeader) + if _, err := c.Write([]byte(reqGet)); err != nil { + t.Fatalf("Write GET failed: %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(c), nil) + if err != nil { + t.Fatalf("ReadResponse failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Expected 200 OK from backend, got %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("ReadAll body failed: %v", err) + } + if string(body) != "hello from spp http backend" { + t.Fatalf("Unexpected body: %s", string(body)) + } +} + +func TestE2E_HTTPProxy_NoAuth(t *testing.T) { + backendAddr, stopBackend := startHTTPTestBackend(t) + defer stopBackend() + + echoAddr, stopEcho := startTCPEchoServer(t) + defer stopEcho() + + serverPort := getFreePort(t) + serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) + + httpPort := getFreePort(t) + httpAddr := fmt.Sprintf("127.0.0.1:%d", httpPort) + + cfg := DefaultConfig() + cfg.Key = "test-noauth-secret" + // Username and Password left empty + + server, err := NewServer(cfg, []string{"tcp"}, []string{serverAddr}) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + defer server.Close() + + client, err := NewClient(cfg, []string{"tcp"}, []string{serverAddr}, "http_client_noauth", "HTTP", []string{"tcp"}, []string{httpAddr}, nil) + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + defer client.Close() + + // 1. CONNECT without auth should succeed directly + c1, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to http proxy: %v", err) + } + defer c1.Close() + + c1.SetDeadline(time.Now().Add(3 * time.Second)) + reqConnect := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", echoAddr, echoAddr) + if _, err := c1.Write([]byte(reqConnect)); err != nil { + t.Fatalf("Write CONNECT failed: %v", err) + } + + reader1 := bufio.NewReader(c1) + connectResp, err := reader1.ReadString('\n') + if err != nil { + t.Fatalf("Read CONNECT response failed: %v", err) + } + if !strings.Contains(connectResp, "200") { + t.Fatalf("Expected 200 Connection Established, got: %s", connectResp) + } + _, _ = reader1.ReadString('\n') + + echoData := []byte("noauth echo") + if _, err := c1.Write(echoData); err != nil { + t.Fatalf("Write echoData failed: %v", err) + } + recvBuf := make([]byte, len(echoData)) + if _, err := io.ReadFull(reader1, recvBuf); err != nil { + t.Fatalf("Read echoData failed: %v", err) + } + if !bytes.Equal(recvBuf, echoData) { + t.Fatalf("Echo mismatch: got %s, want %s", string(recvBuf), string(echoData)) + } + c1.Close() + + // 2. GET without auth should succeed directly + c2, err := waitForPort(httpAddr, 3*time.Second) + if err != nil { + t.Fatalf("Failed to connect to http proxy: %v", err) + } + defer c2.Close() + + c2.SetDeadline(time.Now().Add(3 * time.Second)) + reqGet := fmt.Sprintf("GET http://%s/hello HTTP/1.1\r\nHost: %s\r\n\r\n", backendAddr, backendAddr) + if _, err := c2.Write([]byte(reqGet)); err != nil { + t.Fatalf("Write GET failed: %v", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(c2), nil) + if err != nil { + t.Fatalf("ReadResponse failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Expected 200 OK from backend, got %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("ReadAll body failed: %v", err) + } + if string(body) != "hello from spp http backend" { + t.Fatalf("Unexpected body: %s", string(body)) + } +} diff --git a/proxy/proxy.pb.go b/proxy/proxy.pb.go index 029add0..1ec04cc 100644 --- a/proxy/proxy.pb.go +++ b/proxy/proxy.pb.go @@ -89,6 +89,10 @@ const ( CLIENT_TYPE_REVERSE_SOCKS5 CLIENT_TYPE = 3 // client fromaddr -> server shadowsocks address [SS_LOCAL_HOST:SS_LOCAL_PORT] CLIENT_TYPE_SS_PROXY CLIENT_TYPE = 4 + // client fromaddr -> server (HTTP / HTTPS CONNECT proxy) + CLIENT_TYPE_HTTP CLIENT_TYPE = 5 + // server fromaddr -> client (HTTP / HTTPS CONNECT reverse proxy) + CLIENT_TYPE_REVERSE_HTTP CLIENT_TYPE = 6 ) // Enum value maps for CLIENT_TYPE. @@ -99,6 +103,8 @@ var ( 2: "SOCKS5", 3: "REVERSE_SOCKS5", 4: "SS_PROXY", + 5: "HTTP", + 6: "REVERSE_HTTP", } CLIENT_TYPE_value = map[string]int32{ "PROXY": 0, @@ -106,6 +112,8 @@ var ( "SOCKS5": 2, "REVERSE_SOCKS5": 3, "SS_PROXY": 4, + "HTTP": 5, + "REVERSE_HTTP": 6, } ) @@ -1416,38 +1424,40 @@ var file_proxy_proto_rawDesc = string([]byte{ 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x55, 0x44, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x49, 0x43, 0x4d, 0x50, 0x10, - 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4b, 0x43, 0x50, 0x10, 0x04, 0x2a, 0x59, 0x0a, 0x0b, 0x43, 0x4c, + 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4b, 0x43, 0x50, 0x10, 0x04, 0x2a, 0x75, 0x0a, 0x0b, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x4f, 0x43, 0x4b, 0x53, 0x35, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x5f, 0x53, 0x4f, 0x43, 0x4b, 0x53, 0x35, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x53, 0x5f, 0x50, 0x52, - 0x4f, 0x58, 0x59, 0x10, 0x04, 0x2a, 0x62, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, - 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, - 0x53, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x4e, 0x4f, 0x4e, - 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, - 0x5a, 0x4c, 0x49, 0x42, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, - 0x53, 0x53, 0x5f, 0x5a, 0x53, 0x54, 0x44, 0x10, 0x03, 0x2a, 0x6a, 0x0a, 0x0c, 0x45, 0x4e, 0x43, - 0x52, 0x59, 0x50, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x12, 0x17, 0x0a, 0x13, 0x45, 0x4e, 0x43, - 0x52, 0x59, 0x50, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x5f, 0x4e, 0x4f, - 0x4e, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x5f, - 0x41, 0x45, 0x53, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x45, 0x4e, 0x43, - 0x52, 0x59, 0x50, 0x54, 0x5f, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x10, 0x04, 0x22, - 0x04, 0x08, 0x02, 0x10, 0x02, 0x2a, 0xb0, 0x01, 0x0a, 0x0a, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x47, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x47, 0x49, 0x4e, 0x52, 0x53, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, - 0x04, 0x44, 0x41, 0x54, 0x41, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49, 0x4e, 0x47, 0x10, - 0x03, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x4f, - 0x50, 0x45, 0x4e, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x4f, 0x50, 0x45, 0x4e, 0x52, 0x53, 0x50, - 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x07, 0x12, 0x12, 0x0a, - 0x0e, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x41, 0x4c, 0x4c, 0x45, 0x4e, 0x47, 0x45, 0x10, - 0x08, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4a, 0x4f, 0x49, - 0x4e, 0x10, 0x09, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4a, - 0x4f, 0x49, 0x4e, 0x5f, 0x52, 0x53, 0x50, 0x10, 0x0a, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x50, 0x45, - 0x45, 0x44, 0x54, 0x45, 0x53, 0x54, 0x10, 0x0b, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x58, 0x59, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x05, 0x12, + 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, + 0x06, 0x2a, 0x62, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, + 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x12, + 0x11, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x5a, 0x4c, 0x49, 0x42, + 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x5a, + 0x53, 0x54, 0x44, 0x10, 0x03, 0x2a, 0x6a, 0x0a, 0x0c, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x12, 0x17, 0x0a, 0x13, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, + 0x0a, 0x0c, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x01, + 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x5f, 0x41, 0x45, 0x53, 0x5f, + 0x47, 0x43, 0x4d, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, + 0x5f, 0x43, 0x48, 0x41, 0x43, 0x48, 0x41, 0x32, 0x30, 0x10, 0x04, 0x22, 0x04, 0x08, 0x02, 0x10, + 0x02, 0x2a, 0xb0, 0x01, 0x0a, 0x0a, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x47, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4c, + 0x4f, 0x47, 0x49, 0x4e, 0x52, 0x53, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, + 0x41, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x08, 0x0a, + 0x04, 0x50, 0x4f, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x4f, 0x50, 0x45, 0x4e, 0x10, + 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x4f, 0x50, 0x45, 0x4e, 0x52, 0x53, 0x50, 0x10, 0x06, 0x12, 0x09, + 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x07, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, + 0x48, 0x5f, 0x43, 0x48, 0x41, 0x4c, 0x4c, 0x45, 0x4e, 0x47, 0x45, 0x10, 0x08, 0x12, 0x10, 0x0a, + 0x0c, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4a, 0x4f, 0x49, 0x4e, 0x10, 0x09, 0x12, + 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4a, 0x4f, 0x49, 0x4e, 0x5f, + 0x52, 0x53, 0x50, 0x10, 0x0a, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x50, 0x45, 0x45, 0x44, 0x54, 0x45, + 0x53, 0x54, 0x10, 0x0b, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/proxy/proxy.proto b/proxy/proxy.proto index a2721f8..665ea75 100644 --- a/proxy/proxy.proto +++ b/proxy/proxy.proto @@ -20,6 +20,10 @@ enum CLIENT_TYPE { REVERSE_SOCKS5 = 3; // client fromaddr -> server shadowsocks address [SS_LOCAL_HOST:SS_LOCAL_PORT] SS_PROXY = 4; + // client fromaddr -> server (HTTP / HTTPS CONNECT proxy) + HTTP = 5; + // server fromaddr -> client (HTTP / HTTPS CONNECT reverse proxy) + REVERSE_HTTP = 6; } // COMPRESS_TYPE selects payload compression. UNSPECIFIED means "use peer default". diff --git a/proxy/server.go b/proxy/server.go index e25629c..b262a2a 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -513,7 +513,7 @@ func (s *Server) iniService(wg *thread.Group, f *ProxyFrame, clientConn *ClientC } switch f.LoginFrame.Clienttype { - case CLIENT_TYPE_PROXY, CLIENT_TYPE_SOCKS5, CLIENT_TYPE_SS_PROXY: + case CLIENT_TYPE_PROXY, CLIENT_TYPE_SOCKS5, CLIENT_TYPE_SS_PROXY, CLIENT_TYPE_HTTP: for i, svc := range services { var output *Outputer var err error @@ -549,6 +549,16 @@ func (s *Server) iniService(wg *thread.Group, f *ProxyFrame, clientConn *ClientC clientConn.appendInput(input) loggo.Info("iniService server socks5 input[%d] %s %s", i, svc.Proxyproto.String(), svc.Fromaddr) } + case CLIENT_TYPE_REVERSE_HTTP: + for i, svc := range services { + input, err := NewHttpInputer(wg, svc.Proxyproto.String(), svc.Fromaddr, f.LoginFrame.Clienttype, s.config, &clientConn.ProxyConn, i) + if err != nil { + clientConn.closeServices() + return err + } + clientConn.appendInput(input) + loggo.Info("iniService server http input[%d] %s %s", i, svc.Proxyproto.String(), svc.Fromaddr) + } default: return errors.New("error CLIENT_TYPE " + strconv.Itoa(int(f.LoginFrame.Clienttype))) }