arblant
firefox-vpn-client / core / internal / proxy / http_proxy.go
http_proxy.go197 lines · 7.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
package proxy

import (
	"bufio"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/armanlife/firefox-vpn-client/core/internal/egress"
	"github.com/armanlife/firefox-vpn-client/core/internal/logx"
)

// The SOCKS5 port also serves HTTP proxy clients (the Windows system proxy
// and applications that only know HTTP proxies): a SOCKS5 client starts
// with byte 5, an HTTP client with a request line. HTTPS goes through
// CONNECT tunnels; plain http:// requests are forwarded one per
// connection. Both use the same login, routing, and refused-port notes as
// SOCKS5.

// bufferedConn reads through the reader that peeked at the first byte, so
// nothing the client sent is lost.
type bufferedConn struct {
	net.Conn
	r *bufio.Reader
}

func (c *bufferedConn) Read(p []byte) (int, error) { return c.r.Read(p) }

// CloseWrite keeps half-close working for the relay.
func (c *bufferedConn) CloseWrite() error {
	if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
		return cw.CloseWrite()
	}
	return nil
}

// hopHeaders are meant for the proxy, not for the destination.
var hopHeaders = []string{"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding", "Upgrade"}

func (s *SOCKSServer) serveHTTP(client *bufferedConn, clientAddr string, start time.Time) {
	req, err := http.ReadRequest(client.r)
	if err != nil {
		logx.Debug("HTTP proxy request unreadable client=%s err=%v", clientAddr, err)
		return
	}
	if s.Auth != nil && !s.Auth.matchBasic(req.Header.Get("Proxy-Authorization")) {
		logx.Warn("HTTP proxy client rejected: %s client=%s", errSOCKSAuth, clientAddr)
		writeHTTPError(client, http.StatusProxyAuthRequired, "The proxy needs the username and password set in Settings.", `Basic realm="Firefox VPN"`)
		return
	}
	if s.handshakeTimeout > 0 {
		_ = client.SetDeadline(time.Time{})
	}

	if req.Method == http.MethodConnect {
		target := withDefaultPort(req.Host, "443")
		upstream, err := s.openTarget(client, target, clientAddr)
		if err != nil {
			writeHTTPError(client, httpStatusFor(err), httpErrorText(target, err), "")
			return
		}
		defer upstream.Close()
		if _, err := io.WriteString(client, "HTTP/1.1 200 Connection established\r\n\r\n"); err != nil {
			return
		}
		proxyBidirectional(client, upstream, s.idleTimeout)
		logx.Debug("HTTP proxy client disconnected client=%s target=%s duration=%s", clientAddr, logx.Target(target), time.Since(start).Round(time.Millisecond))
		return
	}

	if req.URL == nil || !strings.EqualFold(req.URL.Scheme, "http") || req.URL.Host == "" {
		writeHTTPError(client, http.StatusBadRequest, "This is a proxy: send a CONNECT request or an absolute http:// URL.", "")
		return
	}
	target := withDefaultPort(req.URL.Host, "80")
	upstream, err := s.openTarget(client, target, clientAddr)
	if err != nil {
		writeHTTPError(client, httpStatusFor(err), httpErrorText(target, err), "")
		return
	}
	defer upstream.Close()
	for _, h := range hopHeaders {
		req.Header.Del(h)
	}
	req.Close = true
	if err := req.Write(upstream); err != nil {
		logx.Debug("HTTP proxy request forwarding failed client=%s target=%s err=%v", clientAddr, logx.Target(target), err)
		return
	}
	resp, err := http.ReadResponse(bufio.NewReader(upstream), req)
	if err != nil {
		writeHTTPError(client, http.StatusBadGateway, "The site sent no valid answer.", "")
		return
	}
	defer resp.Body.Close()
	resp.Close = true
	_ = resp.Write(client)
}

// openTarget opens a tunnel to target for a SOCKS5 or HTTP client and
// notes and logs a port Mozilla's proxy refuses, and a loop.
func (s *SOCKSServer) openTarget(client net.Conn, target, clientAddr string) (net.Conn, error) {
	if egress.OwnDial(target) {
		if loop, warn := loops.note(target, time.Now()); loop {
			host, _, _ := net.SplitHostPort(target)
			s.Notices.record(NoticeLoop, "", host)
			if warn {
				logx.Warn("a loop: this program's own connections come back to its proxy as a client's (client=%s target=%s), so nothing connects; a program sends this program's own traffic into the proxy: in PassWall2 turn Localhost Proxy off, in another VPN program's TUN mode let firefox-vpn-core connect directly", clientAddr, logx.Target(target))
			}
		}
	}
	var conn net.Conn
	var err error
	if affinityUpstream, ok := s.upstream.(clientAffinityTunnelOpener); ok {
		conn, err = affinityUpstream.OpenTunnelForClient(target, clientAffinityKey(client.RemoteAddr()))
	} else {
		conn, err = s.upstream.OpenTunnel(target)
	}
	if err == nil {
		logx.Debug("upstream tunnel open succeeded client=%s target=%s", clientAddr, logx.Target(target))
		return conn, nil
	}
	if portRefused(target, err) {
		host, port, _ := net.SplitHostPort(target)
		s.Notices.record(NoticePort, port, host)
		logx.Warn("Mozilla's proxy does not connect to port %s; it connects to ports %s only client=%s target=%s", port, UpstreamPortsText, clientAddr, logx.Target(target))
	} else {
		logx.Warn("upstream tunnel open failed client=%s target=%s err=%s", clientAddr, logx.Target(target), logx.Err(err))
	}
	return nil, err
}

func withDefaultPort(host, port string) string {
	if _, _, err := net.SplitHostPort(host); err == nil {
		return host
	}
	return net.JoinHostPort(strings.Trim(host, "[]"), port)
}

func httpStatusFor(err error) int {
	var connectErr *proxyConnectHTTPError
	switch {
	case errors.Is(err, ErrBlockedByRule):
		return http.StatusForbidden
	case errors.Is(err, ErrNoUsableSession):
		return http.StatusServiceUnavailable
	case errors.As(err, &connectErr) && connectErr.statusCode == http.StatusBadGateway:
		return http.StatusForbidden
	}
	return http.StatusBadGateway
}

func httpErrorText(target string, err error) string {
	switch {
	case portRefused(target, err):
		_, port, _ := net.SplitHostPort(target)
		return fmt.Sprintf("Mozilla's proxy does not connect to port %s; it connects to ports %s only.", port, UpstreamPortsText)
	case errors.Is(err, ErrThisMachine):
		return "The proxy does not connect to this computer itself."
	case errors.Is(err, ErrBlockedByRule):
		return "Refused by a routing rule (Settings, Routing)."
	case errors.Is(err, ErrNoUsableSession):
		return "No account can supply a connection right now; see the dashboard."
	}
	return "The connection through Mozilla's proxy failed."
}

func writeHTTPError(w io.Writer, status int, text, authenticate string) {
	body := text + "\n"
	header := fmt.Sprintf("HTTP/1.1 %d %s\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n", status, http.StatusText(status), len(body))
	if authenticate != "" {
		header += "Proxy-Authenticate: " + authenticate + "\r\n"
	}
	_, _ = io.WriteString(w, header+"\r\n"+body)
}

// matchBasic checks a Proxy-Authorization header with Basic credentials.
func (c *socksCredentials) matchBasic(header string) bool {
	scheme, encoded, ok := strings.Cut(header, " ")
	if !ok || !strings.EqualFold(scheme, "Basic") {
		return false
	}
	decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
	if err != nil {
		return false
	}
	user, pass, ok := strings.Cut(string(decoded), ":")
	userOK := subtle.ConstantTimeCompare([]byte(user), []byte(c.username)) == 1
	passOK := subtle.ConstantTimeCompare([]byte(pass), []byte(c.password)) == 1
	return ok && userOK && passOK
}