mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-16 01:03:17 +01:00
feat(outbound): support authenticated proxy from environment variables
This commit is contained in:
153
pkg/gotenberg/outbound_proxy_test.go
Normal file
153
pkg/gotenberg/outbound_proxy_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// connectCapture records the CONNECT request a proxy stub received.
|
||||
type connectCapture struct {
|
||||
mu sync.Mutex
|
||||
method string
|
||||
host string
|
||||
auth string
|
||||
}
|
||||
|
||||
func (c *connectCapture) set(method, host, auth string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.method, c.host, c.auth = method, host, auth
|
||||
}
|
||||
|
||||
func (c *connectCapture) get() (string, string, string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.method, c.host, c.auth
|
||||
}
|
||||
|
||||
// startConnectProxyStub starts a raw TCP server that behaves like an HTTP
|
||||
// CONNECT proxy: it reads the CONNECT request, records it, replies 200 with a
|
||||
// greeting appended to the same write (to exercise buffered-byte handling),
|
||||
// then echoes tunnel bytes back to the caller.
|
||||
func startConnectProxyStub(t *testing.T, capture *connectCapture) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
capture.set(req.Method, req.Host, req.Header.Get("Proxy-Authorization"))
|
||||
|
||||
// The greeting rides along with the response so the client's CONNECT
|
||||
// response parser buffers it; bufferedConn must not drop it.
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\nTUNNEL-HELLO"))
|
||||
_, _ = io.Copy(conn, br)
|
||||
}()
|
||||
|
||||
return l.Addr().String()
|
||||
}
|
||||
|
||||
func TestDialThroughProxy(t *testing.T) {
|
||||
capture := &connectCapture{}
|
||||
addr := startConnectProxyStub(t, capture)
|
||||
|
||||
proxyURL := &url.URL{Scheme: "http", Host: addr, User: url.UserPassword("alice", "s3cr3t")}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := DialThroughProxy(ctx, proxyURL, "example.com:443", func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DialThroughProxy: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// The greeting buffered while reading the CONNECT response must survive.
|
||||
greeting := make([]byte, len("TUNNEL-HELLO"))
|
||||
_, err = io.ReadFull(conn, greeting)
|
||||
if err != nil {
|
||||
t.Fatalf("read greeting: %v", err)
|
||||
}
|
||||
if string(greeting) != "TUNNEL-HELLO" {
|
||||
t.Fatalf("greeting = %q, want TUNNEL-HELLO", greeting)
|
||||
}
|
||||
|
||||
// The tunnel must round-trip bytes.
|
||||
_, err = conn.Write([]byte("ping"))
|
||||
if err != nil {
|
||||
t.Fatalf("write to tunnel: %v", err)
|
||||
}
|
||||
echo := make([]byte, 4)
|
||||
_, err = io.ReadFull(conn, echo)
|
||||
if err != nil {
|
||||
t.Fatalf("read echo: %v", err)
|
||||
}
|
||||
if string(echo) != "ping" {
|
||||
t.Fatalf("echo = %q, want ping", echo)
|
||||
}
|
||||
|
||||
method, host, auth := capture.get()
|
||||
if method != http.MethodConnect {
|
||||
t.Fatalf("proxy saw method %q, want CONNECT", method)
|
||||
}
|
||||
if host != "example.com:443" {
|
||||
t.Fatalf("proxy saw target %q, want example.com:443", host)
|
||||
}
|
||||
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("alice:s3cr3t"))
|
||||
if auth != wantAuth {
|
||||
t.Fatalf("proxy saw Proxy-Authorization %q, want %q", auth, wantAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialThroughProxy_RefusedStatus(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
br := bufio.NewReader(conn)
|
||||
_, _ = http.ReadRequest(br)
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"))
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = DialThroughProxy(ctx, &url.URL{Scheme: "http", Host: l.Addr().String()}, "example.com:443", func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when the proxy refuses CONNECT, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user