Merge pull request #74 from caddyserver/caddy2

Add support for Caddy 2
This commit is contained in:
Matt Holt 2024-02-12 07:22:59 -07:00 committed by GitHub
commit a09e685335
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2633 additions and 1846 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"

61
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,61 @@
name: Lint
on:
push:
branches:
- master
- main
paths-ignore:
- "**.md"
pull_request:
branches:
- master
- main
paths-ignore:
- "**.md"
permissions:
contents: read
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
strategy:
matrix:
os:
- linux
- windows
include:
- os: linux
OS_LABEL: ubuntu-latest
- os: windows
OS_LABEL: windows-latest
runs-on: ${{ matrix.OS_LABEL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '~1.22.0'
check-latest: true
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.55.2
args: --timeout 10m
govulncheck:
runs-on: ubuntu-latest
steps:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.22.0'
check-latest: true

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:
- 1.21.x
- 1.22.x
platform:
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.platform }}
steps:
- name: Fetch Repository
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '${{ matrix.go-version }}'
- name: Run test
run: go test -v -race ./...

168
.golanci-lint.yml Normal file
View file

@ -0,0 +1,168 @@
linters-settings:
errcheck:
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
# Skip generated files.
# Default: true
skip-generated: true
# Enable custom order of sections.
# If `true`, make the section order the same as the order of `sections`.
# Default: false
custom-order: true
exhaustive:
ignore-enum-types: reflect.Kind|svc.Cmd
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- exportloopref
- gci
- gofmt
- goimports
- gofumpt
- gosec
- gosimple
- govet
- ineffassign
- importas
- misspell
- prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck
- tenv
- testableexamples
- testifylint
- tparallel
- typecheck
- unconvert
- unused
- wastedassign
- whitespace
- zerologlint
# these are implicitly disabled:
# - containedctx
# - contextcheck
# - cyclop
# - depguard
# - errchkjson
# - errorlint
# - exhaustruct
# - execinquery
# - exhaustruct
# - forbidigo
# - forcetypeassert
# - funlen
# - ginkgolinter
# - gocheckcompilerdirectives
# - gochecknoglobals
# - gochecknoinits
# - gochecksumtype
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - godot
# - godox
# - goerr113
# - goheader
# - gomnd
# - gomoddirectives
# - gomodguard
# - goprintffuncname
# - gosmopolitan
# - grouper
# - inamedparam
# - interfacebloat
# - ireturn
# - lll
# - loggercheck
# - maintidx
# - makezero
# - mirror
# - musttag
# - nakedret
# - nestif
# - nilerr
# - nilnil
# - nlreturn
# - noctx
# - nolintlint
# - nonamedreturns
# - nosprintfhostport
# - paralleltest
# - perfsprint
# - predeclared
# - protogetter
# - reassign
# - revive
# - rowserrcheck
# - stylecheck
# - tagalign
# - tagliatelle
# - testpackage
# - thelper
# - unparam
# - usestdlibvars
# - varnamelen
# - wrapcheck
# - wsl
run:
# default concurrency is a available CPU number.
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
deadline: 5m
issues-exit-code: 1
tests: false
# output configuration options
output:
format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
issues:
exclude-rules:
# we aren't calling unknown URL
- text: 'G107' # G107: Url provided to HTTP request as taint input
linters:
- gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user.
- text: 'G203' # G203: Use of unescaped data in HTML templates
linters:
- gosec
# we're shelling out to known commands, not relying on user-defined input.
- text: 'G204' # G204: Audit use of command execution
linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/logging/filters.go
linters:
- dupl

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

183
README.md
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.
@ -14,31 +88,36 @@ For a complete list of features and their usage, see Caddyfile syntax:
## Caddyfile Syntax (Server Configuration)
The simplest way to enable the forward proxy without authentication just include the `forwardproxy` directive in your Caddyfile. However, this allows anyone to use your server as a proxy, which might not be desirable.
The simplest way to enable the forward proxy without authentication just include the `forward_proxy` directive in your Caddyfile. However, this allows anyone to use your server as a proxy, which might not be desirable.
Open a block for more control; here's an example of all properties in use (note that the syntax is subject to change):
The `forward_proxy` directive has no default order and must be used within a `route` directive to explicitly specify its order of evaluation. In the Caddyfile the addresses must start with `:443` for the `forward_proxy` to work for proxy requests of all origins.
Here's an example of all properties in use (note that the syntax is subject to change):
```
forwardproxy {
basicauth user1 password1
basicauth user2 password2
ports 80 443
hide_ip
hide_via
probe_resistance secret-link-kWWL9Q.com # alternatively you can use a real domain, such as caddyserver.com
serve_pac /secret-proxy.pac
response_timeout 30
dial_timeout 30
upstream https://user:password@extra-upstream-hop.com
acl {
allow *.caddyserver.com
deny 192.168.1.1/32 192.168.0.0/16 *.prohibitedsite.com *.localhost
allow ::1/128 8.8.8.8 github.com *.github.io
allowfile /path/to/whitelist.txt
denyfile /path/to/blacklist.txt
allow all
deny all # unreachable rule, remaining requests are matched by `allow all` above
}
:443, example.com
route {
forward_proxy {
basic_auth user1 0NtCL2JPJBgPPMmlPcJ
basic_auth user2 密码
ports 80 443
hide_ip
hide_via
probe_resistance secret-link-kWWL9Q.com # alternatively you can use a real domain, such as caddyserver.com
serve_pac /secret-proxy.pac
dial_timeout 30
upstream https://user:password@extra-upstream-hop.com
acl {
allow *.caddyserver.com
deny 192.168.1.1/32 192.168.0.0/16 *.prohibitedsite.com *.localhost
allow ::1/128 8.8.8.8 github.com *.github.io
allow_file /path/to/whitelist.txt
deny_file /path/to/blacklist.txt
allow all
deny all # unreachable rule, remaining requests are matched by `allow all` above
}
}
file_server
}
```
@ -46,16 +125,16 @@ forwardproxy {
##### Security
- **basicauth [user] [password]**
Sets basic HTTP auth credentials. This property may be repeated multiple times. Note that this is different from Caddy's built-in `basicauth` directive. BE SURE TO CHECK THE NAME OF THE SITE THAT IS REQUESTING CREDENTIALS BEFORE YOU ENTER THEM.
- **basic_auth [user] [password]**
Sets basic HTTP auth credentials. This property may be repeated multiple times. Note that this is different from Caddy's built-in `basic_auth` directive. BE SURE TO CHECK THE NAME OF THE SITE THAT IS REQUESTING CREDENTIALS BEFORE YOU ENTER THEM.
_Default: no authentication required._
- **probe_resistance [secretlink.tld]**
Attempts to hide the fact that the site is a forward proxy.
Proxy will no longer respond with "407 Proxy Authentication Required" if credentials are incorrect or absent,
and will attempt to mimic a generic Caddy web server as if the forward proxy is not enabled.
Probing resistance works (and makes sense) only if basicauth is set up.
To use your proxy with probe resistance, supply your basicauth credentials to your client configuration.
Probing resistance works (and makes sense) only if `basic_auth` is set up.
To use your proxy with probe resistance, supply your `basic_auth` credentials to your client configuration.
If your proxy client(browser, operating system, browser extension, etc)
allows you to preconfigure credentials, and sends credentials preemptively, you do not need secret link.
If your proxy client does not preemptively send credentials, you will have to visit your secret link in your browser to trigger the authentication.
@ -90,24 +169,24 @@ Specifies **order** and rules for allowed destination IP networks, IP addresses
The hostname in each forwardproxy request will be resolved to an IP address,
and caddy will check the IP address and hostname against the directives in order until a directive matches the request.
acl_directive may be:
- **allow [ip or subnet or hostname] [ip or subnet or hostname]...**
- **allowfile /path/to/whitelist.txt**
- **deny [ip or subnet or hostname] [ip or subnet or hostname]...**
- **denyfile /path/to/blacklist.txt**
- **allow [ip or subnet or hostname] [ip or subnet or hostname]...**
- **allow_file /path/to/whitelist.txt**
- **deny [ip or subnet or hostname] [ip or subnet or hostname]...**
- **deny_file /path/to/blacklist.txt**
If you don't want unmatched requests to be subject to the default policy, you could finish
your acl rules with one of the following to specify action on unmatched requests:
- **allow all**
- **deny all**
For hostname, you can specify `*.` as a prefix to match domain and subdomains. For example,
`*.caddyserver.com` will match `caddyserver.com`, `subdomain.caddyserver.com`, but not `fakecaddyserver.com`.
Note that hostname rules, matched early in the chain, will override later IP rules,
so it is advised to put IP rules first, unless domains are highly trusted and should override the
IP rules. Also note that domain-based blacklists are easily circumventable by directly specifying the IP.
For `allowfile`/`denyfile` directives, syntax is the same, and each entry must be separated by newline.
This policy applies to all requests except requests to the proxy's own domain and port.
Whitelisting/blacklisting of ports on per-host/IP basis is not supported.
If you don't want unmatched requests to be subject to the default policy, you could finish
your acl rules with one of the following to specify action on unmatched requests:
- **allow all**
- **deny all**
For hostname, you can specify `*.` as a prefix to match domain and subdomains. For example,
`*.caddyserver.com` will match `caddyserver.com`, `subdomain.caddyserver.com`, but not `fakecaddyserver.com`.
Note that hostname rules, matched early in the chain, will override later IP rules,
so it is advised to put IP rules first, unless domains are highly trusted and should override the
IP rules. Also note that domain-based blacklists are easily circumventable by directly specifying the IP.
For `allow_file`/`deny_file` directives, syntax is the same, and each entry must be separated by newline.
This policy applies to all requests except requests to the proxy's own domain and port.
Whitelisting/blacklisting of ports on per-host/IP basis is not supported.
_Default policy:_
acl {
    deny 10.0.0.0/8 127.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ::1/128 fe80::/10
@ -117,10 +196,6 @@ _Default deny rules intend to prohibit access to localhost and local networks an
##### Timeouts
- **response_timeout [integer]**
Sets timeout (in seconds) to get full response for HTTP requests made by proxy on behalf of users (does not affect `CONNECT`-method requests).
_Default: no timeout._
- **dial_timeout [integer]**
Sets timeout (in seconds) for establishing TCP connection to target website. Affects all requests.
_Default: 20 seconds._
@ -146,11 +221,11 @@ Don't forget to add `http.forwardproxy` plugin.
#### 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
go install github.com/caddyserver/forwardproxy/cmd/caddy
```
Built `caddy` binary will be stored in $GOPATH/bin.
go install github.com/caddyserver/forwardproxy/cmd/caddy@latest
```
Built `caddy` binary will be stored in $GOPATH/bin.
## Client Configuration

36
acl.go
View file

@ -6,6 +6,12 @@ import (
"strings"
)
// ACLRule describes an ACL rule.
type ACLRule struct {
Subjects []string `json:"subjects,omitempty"`
Allow bool `json:"allow,omitempty"`
}
type aclDecision uint8
const (
@ -31,7 +37,6 @@ func (a *aclIPRule) tryMatch(ip net.IP, domain string) aclDecision {
return aclDecisionAllow
}
return aclDecisionDeny
}
type aclDomainRule struct {
@ -41,9 +46,8 @@ type aclDomainRule struct {
}
func (a *aclDomainRule) tryMatch(ip net.IP, domain string) aclDecision {
if strings.HasSuffix(domain, ".") {
domain = domain[:len(domain)-1]
}
domain = strings.TrimPrefix(domain, ".")
if domain == a.domain ||
a.subdomainsAllowed && strings.HasSuffix(domain, "."+a.domain) {
if a.allow {
@ -65,7 +69,7 @@ func (a *aclAllRule) tryMatch(ip net.IP, domain string) aclDecision {
return aclDecisionDeny
}
func newAclRule(ruleSubject string, allow bool) (aclRule, error) {
func newACLRule(ruleSubject string, allow bool) (aclRule, error) {
if ruleSubject == "all" {
return &aclAllRule{allow: allow}, nil
}
@ -94,3 +98,25 @@ func newAclRule(ruleSubject string, allow bool) (aclRule, error) {
}
return &aclDomainRule{domain: ruleSubject, subdomainsAllowed: subdomainsAllowed, allow: allow}, nil
}
// isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid
func isValidDomainLite(domain string) error {
for i := 0; i < len(domain); i++ {
c := domain[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' ||
c == '-' || c == '.' {
continue
}
return errors.New("character " + string(c) + " is not allowed")
}
sections := strings.Split(domain, ".")
for _, s := range sections {
if len(s) == 0 {
return errors.New("empty section between dots in domain name or trailing dot")
}
if len(s) > 63 {
return errors.New("domain name section is too long")
}
}
return nil
}

View file

@ -9,15 +9,14 @@ import (
test port blocking working
test blacklist allowed
test blacklist refused with correct status
*/
func TestWhitelistAllowing(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -28,11 +27,11 @@ func TestWhitelistAllowing(t *testing.T) {
}
func TestWhitelistBlocking(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -41,10 +40,10 @@ func TestWhitelistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("google.com:6451", resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -55,11 +54,11 @@ func TestWhitelistBlocking(t *testing.T) {
}
func TestLocalhostDefaultForbidden(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("localhost:6451", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -68,10 +67,10 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("127.0.0.1:808", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -80,10 +79,10 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("[::1]:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -94,11 +93,11 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
func TestLocalNetworksDefaultForbidden(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("10.0.0.0:80", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -107,10 +106,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("127.222.34.1:443", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -119,10 +118,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("172.16.0.1:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -131,10 +130,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("192.168.192.168:888", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -145,11 +144,11 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
func TestBlacklistBlocking(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(blacklistedDomain, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -158,10 +157,10 @@ func TestBlacklistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(blacklistedIPv4, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -170,10 +169,10 @@ func TestBlacklistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("["+blacklistedIPv6+"]:80", resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -184,11 +183,11 @@ func TestBlacklistBlocking(t *testing.T) {
}
func TestBlacklistAllowing(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {

187
caddyfile.go Normal file
View file

@ -0,0 +1,187 @@
package forwardproxy
import (
"encoding/base64"
"log"
"strconv"
"strings"
caddy "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
httpcaddyfile.RegisterHandlerDirective("forward_proxy", parseCaddyfile)
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var fp Handler
err := fp.UnmarshalCaddyfile(h.Dispenser)
return &fp, err
}
// EncodeAuthCredentials base64-encode credentials
func EncodeAuthCredentials(user, pass string) (result []byte) {
raw := []byte(user + ":" + pass)
result = make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(result, raw)
return
}
// UnmarshalCaddyfile unmarshals Caddyfile tokens into h.
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.Next() {
return d.ArgErr()
}
args := d.RemainingArgs()
if len(args) > 0 {
return d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
subdirective := d.Val()
args := d.RemainingArgs()
switch subdirective {
case "basic_auth":
if len(args) != 2 {
return d.ArgErr()
}
if len(args[0]) == 0 {
return d.Err("empty usernames are not allowed")
}
// TODO: Evaluate policy of allowing empty passwords.
if strings.Contains(args[0], ":") {
return d.Err("character ':' in usernames is not allowed")
}
if h.AuthCredentials == nil {
h.AuthCredentials = [][]byte{}
}
h.AuthCredentials = append(h.AuthCredentials, EncodeAuthCredentials(args[0], args[1]))
case "hosts":
if len(args) == 0 {
return d.ArgErr()
}
if len(h.Hosts) != 0 {
return d.Err("hosts subdirective specified twice")
}
h.Hosts = caddyhttp.MatchHost(args)
case "ports":
if len(args) == 0 {
return d.ArgErr()
}
if len(h.AllowedPorts) != 0 {
return d.Err("ports subdirective specified twice")
}
h.AllowedPorts = make([]int, len(args))
for i, p := range args {
intPort, err := strconv.Atoi(p)
if intPort <= 0 || intPort > 65535 || err != nil {
return d.Errf("ports are expected to be space-separated and in 0-65535 range, but got: %s", p)
}
h.AllowedPorts[i] = intPort
}
case "hide_ip":
if len(args) != 0 {
return d.ArgErr()
}
h.HideIP = true
case "hide_via":
if len(args) != 0 {
return d.ArgErr()
}
h.HideVia = true
case "probe_resistance":
if len(args) > 1 {
return d.ArgErr()
}
if len(args) == 1 {
lowercaseArg := strings.ToLower(args[0])
if lowercaseArg != args[0] {
log.Println("[WARNING] Secret domain appears to have uppercase letters in it, which are not visitable")
}
h.ProbeResistance = &ProbeResistance{Domain: args[0]}
} else {
h.ProbeResistance = &ProbeResistance{}
}
case "serve_pac":
if len(args) > 1 {
return d.ArgErr()
}
if len(h.PACPath) != 0 {
return d.Err("serve_pac subdirective specified twice")
}
if len(args) == 1 {
h.PACPath = args[0]
if !strings.HasPrefix(h.PACPath, "/") {
h.PACPath = "/" + h.PACPath
}
} else {
h.PACPath = "/proxy.pac"
}
case "dial_timeout":
if len(args) != 1 {
return d.ArgErr()
}
timeout, err := caddy.ParseDuration(args[0])
if err != nil {
return d.ArgErr()
}
if timeout < 0 {
return d.Err("dial_timeout cannot be negative.")
}
h.DialTimeout = caddy.Duration(timeout)
case "upstream":
if len(args) != 1 {
return d.ArgErr()
}
if h.Upstream != "" {
return d.Err("upstream directive specified more than once")
}
h.Upstream = args[0]
case "acl":
for nesting := d.Nesting(); d.NextBlock(nesting); {
aclDirective := d.Val()
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
var ruleSubjects []string
var err error
aclAllow := false
switch aclDirective {
case "allow":
ruleSubjects = args
aclAllow = true
case "allow_file":
if len(args) != 1 {
return d.Err("allowfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
aclAllow = true
case "deny":
ruleSubjects = args
case "deny_file":
if len(args) != 1 {
return d.Err("denyfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
default:
return d.Err("expected acl directive: allow/allowfile/deny/denyfile." +
"got: " + aclDirective)
}
ar := ACLRule{Subjects: ruleSubjects, Allow: aclAllow}
h.ACL = append(h.ACL, ar)
}
default:
return d.ArgErr()
}
}
return nil
}

View file

@ -1,12 +0,0 @@
package main
import (
"github.com/caddyserver/caddy/caddy/caddymain"
_ "github.com/caddyserver/forwardproxy"
)
func main() {
caddymain.EnableTelemetry = false
caddymain.Run()
}

View file

@ -1,34 +1,39 @@
package forwardproxy
import (
"context"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"encoding/json"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"strconv"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
var credentialsEmpty = ""
var credentialsCorrectPlain = "test:pass"
var credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass
var credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass
var credentialsWrong = []string{
"",
"\"\"",
"Basic dzp3",
"Basic \"\"",
"Foo bar",
"Tssssssss",
"Basic dpz3 asp",
}
var (
credentialsEmpty = ""
credentialsCorrectPlain = "test:pass"
credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass
credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass
credentialsWrong = []string{
"",
"\"\"",
"Basic dzp3",
"Basic \"\"",
"Foo bar",
"Tssssssss",
"Basic dpz3 asp",
}
)
/*
Test naming: Test{httpVer}Proxy{Method}{Auth}{Credentials}{httpVer}
@ -36,28 +41,32 @@ GET/CONNECT -- get gets, connect connects and gets
Auth/NoAuth
Empty/Correct/Wrong -- tries different credentials
*/
var testResources = []string{"", "/pic.png"}
var testHttpProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"}
var testHttpTargetVersions = []string{"HTTP/1.1"}
var httpVersionToAlpn = map[string]string{
"HTTP/1.1": "http/1.1",
"HTTP/2.0": "h2",
}
var (
testResources = []string{"/", "/pic.png"}
testHTTPProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"}
testHTTPTargetVersions = []string{"HTTP/1.1"}
httpVersionToALPN = map[string]string{
"HTTP/1.1": "http/1.1",
"HTTP/2.0": "h2",
}
)
var blacklistedDomain = "google-public-dns-a.google.com" // supposed to ever resolve to one of 2 IP addresses below
var blacklistedIPv4 = "8.8.8.8"
var blacklistedIPv6 = "2001:4860:4860::8888"
var (
blacklistedDomain = "google-public-dns-a.google.com" // supposed to ever resolve to one of 2 IP addresses below
blacklistedIPv4 = "8.8.8.8"
blacklistedIPv6 = "2001:4860:4860::8888"
)
type caddyTestServer struct {
*caddy.Instance
addr string // could be http or https
addr string
tls bool
HTTPRedirectPort string // used in probe-resist tests to simulate default Caddy's http->https redirect
root string // expected to have index.html and pic.png
directives []string
proxyEnabled bool
proxyDirectives []string
contents map[string][]byte
httpRedirPort string // used in probe-resist tests to simulate default Caddy's http->https redirect
root string // expected to have index.html and pic.png
_ []string
proxyHandler *Handler
contents map[string][]byte
}
var (
@ -78,137 +87,281 @@ var (
caddyHTTPTestTarget caddyTestServer // serves plain http on 6480
)
func (c *caddyTestServer) marshal() []byte {
mainBlock := []string{c.addr + " {",
"root " + c.root}
mainBlock = append(mainBlock, c.directives...)
if c.proxyEnabled {
if len(c.proxyDirectives) == 0 {
mainBlock = append(mainBlock, "forwardproxy")
} else {
forwardProxyBlock := []string{"forwardproxy {"}
forwardProxyBlock = append(forwardProxyBlock, strings.Join(c.proxyDirectives, "\n"))
forwardProxyBlock = append(forwardProxyBlock, "}")
mainBlock = append(mainBlock, strings.Join(forwardProxyBlock, "\n"))
}
}
mainBlock = append(mainBlock, "}")
if len(c.HTTPRedirectPort) > 0 {
// TODO: this is not good enough, since `func redirPlaintextHost(cfg *SiteConfig) *SiteConfig`
// https://github.com/caddyserver/caddy/blob/master/caddyhttp/httpserver/https.go#L142 can change in future
// and we won't know.
redirectBlock := []string{"http://*:" + c.HTTPRedirectPort + " {",
"redir https://" + c.addr + "{uri}",
"header / Connection close",
"}"}
mainBlock = append(mainBlock, redirectBlock...)
}
return []byte(strings.Join(mainBlock, "\n"))
}
func (c *caddyTestServer) StartTestServer() {
var err error
c.Instance, err = caddy.Start(caddy.CaddyfileInput{Contents: c.marshal(), ServerTypeName: "http"})
func (c *caddyTestServer) server() *caddyhttp.Server {
host, port, err := net.SplitHostPort(c.addr)
if err != nil {
panic(err)
}
handlerJSON := func(h caddyhttp.MiddlewareHandler) json.RawMessage {
return caddyconfig.JSONModuleObject(h, "handler", h.(caddy.Module).CaddyModule().ID.Name(), nil)
}
// create the routes
var routes caddyhttp.RouteList
if c.tls {
// cheap hack for our tests to get TLS certs for the hostnames that
// it needs TLS certs for: create an empty route with a single host
// matcher for that hostname, and auto HTTPS will do the rest
hostMatcherJSON, err := json.Marshal(caddyhttp.MatchHost{host})
if err != nil {
panic(err)
}
matchersRaw := caddyhttp.RawMatcherSets{
caddy.ModuleMap{"host": hostMatcherJSON},
}
routes = append(routes, caddyhttp.Route{MatcherSetsRaw: matchersRaw})
}
if c.proxyHandler != nil {
if host != "" {
// tell the proxy which hostname to serve the proxy on; this must
// be distinct from the host matcher, since the proxy basically
// does its own host matching
c.proxyHandler.Hosts = caddyhttp.MatchHost{host}
}
routes = append(routes, caddyhttp.Route{
HandlersRaw: []json.RawMessage{handlerJSON(c.proxyHandler)},
})
}
if c.root != "" {
routes = append(routes, caddyhttp.Route{
HandlersRaw: []json.RawMessage{
handlerJSON(&fileserver.FileServer{Root: c.root}),
},
})
}
srv := &caddyhttp.Server{
Listen: []string{":" + port},
Routes: routes,
}
if c.tls {
srv.TLSConnPolicies = caddytls.ConnectionPolicies{{}}
} else {
srv.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true}
}
if c.contents == nil {
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 {
panic(err)
}
c.contents[""] = index
c.contents["/"] = 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 {
panic(err)
}
return srv
}
// For simulating/mimicing Caddy's built-in auto-HTTPS redirects. Super hacky but w/e.
func (c *caddyTestServer) redirServer() *caddyhttp.Server {
return &caddyhttp.Server{
Listen: []string{":" + c.httpRedirPort},
Routes: caddyhttp.RouteList{
{
Handlers: []caddyhttp.MiddlewareHandler{
caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{"https://" + c.addr + "/{http.request.uri}"},
"Connection": []string{"close"},
},
Close: true,
},
},
},
},
}
}
func TestMain(m *testing.M) {
caddyForwardProxy = caddyTestServer{addr: "127.0.19.84:1984", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"serve_pac",
"acl {\nallow all\n}"}}
caddyForwardProxy.StartTestServer()
caddyForwardProxyAuth = caddyTestServer{addr: "127.0.0.1:4891", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"acl {\nallow all\n}"}}
caddyForwardProxyAuth.StartTestServer()
caddyHTTPForwardProxyAuth = caddyTestServer{addr: "127.0.69.73:6973", root: "./test/forwardproxy",
directives: []string{"tls off"},
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"acl {\nallow all\n}"}}
caddyHTTPForwardProxyAuth.StartTestServer()
caddyForwardProxyProbeResist = caddyTestServer{addr: "127.0.88.88:8888", root: "./test/forwardproxy",
directives: []string{"tls self_signed"}, HTTPRedirectPort: "8880",
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"probe_resistance test.localhost",
"serve_pac superhiddenfile.pac",
"acl {\nallow all\n}"}}
caddyForwardProxyProbeResist.StartTestServer()
caddyDummyProbeResist = caddyTestServer{addr: "127.0.99.99:9999", root: "./test/forwardproxy",
directives: []string{"tls self_signed"}, HTTPRedirectPort: "9980",
proxyEnabled: false}
caddyDummyProbeResist.StartTestServer()
// 127.0.0.1 and localhost are both used to avoid Caddy matching and routing proxy requests internally
caddyTestTarget = caddyTestServer{addr: "127.0.64.51:6451", root: "./test/index",
directives: []string{},
proxyEnabled: false}
caddyTestTarget.StartTestServer()
caddyHTTPTestTarget = caddyTestServer{addr: "localhost:6480", root: "./test/index",
directives: []string{"tls off"},
proxyEnabled: false}
caddyHTTPTestTarget.StartTestServer()
caddyAuthedUpstreamEnter = caddyTestServer{addr: "127.0.65.25:6585", root: "./test/upstreamingproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"upstream https://test:pass@127.0.0.1:4891",
"basicauth upstreamtest upstreampass"}}
caddyAuthedUpstreamEnter.StartTestServer()
caddyForwardProxyWhiteListing = caddyTestServer{addr: "127.0.87.76:8776", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"acl {\nallow 127.0.64.51\n deny all\n}",
"ports 6451"}}
caddyForwardProxyWhiteListing.StartTestServer()
caddyForwardProxyBlackListing = caddyTestServer{addr: "127.0.66.76:6676", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"acl {\ndeny " + blacklistedIPv4 + "/30\n" +
"deny " + blacklistedIPv6 + "\nallow all\n}"},
caddyForwardProxy = caddyTestServer{
addr: "127.0.19.84:1984",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Allow: true, Subjects: []string{"all"}}},
},
}
caddyForwardProxyBlackListing.StartTestServer()
caddyForwardProxyNoBlacklistOverride = caddyTestServer{addr: "127.0.66.79:6679", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{}}
caddyForwardProxyNoBlacklistOverride.StartTestServer()
caddyForwardProxyAuth = caddyTestServer{
addr: "127.0.0.1:4891",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
}
caddyHTTPForwardProxyAuth = caddyTestServer{
addr: "127.0.69.73:6973",
root: "./test/forwardproxy",
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
}
caddyForwardProxyProbeResist = caddyTestServer{
addr: "127.0.88.88:8888",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: "/superhiddenfile.pac",
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
ProbeResistance: &ProbeResistance{Domain: "test.localhost"},
AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")},
},
httpRedirPort: "8880",
}
caddyDummyProbeResist = caddyTestServer{
addr: "127.0.99.99:9999",
root: "./test/forwardproxy",
tls: true,
httpRedirPort: "9980",
}
caddyTestTarget = caddyTestServer{
addr: "127.0.64.51:6451",
root: "./test/index",
}
caddyHTTPTestTarget = caddyTestServer{
addr: "localhost:6480",
root: "./test/index",
}
caddyAuthedUpstreamEnter = caddyTestServer{
addr: "127.0.65.25:6585",
root: "./test/upstreamingproxy",
tls: true,
proxyHandler: &Handler{
Upstream: "https://test:pass@127.0.0.1:4891",
AuthCredentials: [][]byte{EncodeAuthCredentials("upstreamtest", "upstreampass")},
},
}
caddyForwardProxyWhiteListing = caddyTestServer{
addr: "127.0.87.76:8776",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
ACL: []ACLRule{
{Subjects: []string{"127.0.64.51"}, Allow: true},
{Subjects: []string{"all"}, Allow: false},
},
AllowedPorts: []int{6451},
},
}
caddyForwardProxyBlackListing = caddyTestServer{
addr: "127.0.66.76:6676",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
ACL: []ACLRule{
{Subjects: []string{blacklistedIPv4 + "/30"}, Allow: false},
{Subjects: []string{blacklistedIPv6}, Allow: false},
{Subjects: []string{"all"}, Allow: true},
},
},
}
caddyForwardProxyNoBlacklistOverride = caddyTestServer{
addr: "127.0.66.76:6679",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{},
}
// done configuring all the servers; now build the HTTP app
httpApp := caddyhttp.App{
HTTPPort: 1080, // use a high port to avoid permission issues
Servers: map[string]*caddyhttp.Server{
"caddyForwardProxy": caddyForwardProxy.server(),
"caddyForwardProxyAuth": caddyForwardProxyAuth.server(),
"caddyHTTPForwardProxyAuth": caddyHTTPForwardProxyAuth.server(),
"caddyForwardProxyProbeResist": caddyForwardProxyProbeResist.server(),
"caddyDummyProbeResist": caddyDummyProbeResist.server(),
"caddyTestTarget": caddyTestTarget.server(),
"caddyHTTPTestTarget": caddyHTTPTestTarget.server(),
"caddyAuthedUpstreamEnter": caddyAuthedUpstreamEnter.server(),
"caddyForwardProxyWhiteListing": caddyForwardProxyWhiteListing.server(),
"caddyForwardProxyBlackListing": caddyForwardProxyBlackListing.server(),
"caddyForwardProxyNoBlacklistOverride": caddyForwardProxyNoBlacklistOverride.server(),
// HTTP->HTTPS redirect simulation servers for those which have a redir port configured
"caddyForwardProxyProbeResist_redir": caddyForwardProxyProbeResist.redirServer(),
"caddyDummyProbeResist_redir": caddyDummyProbeResist.redirServer(),
},
GracePeriod: caddy.Duration(1 * time.Second), // keep tests fast
}
httpAppJSON, err := json.Marshal(httpApp)
if err != nil {
panic(err)
}
// ensure we always use internal issuer and not a public CA
tlsApp := caddytls.TLS{
Automation: &caddytls.AutomationConfig{
Policies: []*caddytls.AutomationPolicy{
{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module": "internal"}`)},
},
},
},
}
tlsAppJSON, err := json.Marshal(tlsApp)
if err != nil {
panic(err)
}
// configure the default CA so that we don't try to install trust, just for our tests
falseBool := false
pkiApp := caddypki.PKI{
CAs: map[string]*caddypki.CA{
"local": {InstallTrust: &falseBool},
},
}
pkiAppJSON, err := json.Marshal(pkiApp)
if err != nil {
panic(err)
}
// build final config
cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: caddy.ModuleMap{
"http": httpAppJSON,
"tls": tlsAppJSON,
"pki": pkiAppJSON,
},
}
// start the engines
err = caddy.Run(cfg)
if err != nil {
panic(err)
}
// wait server ready for tls dial
time.Sleep(500 * time.Millisecond)
retCode := m.Run()
caddyForwardProxy.Stop()
caddyForwardProxyAuth.Stop()
caddyHTTPForwardProxyAuth.Stop()
caddyForwardProxyProbeResist.Stop()
caddyDummyProbeResist.Stop()
caddyTestTarget.Stop()
caddyHTTPTestTarget.Stop()
caddyAuthedUpstreamEnter.Stop()
caddyForwardProxyWhiteListing.Stop()
caddyForwardProxyBlackListing.Stop()
caddyForwardProxyNoBlacklistOverride.Stop()
caddy.Stop() // nolint:errcheck // ignore error on shutdown
os.Exit(retCode)
}
@ -216,11 +369,7 @@ func TestMain(m *testing.M) {
// This is a sanity check confirming that target servers actually directly serve what they are expected to.
// (And that they don't serve what they should not)
func TestTheTest(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
// Request index
resp, err := client.Get("http://" + caddyTestTarget.addr)
@ -263,64 +412,20 @@ func TestTheTest(t *testing.T) {
}
}
func debugIoCopy(dst io.Writer, src io.Reader, prefix string) (written int64, err error) {
buf := make([]byte, 32*1024)
flusher, ok := dst.(http.Flusher)
for {
nr, er := src.Read(buf)
fmt.Printf("[%s] Read err %#v\n%s", prefix, er, hex.Dump(buf[0:nr]))
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if ok {
flusher.Flush()
}
fmt.Printf("[%s] Wrote %v %v\n", prefix, nw, ew)
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
var testTransport = &http.Transport{
ResponseHeaderTimeout: 2 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// always dial localhost for testing purposes
return new(net.Dialer).DialContext(ctx, network, addr)
},
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// always dial localhost for testing purposes
conn, err := new(net.Dialer).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
}
fmt.Printf("[%s] Returning with %#v %#v\n", prefix, written, err)
return
return tls.Client(conn, &tls.Config{InsecureSkipVerify: true}), nil
},
}
func httpdump(r interface{}) string {
switch v := r.(type) {
case *http.Request:
if v == nil {
return "httpdump: nil"
}
b, err := httputil.DumpRequest(v, true)
if err != nil {
return err.Error()
} else {
return string(b)
}
case *http.Response:
if v == nil {
return "httpdump: nil"
}
b, err := httputil.DumpResponse(v, true)
if err != nil {
return err.Error()
} else {
return string(b)
}
default:
return "httpdump: wrong type"
}
}
const defaultPACPath = "/proxy.pac"

View file

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

File diff suppressed because it is too large Load diff

View file

@ -17,36 +17,31 @@ package forwardproxy
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"testing"
"time"
_ "github.com/caddyserver/caddy/caddyhttp/header"
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
_ "github.com/caddyserver/caddy/caddyhttp/root"
"github.com/caddyserver/forwardproxy/httpclient"
"golang.org/x/net/http2"
)
func dial(proxyAddr, httpProxyVer string, useTls bool) (net.Conn, error) {
if useTls {
return tls.Dial("tcp", proxyAddr, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
} else {
return net.Dial("tcp", proxyAddr)
func dial(proxyAddr, httpProxyVer string, useTLS bool) (net.Conn, error) {
// always dial localhost for testing purposes
if useTLS {
return tls.Dial("tcp", proxyAddr, &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
}
return net.Dial("tcp", proxyAddr)
}
func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials string, useTls bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTls)
func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials string, useTLS bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS)
if err != nil {
return nil, err
}
@ -54,18 +49,18 @@ func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials
}
// if connect is not successful - that response is returned, otherwise the requested resource
func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxyCredentials, httpProxyVer string, useTls bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTls)
func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxyCredentials, httpProxyVer string, useTLS bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS)
if err != nil {
return nil, err
}
req := http.Request{Header: make(http.Header)}
req := &http.Request{Header: make(http.Header)}
if len(proxyCredentials) > 0 {
req.Header.Set("Proxy-Authorization", proxyCredentials)
}
req.Host = targetHost
req.URL, err = url.Parse("https://" + req.Host)
req.URL, err = url.Parse("https://" + req.Host + "/") // TODO: appending "/" causes file server to NOT issue redirect...
if err != nil {
return nil, err
}
@ -79,13 +74,13 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
req.ProtoMajor = 2
req.ProtoMinor = 0
pr, pw := io.Pipe()
req.Body = ioutil.NopCloser(pr)
req.Body = io.NopCloser(pr)
t := http2.Transport{}
clientConn, err := t.NewClientConn(proxyConn)
if err != nil {
return nil, err
}
resp, err = clientConn.RoundTrip(&req)
resp, err = clientConn.RoundTrip(req)
if err != nil {
return resp, err
}
@ -93,8 +88,8 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
case "HTTP/1.1":
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Write(proxyConn)
resp, err = http.ReadResponse(bufio.NewReader(proxyConn), &req)
req.Write(proxyConn) // nolint:errcheck // we don't care about the error here
resp, err = http.ReadResponse(bufio.NewReader(proxyConn), req)
if err != nil {
return resp, err
}
@ -115,12 +110,12 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTargetVer, proxyCredentials string) (*http.Response, error) {
var err error
req := http.Request{Header: make(http.Header)}
req := &http.Request{Header: make(http.Header)}
if len(proxyCredentials) > 0 {
req.Header.Set("Proxy-Authorization", proxyCredentials)
}
req.Host = targetHost
req.URL, err = url.Parse("http://" + req.Host + resource)
req.URL, err = url.Parse("http://" + targetHost + resource)
if err != nil {
return nil, err
}
@ -137,14 +132,14 @@ func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTarge
if err != nil {
return nil, err
}
return clientConn.RoundTrip(&req)
return clientConn.RoundTrip(req)
case "HTTP/1.1":
req.ProtoMajor = 1
req.ProtoMinor = 1
t := http.Transport{Dial: func(network, addr string) (net.Conn, error) {
return proxyConn, nil
}}
return t.RoundTrip(&req)
return t.RoundTrip(req)
default:
panic("proxy ver: " + httpTargetVer)
}
@ -165,30 +160,26 @@ func responseExpected(res *http.Response, expectedResponse []byte) error {
panic(err)
}
if nTotal == responseLen {
return errors.New(fmt.Sprintf("nTotal == responseLen, but haven't seen io.EOF. Expected response: %s\nGot: %s\n",
expectedResponse, response))
return fmt.Errorf("nTotal == responseLen, but haven't seen io.EOF. Expected response: %s\nGot: %s",
expectedResponse, response)
}
}
response = response[:nTotal]
if len(expectedResponse) != len(response) {
return errors.New(fmt.Sprintf("Expected length: %d. Got thus far: %d. Expected response: %s\nGot: %s\n",
len(expectedResponse), len(response), expectedResponse, response))
return fmt.Errorf("expected length: %d. Got thus far: %d. Expected response: %s\nGot: %s",
len(expectedResponse), len(response), expectedResponse, response)
}
for i := range response {
if response[i] != expectedResponse[i] {
return errors.New(fmt.Sprintf("Response mismatch at character #%d. Expected response: %s\nGot: %s\n",
i, expectedResponse, response))
return fmt.Errorf("response mismatch at character #%d. Expected response: %s\nGot: %s",
i, expectedResponse, response)
}
}
return nil
}
func TestPassthrough(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyForwardProxy.addr)
if err != nil {
t.Fatal(err)
@ -212,10 +203,10 @@ func TestPassthrough(t *testing.T) {
}
func TestGETNoAuth(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxy.addr, httpProxyVer, credentialsEmpty, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxy.addr, httpProxyVer, credentialsEmpty, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -226,10 +217,10 @@ func TestGETNoAuth(t *testing.T) {
}
func TestGETAuthCorrect(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, credentialsCorrect, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -240,11 +231,11 @@ func TestGETAuthCorrect(t *testing.T) {
}
func TestGETAuthWrong(t *testing.T) {
useTls := true
const useTLS = true
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
@ -258,11 +249,11 @@ func TestGETAuthWrong(t *testing.T) {
}
func TestProxySelfGet(t *testing.T) {
useTls := true
const useTLS = true
// GETNoAuth to self
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyForwardProxy.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, useTls)
response, err := getViaProxy(caddyForwardProxy.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyForwardProxy.contents[resource]); err != nil {
@ -272,9 +263,9 @@ func TestProxySelfGet(t *testing.T) {
}
// GETAuthCorrect to self
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyForwardProxyAuth.contents[resource]); err != nil {
@ -289,11 +280,11 @@ func TestProxySelfGet(t *testing.T) {
// Low priority since this is a functionality issue, not security, and it would be easily caught in the wild.
func TestConnectNoAuth(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -305,11 +296,11 @@ func TestConnectNoAuth(t *testing.T) {
}
func TestConnectAuthCorrect(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS)
if err != nil {
t.Fatal(httpProxyVer, httpTargetVer, err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -321,18 +312,18 @@ func TestConnectAuthCorrect(t *testing.T) {
}
func TestConnectAuthWrong(t *testing.T) {
useTls := true
const useTLS = true
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
if response.StatusCode != http.StatusProxyAuthRequired {
t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s\n",
response.StatusCode, response.Status)
t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s (wrongCreds=%s httpProxyVer=%s httpTargetVer=%s resource=%s)",
response.StatusCode, response.Status, wrongCreds, httpProxyVer, httpTargetVer, resource)
}
}
}
@ -341,17 +332,12 @@ func TestConnectAuthWrong(t *testing.T) {
}
func TestPAC(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyForwardProxy.addr + "/proxy.pac")
if err != nil {
t.Fatal(err)
}
splitAddr := strings.Split(caddyForwardProxy.addr, ":")
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, splitAddr[0], splitAddr[1]))); err != nil {
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxy.addr))); err != nil {
t.Fatal(err)
}
@ -359,20 +345,19 @@ func TestPAC(t *testing.T) {
if err != nil {
t.Fatal(err)
}
splitAddr = strings.Split(caddyForwardProxyProbeResist.addr, ":")
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, splitAddr[0], splitAddr[1]))); err != nil {
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxyProbeResist.addr))); err != nil {
t.Fatal(err)
}
}
func TestCONNECTViaUpstream(t *testing.T) {
useTls := true
const useTLS = true
for range make([]byte, 5) { // do several times to test http2 connection reuse
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr,
httpTargetVer, credentialsUpstreamCorrect, httpProxyVer, useTls)
httpTargetVer, credentialsUpstreamCorrect, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -385,12 +370,12 @@ func TestCONNECTViaUpstream(t *testing.T) {
}
func TestGETViaUpstream(t *testing.T) {
useTls := true
const useTLS = true
for range make([]byte, 5) { // do several times to test http2 connection reuse
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr, httpProxyVer,
credentialsUpstreamCorrect, useTls)
credentialsUpstreamCorrect, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -403,11 +388,7 @@ func TestGETViaUpstream(t *testing.T) {
func TestUpstreamPassthrough(t *testing.T) {
// Usptreaming proxy still hosts things as expected
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyAuthedUpstreamEnter.addr)
if err != nil {
t.Fatal(err)

118
go.mod
View file

@ -1,14 +1,114 @@
module github.com/caddyserver/forwardproxy
go 1.12
go 1.21
require (
github.com/caddyserver/caddy v1.0.5
github.com/golang/protobuf v1.4.3 // indirect
github.com/lucas-clemente/quic-go v0.19.3 // indirect
github.com/mholt/caddy v1.0.0
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/sys v0.0.0-20201204225414-ed752295db88 // indirect
google.golang.org/protobuf v1.25.0 // indirect
github.com/caddyserver/caddy/v2 v2.7.6
go.uber.org/zap v1.26.0
golang.org/x/net v0.21.0
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.3.2 // 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.9.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/certmagic v0.20.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // 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.10.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.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.15.1 // indirect
github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.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.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // 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.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.56 // 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.12.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.41.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.25.0 // indirect
github.com/smallstep/nosql v0.6.0 // indirect
github.com/smallstep/truststore v0.12.1 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.7.0 // 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-20230806124524-28a91b69a046 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/yuin/goldmark v1.5.6 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.step.sm/cli-utils v0.8.0 // indirect
go.step.sm/crypto v0.35.1 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/mock v0.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // 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
)

993
go.sum

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// httpclient is used by the upstreaming forwardproxy to establish connections to http(s) upstreams.
// Package httpclient is used by the upstreaming forwardproxy to establish connections to http(s) upstreams.
// it implements x/net/proxy.Dialer interface
package httpclient
@ -33,7 +33,7 @@ import (
// HTTPConnectDialer allows to configure one-time use HTTP CONNECT client
type HTTPConnectDialer struct {
ProxyUrl url.URL
ProxyURL url.URL
DefaultHeader http.Header
// TODO: If spkiFp is set, use it as SPKI fingerprint to confirm identity of the
@ -52,47 +52,47 @@ type HTTPConnectDialer struct {
cachedH2RawConn net.Conn
}
// NewHTTPClient creates a client to issue CONNECT requests and tunnel traffic via HTTPS proxy.
// proxyUrlStr must provide Scheme and Host, may provide credentials and port.
// NewHTTPConnectDialer creates a client to issue CONNECT requests and tunnel traffic via HTTPS proxy.
// proxyURLStr must provide Scheme and Host, may provide credentials and port.
// Example: https://username:password@golang.org:443
func NewHTTPConnectDialer(proxyUrlStr string) (*HTTPConnectDialer, error) {
proxyUrl, err := url.Parse(proxyUrlStr)
func NewHTTPConnectDialer(proxyURLStr string) (*HTTPConnectDialer, error) {
proxyURL, err := url.Parse(proxyURLStr)
if err != nil {
return nil, err
}
if proxyUrl.Host == "" {
return nil, errors.New("misparsed `url=" + proxyUrlStr +
if proxyURL.Host == "" {
return nil, errors.New("misparsed `url=" + proxyURLStr +
"`, make sure to specify full url like https://username:password@hostname.com:443/")
}
switch proxyUrl.Scheme {
switch proxyURL.Scheme {
case "http":
if proxyUrl.Port() == "" {
proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "80")
if proxyURL.Port() == "" {
proxyURL.Host = net.JoinHostPort(proxyURL.Host, "80")
}
case "https":
if proxyUrl.Port() == "" {
proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "443")
if proxyURL.Port() == "" {
proxyURL.Host = net.JoinHostPort(proxyURL.Host, "443")
}
case "":
return nil, errors.New("specify scheme explicitly (https://)")
default:
return nil, errors.New("scheme " + proxyUrl.Scheme + " is not supported")
return nil, errors.New("scheme " + proxyURL.Scheme + " is not supported")
}
client := &HTTPConnectDialer{
ProxyUrl: *proxyUrl,
ProxyURL: *proxyURL,
DefaultHeader: make(http.Header),
SpkiFP: nil,
EnableH2ConnReuse: true,
}
if proxyUrl.User != nil {
if proxyUrl.User.Username() != "" {
password, _ := proxyUrl.User.Password()
if proxyURL.User != nil {
if proxyURL.User.Username() != "" {
password, _ := proxyURL.User.Password()
client.DefaultHeader.Set("Proxy-Authorization", "Basic "+
base64.StdEncoding.EncodeToString([]byte(proxyUrl.User.Username()+":"+password)))
base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username()+":"+password)))
}
}
return client, nil
@ -132,12 +132,12 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
resp, err := h2clientConn.RoundTrip(req)
if err != nil {
rawConn.Close()
err = rawConn.Close()
return nil, err
}
if resp.StatusCode != http.StatusOK {
rawConn.Close()
_ = rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
}
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)
if err != nil {
rawConn.Close()
err = rawConn.Close()
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
if err != nil {
rawConn.Close()
err = rawConn.Close()
return nil, err
}
if resp.StatusCode != http.StatusOK {
rawConn.Close()
_ = rawConn.Close()
return nil, errors.New("Proxy responded with non 200 code: " + resp.Status)
}
return rawConn, nil
@ -191,24 +191,25 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
var err error
var rawConn net.Conn
negotiatedProtocol := ""
switch c.ProxyUrl.Scheme {
switch c.ProxyURL.Scheme {
case "http":
rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyUrl.Host)
rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyURL.Host)
if err != nil {
return nil, err
}
case "https":
if c.DialTLS != nil {
rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyUrl.Host)
rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyURL.Host)
if err != nil {
return nil, err
}
} else {
tlsConf := tls.Config{
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 {
return nil, err
}
@ -220,7 +221,7 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
rawConn = tlsConn
}
default:
return nil, errors.New("scheme " + c.ProxyUrl.Scheme + " is not supported")
return nil, errors.New("scheme " + c.ProxyURL.Scheme + " is not supported")
}
switch negotiatedProtocol {
@ -232,13 +233,13 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
t := http2.Transport{}
h2clientConn, err := t.NewClientConn(rawConn)
if err != nil {
rawConn.Close()
err = rawConn.Close()
return nil, err
}
proxyConn, err := connectHttp2(rawConn, h2clientConn)
if err != nil {
rawConn.Close()
err = rawConn.Close()
return nil, err
}
if c.EnableH2ConnReuse {
@ -249,7 +250,7 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
}
return proxyConn, err
default:
rawConn.Close()
_ = rawConn.Close()
return nil, errors.New("negotiated unsupported application layer protocol: " +
negotiatedProtocol)
}
@ -274,8 +275,13 @@ func (h *http2Conn) Write(p []byte) (n int, err error) {
}
func (h *http2Conn) Close() error {
h.in.Close()
return h.out.Close()
inErr := h.in.Close()
outErr := h.out.Close()
if inErr != nil {
return inErr
}
return outErr
}
func (h *http2Conn) CloseConn() error {

View file

@ -3,6 +3,7 @@ package forwardproxy
import (
"crypto/tls"
"fmt"
"net"
"sync"
"testing"
@ -12,22 +13,30 @@ import (
)
func TestHttpClient(t *testing.T) {
_test := func(proxyUrl string) {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
_test := func(urlSchemeAndCreds, urlAddress string) {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
dialer, err := httpclient.NewHTTPConnectDialer(proxyUrl)
// always dial localhost for testing purposes
proxyURL := fmt.Sprintf("%s@%s", urlSchemeAndCreds, urlAddress)
dialer, err := httpclient.NewHTTPConnectDialer(proxyURL)
if err != nil {
t.Fatal(err)
}
dialer.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
// always dial localhost for testing purposes
conn, err := tls.Dial(network, address, &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
if err != nil {
return nil, "", err
}
return conn, conn.ConnectionState().NegotiatedProtocol, nil
}
// always dial localhost for testing purposes
conn, err := dialer.Dial("tcp", caddyTestTarget.addr)
if err != nil {
t.Fatal(err)
@ -43,8 +52,8 @@ func TestHttpClient(t *testing.T) {
}
}
_test("https://" + credentialsCorrectPlain + "@" + caddyForwardProxyAuth.addr)
_test("http://" + credentialsCorrectPlain + "@" + caddyHTTPForwardProxyAuth.addr)
_test("https://"+credentialsCorrectPlain, caddyForwardProxyAuth.addr)
_test("http://"+credentialsCorrectPlain, caddyHTTPForwardProxyAuth.addr)
}
func TestHttpClientH2Multiplexing(t *testing.T) {
@ -58,8 +67,11 @@ func TestHttpClientH2Multiplexing(t *testing.T) {
t.Fatal(err)
}
dialer.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
// always dial localhost for testing purposes
conn, err := tls.Dial(network, address, &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
if err != nil {
return nil, "", err
}
@ -74,6 +86,7 @@ func TestHttpClientH2Multiplexing(t *testing.T) {
_test := func() {
defer wg.Done()
for _, resource := range testResources {
// always dial localhost for testing purposes
conn, err := dialer.Dial("tcp", caddyTestTarget.addr)
if err != nil {
t.Fatal(err)
@ -90,6 +103,7 @@ func TestHttpClientH2Multiplexing(t *testing.T) {
_test() // do serially at least once
for i := 0; i < retries; i++ {
// nolint:govet // this is a test
go _test()
time.Sleep(sleepInterval)
}

View file

@ -4,17 +4,18 @@ import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"strings"
"testing"
)
func TestGETAuthCorrectProbeResist(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, credentialsCorrect, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -25,47 +26,56 @@ func TestGETAuthCorrectProbeResist(t *testing.T) {
}
func TestGETAuthWrongProbeResist(t *testing.T) {
useTls := true
const useTLS = true
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
responseForwardProxy, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
if responseProbeResist.StatusCode != responseReference.StatusCode {
t.Fatalf("Expected response: %d, Got: %d\n",
responseReference.StatusCode, responseProbeResist.StatusCode)
}
if err = responsesAreEqual(responseProbeResist, responseReference); err != nil {
t.Fatal(err)
var e errorHeaderAlternativeServiceNotEqual
if !errors.As(err, &e) {
t.Fatal(err)
}
if err = e.CheckAlternativeServiceError(caddyForwardProxyProbeResist.addr, caddyDummyProbeResist.addr); err != nil {
t.Fatal(err)
}
}
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil {
t.Fatal("Responses from servers with and without forwardproxy are expected to be different.")
t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+
"\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n",
responseProbeResist, responseForwardProxy)
}
}
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
responseForwardProxy, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
@ -74,10 +84,18 @@ func TestGETAuthWrongProbeResist(t *testing.T) {
responseProbeResist.StatusCode)
}
if err = responsesAreEqual(responseProbeResist, responseReference); err != nil {
t.Fatal(err)
var e errorHeaderAlternativeServiceNotEqual
if !errors.As(err, &e) {
t.Fatal(err)
}
if err = e.CheckAlternativeServiceError(caddyForwardProxyProbeResist.addr, caddyDummyProbeResist.addr); err != nil {
t.Fatal(err)
}
}
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil {
t.Fatal("Responses from servers with and without forwardproxy are expected to be different.")
t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+
"\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n",
responseProbeResist, responseForwardProxy)
}
}
}
@ -86,16 +104,14 @@ func TestGETAuthWrongProbeResist(t *testing.T) {
// test that responses on http redirect port are same
func TestGETAuthWrongProbeResistRedir(t *testing.T) {
useTls := false
const useTLS = false
httpProxyVer := "HTTP/1.1"
for _, wrongCreds := range credentialsWrong {
// request test target
for _, resource := range testResources {
responseProbeResist, rPRerr := getViaProxy(caddyTestTarget.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseProbeResist, rPRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS)
// get response from reference server without forwardproxy and compare them
responseReference, rRerr := getViaProxy(caddyTestTarget.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseReference, rRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS)
if (rPRerr == nil && rRerr != nil) || (rPRerr != nil && rRerr == nil) {
t.Fatalf("Reference error: %s. Probe resist error: %s", rRerr, rPRerr)
}
@ -109,14 +125,12 @@ func TestGETAuthWrongProbeResistRedir(t *testing.T) {
}
// request self
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS)
if err != nil {
t.Fatal(err)
}
@ -132,11 +146,11 @@ func TestGETAuthWrongProbeResistRedir(t *testing.T) {
}
func TestConnectAuthCorrectProbeResist(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS = true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -148,22 +162,22 @@ func TestConnectAuthCorrectProbeResist(t *testing.T) {
}
func TestConnectAuthWrongProbeResist(t *testing.T) {
useTls := true
const useTLS = true
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseForwardProxy, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
@ -172,10 +186,18 @@ func TestConnectAuthWrongProbeResist(t *testing.T) {
responseReference.StatusCode, responseProbeResist.StatusCode)
}
if err = responsesAreEqual(responseProbeResist, responseReference); err != nil {
t.Fatal(err)
var e errorHeaderAlternativeServiceNotEqual
if !errors.As(err, &e) {
t.Fatal(err)
}
if err = e.CheckAlternativeServiceError(caddyForwardProxyProbeResist.addr, caddyDummyProbeResist.addr); err != nil {
t.Fatal(err)
}
}
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil {
t.Fatal("Responses from servers with and without forwardproxy are expected to be different.")
t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+
"\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n",
responseProbeResist, responseForwardProxy)
}
}
// request self
@ -183,25 +205,33 @@ func TestConnectAuthWrongProbeResist(t *testing.T) {
if httpTargetVer != httpProxyVer {
continue
}
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := connectAndGetViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseForwardProxy, err := connectAndGetViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
if err = responsesAreEqual(responseProbeResist, responseReference); err != nil {
t.Fatal(err)
var e errorHeaderAlternativeServiceNotEqual
if !errors.As(err, &e) {
t.Fatal(err)
}
if err = e.CheckAlternativeServiceError(caddyForwardProxyProbeResist.addr, caddyDummyProbeResist.addr); err != nil {
t.Fatal(err)
}
}
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil {
t.Fatal("Responses from servers with and without forwardproxy are expected to be different.")
t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+
"\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n",
responseProbeResist, responseForwardProxy)
}
}
}
@ -211,20 +241,18 @@ func TestConnectAuthWrongProbeResist(t *testing.T) {
// test that responses on http redirect port are same
func TestConnectAuthWrongProbeResistRedir(t *testing.T) {
useTls := false
const useTLS = false
httpProxyVer := "HTTP/1.1"
for _, wrongCreds := range credentialsWrong {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
// request test target
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
@ -238,14 +266,12 @@ func TestConnectAuthWrongProbeResistRedir(t *testing.T) {
}
// request self
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS)
if err != nil {
t.Fatal(err)
}
@ -261,6 +287,37 @@ func TestConnectAuthWrongProbeResistRedir(t *testing.T) {
}
}
type errorHeaderAlternativeServiceNotEqual struct {
ValueA []string
ValueB []string
}
func (e errorHeaderAlternativeServiceNotEqual) Error() string {
return fmt.Sprintf("header 'Alt-Svc' not equal: %v, %v\n", e.ValueA, e.ValueB)
}
func (e errorHeaderAlternativeServiceNotEqual) CheckAlternativeServiceError(serverAddrA, serverAddrB string) error {
if len(e.ValueA) == 0 || len(e.ValueB) == 0 {
return fmt.Errorf("header 'Alt-Svc' is empty: %w", e)
}
_, port, err := net.SplitHostPort(serverAddrA)
if err != nil {
return fmt.Errorf("failed to split server address :%w", err)
}
if !strings.Contains(e.ValueA[0], port) {
return fmt.Errorf("Alt-Svc address :%s does not contain the server port: %s", e.ValueA[0], port)
}
_, port, err = net.SplitHostPort(serverAddrB)
if err != nil {
return fmt.Errorf("failed to split server address :%w", err)
}
if !strings.Contains(e.ValueB[0], port) {
return fmt.Errorf("Alt-Svc address :%s does not contain the server port: %s", e.ValueB[0], port)
}
return nil
}
// returns nil if are equal
func responsesAreEqual(res1, res2 *http.Response) error {
if res1 == nil {
@ -270,36 +327,31 @@ func responsesAreEqual(res1, res2 *http.Response) error {
return errors.New("res2 is nil")
}
if res1.Status != res2.Status {
return errors.New("Status is different")
return fmt.Errorf("status is different; %s != %s", res1.Status, res2.Status)
}
if res1.StatusCode != res2.StatusCode {
return errors.New("StatusCode is different")
return fmt.Errorf("status code is different; %d != %d", res1.StatusCode, res2.StatusCode)
}
if res1.ProtoMajor != res2.ProtoMajor {
return errors.New("ProtoMajor is different")
return fmt.Errorf("proto major is different; %d != %d", res1.ProtoMajor, res2.ProtoMajor)
}
if res1.Close != res2.Close {
return errors.New("Close is different")
}
if res1.ProtoMinor != res2.ProtoMinor {
return errors.New("ProtoMinor is different")
return fmt.Errorf("proto minor is different; %d != %d", res1.ProtoMinor, res2.ProtoMinor)
}
if res1.Close != res2.Close {
return fmt.Errorf("close is different; %t != %t", res1.Close, res2.Close)
}
if res1.ContentLength != res2.ContentLength {
return errors.New("ContentLength is different")
return fmt.Errorf("content length is different; %d != %d", res1.ContentLength, res2.ContentLength)
}
if res1.Uncompressed != res2.Uncompressed {
return errors.New("Uncompressed is different")
return fmt.Errorf("uncompressed is different; %t != %t", res1.Uncompressed, res2.Uncompressed)
}
if res1.Proto != res2.Proto {
return errors.New("Proto is different")
return fmt.Errorf("proto is different; %s != %s", res1.Proto, res2.Proto)
}
if len(res1.TransferEncoding) != len(res2.TransferEncoding) {
return errors.New("TransferEncodings have different length")
return fmt.Errorf("transfer encodings have different lenght; %d != %d", len(res1.TransferEncoding), len(res2.TransferEncoding))
}
// returns "" if equal
@ -318,6 +370,7 @@ func responsesAreEqual(res1, res2 *http.Response) error {
if len(s1) != len(s2) {
return fmt.Sprintf("different length: %d vs %d", len(s1), len(s2))
}
for i := range s1 {
if s1[i] != s2[i] {
return fmt.Sprintf("different string at position %d: %s vs %s", i, s1[i], s2[i])
@ -334,6 +387,7 @@ func responsesAreEqual(res1, res2 *http.Response) error {
if len(res1.Header) != len(res2.Header) {
return errors.New("Headers have different length")
}
for k1, v1 := range res1.Header {
k1Lower := strings.ToLower(k1)
if k1Lower == "date" {
@ -341,28 +395,23 @@ func responsesAreEqual(res1, res2 *http.Response) error {
}
v2, ok := res2.Header[k1]
if !ok {
return errors.New(fmt.Sprintf("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)
}
return fmt.Errorf("header \"%s: %s\" is absent in res2", k1, v1)
}
if errStr = stringSlicesAreEqual(v1, v2); errStr != "" {
return errors.New(fmt.Sprintf("Header \"%s\" is different: %s", k1, errStr))
if k1 == "Alt-Svc" {
return errorHeaderAlternativeServiceNotEqual{v1, v2}
}
return fmt.Errorf("header \"%s\" is different: %s", k1, errStr)
}
}
// Compare bodies
buf1, err1 := ioutil.ReadAll(res1.Body)
buf2, err2 := ioutil.ReadAll(res2.Body)
buf1, err1 := io.ReadAll(res1.Body)
buf2, err2 := io.ReadAll(res2.Body)
n1 := len(buf1)
n2 := len(buf2)
makeBodyError := func(s string) error {
return errors.New(fmt.Sprintf("Bodies are different: %s. n1 = %d, n2 = %d. err1 = %v, err2 = %v. buf1 = %s, buf2 = %s",
s, n1, n2, err1, err2, buf1[:n1], buf2[:n2]))
return fmt.Errorf("bodies are different: %s. n1 = %d, n2 = %d. err1 = %v, err2 = %v. buf1 = %s, buf2 = %s",
s, n1, n2, err1, err2, buf1[:n1], buf2[:n2])
}
if n2 != n1 {
return makeBodyError("Body sizes are different")
@ -383,13 +432,17 @@ func responsesAreEqual(res1, res2 *http.Response) error {
// Responses from forwardproxy + proberesist and generic caddy can have different addresses present in headers.
// To avoid false positives - remove addresses before comparing.
func removeAddressesByte(b []byte) []byte {
b = bytes.Replace(b, []byte(caddyForwardProxyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyForwardProxyProbeResist.addr)), -1)
b = bytes.Replace(b, []byte(caddyDummyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyDummyProbeResist.addr)), -1)
b = bytes.ReplaceAll(b, []byte(caddyForwardProxyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyForwardProxyProbeResist.addr)))
b = bytes.ReplaceAll(b, []byte(caddyDummyProbeResist.addr),
bytes.Repeat([]byte{'#'}, len(caddyDummyProbeResist.addr)))
return b
}
func removeAddressesStr(s string) string {
return string(removeAddressesByte([]byte(s)))
func changePort(inputAddr, toPort string) string {
host, _, err := net.SplitHostPort(inputAddr)
if err != nil {
panic(err)
}
return net.JoinHostPort(host, toPort)
}

409
setup.go
View file

@ -1,409 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package forwardproxy
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/forwardproxy/httpclient"
"golang.org/x/net/proxy"
)
func setup(c *caddy.Controller) error {
httpserver.GetConfig(c).FallbackSite = true
fp := &ForwardProxy{
dialTimeout: time.Second * 20,
hostname: httpserver.GetConfig(c).Host(), port: httpserver.GetConfig(c).Port(),
httpTransport: http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 50,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
c.Next() // skip the directive name
args := c.RemainingArgs()
if len(args) > 0 {
return c.ArgErr()
}
for c.NextBlock() {
subdirective := c.Val()
args := c.RemainingArgs()
switch subdirective {
case "basicauth":
if len(args) != 2 {
return c.ArgErr()
}
if len(args[0]) == 0 {
return c.Err("empty usernames are not allowed")
}
// TODO: Evaluate policy of allowing empty passwords.
if strings.Contains(args[0], ":") {
return c.Err("character ':' in usernames is not allowed")
}
if fp.authCredentials == nil {
fp.authCredentials = [][]byte{}
}
// base64-encode credentials
buf := make([]byte, base64.StdEncoding.EncodedLen(len(args[0])+1+len(args[1])))
base64.StdEncoding.Encode(buf, []byte(args[0]+":"+args[1]))
fp.authCredentials = append(fp.authCredentials, buf)
fp.authRequired = true
case "ports":
if len(args) == 0 {
return c.ArgErr()
}
if len(fp.whitelistedPorts) != 0 {
return c.Err("ports subdirective specified twice")
}
fp.whitelistedPorts = make([]int, len(args))
for i, p := range args {
intPort, err := strconv.Atoi(p)
if intPort <= 0 || intPort > 65535 || err != nil {
return c.Err("ports are expected to be space-separated" +
" and in 0-65535 range. Got: " + p)
}
fp.whitelistedPorts[i] = intPort
}
case "hide_ip":
if len(args) != 0 {
return c.ArgErr()
}
fp.hideIP = true
case "hide_via":
if len(args) != 0 {
return c.ArgErr()
}
fp.hideVia = true
case "probe_resistance":
if len(args) > 1 {
return c.ArgErr()
}
fp.probeResistEnabled = true
if len(args) == 1 {
lowercaseArg := strings.ToLower(args[0])
if lowercaseArg != args[0] {
log.Println("WARNING: secret domain appears to have uppercase letters in it, which are not visitable")
}
fp.probeResistDomain = args[0]
}
case "serve_pac":
if len(args) > 1 {
return c.ArgErr()
}
if len(fp.pacFilePath) != 0 {
return c.Err("serve_pac subdirective specified twice")
}
if len(args) == 1 {
fp.pacFilePath = args[0]
if !strings.HasPrefix(fp.pacFilePath, "/") {
fp.pacFilePath = "/" + fp.pacFilePath
}
} else {
fp.pacFilePath = "/proxy.pac"
}
log.Printf("Proxy Auto-Config will be served at %s%s\n", fp.hostname, fp.pacFilePath)
case "response_timeout":
if len(args) != 1 {
return c.ArgErr()
}
timeout, err := strconv.Atoi(args[0])
if err != nil {
return c.ArgErr()
}
if timeout < 0 {
return c.Err("response_timeout cannot be negative.")
}
fp.httpTransport.ResponseHeaderTimeout = time.Duration(timeout) * time.Second
case "dial_timeout":
if len(args) != 1 {
return c.ArgErr()
}
timeout, err := strconv.Atoi(args[0])
if err != nil {
return c.ArgErr()
}
if timeout < 0 {
return c.Err("dial_timeout cannot be negative.")
}
fp.dialTimeout = time.Second * time.Duration(timeout)
case "upstream":
if len(args) != 1 {
return c.ArgErr()
}
if fp.upstream != nil {
return c.Err("upstream directive specified more than once")
}
var err error
fp.upstream, err = url.Parse(args[0])
if err != nil {
return c.Err("failed to parse upstream address: " + err.Error())
}
case "acl":
if len(args) != 0 {
return c.Err("acl should be only subdirective on the line")
}
args := c.RemainingArgs()
if len(args) > 0 {
return c.ArgErr()
}
c.Next()
if c.Val() != "{" {
return c.Err("acl directive must be followed by opening curly braces \"{\"")
}
for {
if !c.Next() {
return c.Err("acl blockmust be ended by closing curly braces \"}\"")
}
aclDirective := c.Val()
args := c.RemainingArgs()
if aclDirective == "}" {
break
}
if len(args) == 0 {
return c.ArgErr()
}
var ruleSubjects []string
var err error
aclAllow := false
switch aclDirective {
case "allow":
ruleSubjects = args[:]
aclAllow = true
case "allowfile":
if len(args) != 1 {
return c.Err("allowfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
aclAllow = true
case "deny":
ruleSubjects = args[:]
case "denyfile":
if len(args) != 1 {
return c.Err("denyfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
default:
return c.Err("expected acl directive: allow/allowfile/deny/denyfile." +
"got: " + aclDirective)
}
for _, rs := range ruleSubjects {
ar, err := newAclRule(rs, aclAllow)
if err != nil {
return err
}
fp.aclRules = append(fp.aclRules, ar)
}
}
default:
return c.ArgErr()
}
}
if fp.upstream != nil && (fp.aclRules != nil || len(fp.whitelistedPorts) != 0) {
return c.Err("upstream subdirective is incompatible with acl/ports subdirectives")
}
for _, ipDeny := range []string{
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fe80::/10",
} {
ar, err := newAclRule(ipDeny, false)
if err != nil {
panic(err)
}
fp.aclRules = append(fp.aclRules, ar)
}
fp.aclRules = append(fp.aclRules, &aclAllRule{allow: true})
if fp.probeResistEnabled {
if !fp.authRequired {
return c.Err("probing resistance requires authentication: " +
"add `basicauth username password` to forwardproxy")
}
if len(fp.probeResistDomain) > 0 {
log.Printf("Secret domain used to connect to proxy: %s\n", fp.probeResistDomain)
}
}
dialer := &net.Dialer{
Timeout: fp.dialTimeout,
KeepAlive: 30 * time.Second,
DualStack: true,
}
fp.dialContext = dialer.DialContext
fp.httpTransport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
conn, err := fp.dialContextCheckACL(ctx, network, address)
if err != nil {
return conn, err
}
return conn, nil
}
if fp.upstream != nil {
if !isLocalhost(fp.upstream.Hostname()) && fp.upstream.Scheme != "https" {
return errors.New("insecure schemes are only allowed to localhost upstreams")
}
registerHTTPDialer := func(u *url.URL, _ proxy.Dialer) (proxy.Dialer, error) {
// CONNECT request is proxied as-is, so we don't care about target url, but it could be
// useful in future to implement policies of choosing between multiple upstream servers.
// Given dialer is not used, since it's the same dialer provided by us.
d, err := httpclient.NewHTTPConnectDialer(fp.upstream.String())
if err != nil {
return nil, err
}
d.Dialer = *dialer
if isLocalhost(fp.upstream.Hostname()) && fp.upstream.Scheme == "https" {
// disabling verification helps with testing the package and setups
// either way, it's impossible to have a legit TLS certificate for "127.0.0.1"
log.Println("Localhost upstream detected, disabling verification of TLS certificate")
d.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, "", err
}
return conn, conn.ConnectionState().NegotiatedProtocol, nil
}
}
return d, nil
}
proxy.RegisterDialerType("https", registerHTTPDialer)
proxy.RegisterDialerType("http", registerHTTPDialer)
upstreamDialer, err := proxy.FromURL(fp.upstream, dialer)
if err != nil {
return errors.New("failed to create proxy to upstream: " + err.Error())
}
if ctxDialer, ok := upstreamDialer.(interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}); ok {
// upstreamDialer has DialContext - use it
fp.dialContext = ctxDialer.DialContext
} else {
// upstreamDialer does not have DialContext - ignore the context :(
fp.dialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
return upstreamDialer.Dial(network, address)
}
}
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
fp.Next = next
return fp
})
makeBuffer := func() interface{} { return make([]byte, 0, 32*1024) }
bufferPool = sync.Pool{New: makeBuffer}
return nil
}
func init() {
caddy.RegisterPlugin("forwardproxy", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
func isLocalhost(hostname string) bool {
if hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1" {
return true
}
return false
}
func readLinesFromFile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var hostnames []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
hostnames = append(hostnames, scanner.Text())
}
return hostnames, scanner.Err()
}
// isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid
func isValidDomainLite(domain string) error {
for i := 0; i < len(domain); i++ {
c := domain[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' ||
c == '-' || c == '.' {
continue
}
return errors.New("character " + string(c) + " is not allowed")
}
sections := strings.Split(domain, ".")
for _, s := range sections {
if len(s) == 0 {
return errors.New("empty section between dots in domain name or trailing dot")
}
if len(s) > 63 {
return errors.New("domain name section is too long")
}
}
return nil
}
type ProxyError struct {
S string
Code int
}
func (e *ProxyError) Error() string {
return fmt.Sprintf("[%v] %s", e.Code, e.S)
}
func (e *ProxyError) SplitCodeError() (int, error) {
if e == nil {
return 200, nil
}
return e.Code, errors.New(e.S)
}

View file

@ -1,139 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package forwardproxy
import (
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", "forwardproxy string")
err := setup(c)
if err == nil {
t.Fatal("Expected: failure. Got: success. Input: forwardproxy string")
}
testParsing := func(subdirectives []string, shouldSucceed bool) {
input := "forwardproxy"
if len(subdirectives) > 0 {
input += " {\n"
for _, s := range subdirectives {
input += s + "\n"
}
input += "}"
}
c := caddy.NewTestController("http", input)
err := setup(c)
if shouldSucceed && err != nil {
t.Fatalf("Expected: success. Got: %v. Input:\n%s\n", err, input)
}
if !shouldSucceed && err == nil {
t.Fatalf("Expected: failure. Got: success. Input:\n%s\n", input)
}
}
testParsing(nil, true)
testParsing([]string{}, true)
testParsing([]string{"qweqwe"}, false)
testParsing([]string{"0"}, false)
testParsing([]string{"basicauth john"}, false)
testParsing([]string{"basicauth john \"\""}, true)
testParsing([]string{"basicauth john", "basicauth john \"\""}, false)
testParsing([]string{"basicauth john doe"}, true)
testParsing([]string{"basicauth john doe foo"}, false)
testParsing([]string{"basicauth john doe foo bar"}, false)
testParsing([]string{"basicauth \"\" doe"}, false)
testParsing([]string{"basicauth \"\" \"\""}, false)
testParsing([]string{"basicauth 0"}, false)
testParsing([]string{"basicauth 0 0"}, true)
testParsing([]string{"basicauth 0 0 0"}, false)
testParsing([]string{"basicauth 秘密"}, false)
testParsing([]string{"basicauth 秘密 秘密"}, true)
testParsing([]string{"basicauth 秘密 秘密 秘密"}, false)
testParsing([]string{"basicauth cyrillic пароль"}, true)
testParsing([]string{"basicauth john \"\"", "basicauth john doe", "basicauth 0 0", "basicauth 秘密 秘密", "basicauth cyrillic пароль"}, true)
testParsing([]string{"ports"}, false)
testParsing([]string{"ports 0"}, false)
testParsing([]string{"ports 0 1"}, false)
testParsing([]string{"ports -1"}, false)
testParsing([]string{"ports hi!"}, false)
testParsing([]string{"ports 11, 122, 33"}, false)
testParsing([]string{"ports 11, 122, 33"}, false)
testParsing([]string{"ports 11111 99999"}, false)
testParsing([]string{"ports 11 12"}, true)
testParsing([]string{"ports 1"}, true)
testParsing([]string{"ports 1 11 111 332 324 6546 33333"}, true)
testParsing([]string{"ports 1 11 111 332 324 6546 33333", "ports 1 11 111 332 324 6546 33333"}, false)
testParsing([]string{"ports 1", "ports 2"}, false)
testParsing([]string{"hide_ip"}, true)
testParsing([]string{"hide_ip 0"}, false)
testParsing([]string{"hide_ip 0 1"}, false)
testParsing([]string{"hide_via"}, true)
testParsing([]string{"hide_via 0"}, false)
testParsing([]string{"hide_via 0 1"}, false)
testParsing([]string{"probe_resistance"}, false)
testParsing([]string{"probe_resistance local.host"}, false)
testParsing([]string{"probe_resistance local.host very.local.host"}, false)
testParsing([]string{"probe_resistance", "basicauth john doe"}, true)
testParsing([]string{"probe_resistance local.host", "basicauth john doe"}, true)
testParsing([]string{"probe_resistance local.host very.local.host", "basicauth john doe"}, false)
testParsing([]string{"serve_pac"}, true)
testParsing([]string{"serve_pac \"\""}, true)
testParsing([]string{"serve_pac proxyautoconfig.pac"}, true)
testParsing([]string{"serve_pac 1.pac 2.pac"}, false)
testParsing([]string{"response_timeout"}, false)
testParsing([]string{"response_timeout -1"}, false)
testParsing([]string{"response_timeout 1 2"}, false)
testParsing([]string{"response_timeout seven"}, false)
testParsing([]string{"response_timeout 2"}, true)
testParsing([]string{"dial_timeout"}, false)
testParsing([]string{"dial_timeout -1"}, false)
testParsing([]string{"dial_timeout 1 2"}, false)
testParsing([]string{"dial_timeout seven"}, false)
testParsing([]string{"dial_timeout 2"}, true)
testParsing([]string{"upstream proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site https://proxy.site"}, false)
testParsing([]string{"upstream http://localhost:1230"}, true)
testParsing([]string{"upstream socks5://127.0.0.1:999"}, true)
testParsing([]string{"upstream http://proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site https://proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site"}, true)
testParsing([]string{"upstream https://caddyserver.com", "acl {\nallow all\n}"}, false)
testParsing([]string{"upstream https://caddyserver.com", "ports 123"}, false)
testParsing([]string{"upstream https://username:password@caddyserver.com", "ports 123"}, false)
testParsing([]string{"upstream https://username:password@caddyserver.com:90", "ports 123"}, false)
testParsing([]string{"acl {\nallow all\n}"}, true)
testParsing([]string{"acl {\nallow localhost 128.32.22.1/32 1.1.1.1 caddyserver.com\n deny all\n}"}, true)
testParsing([]string{"acl {\nallowfile test/parseable_acl.txt\n}"}, true)
testParsing([]string{"acl {\ndenyfile test/parseable_acl.txt\n}"}, true)
testParsing([]string{"acl {\nallowfile test/unparseable_acl.txt\n}"}, false)
testParsing([]string{"acl {\ndenyfile test/unparseable_acl.txt\n}"}, false)
//testParsing([]string{"acl {\nallow all\n"}, false) // doesn't fail, but should: caddy itself doesn't demand curly brace to be closed
testParsing([]string{"acl {\nallow all\n", "serve_pac"}, false) // this does fail
testParsing([]string{"acl \nallow all\n}"}, false)
testParsing([]string{"acl {allow all\n}"}, false)
//testParsing([]string{"acl {\nallow all}"}, false) // '}' is not on the next line, "all}" parses as regexp
}