Merge branch 'caddy2' into v2multiauth

This commit is contained in:
Mygod 2023-07-19 13:33:38 -04:00
commit a29f011386
20 changed files with 1071 additions and 698 deletions

14
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"

33
.github/workflows/go-critic.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Gocritic Scan
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
gocritic-scan:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
check-latest: true
cache: false
- name: Install go-critic
run: go install -v github.com/go-critic/go-critic/cmd/gocritic@latest
- name: Run gocritic
run: gocritic check -checkTests=False ./...

25
.github/workflows/golangci-lint.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Golangci Lint Check
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Run golangci-lint
uses: reviewdog/action-golangci-lint@v2
with:
golangci_lint_flags: "--tests=false"

33
.github/workflows/gosec.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Gosec Security Scan
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
gosec-scan:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
check-latest: true
cache: false
- name: Install Gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: Run Gosec
run: gosec ./...

33
.github/workflows/govulncheck.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Govulncheck Security Scan
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
govulncheck-check:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
check-latest: true
cache: false
- name: Install Govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run Govulncheck
run: govulncheck ./...

33
.github/workflows/staticcheck.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Staticcheck Scan
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
staticcheck-scan:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
check-latest: true
cache: false
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- name: Run staticcheck
run: staticcheck -tests=False ./...

35
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: "Tests"
on:
push:
branches:
- "master"
- "main"
paths-ignore:
- "**.md"
pull_request:
branches:
- "*"
paths-ignore:
- "**.md"
jobs:
Tests:
strategy:
matrix:
go-version:
- stable
platform:
- ubuntu-latest
runs-on: ${{ matrix.platform }}
steps:
- name: Fetch Repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '${{ matrix.go-version }}'
- name: Run test
run: go test -v ./...
- name: Run test -race
run: go test -v -race ./...

View file

@ -1,4 +0,0 @@
# golangci linter currently requires a config file to
# disable checking of the test files
run:
tests: false

View file

@ -1,16 +0,0 @@
language: go
go:
- 1.15.x
env:
- GO111MODULE=on
dist: trusty
install:
- go get -v -t -d ./...
- go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
script:
- go test -race -v .
- golangci-lint run -E gofmt -E goimports -E misspell -E ineffassign -E staticcheck -E gosimple -D errcheck

View file

@ -1,7 +1,81 @@
# Secure forward proxy plugin for the Caddy web server # Secure forward proxy for the Caddy web server
This package registers the `http.handlers.forward_proxy` module, which acts as an HTTPS proxy for accessing remote networks.
## :warning: Experimental!
This module is EXPERIMENTAL. We need more users to test this module for bugs and weaknesses before we recommend its use from within surveilled networks or regions with active censorship. Do not rely on this code in situations where personal safety, freedom, or privacy are at risk.
**You can help by:**
- Safely deploying this module
- Trying to break it
- Contributing to the code and tests in this repo to make it better
We are also seeking experienced maintainers who have experience with these kinds of technologies and who are interested in continuing its development.
**Expect breaking changes.**
## Features
- HTTP/1.1 and HTTP/2 support
- Authentication
- Access control lists
- Optional probe resistance
- PAC file
## Introduction
This Caddy module allows you to use your web server as a proxy server, configurable by numerous HTTP clients such as operating systems, web browsers, mobile devices, and apps. However, the feature set of each client varies widely, as does their correctness and security guarantees. You will have to be aware of each clients' individual weaknesses or shortcomings.
## Quick start
First, you will have to know [how to use Caddy](https://caddyserver.com/docs/getting-started).
Build Caddy with this plugin. You can add it from [Caddy's download page](https://caddyserver.com/download) or build it yourself with [xcaddy](https://github.com/caddyserver/xcaddy):
```
$ xcaddy build --with github.com/caddyserver/forwardproxy@caddy2
```
Most people prefer the [Caddyfile](https://caddyserver.com/docs/caddyfile) for configuration. You can stand up a simple, wide-open unauthenticated forward proxy like this:
```
example.com
route {
# UNAUTHENTICATED! USE ONLY FOR TESTING
forward_proxy
}
```
(Obviously, replace `example.com` with your domain name which is pointed at your machine.)
Because `forward_proxy` is not a standard directive, its ordering relative to other handler directives is not defined, so we put it inside a `route` block. You can alternatively do something like this:
```
{
order forward_proxy before file_server
}
example.com
# UNAUTHENTICATED! USE ONLY FOR TESTING
forward_proxy
```
to define its position globally; then you don't need `route` blocks. The correct order is up to you and depends on your config.
[![Build Status](https://travis-ci.org/caddyserver/forwardproxy.svg?branch=master)](https://travis-ci.org/caddyserver/forwardproxy)
[![Join the chat at https://gitter.im/forwardproxy/Lobby](https://badges.gitter.im/forwardproxy/Lobby.svg)](https://gitter.im/forwardproxy/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This plugin enables [Caddy](https://caddyserver.com) to act as a forward proxy, with support for HTTP/2.0 and HTTP/1.1 requests. HTTP/2.0 will usually improve performance due to multiplexing. This plugin enables [Caddy](https://caddyserver.com) to act as a forward proxy, with support for HTTP/2.0 and HTTP/1.1 requests. HTTP/2.0 will usually improve performance due to multiplexing.
@ -24,8 +98,8 @@ Here's an example of all properties in use (note that the syntax is subject to c
:443, example.com :443, example.com
route { route {
forward_proxy { forward_proxy {
basicauth user1 password1 basic_auth user1 0NtCL2JPJBgPPMmlPcJ
basicauth user2 password2 basic_auth user2 密码
ports 80 443 ports 80 443
hide_ip hide_ip
hide_via hide_via
@ -147,9 +221,9 @@ Don't forget to add `http.forwardproxy` plugin.
#### Build from source #### Build from source
0. Install latest Golang 1.12 or above and set export GO111MODULE=on 0. Install latest Golang 1.20 or above and set export GO111MODULE=on
1. ```bash 1. ```bash
go install github.com/caddyserver/forwardproxy/cmd/caddy go install github.com/caddyserver/forwardproxy/cmd/caddy@latest
``` ```
Built `caddy` binary will be stored in $GOPATH/bin. Built `caddy` binary will be stored in $GOPATH/bin.

6
acl.go
View file

@ -37,7 +37,6 @@ func (a *aclIPRule) tryMatch(ip net.IP, domain string) aclDecision {
return aclDecisionAllow return aclDecisionAllow
} }
return aclDecisionDeny return aclDecisionDeny
} }
type aclDomainRule struct { type aclDomainRule struct {
@ -47,9 +46,8 @@ type aclDomainRule struct {
} }
func (a *aclDomainRule) tryMatch(ip net.IP, domain string) aclDecision { func (a *aclDomainRule) tryMatch(ip net.IP, domain string) aclDecision {
if strings.HasSuffix(domain, ".") { domain = strings.TrimPrefix(domain, ".")
domain = domain[:len(domain)-1]
}
if domain == a.domain || if domain == a.domain ||
a.subdomainsAllowed && strings.HasSuffix(domain, "."+a.domain) { a.subdomainsAllowed && strings.HasSuffix(domain, "."+a.domain) {
if a.allow { if a.allow {

View file

@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/caddy/v2" caddy "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@ -147,7 +147,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
aclAllow := false aclAllow := false
switch aclDirective { switch aclDirective {
case "allow": case "allow":
ruleSubjects = args[:] ruleSubjects = args
aclAllow = true aclAllow = true
case "allow_file": case "allow_file":
if len(args) != 1 { if len(args) != 1 {
@ -159,7 +159,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
aclAllow = true aclAllow = true
case "deny": case "deny":
ruleSubjects = args[:] ruleSubjects = args
case "deny_file": case "deny_file":
if len(args) != 1 { if len(args) != 1 {
return d.Err("denyfile accepts a single filename argument") return d.Err("denyfile accepts a single filename argument")

View file

@ -8,7 +8,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -25,11 +24,12 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
) )
var credentialsEmpty = "" var (
var credentialsCorrectPlain = "test:pass" credentialsEmpty = ""
var credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass credentialsCorrectPlain = "test:pass"
var credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass
var credentialsWrong = []string{ credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass
credentialsWrong = []string{
"", "",
"\"\"", "\"\"",
"Basic dzp3", "Basic dzp3",
@ -37,7 +37,8 @@ var credentialsWrong = []string{
"Foo bar", "Foo bar",
"Tssssssss", "Tssssssss",
"Basic dpz3 asp", "Basic dpz3 asp",
} }
)
/* /*
Test naming: Test{httpVer}Proxy{Method}{Auth}{Credentials}{httpVer} Test naming: Test{httpVer}Proxy{Method}{Auth}{Credentials}{httpVer}
@ -45,17 +46,21 @@ GET/CONNECT -- get gets, connect connects and gets
Auth/NoAuth Auth/NoAuth
Empty/Correct/Wrong -- tries different credentials Empty/Correct/Wrong -- tries different credentials
*/ */
var testResources = []string{"/", "/pic.png"} var (
var testHTTPProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"} testResources = []string{"/", "/pic.png"}
var testHTTPTargetVersions = []string{"HTTP/1.1"} testHTTPProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"}
var httpVersionToALPN = map[string]string{ testHTTPTargetVersions = []string{"HTTP/1.1"}
httpVersionToALPN = map[string]string{
"HTTP/1.1": "http/1.1", "HTTP/1.1": "http/1.1",
"HTTP/2.0": "h2", "HTTP/2.0": "h2",
} }
)
var blacklistedDomain = "google-public-dns-a.google.com" // supposed to ever resolve to one of 2 IP addresses below var (
var blacklistedIPv4 = "8.8.8.8" blacklistedDomain = "google-public-dns-a.google.com" // supposed to ever resolve to one of 2 IP addresses below
var blacklistedIPv6 = "2001:4860:4860::8888" blacklistedIPv4 = "8.8.8.8"
blacklistedIPv6 = "2001:4860:4860::8888"
)
type caddyTestServer struct { type caddyTestServer struct {
addr string addr string
@ -144,14 +149,14 @@ func (c *caddyTestServer) server() *caddyhttp.Server {
if c.contents == nil { if c.contents == nil {
c.contents = make(map[string][]byte) c.contents = make(map[string][]byte)
} }
index, err := ioutil.ReadFile(c.root + "/index.html") index, err := os.ReadFile(c.root + "/index.html")
if err != nil { if err != nil {
panic(err) panic(err)
} }
c.contents[""] = index c.contents[""] = index
c.contents["/"] = index c.contents["/"] = index
c.contents["/index.html"] = index c.contents["/index.html"] = index
c.contents["/pic.png"], err = ioutil.ReadFile(c.root + "/pic.png") c.contents["/pic.png"], err = os.ReadFile(c.root + "/pic.png")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -160,6 +165,7 @@ func (c *caddyTestServer) server() *caddyhttp.Server {
} }
// For simulating/mimicing Caddy's built-in auto-HTTPS redirects. Super hacky but w/e. // For simulating/mimicing Caddy's built-in auto-HTTPS redirects. Super hacky but w/e.
func (c *caddyTestServer) redirServer() *caddyhttp.Server { func (c *caddyTestServer) redirServer() *caddyhttp.Server {
return &caddyhttp.Server{ return &caddyhttp.Server{
Listen: []string{":" + c.httpRedirPort}, Listen: []string{":" + c.httpRedirPort},

View file

@ -1,4 +1,4 @@
FROM alpine:3.6 FROM alpine:3.18.2
LABEL description="Docker image for caddy+forwardproxy plugin." LABEL description="Docker image for caddy+forwardproxy plugin."
LABEL maintainer="SergeyFrolov@colorado.edu" LABEL maintainer="SergeyFrolov@colorado.edu"

View file

@ -26,17 +26,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/caddyserver/caddy/v2" caddy "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/forwardproxy/httpclient" "github.com/caddyserver/forwardproxy/httpclient"
@ -60,7 +60,7 @@ type Handler struct {
// If true, the Forwarded header will not be augmented with your IP address. // If true, the Forwarded header will not be augmented with your IP address.
HideIP bool `json:"hide_ip,omitempty"` HideIP bool `json:"hide_ip,omitempty"`
// If true, the Via heaeder will not be added. // If true, the Via header will not be added.
HideVia bool `json:"hide_via,omitempty"` HideVia bool `json:"hide_via,omitempty"`
// Host(s) (and ports) of the proxy. When you configure a client, // Host(s) (and ports) of the proxy. When you configure a client,
@ -188,7 +188,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
// either way, it's impossible to have a legit TLS certificate for "127.0.0.1" - TODO: not true anymore // either way, it's impossible to have a legit TLS certificate for "127.0.0.1" - TODO: not true anymore
h.logger.Info("Localhost upstream detected, disabling verification of TLS certificate") h.logger.Info("Localhost upstream detected, disabling verification of TLS certificate")
d.DialTLS = func(network string, address string) (net.Conn, string, error) { d.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true}) conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true}) // #nosec G402
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -345,17 +345,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
// make sure request is idempotent and could be retried by saving the Body // make sure request is idempotent and could be retried by saving the Body
// None of those methods are supposed to have body, // None of those methods are supposed to have body,
// but we still need to copy the r.Body, even if it's empty // but we still need to copy the r.Body, even if it's empty
rBodyBuf, err := ioutil.ReadAll(r.Body) rBodyBuf, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return caddyhttp.Error(http.StatusBadRequest, return caddyhttp.Error(http.StatusBadRequest,
fmt.Errorf("failed to read request body: %v", err)) fmt.Errorf("failed to read request body: %v", err))
} }
r.GetBody = func() (io.ReadCloser, error) { r.GetBody = func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(rBodyBuf)), nil return io.NopCloser(bytes.NewReader(rBodyBuf)), nil
} }
r.Body, _ = r.GetBody() r.Body, _ = r.GetBody()
} }
response, err = h.httpTransport.RoundTrip(r) response, _ = h.httpTransport.RoundTrip(r)
} else { } else {
// Upstream requests don't interact well with Transport: connections could always be // Upstream requests don't interact well with Transport: connections could always be
// reused, but Transport thinks they go to different Hosts, so it spawns tons of // reused, but Transport thinks they go to different Hosts, so it spawns tons of
@ -384,7 +384,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
fmt.Errorf("failed to read upstream response: %v", err)) fmt.Errorf("failed to read upstream response: %v", err))
} }
} }
r.Body.Close() err = r.Body.Close()
if response != nil { if response != nil {
defer response.Body.Close() defer response.Body.Close()
@ -406,7 +406,7 @@ func (h Handler) checkCredentials(r *http.Request) error {
return errors.New("Proxy-Authorization is required! Expected format: <type> <credentials>") return errors.New("Proxy-Authorization is required! Expected format: <type> <credentials>")
} }
if strings.ToLower(pa[0]) != "basic" { if strings.ToLower(pa[0]) != "basic" {
return errors.New("Auth type is not supported") return errors.New("auth type is not supported")
} }
for _, creds := range h.AuthCredentials { for _, creds := range h.AuthCredentials {
if subtle.ConstantTimeCompare(creds, []byte(pa[1])) == 1 { if subtle.ConstantTimeCompare(creds, []byte(pa[1])) == 1 {
@ -421,7 +421,7 @@ func (h Handler) checkCredentials(r *http.Request) error {
return nil return nil
} }
} }
return errors.New("Invalid credentials") return errors.New("invalid credentials")
} }
func (h Handler) shouldServePACFile(r *http.Request) bool { func (h Handler) shouldServePACFile(r *http.Request) bool {
@ -543,10 +543,10 @@ func serveHiddenPage(w http.ResponseWriter, authErr error) error {
if authErr != nil { if authErr != nil {
w.Header().Set("Proxy-Authenticate", "Basic realm=\"Caddy Secure Web Proxy\"") w.Header().Set("Proxy-Authenticate", "Basic realm=\"Caddy Secure Web Proxy\"")
w.WriteHeader(http.StatusProxyAuthRequired) w.WriteHeader(http.StatusProxyAuthRequired)
w.Write([]byte(fmt.Sprintf(hiddenPage, AuthFail))) _, _ = w.Write([]byte(fmt.Sprintf(hiddenPage, AuthFail)))
return authErr return authErr
} }
w.Write([]byte(fmt.Sprintf(hiddenPage, AuthOk))) _, _ = w.Write([]byte(fmt.Sprintf(hiddenPage, AuthOk)))
return nil return nil
} }
@ -572,12 +572,14 @@ func serveHijack(w http.ResponseWriter, targetConn net.Conn) error {
if err != nil { if err != nil {
return caddyhttp.Error(http.StatusBadGateway, err) return caddyhttp.Error(http.StatusBadGateway, err)
} }
targetConn.Write(rbuf) _, _ = targetConn.Write(rbuf)
} }
} }
// Since we hijacked the connection, we lost the ability to write and flush headers via w. // Since we hijacked the connection, we lost the ability to write and flush headers via w.
// Let's handcraft the response and send it manually. // Let's handcraft the response and send it manually.
res := &http.Response{StatusCode: http.StatusOK, res := &http.Response{
StatusCode: http.StatusOK,
Proto: "HTTP/1.1", Proto: "HTTP/1.1",
ProtoMajor: 1, ProtoMajor: 1,
ProtoMinor: 1, ProtoMinor: 1,
@ -585,7 +587,13 @@ func serveHijack(w http.ResponseWriter, targetConn net.Conn) error {
} }
res.Header.Set("Server", "Caddy") res.Header.Set("Server", "Caddy")
err = res.Write(clientConn) buf := bufio.NewWriter(clientConn)
err = res.Write(buf)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("failed to write response: %v", err))
}
err = buf.Flush()
if err != nil { if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("failed to send response to client: %v", err)) fmt.Errorf("failed to send response to client: %v", err))
@ -600,16 +608,18 @@ func serveHijack(w http.ResponseWriter, targetConn net.Conn) error {
func dualStream(target net.Conn, clientReader io.ReadCloser, clientWriter io.Writer) error { func dualStream(target net.Conn, clientReader io.ReadCloser, clientWriter io.Writer) error {
stream := func(w io.Writer, r io.Reader) error { stream := func(w io.Writer, r io.Reader) error {
// copy bytes from r to w // copy bytes from r to w
buf := bufferPool.Get().([]byte) bufPtr := bufferPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[0:cap(buf)] buf = buf[0:cap(buf)]
_, _err := flushingIoCopy(w, r, buf) _, _err := flushingIoCopy(w, r, buf)
bufferPool.Put(buf) bufferPool.Put(bufPtr)
if cw, ok := w.(closeWriter); ok { if cw, ok := w.(closeWriter); ok {
cw.CloseWrite() _ = cw.CloseWrite()
} }
return _err return _err
} }
go stream(target, clientReader) go stream(target, clientReader) //nolint: errcheck
return stream(clientWriter, target) return stream(clientWriter, target)
} }
@ -664,10 +674,11 @@ func forwardResponse(w http.ResponseWriter, response *http.Response) error {
} }
removeHopByHop(w.Header()) removeHopByHop(w.Header())
w.WriteHeader(response.StatusCode) w.WriteHeader(response.StatusCode)
buf := bufferPool.Get().([]byte) bufPtr := bufferPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[0:cap(buf)] buf = buf[0:cap(buf)]
_, err := io.CopyBuffer(w, response.Body, buf) _, err := io.CopyBuffer(w, response.Body, buf)
bufferPool.Put(buf) bufferPool.Put(bufPtr)
return err return err
} }
@ -703,7 +714,8 @@ function FindProxyForURL(url, host) {
var bufferPool = sync.Pool{ var bufferPool = sync.Pool{
New: func() interface{} { New: func() interface{} {
return make([]byte, 0, 32*1024) buffer := make([]byte, 0, 32*1024)
return &buffer
}, },
} }
@ -725,7 +737,8 @@ type ProbeResistance struct {
} }
func readLinesFromFile(filename string) ([]string, error) { func readLinesFromFile(filename string) ([]string, error) {
file, err := os.Open(filename) cleanFilename := filepath.Clean(filename)
file, err := os.Open(cleanFilename)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -19,7 +19,6 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -75,7 +74,7 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
req.ProtoMajor = 2 req.ProtoMajor = 2
req.ProtoMinor = 0 req.ProtoMinor = 0
pr, pw := io.Pipe() pr, pw := io.Pipe()
req.Body = ioutil.NopCloser(pr) req.Body = io.NopCloser(pr)
t := http2.Transport{} t := http2.Transport{}
clientConn, err := t.NewClientConn(proxyConn) clientConn, err := t.NewClientConn(proxyConn)
if err != nil { if err != nil {

115
go.mod
View file

@ -1,9 +1,116 @@
module github.com/caddyserver/forwardproxy module github.com/caddyserver/forwardproxy
go 1.14 go 1.20
require ( require (
github.com/caddyserver/caddy/v2 v2.4.0-beta.1 github.com/caddyserver/caddy/v2 v2.6.4
go.uber.org/zap v1.16.0 go.uber.org/zap v1.24.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 golang.org/x/net v0.11.0
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/alecthomas/chroma/v2 v2.5.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/certmagic v0.17.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.13.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.1.0 // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/quic-go/quic-go v0.32.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/slackhq/nebula v1.6.1 // indirect
github.com/smallstep/certificates v0.23.2 // indirect
github.com/smallstep/nosql v0.5.0 // indirect
github.com/smallstep/truststore v0.12.1 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2 // indirect
github.com/urfave/cli v1.22.12 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.step.sm/cli-utils v0.7.5 // indirect
go.step.sm/crypto v0.23.2 // indirect
go.step.sm/linkedca v0.19.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
) )

1075
go.sum

File diff suppressed because it is too large Load diff

View file

@ -132,12 +132,12 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
resp, err := h2clientConn.RoundTrip(req) resp, err := h2clientConn.RoundTrip(req)
if err != nil { if err != nil {
rawConn.Close() err = rawConn.Close()
return nil, err return nil, err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
rawConn.Close() _ = rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
} }
return NewHttp2Conn(rawConn, pw, resp.Body), nil return NewHttp2Conn(rawConn, pw, resp.Body), nil
@ -150,18 +150,18 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
err := req.Write(rawConn) err := req.Write(rawConn)
if err != nil { if err != nil {
rawConn.Close() err = rawConn.Close()
return nil, err return nil, err
} }
resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
if err != nil { if err != nil {
rawConn.Close() err = rawConn.Close()
return nil, err return nil, err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
rawConn.Close() _ = rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
} }
return rawConn, nil return rawConn, nil
@ -207,6 +207,7 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
tlsConf := tls.Config{ tlsConf := tls.Config{
NextProtos: []string{"h2", "http/1.1"}, NextProtos: []string{"h2", "http/1.1"},
ServerName: c.ProxyURL.Hostname(), ServerName: c.ProxyURL.Hostname(),
MinVersion: tls.VersionTLS12,
} }
tlsConn, err := tls.Dial(network, c.ProxyURL.Host, &tlsConf) tlsConn, err := tls.Dial(network, c.ProxyURL.Host, &tlsConf)
if err != nil { if err != nil {
@ -232,13 +233,13 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
t := http2.Transport{} t := http2.Transport{}
h2clientConn, err := t.NewClientConn(rawConn) h2clientConn, err := t.NewClientConn(rawConn)
if err != nil { if err != nil {
rawConn.Close() err = rawConn.Close()
return nil, err return nil, err
} }
proxyConn, err := connectHttp2(rawConn, h2clientConn) proxyConn, err := connectHttp2(rawConn, h2clientConn)
if err != nil { if err != nil {
rawConn.Close() err = rawConn.Close()
return nil, err return nil, err
} }
if c.EnableH2ConnReuse { if c.EnableH2ConnReuse {
@ -249,7 +250,7 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
} }
return proxyConn, err return proxyConn, err
default: default:
rawConn.Close() _ = rawConn.Close()
return nil, errors.New("negotiated unsupported application layer protocol: " + return nil, errors.New("negotiated unsupported application layer protocol: " +
negotiatedProtocol) negotiatedProtocol)
} }
@ -274,8 +275,13 @@ func (h *http2Conn) Write(p []byte) (n int, err error) {
} }
func (h *http2Conn) Close() error { func (h *http2Conn) Close() error {
h.in.Close() inErr := h.in.Close()
return h.out.Close() outErr := h.out.Close()
if inErr != nil {
return inErr
}
return outErr
} }
func (h *http2Conn) CloseConn() error { func (h *http2Conn) CloseConn() error {

View file

@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"strings" "strings"
@ -44,6 +44,7 @@ func TestGETAuthWrongProbeResist(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if responseProbeResist.StatusCode != responseReference.StatusCode { if responseProbeResist.StatusCode != responseReference.StatusCode {
t.Fatalf("Expected response: %d, Got: %d\n", t.Fatalf("Expected response: %d, Got: %d\n",
responseReference.StatusCode, responseProbeResist.StatusCode) responseReference.StatusCode, responseProbeResist.StatusCode)
@ -314,6 +315,7 @@ func responsesAreEqual(res1, res2 *http.Response) error {
if len(s1) != len(s2) { if len(s1) != len(s2) {
return fmt.Sprintf("different length: %d vs %d", len(s1), len(s2)) return fmt.Sprintf("different length: %d vs %d", len(s1), len(s2))
} }
for i := range s1 { for i := range s1 {
if s1[i] != s2[i] { if s1[i] != s2[i] {
return fmt.Sprintf("different string at position %d: %s vs %s", i, s1[i], s2[i]) return fmt.Sprintf("different string at position %d: %s vs %s", i, s1[i], s2[i])
@ -330,6 +332,7 @@ func responsesAreEqual(res1, res2 *http.Response) error {
if len(res1.Header) != len(res2.Header) { if len(res1.Header) != len(res2.Header) {
return errors.New("Headers have different length") return errors.New("Headers have different length")
} }
for k1, v1 := range res1.Header { for k1, v1 := range res1.Header {
k1Lower := strings.ToLower(k1) k1Lower := strings.ToLower(k1)
if k1Lower == "date" { if k1Lower == "date" {
@ -339,21 +342,13 @@ func responsesAreEqual(res1, res2 *http.Response) error {
if !ok { if !ok {
return fmt.Errorf("header \"%s: %s\" is absent in res2", k1, v1) return fmt.Errorf("header \"%s: %s\" is absent in res2", k1, v1)
} }
// if k1Lower == "location" {
// for i, h := range v2 {
// v2[i] = removeAddressesStr(h)
// }
// for i, h := range v1 {
// v1[i] = removeAddressesStr(h)
// }
// }
if errStr = stringSlicesAreEqual(v1, v2); errStr != "" { if errStr = stringSlicesAreEqual(v1, v2); errStr != "" {
return fmt.Errorf("header \"%s\" is different: %s", k1, errStr) return fmt.Errorf("header \"%s\" is different: %s", k1, errStr)
} }
} }
// Compare bodies // Compare bodies
buf1, err1 := ioutil.ReadAll(res1.Body) buf1, err1 := io.ReadAll(res1.Body)
buf2, err2 := ioutil.ReadAll(res2.Body) buf2, err2 := io.ReadAll(res2.Body)
n1 := len(buf1) n1 := len(buf1)
n2 := len(buf2) n2 := len(buf2)
makeBodyError := func(s string) error { makeBodyError := func(s string) error {
@ -379,17 +374,13 @@ func responsesAreEqual(res1, res2 *http.Response) error {
// Responses from forwardproxy + proberesist and generic caddy can have different addresses present in headers. // Responses from forwardproxy + proberesist and generic caddy can have different addresses present in headers.
// To avoid false positives - remove addresses before comparing. // To avoid false positives - remove addresses before comparing.
func removeAddressesByte(b []byte) []byte { func removeAddressesByte(b []byte) []byte {
b = bytes.Replace(b, []byte(caddyForwardProxyProbeResist.addr), b = bytes.ReplaceAll(b, []byte(caddyForwardProxyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyForwardProxyProbeResist.addr)), -1) bytes.Repeat([]byte{'#'}, len(caddyForwardProxyProbeResist.addr)))
b = bytes.Replace(b, []byte(caddyDummyProbeResist.addr), b = bytes.ReplaceAll(b, []byte(caddyDummyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyDummyProbeResist.addr)), -1) bytes.Repeat([]byte{'#'}, len(caddyDummyProbeResist.addr)))
return b return b
} }
func removeAddressesStr(s string) string {
return string(removeAddressesByte([]byte(s)))
}
func changePort(inputAddr, toPort string) string { func changePort(inputAddr, toPort string) string {
host, _, err := net.SplitHostPort(inputAddr) host, _, err := net.SplitHostPort(inputAddr)
if err != nil { if err != nil {