commit
a09e685335
22 changed files with 2633 additions and 1846 deletions
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal 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
61
.github/workflows/lint.yml
vendored
Normal 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
35
.github/workflows/tests.yml
vendored
Normal 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
168
.golanci-lint.yml
Normal 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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# golangci linter currently requires a config file to
|
||||
# disable checking of the test files
|
||||
run:
|
||||
tests: false
|
||||
16
.travis.yml
16
.travis.yml
|
|
@ -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
|
||||
123
README.md
123
README.md
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[](https://travis-ci.org/caddyserver/forwardproxy)
|
||||
[](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,48 +88,53 @@ 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
|
||||
: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
|
||||
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_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
|
||||
}
|
||||
```
|
||||
|
||||
(The square brackets `[ ]` indicate values you should replace; do not actually include the brackets.)
|
||||
|
||||
##### 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.
|
||||
|
|
@ -91,9 +170,9 @@ 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**
|
||||
- **allow_file /path/to/whitelist.txt**
|
||||
- **deny [ip or subnet or hostname] [ip or subnet or hostname]...**
|
||||
- **denyfile /path/to/blacklist.txt**
|
||||
- **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:
|
||||
|
|
@ -105,7 +184,7 @@ acl_directive may be:
|
|||
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.
|
||||
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:_
|
||||
|
|
@ -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,9 +221,9 @@ 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
|
||||
go install github.com/caddyserver/forwardproxy/cmd/caddy@latest
|
||||
```
|
||||
Built `caddy` binary will be stored in $GOPATH/bin.
|
||||
|
||||
|
|
|
|||
36
acl.go
36
acl.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
69
acl_test.go
69
acl_test.go
|
|
@ -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
187
caddyfile.go
Normal 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
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
_ "github.com/caddyserver/forwardproxy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
caddymain.EnableTelemetry = false
|
||||
caddymain.Run()
|
||||
}
|
||||
497
common_test.go
497
common_test.go
|
|
@ -1,26 +1,30 @@
|
|||
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{
|
||||
var (
|
||||
credentialsEmpty = ""
|
||||
credentialsCorrectPlain = "test:pass"
|
||||
credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass
|
||||
credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass
|
||||
credentialsWrong = []string{
|
||||
"",
|
||||
"\"\"",
|
||||
"Basic dzp3",
|
||||
|
|
@ -29,6 +33,7 @@ var credentialsWrong = []string{
|
|||
"Tssssssss",
|
||||
"Basic dpz3 asp",
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
Test naming: Test{httpVer}Proxy{Method}{Auth}{Credentials}{httpVer}
|
||||
|
|
@ -36,27 +41,31 @@ 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{
|
||||
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
|
||||
|
||||
httpRedirPort string // used in probe-resist tests to simulate default Caddy's http->https redirect
|
||||
|
||||
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
|
||||
_ []string
|
||||
proxyHandler *Handler
|
||||
contents map[string][]byte
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
862
forwardproxy.go
862
forwardproxy.go
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
||||
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
118
go.mod
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
409
setup.go
|
|
@ -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)
|
||||
}
|
||||
139
setup_test.go
139
setup_test.go
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue