Files
opera-proxy/handler/handler.go
T

278 lines
6.8 KiB
Go
Raw Normal View History

2024-11-03 15:17:29 +02:00
package handler
2021-03-26 22:34:43 +02:00
import (
2024-11-03 15:17:29 +02:00
"bufio"
"context"
"errors"
2021-03-26 22:34:43 +02:00
"fmt"
2024-11-03 15:17:29 +02:00
"io"
"net"
2021-03-26 22:34:43 +02:00
"net/http"
"strings"
2024-11-03 15:17:29 +02:00
"sync"
2021-03-26 22:34:43 +02:00
"time"
2026-03-25 15:26:39 +03:00
"github.com/Alexey71/opera-proxy/dialer"
clog "github.com/Alexey71/opera-proxy/log"
2024-11-03 15:17:29 +02:00
)
2021-03-26 22:34:43 +02:00
2024-11-03 15:17:29 +02:00
const (
COPY_BUF = 128 * 1024
BAD_REQ_MSG = "Bad Request\n"
2026-04-30 19:04:28 +03:00
// Reduced idle pool: the proxy handler makes upstream connections per request,
// not persistent keep-alive sessions. 10 total / 2 per host is plenty and
// avoids leaking hundreds of idle goroutines/sockets under bursty traffic.
TRANSPORT_MAX_IDLE_CONNS = 10
TRANSPORT_MAX_IDLE_CONNS_PER_HOST = 2
TRANSPORT_IDLE_CONN_TIMEOUT = 60 * time.Second
2024-11-03 15:17:29 +02:00
)
2021-03-26 22:34:43 +02:00
2026-04-30 19:04:28 +03:00
// copyBufPool reuses 128 KiB buffers for bidirectional data relay,
// avoiding per-connection heap allocations.
var copyBufPool = sync.Pool{
New: func() any {
b := make([]byte, COPY_BUF)
return &b
},
}
2021-03-26 22:34:43 +02:00
type ProxyHandler struct {
2024-11-03 15:17:29 +02:00
logger *clog.CondLogger
dialer dialer.ContextDialer
2021-03-26 22:34:43 +02:00
httptransport http.RoundTripper
2026-04-26 15:35:01 +03:00
fakeSNI string
2021-03-26 22:34:43 +02:00
}
2026-04-30 19:04:28 +03:00
func NewProxyHandler(d dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *ProxyHandler {
2021-03-26 22:34:43 +02:00
httptransport := &http.Transport{
2026-04-30 19:04:28 +03:00
MaxIdleConns: TRANSPORT_MAX_IDLE_CONNS,
MaxIdleConnsPerHost: TRANSPORT_MAX_IDLE_CONNS_PER_HOST,
IdleConnTimeout: TRANSPORT_IDLE_CONN_TIMEOUT,
2021-03-26 22:34:43 +02:00
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
2026-04-30 19:04:28 +03:00
DialContext: d.DialContext,
2021-03-26 22:34:43 +02:00
}
return &ProxyHandler{
logger: logger,
2026-04-30 19:04:28 +03:00
dialer: d,
2021-03-26 22:34:43 +02:00
httptransport: httptransport,
2026-04-26 15:35:01 +03:00
fakeSNI: fakeSNI,
2021-03-26 22:34:43 +02:00
}
}
func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
ctx := req.Context()
conn, err := s.dialer.DialContext(ctx, "tcp", req.RequestURI)
if err != nil {
s.logger.Error("Can't satisfy CONNECT request: %v", err)
http.Error(wr, "Can't satisfy CONNECT request", http.StatusBadGateway)
return
}
if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
// Upgrade client connection
2026-04-26 15:35:01 +03:00
localconn, rw, err := hijack(wr)
2021-03-26 22:34:43 +02:00
if err != nil {
s.logger.Error("Can't hijack client connection: %v", err)
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
return
}
defer localconn.Close()
// Inform client connection is built
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
2026-04-26 15:35:01 +03:00
clientReader := io.Reader(localconn)
if rw != nil && rw.Reader.Buffered() > 0 {
clientReader = io.MultiReader(rw.Reader, localconn)
}
proxy(req.Context(), localconn, clientReader, conn, s.fakeSNI)
2021-03-26 22:34:43 +02:00
} else if req.ProtoMajor == 2 {
wr.Header()["Date"] = nil
wr.WriteHeader(http.StatusOK)
flush(wr)
2026-04-26 15:35:01 +03:00
proxyh2(req.Context(), req.Body, wr, conn, s.fakeSNI)
2021-03-26 22:34:43 +02:00
} else {
s.logger.Error("Unsupported protocol version: %s", req.Proto)
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
return
}
}
func (s *ProxyHandler) HandleRequest(wr http.ResponseWriter, req *http.Request) {
req.RequestURI = ""
if req.ProtoMajor == 2 {
req.URL.Scheme = "http" // We can't access :scheme pseudo-header, so assume http
req.URL.Host = req.Host
}
resp, err := s.httptransport.RoundTrip(req)
if err != nil {
s.logger.Error("HTTP fetch error: %v", err)
http.Error(wr, "Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
s.logger.Info("%v %v %v %v", req.RemoteAddr, req.Method, req.URL, resp.Status)
delHopHeaders(resp.Header)
copyHeader(wr.Header(), resp.Header)
wr.WriteHeader(resp.StatusCode)
flush(wr)
copyBody(wr, resp.Body)
}
func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
s.logger.Info("Request: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.URL)
isConnect := strings.ToUpper(req.Method) == "CONNECT"
if (req.URL.Host == "" || req.URL.Scheme == "" && !isConnect) && req.ProtoMajor < 2 ||
req.Host == "" && req.ProtoMajor == 2 {
http.Error(wr, BAD_REQ_MSG, http.StatusBadRequest)
return
}
delHopHeaders(req.Header)
if isConnect {
s.HandleTunnel(wr, req)
} else {
s.HandleRequest(wr, req)
}
}
2024-11-03 15:17:29 +02:00
2026-04-26 15:35:01 +03:00
func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.Conn, fakeSNI string) {
2024-11-03 15:17:29 +02:00
wg := sync.WaitGroup{}
2026-04-26 15:35:01 +03:00
ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done()
copyWithSNIRewrite(dst, src, fakeSNI)
dst.Close()
}
rtl := func(dst, src net.Conn) {
2024-11-03 15:17:29 +02:00
defer wg.Done()
2026-04-30 19:04:28 +03:00
// Grab a pooled buffer for this copy direction.
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
io.CopyBuffer(dst, src, *bufp)
2024-11-03 15:17:29 +02:00
dst.Close()
}
wg.Add(2)
2026-04-26 15:35:01 +03:00
go ltr(right, leftReader)
go rtl(left, right)
2024-11-03 15:17:29 +02:00
groupdone := make(chan struct{})
go func() {
wg.Wait()
groupdone <- struct{}{}
}()
select {
case <-ctx.Done():
left.Close()
right.Close()
case <-groupdone:
return
}
<-groupdone
return
}
2026-04-26 15:35:01 +03:00
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn, fakeSNI string) {
2024-11-03 15:17:29 +02:00
wg := sync.WaitGroup{}
ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done()
2026-04-30 19:04:28 +03:00
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
io.CopyBuffer(dst, src, *bufp)
2026-04-26 15:35:01 +03:00
copyWithSNIRewrite(dst, src, fakeSNI)
2024-11-03 15:17:29 +02:00
dst.Close()
}
rtl := func(dst io.Writer, src io.Reader) {
defer wg.Done()
copyBody(dst, src)
}
wg.Add(2)
go ltr(right, leftreader)
go rtl(leftwriter, right)
groupdone := make(chan struct{}, 1)
go func() {
wg.Wait()
groupdone <- struct{}{}
}()
select {
case <-ctx.Done():
leftreader.Close()
right.Close()
case <-groupdone:
return
}
<-groupdone
return
}
// Hop-by-hop headers. These are removed when sent to the backend.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
var hopHeaders = []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Connection",
"Te", // canonicalized version of "TE"
"Trailers",
"Transfer-Encoding",
"Upgrade",
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func delHopHeaders(header http.Header) {
for _, h := range hopHeaders {
header.Del(h)
}
}
func hijack(hijackable interface{}) (net.Conn, *bufio.ReadWriter, error) {
hj, ok := hijackable.(http.Hijacker)
if !ok {
return nil, nil, errors.New("Connection doesn't support hijacking")
}
conn, rw, err := hj.Hijack()
if err != nil {
return nil, nil, err
}
var emptytime time.Time
err = conn.SetDeadline(emptytime)
if err != nil {
conn.Close()
return nil, nil, err
}
return conn, rw, nil
}
func flush(flusher interface{}) bool {
f, ok := flusher.(http.Flusher)
if !ok {
return false
}
f.Flush()
return true
}
func copyBody(wr io.Writer, body io.Reader) {
2026-04-30 19:04:28 +03:00
// Use pooled buffer to avoid per-call allocation.
bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
2024-11-03 15:17:29 +02:00
for {
2026-04-30 19:04:28 +03:00
bread, read_err := body.Read(*bufp)
2024-11-03 15:17:29 +02:00
var write_err error
if bread > 0 {
2026-04-30 19:04:28 +03:00
_, write_err = wr.Write((*bufp)[:bread])
2024-11-03 15:17:29 +02:00
flush(wr)
}
if read_err != nil || write_err != nil {
break
}
}
}