dep: add go-http-tunnel

This commit is contained in:
Cadey Ratio 2017-10-26 08:46:27 -07:00
parent e8dbca1d99
commit 00819e8dc1
No known key found for this signature in database
GPG Key ID: D607EE27C2E7F89A
56 changed files with 4483 additions and 1 deletions

14
Gopkg.lock generated
View File

@ -67,6 +67,12 @@
revision = "0cf029d5748c52beb2c9d20c81880cb4bdf8f788"
version = "v3.0"
[[projects]]
name = "github.com/calmh/luhn"
packages = ["."]
revision = "5b2abb343e70180dbf456397c5fd93f14471b08e"
version = "v2.0.0"
[[projects]]
branch = "master"
name = "github.com/dgryski/go-failure"
@ -145,6 +151,12 @@
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
branch = "master"
name = "github.com/mmatczuk/go-http-tunnel"
packages = [".","id","log","proto"]
revision = "fd3fa5dce50b3a3cd5e2ed54e53230eb79ac8ca9"
[[projects]]
branch = "master"
name = "github.com/mtneug/pkg"
@ -256,6 +268,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e36c7dd8cc83fc16b10af5a8e3cfd768a3da534480cff4e0384ca1f3b4d050f5"
inputs-digest = "de62ccc691dac957d2f25dd25caa9a5fb889e4260029ad3dd69e8660e424d3b1"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -128,3 +128,7 @@
[[constraint]]
name = "gopkg.in/alecthomas/kingpin.v2"
version = "2.2.5"
[[constraint]]
branch = "master"
name = "github.com/mmatczuk/go-http-tunnel"

19
vendor/github.com/calmh/luhn/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (C) 2014 Jakob Borg
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
vendor/github.com/calmh/luhn/luhn.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
// Copyright (C) 2014 Jakob Borg
// Package luhn generates and validates Luhn mod N check digits.
package luhn
import (
"fmt"
"strings"
)
// An alphabet is a string of N characters, representing the digits of a given
// base N.
type Alphabet string
var (
Base32 Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
)
// Generate returns a check digit for the string s, which should be composed
// of characters from the Alphabet a.
func (a Alphabet) Generate(s string) (rune, error) {
factor := 1
if len(s)%2 == 1 {
factor = 2
}
sum := 0
n := len(a)
for i := range s {
codepoint := strings.IndexByte(string(a), s[i])
if codepoint == -1 {
return 0, fmt.Errorf("Digit %q not valid in alphabet %q", s[i], a)
}
addend := factor * codepoint
if factor == 2 {
factor = 1
} else {
factor = 2
}
addend = (addend / n) + (addend % n)
sum += addend
}
remainder := sum % n
checkCodepoint := (n - remainder) % n
return rune(a[checkCodepoint]), nil
}
// Validate returns true if the last character of the string s is correct, for
// a string s composed of characters in the alphabet a.
func (a Alphabet) Validate(s string) bool {
t := s[:len(s)-1]
c, err := a.Generate(t)
if err != nil {
return false
}
return rune(s[len(s)-1]) == c
}
// NewAlphabet converts the given string an an Alphabet, verifying that it
// is correct.
func NewAlphabet(s string) (Alphabet, error) {
cm := make(map[byte]bool, len(s))
for i := range s {
if cm[s[i]] {
return "", fmt.Errorf("Digit %q non-unique in alphabet %q", s[i], s)
}
cm[s[i]] = true
}
return Alphabet(s), nil
}

77
vendor/github.com/calmh/luhn/luhn_test.go generated vendored Normal file
View File

@ -0,0 +1,77 @@
// Copyright (C) 2014 Jakob Borg
package luhn_test
import (
"testing"
"github.com/calmh/luhn"
)
func TestGenerate(t *testing.T) {
// Base 6 Luhn
a := luhn.Alphabet("abcdef")
c, err := a.Generate("abcdef")
if err != nil {
t.Fatal(err)
}
if c != 'e' {
t.Errorf("Incorrect check digit %c != e", c)
}
// Base 10 Luhn
a = luhn.Alphabet("0123456789")
c, err = a.Generate("7992739871")
if err != nil {
t.Fatal(err)
}
if c != '3' {
t.Errorf("Incorrect check digit %c != 3", c)
}
}
func TestInvalidString(t *testing.T) {
a := luhn.Alphabet("ABC")
_, err := a.Generate("7992739871")
t.Log(err)
if err == nil {
t.Error("Unexpected nil error")
}
}
func TestBadAlphabet(t *testing.T) {
_, err := luhn.NewAlphabet("01234566789")
t.Log(err)
if err == nil {
t.Error("Unexpected nil error")
}
}
func TestValidate(t *testing.T) {
a := luhn.Alphabet("abcdef")
if !a.Validate("abcdefe") {
t.Errorf("Incorrect validation response for abcdefe")
}
if a.Validate("abcdefd") {
t.Errorf("Incorrect validation response for abcdefd")
}
}
func TestValidateRosetta(t *testing.T) {
// http://rosettacode.org/wiki/Luhn_test_of_credit_card_numbers
a := luhn.Alphabet("0123456789")
cases := []struct {
v string
ok bool
}{
{"49927398716", true},
{"49927398717", false},
{"1234567812345678", false},
{"1234567812345670", true},
}
for _, tc := range cases {
if res := a.Validate(tc.v); res != tc.ok {
t.Errorf("Validate(%q) => %v, expected %v", tc.v, res, tc.ok)
}
}
}

View File

@ -0,0 +1,14 @@
; http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 8

24
vendor/github.com/mmatczuk/go-http-tunnel/.gitignore generated vendored Normal file
View File

@ -0,0 +1,24 @@
### Release
build
### Go
*.cov
*.prof
*.mprof
vendor
### IntelliJ
.idea/
*.iml
### Vim
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags

15
vendor/github.com/mmatczuk/go-http-tunnel/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,15 @@
language: go
go:
- 1.x
addons:
apt:
packages:
- moreutils
install:
- make get-tools
- make get-deps
script:
- make

45
vendor/github.com/mmatczuk/go-http-tunnel/Gopkg.lock generated vendored Normal file
View File

@ -0,0 +1,45 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/calmh/luhn"
packages = ["."]
revision = "5b2abb343e70180dbf456397c5fd93f14471b08e"
version = "v2.0.0"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
[[projects]]
name = "github.com/golang/mock"
packages = ["gomock"]
revision = "13f360950a79f5864a972c786a10a50e44b69541"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","http2","http2/hpack","idna","lex/httplex"]
revision = "0a9397675ba34b2845f758fe3cd68828369c6517"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
revision = "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "5f9ce2cfc77119828bda8371ed4da5c6a78ad469287571a166b9f412fecb73ae"
solver-name = "gps-cdcl"
solver-version = 1

46
vendor/github.com/mmatczuk/go-http-tunnel/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,46 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/calmh/luhn"
version = "2.0.0"
[[constraint]]
name = "github.com/cenkalti/backoff"
version = "1.1.0"
[[constraint]]
name = "github.com/golang/mock"
version = "1.0.0"
[[constraint]]
name = "github.com/google/gops"
version = "0.3.2"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"

27
vendor/github.com/mmatczuk/go-http-tunnel/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2017 Michał Matczuk. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

84
vendor/github.com/mmatczuk/go-http-tunnel/Makefile generated vendored Normal file
View File

@ -0,0 +1,84 @@
all: clean check test
.PHONY: clean
clean:
@go clean -r
.PHONY: check
check: .check-fmt .check-vet .check-lint .check-ineffassign .check-mega .check-misspell
.PHONY: .check-fmt
.check-fmt:
@go fmt ./... | tee /dev/stderr | ifne false
.PHONY: .check-vet
.check-vet:
@go vet ./...
.PHONY: .check-lint
.check-lint:
@golint `go list ./...` \
| grep -v /id/ \
| grep -v /tunnelmock/ \
| tee /dev/stderr | ifne false
.PHONY: .check-ineffassign
.check-ineffassign:
@ineffassign ./
.PHONY: .check-misspell
.check-misspell:
@misspell ./...
.PHONY: .check-mega
.check-mega:
@megacheck ./...
.PHONY: test
test:
@echo "==> Running tests (race)..."
@go test -cover -race ./...
.PHONY: get-deps
get-deps:
@echo "==> Installing dependencies..."
@dep ensure
.PHONY: get-tools
get-tools:
@echo "==> Installing tools..."
@go get -u github.com/golang/dep/cmd/dep
@go get -u github.com/golang/lint/golint
@go get -u github.com/golang/mock/gomock
@go get -u github.com/client9/misspell/cmd/misspell
@go get -u github.com/gordonklaus/ineffassign
@go get -u github.com/mitchellh/gox
@go get -u github.com/tcnksm/ghr
@go get -u honnef.co/go/tools/cmd/megacheck
#OUTPUT_DIR = build
#OS = "darwin freebsd linux windows"
#ARCH = "amd64 arm"
#OSARCH = "!darwin/arm !windows/arm"
#GIT_COMMIT = $(shell git describe --always)
#
#.PHONY: release
#release: check test clean build package
#
#.PHONY: build
#build:
# mkdir ${OUTPUT_DIR}
# GOARM=5 gox -ldflags "-X main.version=$(GIT_COMMIT)" \
# -os=${OS} -arch=${ARCH} -osarch=${OSARCH} -output "${OUTPUT_DIR}/pkg/{{.OS}}_{{.Arch}}/{{.Dir}}" \
# ./cmd/tunnel ./cmd/tunneld
#
#.PHONY: package
#package:
# mkdir ${OUTPUT_DIR}/dist
# cd ${OUTPUT_DIR}/pkg/; for osarch in *; do (cd $$osarch; tar zcvf ../../dist/tunnel_$$osarch.tar.gz ./*); done;
# cd ${OUTPUT_DIR}/dist; sha256sum * > ./SHA256SUMS
#
#.PHONY: publish
#publish:
# ghr -recreate -u mmatczuk -t ${GITHUB_TOKEN} -r go-http-tunnel pre-release ${OUTPUT_DIR}/dist

131
vendor/github.com/mmatczuk/go-http-tunnel/README.md generated vendored Normal file
View File

@ -0,0 +1,131 @@
# Tunnel [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/mmatczuk/go-http-tunnel) [![Go Report Card](https://goreportcard.com/badge/github.com/mmatczuk/go-http-tunnel)](https://goreportcard.com/report/github.com/mmatczuk/go-http-tunnel) [![Build Status](http://img.shields.io/travis/mmatczuk/go-http-tunnel.svg?style=flat-square)](https://travis-ci.org/mmatczuk/go-http-tunnel.svg?branch=master)
Tunnel is fast and secure client/server package that enables proxying public connections to your local machine over a tunnel connection from the local machine to the public server. **It enables you to share your localhost when you don't have a public IP or you are hidden by a firewall**.
It can help you:
* Demo without deploying
* Simplify mobile device testing
* Build webhook integrations with ease
* Run personal cloud services from your own private network
It is based on HTTP/2 for speed and security. Server accepts TLS connection from known clients, client is recognised by it's TLS certificate id. Server can protect HTTP tunnels with [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
## Installation
Download latest release from [here](https://github.com/mmatczuk/go-http-tunnel/releases/latest). The release contains two executables:
* `tunneld` - the tunnel server, to be run on publicly available host like AWS or GCE
* `tunnel` - the tunnel client, to be run on your local machine or in your private network
To get help on the command parameters run `tunneld -h` or `tunnel -h`.
## Configuration
The tunnel client `tunnel` requires configuration file, by default it will try reading `tunnel.yml` in your current working directory. If you want to specify other file use `-config` flag.
Sample configuration that exposes:
* `localhost:8080` as `webui.my-tunnel-host.com`
* host in private network for ssh connections
looks like this
```yaml
server_addr: SERVER_IP:4443
insecure_skip_verify: true
tunnels:
webui:
proto: http
addr: localhost:8080
auth: user:password
host: webui.my-tunnel-host.com
ssh:
proto: tcp
addr: 192.168.0.5:22
remote_addr: 0.0.0.0:22
```
Configuration options:
* `server_addr`: server TCP address, i.e. `54.12.12.45:4443`
* `insecure_skip_verify`: controls whether a client verifies the server's certificate chain and host name, if using self signed certificates must be set to `true`, *default:* `false`
* `tls_crt`: path to client TLS certificate, *default:* `client.crt` *in the config file directory*
* `tls_key`: path to client TLS certificate key, *default:* `client.key` *in the config file directory*
* `tunnels / [name]`
* `proto`: tunnel protocol, `http` or `tcp`
* `addr`: forward traffic to this local port number or network address, for `proto=http` this can be full URL i.e. `https://machine/sub/path/?plus=params`, supports URL schemes `http` and `https`
* `auth`: (`proto=http`) (optional) basic authentication credentials to enforce on tunneled requests, format `user:password`
* `host`: (`proto=http`) hostname to request (requires reserved name and DNS CNAME)
* `remote_addr`: (`proto=tcp`) bind the remote TCP address
* `backoff`
* `interval`: how long client would wait before redialing the server if connection was lost, exponential backoff initial interval, *default:* `500ms`
* `multiplier`: interval multiplier if reconnect failed, *default:* `1.5`
* `max_interval`: maximal time client would wait before redialing the server, *default:* `1m`
* `max_time`: maximal time client would try to reconnect to the server if connection was lost, set `0` to never stop trying, *default:* `15m`
## Running
Tunnel requires TLS certificates for both client and server.
```bash
$ openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout client.key -out client.crt
$ openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout server.key -out server.crt
```
Run client:
* Install `tunnel` binary
* Make `.tunnel` directory in your project directory
* Copy `client.key`, `client.crt` to `.tunnel`
* Create configuration file `tunnel.yml` in `.tunnel`
* Start all tunnels
```bash
$ tunnel -config ./tunnel/tunnel.yml start-all
```
Run server:
* Install `tunneld` binary
* Make `.tunneld` directory
* Copy `server.key`, `server.crt` to `.tunneld`
* Get client identifier (`tunnel -config ./tunnel/tunnel.yml id`), identifier should look like this `YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4`
* Start tunnel server
```bash
$ tunneld -tlsCrt .tunneld/server.crt -tlsKey .tunneld/server.key -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4
```
This will run HTTP server on port `80` and HTTPS (HTTP/2) server on port `443`. If you want to use HTTPS it's recommended to get a properly signed certificate to avoid security warnings.
## Using as a library
Install the package:
```bash
$ go get -u github.com/mmatczuk/go-http-tunnel
```
The `tunnel` package is designed to be simple, extensible, with little dependencies. It is based on HTTP/2 for client server connectivity, this avoids usage of third party tools for multiplexing tunneled connections. HTTP/2 is faster, more stable and much more tested then any other multiplexing technology. You may see [benchmark](benchmark) comparing the `tunnel` package to a koding tunnel.
The `tunnel` package:
* custom dialer and listener for `Client` and `Server`
* easy modifications of HTTP proxy (based on [ReverseProxy](https://golang.org/pkg/net/http/httputil/#ReverseProxy))
* proxy anything, [ProxyFunc](https://godoc.org/github.com/mmatczuk/go-http-tunnel#ProxyFunc) architecture
* structured logs with go-kit compatible minimal logger interface
See:
* [ClientConfig](https://godoc.org/github.com/mmatczuk/go-http-tunnel#ClientConfig)
* [ServerConfig](https://godoc.org/github.com/mmatczuk/go-http-tunnel#ServerConfig)
* [ControlMessage](https://godoc.org/github.com/mmatczuk/go-http-tunnel/proto#ControlMessage)
## License
Copyright (C) 2017 Michał Matczuk
This project is distributed under the BSD-3 license. See the [LICENSE](https://github.com/mmatczuk/go-http-tunnel/blob/master/LICENSE) file for details.
GitHub star is always appreciated!

30
vendor/github.com/mmatczuk/go-http-tunnel/auth.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import "strings"
// Auth holds user and password.
type Auth struct {
User string
Password string
}
// NewAuth creates new auth from string representation "user:password".
func NewAuth(auth string) *Auth {
if auth == "" {
return nil
}
s := strings.SplitN(auth, ":", 2)
a := &Auth{
User: s[0],
}
if len(s) > 1 {
a.Password = s[1]
}
return a
}

28
vendor/github.com/mmatczuk/go-http-tunnel/auth_test.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"reflect"
"testing"
)
func TestNewAuth(t *testing.T) {
tests := []struct {
actual string
expected *Auth
}{
{"", nil},
{"user", &Auth{User: "user"}},
{"user:password", &Auth{User: "user", Password: "password"}},
{"user:pass:word", &Auth{User: "user", Password: "pass:word"}},
}
for _, tt := range tests {
if !reflect.DeepEqual(NewAuth(tt.actual), tt.expected) {
t.Errorf("Invalid auth for %s", tt.actual)
}
}
}

19
vendor/github.com/mmatczuk/go-http-tunnel/backoff.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import "time"
// Backoff defines behavior of staggering reconnection retries.
type Backoff interface {
// Next returns the duration to sleep before retrying to reconnect.
// If the returned value is negative, the retry is aborted.
NextBackOff() time.Duration
// Reset is used to signal a reconnection was successful and next
// call to Next should return desired time duration for 1st reconnection
// attempt.
Reset()
}

View File

@ -0,0 +1,50 @@
# Benchmark report
The benchmark compares [tunnel](https://github.com/mmatczuk/go-http-tunnel) to [koding tunnel](https://github.com/koding/tunnel) on serving 184 midsized files that were gathered by saving `amazon.com` for offline view. The data set consists of images and text data (js, css, html). On start client loads the files into memory and act as a file server.
The diagrams were rendered using [hdrhistogram](http://hdrhistogram.github.io/HdrHistogram/plotFiles.html) and the input files were generated with help of [github.com/codahale/hdrhistogram](https://github.com/codahale/hdrhistogram) library. The vegeta raw results were corrected for stalls using [hdr correction method](https://godoc.org/github.com/codahale/hdrhistogram#Histogram.RecordCorrectedValue).
## Environment
Tests were done on four AWS `t2.micro` instances. An instance for client, an instance for server and two instances for load generator. For load generation we used [vegeta](https://github.com/tsenart/vegeta) in distributed mode. On all machines open files limit (`ulimit -n`) was increased to `20000`.
## Load spike
This test compares performance on two minute load spikes. tunnel handles 900 req/sec without dropping a message while preserving good latency. At 1000 req/sec tunnel still works but drops 0,20% requests and latency is much worse. Koding tunnel is faster at 800 req/sec, but at higher request rates latency degrades giving maximum values of 1.65s at 900 req/sec and 23.50s at 1000 req/sec (with 5% error rate).
![](spike.png)
Detailed results of load spike test.
| Impl. | Req/sec | Success rate | P99 (corrected)| Max |
|-------:| -------:|-------------:| --------------:| --------------:|
| tunnel | 600 | 100% | 40.079103ms | 147.310766ms |
| tunnel | 800 | 100% | 161.093631ms | 308.993573ms |
| tunnel | 900 | 100% | 172.114943ms | 376.924512ms |
| tunnel | 1000 | 99.90% | 793.423871ms | 1228.133135ms |
| koding | 600 | 100% | 43.161855ms | 173.871604ms |
| koding | 800 | 100% | 53.311743ms | 180.344454ms |
| koding | 900 | 100% | 1003.495423ms | 1648.814589ms |
| koding | 1000 | 94.95% | 16081.551359ms | 23494.866864ms |
## Constant pressure
This test compares performance on twenty minutes constant pressure runs. tunnel shows ability to trade latency for throughput. It runs fine at 300 req/sec but at higher request rates we observe poor latency and some message drops. Koding tunnel has acceptable performance at 300 req/sec, however, with increased load it just breaks.
Both implementations have a connection (or memory) leak when dealing with too high loads. This results in process (or machine) crash as machine runs out of memory. It's 100% reproducible, when process crashes it has few hundred thousands go routines waiting on select in a connection and memory full of connection buffers.
![](constload.png)
Detailed results of constant pressure test.
| Impl. | Req/sec | Success rate | P99 (corrected)| Max |
|-------:| -------:|-------------:| --------------:| --------------:|
| tunnel | 300 | 100% | 16.614527ms | 199.479958ms |
| tunnel | 400 | 99.98% | 1175.904255 | 1568.012326ms |
| tunnel | 500 | 99.96% | 1457.364991ms | 1917.406792ms |
| koding | 300 | 100% | 66.436607ms | 354.531247ms |
| koding | 400 | 82.66% | - | - |
| koding | 500 | 63.16% | - | - |

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

314
vendor/github.com/mmatczuk/go-http-tunnel/client.go generated vendored Normal file
View File

@ -0,0 +1,314 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"sync"
"time"
"golang.org/x/net/http2"
"github.com/mmatczuk/go-http-tunnel/log"
"github.com/mmatczuk/go-http-tunnel/proto"
)
var (
// DefaultTimeout specifies general purpose timeout.
DefaultTimeout = 10 * time.Second
)
// ClientConfig is configuration of the Client.
type ClientConfig struct {
// ServerAddr specifies TCP address of the tunnel server.
ServerAddr string
// TLSClientConfig specifies the tls configuration to use with
// tls.Client.
TLSClientConfig *tls.Config
// DialTLS specifies an optional dial function that creates a tls
// connection to the server. If DialTLS is nil, tls.Dial is used.
DialTLS func(network, addr string, config *tls.Config) (net.Conn, error)
// Backoff specifies backoff policy on server connection retry. If nil
// when dial fails it will not be retried.
Backoff Backoff
// Tunnels specifies the tunnels client requests to be opened on server.
Tunnels map[string]*proto.Tunnel
// Proxy is ProxyFunc responsible for transferring data between server
// and local services.
Proxy ProxyFunc
// Logger is optional logger. If nil logging is disabled.
Logger log.Logger
}
// Client is responsible for creating connection to the server, handling control
// messages. It uses ProxyFunc for transferring data between server and local
// services.
type Client struct {
config *ClientConfig
conn net.Conn
connMu sync.Mutex
httpServer *http2.Server
serverErr error
lastDisconnect time.Time
logger log.Logger
}
// NewClient creates a new unconnected Client based on configuration. Caller
// must invoke Start() on returned instance in order to connect server.
func NewClient(config *ClientConfig) *Client {
if config.ServerAddr == "" {
panic("missing ServerAddr")
}
if config.TLSClientConfig == nil {
panic("missing TLSClientConfig")
}
if config.Tunnels == nil || len(config.Tunnels) == 0 {
panic("missing Tunnels")
}
if config.Proxy == nil {
panic("missing Proxy")
}
logger := config.Logger
if logger == nil {
logger = log.NewNopLogger()
}
c := &Client{
config: config,
httpServer: &http2.Server{},
logger: logger,
}
return c
}
// Start connects client to the server, it returns error if there is a
// connection error, or server cannot open requested tunnels. On connection
// error a backoff policy is used to reestablish the connection. When connected
// HTTP/2 server is started to handle ControlMessages.
func (c *Client) Start() error {
c.logger.Log(
"level", 1,
"action", "start",
)
for {
conn, err := c.connect()
if err != nil {
return err
}
c.httpServer.ServeConn(conn, &http2.ServeConnOpts{
Handler: http.HandlerFunc(c.serveHTTP),
})
c.logger.Log(
"level", 1,
"action", "disconnected",
)
c.connMu.Lock()
now := time.Now()
err = c.serverErr
// detect disconnect hiccup
if err == nil && now.Sub(c.lastDisconnect).Seconds() < 5 {
err = fmt.Errorf("connection is being cut")
}
c.conn = nil
c.serverErr = nil
c.lastDisconnect = now
c.connMu.Unlock()
if err != nil {
return err
}
}
}
func (c *Client) connect() (net.Conn, error) {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.conn != nil {
return nil, fmt.Errorf("already connected")
}
conn, err := c.dial()
if err != nil {
return nil, fmt.Errorf("failed to connect to server: %s", err)
}
c.conn = conn
return conn, nil
}
func (c *Client) dial() (net.Conn, error) {
var (
network = "tcp"
addr = c.config.ServerAddr
tlsConfig = c.config.TLSClientConfig
)
doDial := func() (conn net.Conn, err error) {
c.logger.Log(
"level", 1,
"action", "dial",
"network", network,
"addr", addr,
)
if c.config.DialTLS != nil {
conn, err = c.config.DialTLS(network, addr, tlsConfig)
} else {
conn, err = tls.DialWithDialer(
&net.Dialer{Timeout: DefaultTimeout},
network, addr, tlsConfig,
)
}
if err != nil {
c.logger.Log(
"level", 0,
"msg", "dial failed",
"network", network,
"addr", addr,
"err", err,
)
}
return
}
b := c.config.Backoff
if b == nil {
return doDial()
}
for {
conn, err := doDial()
// success
if err == nil {
b.Reset()
return conn, err
}
// failure
d := b.NextBackOff()
if d < 0 {
return conn, fmt.Errorf("backoff limit exeded: %s", err)
}
// backoff
c.logger.Log(
"level", 1,
"action", "backoff",
"sleep", d,
)
time.Sleep(d)
}
}
func (c *Client) serveHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
if r.Header.Get(proto.HeaderError) != "" {
c.handleHandshakeError(w, r)
} else {
c.handleHandshake(w, r)
}
return
}
msg, err := proto.ReadControlMessage(r.Header)
if err != nil {
c.logger.Log(
"level", 1,
"err", err,
)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.logger.Log(
"level", 2,
"action", "handle",
"ctrlMsg", msg,
)
switch msg.Action {
case proto.ActionProxy:
c.config.Proxy(w, r.Body, msg)
default:
c.logger.Log(
"level", 0,
"msg", "unknown action",
"ctrlMsg", msg,
)
http.Error(w, err.Error(), http.StatusBadRequest)
}
c.logger.Log(
"level", 2,
"action", "done",
"ctrlMsg", msg,
)
}
func (c *Client) handleHandshakeError(w http.ResponseWriter, r *http.Request) {
err := fmt.Errorf(r.Header.Get(proto.HeaderError))
c.logger.Log(
"level", 1,
"action", "handshake error",
"addr", r.RemoteAddr,
"err", err,
)
c.connMu.Lock()
c.serverErr = fmt.Errorf("server error: %s", err)
c.connMu.Unlock()
}
func (c *Client) handleHandshake(w http.ResponseWriter, r *http.Request) {
c.logger.Log(
"level", 1,
"action", "handshake",
"addr", r.RemoteAddr,
)
w.WriteHeader(http.StatusOK)
b, err := json.Marshal(c.config.Tunnels)
if err != nil {
c.logger.Log(
"level", 0,
"msg", "handshake failed",
"err", err,
)
return
}
w.Write(b)
}
// Stop disconnects client from server.
func (c *Client) Stop() {
c.connMu.Lock()
defer c.connMu.Unlock()
c.logger.Log(
"level", 1,
"action", "stop",
)
if c.conn != nil {
c.conn.Close()
}
c.conn = nil
}

View File

@ -0,0 +1,81 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"crypto/tls"
"errors"
"net"
"net/http/httptest"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/mmatczuk/go-http-tunnel/proto"
"github.com/mmatczuk/go-http-tunnel/tunnelmock"
)
func TestClient_Dial(t *testing.T) {
t.Parallel()
s := httptest.NewTLSServer(nil)
defer s.Close()
c := NewClient(&ClientConfig{
ServerAddr: s.Listener.Addr().String(),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Tunnels: map[string]*proto.Tunnel{"test": {}},
Proxy: Proxy(ProxyFuncs{}),
})
conn, err := c.dial()
if err != nil {
t.Fatal("Dial error", err)
}
if conn == nil {
t.Fatal("Expected connection", err)
}
conn.Close()
}
func TestClient_DialBackoff(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
b := tunnelmock.NewMockBackoff(ctrl)
gomock.InOrder(
b.EXPECT().NextBackOff().Return(50*time.Millisecond).Times(2),
b.EXPECT().NextBackOff().Return(-time.Millisecond),
)
d := func(network, addr string, config *tls.Config) (net.Conn, error) {
return nil, errors.New("foobar")
}
c := NewClient(&ClientConfig{
ServerAddr: "8.8.8.8",
TLSClientConfig: &tls.Config{},
DialTLS: d,
Backoff: b,
Tunnels: map[string]*proto.Tunnel{"test": {}},
Proxy: Proxy(ProxyFuncs{}),
})
start := time.Now()
_, err := c.dial()
end := time.Now()
if end.Sub(start) < 100*time.Millisecond {
t.Fatal("Wait mismatch", err)
}
if err.Error() != "backoff limit exeded: foobar" {
t.Fatal("Error mismatch", err)
}
}

View File

@ -0,0 +1,156 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"io/ioutil"
"path/filepath"
"time"
"gopkg.in/yaml.v2"
"github.com/mmatczuk/go-http-tunnel/proto"
)
type backoffConfig struct {
InitialInterval time.Duration `yaml:"interval,omitempty"`
Multiplier float64 `yaml:"multiplier,omitempty"`
MaxInterval time.Duration `yaml:"max_interval,omitempty"`
MaxElapsedTime time.Duration `yaml:"max_time,omitempty"`
}
type tunnelConfig struct {
Protocol string `yaml:"proto,omitempty"`
Addr string `yaml:"addr,omitempty"`
Auth string `yaml:"auth,omitempty"`
Host string `yaml:"host,omitempty"`
RemoteAddr string `yaml:"remote_addr,omitempty"`
}
type config struct {
ServerAddr string `yaml:"server_addr,omitempty"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"`
TLSCrt string `yaml:"tls_crt,omitempty"`
TLSKey string `yaml:"tls_key,omitempty"`
Backoff *backoffConfig `yaml:"backoff,omitempty"`
Tunnels map[string]*tunnelConfig `yaml:"tunnels,omitempty"`
}
var defaultBackoffConfig = backoffConfig{
InitialInterval: 500 * time.Millisecond,
Multiplier: 1.5,
MaxInterval: 60 * time.Second,
MaxElapsedTime: 15 * time.Minute,
}
func loadConfiguration(path string) (*config, error) {
configBuf, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file %q: %s", path, err)
}
// deserialize/parse the config
var config config
if err = yaml.Unmarshal(configBuf, &config); err != nil {
return nil, fmt.Errorf("failed to parse file %q: %s", path, err)
}
// set default values
if config.TLSCrt == "" {
config.TLSCrt = filepath.Join(filepath.Dir(path), "client.crt")
}
if config.TLSKey == "" {
config.TLSKey = filepath.Join(filepath.Dir(path), "client.key")
}
if config.Backoff == nil {
config.Backoff = &defaultBackoffConfig
} else {
if config.Backoff.InitialInterval == 0 {
config.Backoff.InitialInterval = defaultBackoffConfig.InitialInterval
}
if config.Backoff.Multiplier == 0 {
config.Backoff.Multiplier = defaultBackoffConfig.Multiplier
}
if config.Backoff.MaxInterval == 0 {
config.Backoff.MaxInterval = defaultBackoffConfig.MaxInterval
}
if config.Backoff.MaxElapsedTime == 0 {
config.Backoff.MaxElapsedTime = defaultBackoffConfig.MaxElapsedTime
}
}
// validate and normalize configuration
if config.ServerAddr == "" {
return nil, fmt.Errorf("server_addr: missing")
}
if config.ServerAddr, err = normalizeAddress(config.ServerAddr); err != nil {
return nil, fmt.Errorf("server_addr: %s", err)
}
for name, t := range config.Tunnels {
switch t.Protocol {
case proto.HTTP:
if err := validateHTTP(t); err != nil {
return nil, fmt.Errorf("%s %s", name, err)
}
case proto.TCP, proto.TCP4, proto.TCP6:
if err := validateTCP(t); err != nil {
return nil, fmt.Errorf("%s %s", name, err)
}
default:
return nil, fmt.Errorf("%s invalid protocol %q", name, t.Protocol)
}
}
return &config, nil
}
func validateHTTP(t *tunnelConfig) error {
var err error
if t.Host == "" {
return fmt.Errorf("host: missing")
}
if t.Addr == "" {
return fmt.Errorf("addr: missing")
}
if t.Addr, err = normalizeURL(t.Addr); err != nil {
return fmt.Errorf("addr: %s", err)
}
// unexpected
if t.RemoteAddr != "" {
return fmt.Errorf("remote_addr: unexpected")
}
return nil
}
func validateTCP(t *tunnelConfig) error {
var err error
if t.RemoteAddr, err = normalizeAddress(t.RemoteAddr); err != nil {
return fmt.Errorf("remote_addr: %s", err)
}
if t.Addr == "" {
return fmt.Errorf("addr: missing")
}
if t.Addr, err = normalizeAddress(t.Addr); err != nil {
return fmt.Errorf("addr: %s", err)
}
// unexpected
if t.Host != "" {
return fmt.Errorf("host: unexpected")
}
if t.Auth != "" {
return fmt.Errorf("auth: unexpected")
}
return nil
}

View File

@ -0,0 +1,56 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"net"
"net/url"
"strconv"
"strings"
)
func normalizeAddress(addr string) (string, error) {
// normalize port to addr
if _, err := strconv.Atoi(addr); err == nil {
addr = ":" + addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return "", err
}
if host == "" {
host = "127.0.0.1"
}
return fmt.Sprintf("%s:%s", host, port), nil
}
func normalizeURL(rawurl string) (string, error) {
// check scheme
s := strings.SplitN(rawurl, "://", 2)
if len(s) > 1 {
switch s[0] {
case "http", "https":
default:
return "", fmt.Errorf("unsupported url schema, choose 'http' or 'https'")
}
} else {
rawurl = fmt.Sprint("http://", rawurl)
}
u, err := url.Parse(rawurl)
if err != nil {
return "", err
}
if u.Path != "" && !strings.HasSuffix(u.Path, "/") {
return "", fmt.Errorf("url must end with '/'")
}
return rawurl, nil
}

View File

@ -0,0 +1,115 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"strings"
"testing"
)
func TestNormalizeAddress(t *testing.T) {
t.Parallel()
tests := []struct {
addr string
expected string
error string
}{
{
addr: "22",
expected: "127.0.0.1:22",
},
{
addr: ":22",
expected: "127.0.0.1:22",
},
{
addr: "0.0.0.0:22",
expected: "0.0.0.0:22",
},
{
addr: "0.0.0.0",
error: "missing port",
},
{
addr: "",
error: "missing port",
},
}
for i, tt := range tests {
actual, err := normalizeAddress(tt.addr)
if actual != tt.expected {
t.Errorf("[%d] expected %q got %q err: %s", i, tt.expected, actual, err)
}
if tt.error != "" && err == nil {
t.Errorf("[%d] expected error", i)
}
if err != nil && (tt.error == "" || !strings.Contains(err.Error(), tt.error)) {
t.Errorf("[%d] expected error contains %q, got %q", i, tt.error, err)
}
}
}
func TestNormalizeURL(t *testing.T) {
t.Parallel()
tests := []struct {
rawurl string
expected string
error string
}{
{
rawurl: "localhost",
expected: "http://localhost",
},
{
rawurl: "localhost:80",
expected: "http://localhost:80",
},
{
rawurl: "localhost:80/path/",
expected: "http://localhost:80/path/",
},
{
rawurl: "localhost:80/path",
error: "/",
},
{
rawurl: "https://localhost",
expected: "https://localhost",
},
{
rawurl: "https://localhost:443",
expected: "https://localhost:443",
},
{
rawurl: "https://localhost:443/path/",
expected: "https://localhost:443/path/",
},
{
rawurl: "https://localhost:443/path",
error: "/",
},
{
rawurl: "ftp://localhost",
error: "unsupported url schema",
},
}
for i, tt := range tests {
actual, err := normalizeURL(tt.rawurl)
if actual != tt.expected {
t.Errorf("[%d] expected %q got %q, err: %s", i, tt.expected, actual, err)
}
if tt.error != "" && err == nil {
t.Errorf("[%d] expected error", i)
}
if err != nil && (tt.error == "" || !strings.Contains(err.Error(), tt.error)) {
t.Errorf("[%d] expected error contains %q, got %q", i, tt.error, err)
}
}
}

View File

@ -0,0 +1,113 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"os"
)
const usage1 string = `Usage: tunnel [OPTIONS] <command> [command args] [...]
options:
`
const usage2 string = `
Commands:
tunnel id Show client identifier
tunnel list List tunnel names from config file
tunnel start [tunnel] [...] Start tunnels by name from config file
tunnel start-all Start all tunnels defined in config file
Examples:
tunnel start www ssh
tunnel -config config.yaml -log stdout -log-level 2 start ssh
tunnel start-all
config.yaml:
server_addr: SERVER_IP:4443
insecure_skip_verify: true
tunnels:
webui:
proto: http
addr: localhost:8080
auth: user:password
host: webui.my-tunnel-host.com
ssh:
proto: tcp
addr: 192.168.0.5:22
remote_addr: 0.0.0.0:22
Author:
Written by M. Matczuk (mmatczuk@gmail.com)
Bugs:
Submit bugs to https://github.com/mmatczuk/go-http-tunnel/issues
`
func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage1)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, usage2)
}
}
type options struct {
debug bool
config string
logTo string
logLevel int
version bool
command string
args []string
}
func parseArgs() (*options, error) {
debug := flag.Bool("debug", false, "Starts gops agent")
config := flag.String("config", "tunnel.yml", "Path to tunnel configuration file")
logTo := flag.String("log", "stdout", "Write log messages to this file, file name or 'stdout', 'stderr', 'none'")
logLevel := flag.Int("log-level", 1, "Level of messages to log, 0-3")
version := flag.Bool("version", false, "Prints tunnel version")
flag.Parse()
opts := &options{
debug: *debug,
config: *config,
logTo: *logTo,
logLevel: *logLevel,
version: *version,
command: flag.Arg(0),
}
if opts.version {
return opts, nil
}
switch opts.command {
case "":
flag.Usage()
os.Exit(2)
case "id", "list":
opts.args = flag.Args()[1:]
if len(opts.args) > 0 {
return nil, fmt.Errorf("list takes no arguments")
}
case "start":
opts.args = flag.Args()[1:]
if len(opts.args) == 0 {
return nil, fmt.Errorf("you must specify at least one tunnel to start")
}
case "start-all":
opts.args = flag.Args()[1:]
if len(opts.args) > 0 {
return nil, fmt.Errorf("start-all takes no arguments")
}
default:
return nil, fmt.Errorf("unknown command %q", opts.command)
}
return opts, nil
}

View File

@ -0,0 +1,168 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"
"os"
"sort"
"gopkg.in/yaml.v2"
"github.com/cenkalti/backoff"
"github.com/mmatczuk/go-http-tunnel"
"github.com/mmatczuk/go-http-tunnel/id"
"github.com/mmatczuk/go-http-tunnel/log"
"github.com/mmatczuk/go-http-tunnel/proto"
)
func main() {
opts, err := parseArgs()
if err != nil {
fatal(err.Error())
}
if opts.version {
fmt.Println(version)
return
}
logger, err := log.NewLogger(opts.logTo, opts.logLevel)
if err != nil {
fatal("failed to init logger: %s", err)
}
// read configuration file
config, err := loadConfiguration(opts.config)
if err != nil {
fatal("configuration error: %s", err)
}
switch opts.command {
case "id":
cert, err := tls.LoadX509KeyPair(config.TLSCrt, config.TLSKey)
if err != nil {
fatal("failed to load key pair: %s", err)
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
fatal("failed to parse certificate: %s", err)
}
fmt.Println(id.New(x509Cert.Raw))
return
case "list":
var names []string
for n := range config.Tunnels {
names = append(names, n)
}
sort.Strings(names)
for _, n := range names {
fmt.Println(n)
}
return
case "start":
tunnels := make(map[string]*tunnelConfig)
for _, arg := range opts.args {
t, ok := config.Tunnels[arg]
if !ok {
fatal("no such tunnel %q", arg)
}
tunnels[arg] = t
}
config.Tunnels = tunnels
}
cert, err := tls.LoadX509KeyPair(config.TLSCrt, config.TLSKey)
if err != nil {
fatal("failed to load certificate: %s", err)
}
b, err := yaml.Marshal(config)
if err != nil {
fatal("failed to load config: %s", err)
}
logger.Log("config", string(b))
client := tunnel.NewClient(&tunnel.ClientConfig{
ServerAddr: config.ServerAddr,
TLSClientConfig: tlsConfig(cert, config),
Backoff: expBackoff(config.Backoff),
Tunnels: tunnels(config.Tunnels),
Proxy: proxy(config.Tunnels, logger),
Logger: logger,
})
if err := client.Start(); err != nil {
fatal("%s", err)
}
}
func tlsConfig(cert tls.Certificate, config *config) *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: config.InsecureSkipVerify,
}
}
func expBackoff(config *backoffConfig) *backoff.ExponentialBackOff {
b := backoff.NewExponentialBackOff()
b.InitialInterval = config.InitialInterval
b.Multiplier = config.Multiplier
b.MaxInterval = config.MaxInterval
b.MaxElapsedTime = config.MaxElapsedTime
return b
}
func tunnels(m map[string]*tunnelConfig) map[string]*proto.Tunnel {
p := make(map[string]*proto.Tunnel)
for name, t := range m {
p[name] = &proto.Tunnel{
Protocol: t.Protocol,
Host: t.Host,
Auth: t.Auth,
Addr: t.RemoteAddr,
}
}
return p
}
func proxy(m map[string]*tunnelConfig, logger log.Logger) tunnel.ProxyFunc {
httpURL := make(map[string]*url.URL)
tcpAddr := make(map[string]string)
for _, t := range m {
switch t.Protocol {
case proto.HTTP:
u, err := url.Parse(t.Addr)
if err != nil {
fatal("invalid tunnel address: %s", err)
}
httpURL[t.Host] = u
case proto.TCP, proto.TCP4, proto.TCP6:
tcpAddr[t.RemoteAddr] = t.Addr
}
}
return tunnel.Proxy(tunnel.ProxyFuncs{
HTTP: tunnel.NewMultiHTTPProxy(httpURL, log.NewContext(logger).WithPrefix("proxy", "HTTP")).Proxy,
TCP: tunnel.NewMultiTCPProxy(tcpAddr, log.NewContext(logger).WithPrefix("proxy", "TCP")).Proxy,
})
}
func fatal(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
fmt.Fprint(os.Stderr, "\n")
os.Exit(1)
}

View File

@ -0,0 +1,7 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
var version = "snapshot"

View File

@ -0,0 +1,76 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"os"
)
const usage1 string = `Usage: tunneld [OPTIONS]
options:
`
const usage2 string = `
Example:
tuneld -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4
tuneld -httpAddr :8080 -httpsAddr "" -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4
Author:
Written by M. Matczuk (mmatczuk@gmail.com)
Bugs:
Submit bugs to https://github.com/mmatczuk/go-http-tunnel/issues
`
func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage1)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, usage2)
}
}
// options specify arguments read command line arguments.
type options struct {
debug bool
httpAddr string
httpsAddr string
tunnelAddr string
tlsCrt string
tlsKey string
clients string
logTo string
logLevel int
version bool
}
func parseArgs() *options {
debug := flag.Bool("debug", false, "Starts gops agent")
httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable")
httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
tunnelAddr := flag.String("tunnelAddr", ":4443", "Public address listening for tunnel client")
tlsCrt := flag.String("tlsCrt", "server.crt", "Path to a TLS certificate file")
tlsKey := flag.String("tlsKey", "server.key", "Path to a TLS key file")
clients := flag.String("clients", "", "Comma-separated list of tunnel client ids")
logTo := flag.String("log", "stdout", "Write log messages to this file, file name or 'stdout', 'stderr', 'none'")
logLevel := flag.Int("log-level", 1, "Level of messages to log, 0-3")
version := flag.Bool("version", false, "Prints tunneld version")
flag.Parse()
return &options{
debug: *debug,
httpAddr: *httpAddr,
httpsAddr: *httpsAddr,
tunnelAddr: *tunnelAddr,
tlsCrt: *tlsCrt,
tlsKey: *tlsKey,
clients: *clients,
logTo: *logTo,
logLevel: *logLevel,
version: *version,
}
}

View File

@ -0,0 +1,120 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"crypto/tls"
"fmt"
"net/http"
"os"
"strings"
"golang.org/x/net/http2"
"github.com/mmatczuk/go-http-tunnel"
"github.com/mmatczuk/go-http-tunnel/id"
"github.com/mmatczuk/go-http-tunnel/log"
)
func main() {
opts := parseArgs()
if opts.version {
fmt.Println(version)
return
}
logger, err := log.NewLogger(opts.logTo, opts.logLevel)
if err != nil {
fatal("failed to init logger: %s", err)
}
// load certs
cert, err := tls.LoadX509KeyPair(opts.tlsCrt, opts.tlsKey)
if err != nil {
fatal("failed to load certificate: %s", err)
}
// setup server
server, err := tunnel.NewServer(&tunnel.ServerConfig{
Addr: opts.tunnelAddr,
TLSConfig: tlsConfig(cert),
Logger: logger,
})
if err != nil {
fatal("failed to create server: %s", err)
}
if opts.clients == "" {
logger.Log(
"level", 0,
"msg", "No clients",
)
} else {
for _, c := range strings.Split(opts.clients, ",") {
if c == "" {
fatal("empty client id")
}
identifier := id.ID{}
err := identifier.UnmarshalText([]byte(c))
if err != nil {
fatal("invalid identifier %q: %s", c, err)
}
server.Subscribe(identifier)
}
}
// start HTTP
if opts.httpAddr != "" {
go func() {
logger.Log(
"level", 1,
"action", "start http",
"addr", opts.httpAddr,
)
fatal("failed to start HTTP: %s", http.ListenAndServe(opts.httpAddr, server))
}()
}
// start HTTPS
if opts.httpsAddr != "" {
go func() {
logger.Log(
"level", 1,
"action", "start https",
"addr", opts.httpsAddr,
)
s := &http.Server{
Addr: opts.httpsAddr,
Handler: server,
}
http2.ConfigureServer(s, nil)
fatal("failed to start HTTPS: %s", s.ListenAndServeTLS(opts.tlsCrt, opts.tlsKey))
}()
}
server.Start()
}
func tlsConfig(cert tls.Certificate) *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
PreferServerCipherSuites: true,
NextProtos: []string{"h2"},
}
}
func fatal(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
fmt.Fprint(os.Stderr, "\n")
os.Exit(1)
}

View File

@ -0,0 +1,7 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
var version = "snapshot"

8
vendor/github.com/mmatczuk/go-http-tunnel/doc.go generated vendored Normal file
View File

@ -0,0 +1,8 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tunnel is fast and secure client/server package that enables proxying
// public connections to your local machine over a tunnel connection from the
// local machine to the public server.
package tunnel

15
vendor/github.com/mmatczuk/go-http-tunnel/errors.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import "errors"
var (
errClientNotSubscribed = errors.New("client not subscribed")
errClientNotConnected = errors.New("client not connected")
errClientAlreadyConnected = errors.New("client already connected")
errUnauthorised = errors.New("unauthorised")
)

188
vendor/github.com/mmatczuk/go-http-tunnel/httpproxy.go generated vendored Normal file
View File

@ -0,0 +1,188 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"path"
"github.com/mmatczuk/go-http-tunnel/log"
"github.com/mmatczuk/go-http-tunnel/proto"
)
// HTTPProxy forwards HTTP traffic.
type HTTPProxy struct {
httputil.ReverseProxy
// localURL specifies default base URL of local service.
localURL *url.URL
// localURLMap specifies mapping from ControlMessage ForwardedBy to
// local service URL, keys may contain host and port, only host or
// only port. The order of precedence is the following
// * host and port
// * port
// * host
localURLMap map[string]*url.URL
// logger is the proxy logger.
logger log.Logger
}
// NewHTTPProxy creates a new direct HTTPProxy, everything will be proxied to
// localURL.
func NewHTTPProxy(localURL *url.URL, logger log.Logger) *HTTPProxy {
if localURL == nil {
panic("empty localURL")
}
if logger == nil {
logger = log.NewNopLogger()
}
p := &HTTPProxy{
localURL: localURL,
logger: logger,
}
p.ReverseProxy.Director = p.Director
return p
}
// NewMultiHTTPProxy creates a new dispatching HTTPProxy, requests may go to
// different backends based on localURLMap.
func NewMultiHTTPProxy(localURLMap map[string]*url.URL, logger log.Logger) *HTTPProxy {
if localURLMap == nil {
panic("empty localURLMap")
}
if logger == nil {
logger = log.NewNopLogger()
}
p := &HTTPProxy{
localURLMap: localURLMap,
logger: logger,
}
p.ReverseProxy.Director = p.Director
return p
}
// Proxy is a ProxyFunc.
func (p *HTTPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
if msg.Protocol != proto.HTTP {
p.logger.Log(
"level", 0,
"msg", "unsupported protocol",
"ctrlMsg", msg,
)
return
}
rw, ok := w.(http.ResponseWriter)
if !ok {
panic(fmt.Sprintf("Expected http.ResponseWriter got %T", w))
}
req, err := http.ReadRequest(bufio.NewReader(r))
if err != nil {
p.logger.Log(
"level", 0,
"msg", "failed to read request",
"ctrlMsg", msg,
"err", err,
)
return
}
req.URL.Host = msg.ForwardedBy
p.ServeHTTP(rw, req)
}
// Director is ReverseProxy Director it changes request URL so that the request
// is correctly routed based on localURL and localURLMap. If no URL can be found
// the request is canceled.
func (p *HTTPProxy) Director(req *http.Request) {
orig := *req.URL
target := p.localURLFor(req.URL)
if target == nil {
p.logger.Log(
"level", 1,
"msg", "no target",
"url", req.URL,
)
_, cancel := context.WithCancel(req.Context())
cancel()
return
}
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
targetQuery := target.RawQuery
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
req.Host = req.URL.Host
p.logger.Log(
"level", 2,
"action", "url rewrite",
"from", &orig,
"to", req.URL,
)
}
func singleJoiningSlash(a, b string) string {
if a == "" || a == "/" {
return b
}
if b == "" || b == "/" {
return a
}
return path.Join(a, b)
}
func (p *HTTPProxy) localURLFor(u *url.URL) *url.URL {
if p.localURLMap == nil {
return p.localURL
}
// try host and port
hostPort := u.Host
if addr := p.localURLMap[hostPort]; addr != nil {
return addr
}
// try port
host, port, _ := net.SplitHostPort(hostPort)
if addr := p.localURLMap[port]; addr != nil {
return addr
}
// try host
if addr := p.localURLMap[host]; addr != nil {
return addr
}
return p.localURL
}

194
vendor/github.com/mmatczuk/go-http-tunnel/id/id.go generated vendored Normal file
View File

@ -0,0 +1,194 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package id
import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"encoding/base32"
"errors"
"fmt"
"regexp"
"strings"
"github.com/calmh/luhn"
)
// ID is the type representing a generated ID.
type ID [32]byte
// New generates a new ID from the given input bytes.
func New(data []byte) ID {
var id ID
hasher := sha256.New()
hasher.Write(data)
hasher.Sum(id[:0])
return id
}
// NewFromString creates a new ID from the given string.
func NewFromString(s string) ID {
return New([]byte(s))
}
// NewFromBytes creates a new ID with the value of the given byte slice. The
// given byte slice must be 32 bytes long (the same length as ID), or this
// function will panic.
func NewFromBytes(b []byte) ID {
var id ID
if len(b) != len(id) {
panic("invalid slice length for id")
}
copy(id[0:], b)
return id
}
// String returns the canonical representation of the ID.
func (i ID) String() string {
ss := base32.StdEncoding.EncodeToString(i[:])
ss = strings.Trim(ss, "=")
// Add a Luhn check 'digit' for the ID.
ss, err := luhnify(ss)
if err != nil {
// Should never happen
panic(err)
}
// Return the given ID as chunks.
ss = chunkify(ss)
return ss
}
// Compares the two given IDs. Note that this function is NOT SAFE AGAINST
// TIMING ATTACKS. If you are simply checking for equality, please use the
// Equals function, which is.
func (i ID) Compare(other ID) int {
return bytes.Compare(i[:], other[:])
}
// Checks the two given IDs for equality. This function uses a constant-time
// comparison algorithm to prevent timing attacks.
func (i ID) Equals(other ID) bool {
return subtle.ConstantTimeCompare(i[:], other[:]) == 1
}
// Implements the `TextMarshaler` interface from the encoding package.
func (i *ID) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// Implements the `TextUnmarshaler` interface from the encoding package.
func (i *ID) UnmarshalText(bs []byte) (err error) {
// Convert to the canonical encoding - uppercase, no '=', no chunks, and
// with any potential typos fixed.
id := string(bs)
id = strings.Trim(id, "=")
id = strings.ToUpper(id)
id = untypeoify(id)
id = unchunkify(id)
if len(id) != 56 {
return errors.New("device ID invalid: incorrect length")
}
// Remove & verify Luhn check digits
id, err = unluhnify(id)
if err != nil {
return err
}
// Base32 decode
dec, err := base32.StdEncoding.DecodeString(id + "====")
if err != nil {
return err
}
// Done!
copy(i[:], dec)
return nil
}
// Add Luhn check digits to a string, returning the new one.
func luhnify(s string) (string, error) {
if len(s) != 52 {
panic("unsupported string length")
}
// Split the string into chunks of length 13, and add a Luhn check digit to
// each one.
res := make([]string, 0, 4)
for i := 0; i < 4; i++ {
chunk := s[i*13 : (i+1)*13]
l, err := luhn.Base32.Generate(chunk)
if err != nil {
return "", err
}
res = append(res, fmt.Sprintf("%s%c", chunk, l))
}
return res[0] + res[1] + res[2] + res[3], nil
}
// Remove Luhn check digits from the given string, validating that they are
// correct.
func unluhnify(s string) (string, error) {
if len(s) != 56 {
return "", fmt.Errorf("unsupported string length %d", len(s))
}
res := make([]string, 0, 4)
for i := 0; i < 4; i++ {
// 13 characters, plus the Luhn digit.
chunk := s[i*14 : (i+1)*14]
// Get the expected check digit.
l, err := luhn.Base32.Generate(chunk[0:13])
if err != nil {
return "", err
}
// Validate the digits match.
if fmt.Sprintf("%c", l) != chunk[13:] {
return "", errors.New("check digit incorrect")
}
res = append(res, chunk[0:13])
}
return res[0] + res[1] + res[2] + res[3], nil
}
// Returns a string split into chunks of size 7.
func chunkify(s string) string {
s = regexp.MustCompile("(.{7})").ReplaceAllString(s, "$1-")
s = strings.Trim(s, "-")
return s
}
// Un-chunks a string by removing all hyphens and spaces.
func unchunkify(s string) string {
s = strings.Replace(s, "-", "", -1)
s = strings.Replace(s, " ", "", -1)
return s
}
// We use base32 encoding, which uses 26 characters, and then the numbers
// 234567. This is useful since the alphabet doesn't contain the numbers 0, 1,
// or 8, which means we can replace them with their letter-lookalikes.
func untypeoify(s string) string {
s = strings.Replace(s, "0", "O", -1)
s = strings.Replace(s, "1", "I", -1)
s = strings.Replace(s, "8", "B", -1)
return s
}

47
vendor/github.com/mmatczuk/go-http-tunnel/id/ptls.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package id
import (
"crypto/tls"
"fmt"
)
var emptyID [32]byte
// PeerID is modified https://github.com/andrew-d/ptls/blob/b89c7dcc94630a77f225a48befd3710144c7c10e/ptls.go#L81
func PeerID(conn *tls.Conn) (ID, error) {
// Try a TLS connection over the given connection. We explicitly perform
// the handshake, since we want to maintain the invariant that, if this
// function returns successfully, then the connection should be valid
// and verified.
if err := conn.Handshake(); err != nil {
return emptyID, err
}
cs := conn.ConnectionState()
// We should have exactly one peer certificate.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
return emptyID, ImproperCertsNumberError{cl}
}
// Get remote cert's ID.
remoteCert := certs[0]
remoteID := New(remoteCert.Raw)
return remoteID, nil
}
// ImproperCertsNumberError is returned from Server/Client whenever the remote
// peer presents a number of PeerCertificates that is not 1.
type ImproperCertsNumberError struct {
n int
}
func (e ImproperCertsNumberError) Error() string {
return fmt.Sprintf("ptls: expecting 1 peer certificate, got %d", e.n)
}

View File

@ -0,0 +1,329 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel_test
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"
"github.com/mmatczuk/go-http-tunnel"
"github.com/mmatczuk/go-http-tunnel/id"
"github.com/mmatczuk/go-http-tunnel/proto"
)
const (
payloadInitialSize = 512
payloadLen = 10
)
// echoHTTP starts serving HTTP requests on listener l, it accepts connections,
// reads request body and writes is back in response.
func echoHTTP(l net.Listener) {
http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if r.Body != nil {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
w.Write(body)
}
}))
}
// echoTCP accepts connections and copies back received bytes.
func echoTCP(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
return
}
go func() {
io.Copy(conn, conn)
}()
}
}
func makeEcho(t *testing.T) (http net.Listener, tcp net.Listener) {
var err error
// TCP echo
tcp, err = net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
go echoTCP(tcp)
// HTTP echo
http, err = net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
go echoHTTP(http)
return
}
func makeTunnelServer(t *testing.T) *tunnel.Server {
cert, identifier := selfSignedCert()
s, err := tunnel.NewServer(&tunnel.ServerConfig{
Addr: ":0",
TLSConfig: tlsConfig(cert),
})
if err != nil {
t.Fatal(err)
}
s.Subscribe(identifier)
go s.Start()
return s
}
func makeTunnelClient(t *testing.T, serverAddr string, httpLocalAddr, httpAddr, tcpLocalAddr, tcpAddr net.Addr) *tunnel.Client {
httpProxy := tunnel.NewMultiHTTPProxy(map[string]*url.URL{
"localhost:" + port(httpLocalAddr): {
Scheme: "http",
Host: "127.0.0.1:" + port(httpAddr),
},
}, nil)
tcpProxy := tunnel.NewMultiTCPProxy(map[string]string{
port(tcpLocalAddr): tcpAddr.String(),
}, nil)
tunnels := map[string]*proto.Tunnel{
proto.HTTP: {
Protocol: proto.HTTP,
Host: "localhost",
Auth: "user:password",
},
proto.TCP: {
Protocol: proto.TCP,
Addr: tcpLocalAddr.String(),
},
}
cert, _ := selfSignedCert()
c := tunnel.NewClient(&tunnel.ClientConfig{
ServerAddr: serverAddr,
TLSClientConfig: tlsConfig(cert),
Tunnels: tunnels,
Proxy: tunnel.Proxy(tunnel.ProxyFuncs{
HTTP: httpProxy.Proxy,
TCP: tcpProxy.Proxy,
}),
})
go c.Start()
return c
}
func TestIntegration(t *testing.T) {
// local services
http, tcp := makeEcho(t)
defer http.Close()
defer tcp.Close()
// server
s := makeTunnelServer(t)
defer s.Stop()
h := httptest.NewServer(s)
defer h.Close()
httpLocalAddr := h.Listener.Addr()
tcpLocalAddr := freeAddr()
// client
c := makeTunnelClient(t, s.Addr(),
httpLocalAddr, http.Addr(),
tcpLocalAddr, tcp.Addr(),
)
// FIXME: replace sleep with client state change watch when ready
time.Sleep(500 * time.Millisecond)
defer c.Stop()
payload := randPayload(payloadInitialSize, payloadLen)
table := []struct {
S []uint
}{
{[]uint{200, 160, 120, 80, 40, 20}},
{[]uint{40, 80, 120, 160, 200}},
{[]uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 200}},
}
var wg sync.WaitGroup
for _, test := range table {
for i, repeat := range test.S {
p := payload[i]
r := repeat
wg.Add(1)
go func() {
testHTTP(t, h.Listener.Addr(), p, r)
wg.Done()
}()
wg.Add(1)
go func() {
testTCP(t, tcpLocalAddr, p, r)
wg.Done()
}()
}
}
wg.Wait()
}
func testHTTP(t *testing.T, addr net.Addr, payload []byte, repeat uint) {
url := fmt.Sprintf("http://localhost:%s/some/path", port(addr))
for repeat > 0 {
r, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
t.Fatal("Failed to create request")
}
r.SetBasicAuth("user", "password")
resp, err := http.DefaultClient.Do(r)
if err != nil {
t.Error(err)
}
if resp.StatusCode != http.StatusOK {
t.Error("Unexpected status code", resp)
}
if resp.Body == nil {
t.Error("No body")
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error("Read error")
}
n, m := len(b), len(payload)
if n != m {
t.Error("Write read mismatch", n, m)
}
repeat--
}
}
func testTCP(t *testing.T, addr net.Addr, payload []byte, repeat uint) {
conn, err := net.Dial("tcp", addr.String())
if err != nil {
t.Fatal("Dial failed", err)
}
defer conn.Close()
var buf = make([]byte, 10*1024*1024)
var read, write int
for repeat > 0 {
m, err := conn.Write(payload)
if err != nil {
t.Error("Write failed", err)
}
if m != len(payload) {
t.Log("Write mismatch", m, len(payload))
}
write += m
n, err := conn.Read(buf)
if err != nil {
t.Error("Read failed", err)
}
read += n
repeat--
}
for read < write {
t.Log("No yet read everything", "write", write, "read", read)
time.Sleep(50 * time.Millisecond)
n, err := conn.Read(buf)
if err != nil {
t.Error("Read failed", err)
}
read += n
}
if read != write {
t.Fatal("Write read mismatch", read, write)
}
}
//
// helpers
//
// randPayload returns slice of randomly initialised data buffers.
func randPayload(initialSize, n int) [][]byte {
payload := make([][]byte, n)
l := initialSize
for i := 0; i < n; i++ {
payload[i] = randBytes(l)
l *= 2
}
return payload
}
func randBytes(n int) []byte {
b := make([]byte, n)
read, err := rand.Read(b)
if err != nil {
panic(err)
}
if read != n {
panic("read did not fill whole slice")
}
return b
}
func freeAddr() net.Addr {
l, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
defer l.Close()
return l.Addr()
}
func port(addr net.Addr) string {
return fmt.Sprint(addr.(*net.TCPAddr).Port)
}
func selfSignedCert() (tls.Certificate, id.ID) {
cert, err := tls.LoadX509KeyPair("./testdata/selfsigned.crt", "./testdata/selfsigned.key")
if err != nil {
panic(err)
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
panic(err)
}
return cert, id.New(x509Cert.Raw)
}
func tlsConfig(cert tls.Certificate) *tls.Config {
c := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
PreferServerCipherSuites: true,
NextProtos: []string{"h2"},
}
c.BuildNameToCertificate()
return c
}

View File

@ -0,0 +1,48 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
// Context is simplified version of go-kit log Context
// https://godoc.org/github.com/go-kit/kit/log#Context.
type Context struct {
prefix []interface{}
suffix []interface{}
logger Logger
}
// NewContext returns a logger that adds prefix before keyvals.
func NewContext(logger Logger) *Context {
return &Context{
prefix: make([]interface{}, 0),
suffix: make([]interface{}, 0),
logger: logger,
}
}
// With returns a new Context with keyvals appended to those of the receiver.
func (c *Context) With(keyvals ...interface{}) *Context {
return &Context{
prefix: c.prefix,
suffix: append(c.suffix, keyvals...),
logger: c.logger,
}
}
// WithPrefix returns a new Context with keyvals prepended to those of the
// receiver.
func (c *Context) WithPrefix(keyvals ...interface{}) *Context {
return &Context{
prefix: append(c.prefix, keyvals...),
suffix: c.suffix,
logger: c.logger,
}
}
// Log adds prefix and suffix to keyvals and calls internal logger.
func (c *Context) Log(keyvals ...interface{}) error {
s := append(c.prefix, keyvals...)
s = append(s, c.suffix...)
return c.logger.Log(s...)
}

View File

@ -0,0 +1,48 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
type filterLogger struct {
level int
logger Logger
}
// NewFilterLogger returns a Logger that accepts only log messages with
// "level" value <= level. Currently there are four levels 0 - error, 1 - info,
// 2 - debug, 3 - trace.
func NewFilterLogger(logger Logger, level int) Logger {
return filterLogger{
level: level,
logger: logger,
}
}
func (p filterLogger) Log(keyvals ...interface{}) error {
for i := 0; i < len(keyvals); i += 2 {
k := keyvals[i]
s, ok := k.(string)
if !ok {
continue
}
if s != "level" {
continue
}
if i+1 >= len(keyvals) {
break
}
v := keyvals[i+1]
level, ok := v.(int)
if !ok {
break
}
if level > p.level {
return nil
}
}
return p.logger.Log(keyvals...)
}

View File

@ -0,0 +1,31 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/mmatczuk/go-http-tunnel/tunnelmock"
)
func TestFilterLogger_Log(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
b := tunnelmock.NewMockLogger(ctrl)
f := NewFilterLogger(b, 2)
b.EXPECT().Log("level", 0)
f.Log("level", 0)
b.EXPECT().Log("level", 1)
f.Log("level", 1)
b.EXPECT().Log("level", 2)
f.Log("level", 2)
f.Log("level", 3)
f.Log("level", 4)
}

47
vendor/github.com/mmatczuk/go-http-tunnel/log/log.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
import (
"io"
"log"
"os"
)
// Logger is the fundamental interface for all log operations. Log creates a
// log event from keyvals, a variadic sequence of alternating keys and values.
// Implementations must be safe for concurrent use by multiple goroutines. In
// particular, any implementation of Logger that appends to keyvals or
// modifies any of its elements must make a copy first.
type Logger interface {
Log(keyvals ...interface{}) error
}
// NewLogger returns logfmt based logger, printing messages up to log level
// logLevel.
func NewLogger(to string, level int) (Logger, error) {
var w io.Writer
switch to {
case "none":
return NewNopLogger(), nil
case "stdout":
w = os.Stdout
case "stderr":
w = os.Stderr
default:
f, err := os.Create(to)
if err != nil {
return nil, err
}
w = f
}
log.SetOutput(w)
l := NewStdLogger()
l = NewFilterLogger(l, level)
return l, nil
}

View File

@ -0,0 +1,29 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/mmatczuk/go-http-tunnel/tunnelmock"
)
func TestContext_Log(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
b := tunnelmock.NewMockLogger(ctrl)
b.EXPECT().Log("key", "val", "sufix", "")
NewContext(b).With("sufix", "").Log("key", "val")
b.EXPECT().Log("prefix", "", "key", "val")
NewContext(b).WithPrefix("prefix", "").Log("key", "val")
b.EXPECT().Log("prefix", "", "key", "val", "sufix", "")
NewContext(b).With("sufix", "").WithPrefix("prefix", "").Log("key", "val")
}

View File

@ -0,0 +1,12 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
type nopLogger struct{}
// NewNopLogger returns a logger that doesn't do anything.
func NewNopLogger() Logger { return nopLogger{} }
func (nopLogger) Log(...interface{}) error { return nil }

View File

@ -0,0 +1,19 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package log
import (
"log"
)
type stdLogger struct{}
// NewStdLogger returns logger based on standard "log" package.
func NewStdLogger() Logger { return stdLogger{} }
func (p stdLogger) Log(keyvals ...interface{}) error {
log.Println(keyvals...)
return nil
}

116
vendor/github.com/mmatczuk/go-http-tunnel/pool.go generated vendored Normal file
View File

@ -0,0 +1,116 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"fmt"
"net"
"net/http"
"sync"
"golang.org/x/net/http2"
"github.com/mmatczuk/go-http-tunnel/id"
)
type onDisconnectListener func(identifier id.ID)
type connPair struct {
conn net.Conn
clientConn *http2.ClientConn
}
type connPool struct {
t *http2.Transport
conns map[string]connPair // key is host:port
listener onDisconnectListener
mu sync.RWMutex
}
func newConnPool(t *http2.Transport, l onDisconnectListener) *connPool {
return &connPool{
t: t,
listener: l,
conns: make(map[string]connPair),
}
}
func (p *connPool) GetClientConn(req *http.Request, addr string) (*http2.ClientConn, error) {
p.mu.RLock()
defer p.mu.RUnlock()
if cp, ok := p.conns[addr]; ok && cp.clientConn.CanTakeNewRequest() {
return cp.clientConn, nil
}
return nil, errClientNotConnected
}
func (p *connPool) MarkDead(c *http2.ClientConn) {
p.mu.Lock()
defer p.mu.Unlock()
for addr, cp := range p.conns {
if cp.clientConn == c {
cp.conn.Close()
delete(p.conns, addr)
if p.listener != nil {
p.listener(p.addrToIdentifier(addr))
}
return
}
}
}
func (p *connPool) AddConn(conn net.Conn, identifier id.ID) error {
p.mu.Lock()
defer p.mu.Unlock()
addr := p.addr(identifier)
if _, ok := p.conns[addr]; ok {
return errClientAlreadyConnected
}
c, err := p.t.NewClientConn(conn)
if err != nil {
return err
}
p.conns[addr] = connPair{
conn: conn,
clientConn: c,
}
return nil
}
func (p *connPool) DeleteConn(identifier id.ID) {
p.mu.Lock()
defer p.mu.Unlock()
addr := p.addr(identifier)
if cp, ok := p.conns[addr]; ok {
cp.conn.Close()
delete(p.conns, addr)
if p.listener != nil {
p.listener(identifier)
}
}
}
func (p *connPool) URL(identifier id.ID) string {
return fmt.Sprint("https://", identifier)
}
func (p *connPool) addr(identifier id.ID) string {
return fmt.Sprint(identifier.String(), ":443")
}
func (p *connPool) addrToIdentifier(addr string) id.ID {
identifier := id.ID{}
identifier.UnmarshalText([]byte(addr[:len(addr)-4]))
return identifier
}

View File

@ -0,0 +1,87 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package proto
import (
"fmt"
"net/http"
)
// Protocol HTTP headers.
const (
HeaderAction = "T-Action"
HeaderError = "T-Error"
HeaderForwardedBy = "T-Forwarded-By"
HeaderForwardedFor = "T-Forwarded-For"
HeaderPath = "T-Path"
HeaderProtocol = "T-Proto"
)
// Known actions.
const (
ActionProxy string = "proxy"
)
// Known protocol types.
const (
HTTP = "http"
TCP = "tcp"
TCP4 = "tcp4"
TCP6 = "tcp6"
UNIX = "unix"
WS = "ws"
)
// ControlMessage is sent from server to client before streaming data. It's
// used to inform client about the data and action to take. Based on that client
// routes requests to backend services.
type ControlMessage struct {
Action string
Protocol string
ForwardedFor string
ForwardedBy string
Path string
}
// ReadControlMessage reads ControlMessage from HTTP headers.
func ReadControlMessage(h http.Header) (*ControlMessage, error) {
msg := ControlMessage{
Action: h.Get(HeaderAction),
Protocol: h.Get(HeaderProtocol),
ForwardedFor: h.Get(HeaderForwardedFor),
ForwardedBy: h.Get(HeaderForwardedBy),
Path: h.Get(HeaderPath),
}
var missing []string
if msg.Action == "" {
missing = append(missing, HeaderAction)
}
if msg.Protocol == "" {
missing = append(missing, HeaderProtocol)
}
if msg.ForwardedFor == "" {
missing = append(missing, HeaderForwardedFor)
}
if msg.ForwardedBy == "" {
missing = append(missing, HeaderForwardedBy)
}
if len(missing) != 0 {
return nil, fmt.Errorf("missing headers: %s", missing)
}
return &msg, nil
}
// Update writes ControlMessage to HTTP header.
func (c *ControlMessage) Update(h http.Header) {
h.Set(HeaderAction, string(c.Action))
h.Set(HeaderProtocol, c.Protocol)
h.Set(HeaderForwardedFor, c.ForwardedFor)
h.Set(HeaderForwardedBy, c.ForwardedBy)
h.Set(HeaderPath, c.Path)
}

View File

@ -0,0 +1,80 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package proto
import (
"errors"
"net/http"
"reflect"
"testing"
)
func TestControlMessage_WriteParse(t *testing.T) {
t.Parallel()
data := []struct {
msg *ControlMessage
err error
}{
{
&ControlMessage{
Action: "action",
Protocol: "protocol",
ForwardedFor: "host-for",
ForwardedBy: "host-by",
},
nil,
},
{
&ControlMessage{
Protocol: "protocol",
ForwardedFor: "host-for",
ForwardedBy: "host-by",
},
errors.New("missing headers: [T-Action]"),
},
{
&ControlMessage{
Action: "action",
ForwardedFor: "host-for",
ForwardedBy: "host-by",
},
errors.New("missing headers: [T-Proto]"),
},
{
&ControlMessage{
Action: "action",
Protocol: "protocol",
ForwardedBy: "host-by",
},
errors.New("missing headers: [T-Forwarded-For]"),
},
{
&ControlMessage{
Action: "action",
Protocol: "protocol",
ForwardedFor: "host-for",
},
errors.New("missing headers: [T-Forwarded-By]"),
},
}
for i, tt := range data {
h := http.Header{}
tt.msg.Update(h)
actual, err := ReadControlMessage(h)
if tt.err != nil {
if err == nil {
t.Error(i, "expected error")
} else if tt.err.Error() != err.Error() {
t.Error(i, tt.err, err)
}
} else {
if !reflect.DeepEqual(tt.msg, actual) {
t.Error(i, tt.msg, actual)
}
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package proto
// Tunnel describes a single tunnel between client and server. When connecting
// client sends tunnels to server. If client gets connected server proxies
// connections to given Host and Addr to the client.
type Tunnel struct {
// Protocol specifies tunnel protocol, must be one of protocols known
// by the server.
Protocol string
// Host specified HTTP request host, it's required for HTTP and WS
// tunnels.
Host string
// Auth specifies HTTP basic auth credentials in form "user:password",
// if set server would protect HTTP and WS tunnels with basic auth.
Auth string
// Addr specifies TCP address server would listen on, it's required
// for TCP tunnels.
Addr string
}

42
vendor/github.com/mmatczuk/go-http-tunnel/proxy.go generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"io"
"github.com/mmatczuk/go-http-tunnel/proto"
)
// ProxyFunc is responsible for forwarding a remote connection to local server
// and writing the response.
type ProxyFunc func(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage)
// ProxyFuncs is a collection of ProxyFunc.
type ProxyFuncs struct {
// HTTP is custom implementation of HTTP proxing.
HTTP ProxyFunc
// TCP is custom implementation of TCP proxing.
TCP ProxyFunc
}
// Proxy returns a ProxyFunc that uses custom function if provided.
func Proxy(p ProxyFuncs) ProxyFunc {
return func(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
var f ProxyFunc
switch msg.Protocol {
case proto.HTTP:
f = p.HTTP
case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX:
f = p.TCP
}
if f == nil {
return
}
f(w, r, msg)
}
}

194
vendor/github.com/mmatczuk/go-http-tunnel/registry.go generated vendored Normal file
View File

@ -0,0 +1,194 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"fmt"
"net"
"sync"
"github.com/mmatczuk/go-http-tunnel/id"
"github.com/mmatczuk/go-http-tunnel/log"
)
// RegistryItem holds information about hosts and listeners associated with a
// client.
type RegistryItem struct {
Hosts []*HostAuth
Listeners []net.Listener
}
// HostAuth holds host and authentication info.
type HostAuth struct {
Host string
Auth *Auth
}
type hostInfo struct {
identifier id.ID
auth *Auth
}
type registry struct {
items map[id.ID]*RegistryItem
hosts map[string]*hostInfo
mu sync.RWMutex
logger log.Logger
}
func newRegistry(logger log.Logger) *registry {
if logger == nil {
logger = log.NewNopLogger()
}
return &registry{
items: make(map[id.ID]*RegistryItem),
hosts: make(map[string]*hostInfo),
logger: logger,
}
}
var voidRegistryItem = &RegistryItem{}
// Subscribe allows to connect client with a given identifier.
func (r *registry) Subscribe(identifier id.ID) {
r.logger.Log(
"level", 1,
"action", "subscribe",
"identifier", identifier,
)
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.items[identifier]; ok {
return
}
r.items[identifier] = voidRegistryItem
}
// IsSubscribed returns true if client is subscribed.
func (r *registry) IsSubscribed(identifier id.ID) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.items[identifier]
return ok
}
// Subscriber returns client identifier assigned to given host.
func (r *registry) Subscriber(hostPort string) (id.ID, *Auth, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
h, ok := r.hosts[trimPort(hostPort)]
if !ok {
return id.ID{}, nil, false
}
return h.identifier, h.auth, ok
}
// Unsubscribe removes client from registry and returns it's RegistryItem.
func (r *registry) Unsubscribe(identifier id.ID) *RegistryItem {
r.logger.Log(
"level", 1,
"action", "unsubscribe",
"identifier", identifier,
)
r.mu.Lock()
defer r.mu.Unlock()
i, ok := r.items[identifier]
if !ok {
return nil
}
if i.Hosts != nil {
for _, h := range i.Hosts {
delete(r.hosts, h.Host)
}
}
delete(r.items, identifier)
return i
}
func (r *registry) set(i *RegistryItem, identifier id.ID) error {
r.logger.Log(
"level", 2,
"action", "set registry item",
"identifier", identifier,
)
r.mu.Lock()
defer r.mu.Unlock()
j, ok := r.items[identifier]
if !ok {
return errClientNotSubscribed
}
if j != voidRegistryItem {
return fmt.Errorf("attempt to overwrite registry item")
}
if i.Hosts != nil {
for _, h := range i.Hosts {
if h.Auth != nil && h.Auth.User == "" {
return fmt.Errorf("missing auth user")
}
if _, ok := r.hosts[trimPort(h.Host)]; ok {
return fmt.Errorf("host %q is occupied", h.Host)
}
}
for _, h := range i.Hosts {
r.hosts[trimPort(h.Host)] = &hostInfo{
identifier: identifier,
auth: h.Auth,
}
}
}
r.items[identifier] = i
return nil
}
func (r *registry) clear(identifier id.ID) *RegistryItem {
r.logger.Log(
"level", 2,
"action", "clear registry item",
"identifier", identifier,
)
r.mu.Lock()
defer r.mu.Unlock()
i, ok := r.items[identifier]
if !ok || i == voidRegistryItem {
return nil
}
if i.Hosts != nil {
for _, h := range i.Hosts {
delete(r.hosts, trimPort(h.Host))
}
}
r.items[identifier] = voidRegistryItem
return i
}
func trimPort(hostPort string) (host string) {
host, _, _ = net.SplitHostPort(hostPort)
if host == "" {
host = hostPort
}
return
}

648
vendor/github.com/mmatczuk/go-http-tunnel/server.go generated vendored Normal file
View File

@ -0,0 +1,648 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"golang.org/x/net/http2"
"github.com/mmatczuk/go-http-tunnel/id"
"github.com/mmatczuk/go-http-tunnel/log"
"github.com/mmatczuk/go-http-tunnel/proto"
)
// ServerConfig defines configuration for the Server.
type ServerConfig struct {
// Addr is TCP address to listen for client connections. If empty ":0"
// is used.
Addr string
// TLSConfig specifies the tls configuration to use with tls.Listener.
TLSConfig *tls.Config
// Listener specifies optional listener for client connections. If nil
// tls.Listen("tcp", Addr, TLSConfig) is used.
Listener net.Listener
// Logger is optional logger. If nil logging is disabled.
Logger log.Logger
}
// Server is responsible for proxying public connections to the client over a
// tunnel connection.
type Server struct {
*registry
config *ServerConfig
listener net.Listener
connPool *connPool
httpClient *http.Client
logger log.Logger
}
// NewServer creates a new Server.
func NewServer(config *ServerConfig) (*Server, error) {
listener, err := listener(config)
if err != nil {
return nil, fmt.Errorf("tls listener failed: %s", err)
}
logger := config.Logger
if logger == nil {
logger = log.NewNopLogger()
}
s := &Server{
registry: newRegistry(logger),
config: config,
listener: listener,
logger: logger,
}
t := &http2.Transport{}
pool := newConnPool(t, s.disconnected)
t.ConnPool = pool
s.connPool = pool
s.httpClient = &http.Client{Transport: t}
return s, nil
}
func listener(config *ServerConfig) (net.Listener, error) {
if config.Listener != nil {
return config.Listener, nil
}
if config.Addr == "" {
return nil, errors.New("missing Addr")
}
if config.TLSConfig == nil {
return nil, errors.New("missing TLSConfig")
}
return tls.Listen("tcp", config.Addr, config.TLSConfig)
}
// disconnected clears resources used by client, it's invoked by connection pool
// when client goes away.
func (s *Server) disconnected(identifier id.ID) {
s.logger.Log(
"level", 1,
"action", "disconnected",
"identifier", identifier,
)
i := s.registry.clear(identifier)
if i == nil {
return
}
for _, l := range i.Listeners {
s.logger.Log(
"level", 2,
"action", "close listener",
"identifier", identifier,
"addr", l.Addr(),
)
l.Close()
}
}
// Start starts accepting connections form clients. For accepting http traffic
// from end users server must be run as handler on http server.
func (s *Server) Start() {
addr := s.listener.Addr().String()
s.logger.Log(
"level", 1,
"action", "start",
"addr", addr,
)
for {
conn, err := s.listener.Accept()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
s.logger.Log(
"level", 1,
"action", "control connection listener closed",
"addr", addr,
)
return
}
s.logger.Log(
"level", 0,
"msg", "accept control connection failed",
"addr", addr,
"err", err,
)
continue
}
go s.handleClient(conn)
}
}
func (s *Server) handleClient(conn net.Conn) {
logger := log.NewContext(s.logger).With("addr", conn.RemoteAddr())
logger.Log(
"level", 1,
"action", "try connect",
)
var (
identifier id.ID
req *http.Request
resp *http.Response
tunnels map[string]*proto.Tunnel
err error
ok bool
inConnPool bool
)
tlsConn, ok := conn.(*tls.Conn)
if !ok {
logger.Log(
"level", 0,
"msg", "invalid connection type",
"err", fmt.Errorf("expected tls conn, got %T", conn),
)
goto reject
}
identifier, err = id.PeerID(tlsConn)
if err != nil {
logger.Log(
"level", 2,
"msg", "certificate error",
"err", err,
)
goto reject
}
logger = logger.With("identifier", identifier)
if !s.IsSubscribed(identifier) {
logger.Log(
"level", 2,
"msg", "unknown client",
)
goto reject
}
if err = conn.SetDeadline(time.Time{}); err != nil {
logger.Log(
"level", 2,
"msg", "setting infinite deadline failed",
"err", err,
)
goto reject
}
if err := s.connPool.AddConn(conn, identifier); err != nil {
logger.Log(
"level", 2,
"msg", "adding connection failed",
"err", err,
)
goto reject
}
inConnPool = true
req, err = http.NewRequest(http.MethodConnect, s.connPool.URL(identifier), nil)
if err != nil {
logger.Log(
"level", 2,
"msg", "handshake request creation failed",
"err", err,
)
goto reject
}
{
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
req = req.WithContext(ctx)
}
resp, err = s.httpClient.Do(req)
if err != nil {
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Status %s", resp.Status)
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
if resp.ContentLength == 0 {
err = fmt.Errorf("Tunnels Content-Legth: 0")
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
if err = json.NewDecoder(&io.LimitedReader{R: resp.Body, N: 126976}).Decode(&tunnels); err != nil {
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
if len(tunnels) == 0 {
err = fmt.Errorf("No tunnels")
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
if err = s.addTunnels(tunnels, identifier); err != nil {
logger.Log(
"level", 2,
"msg", "handshake failed",
"err", err,
)
goto reject
}
logger.Log(
"level", 1,
"action", "connected",
)
return
reject:
logger.Log(
"level", 1,
"action", "rejected",
)
if inConnPool {
s.notifyError(err, identifier)
s.connPool.DeleteConn(identifier)
}
conn.Close()
}
// notifyError tries to send error to client.
func (s *Server) notifyError(serverError error, identifier id.ID) {
if serverError == nil {
return
}
req, err := http.NewRequest(http.MethodConnect, s.connPool.URL(identifier), nil)
if err != nil {
s.logger.Log(
"level", 2,
"action", "client error notification failed",
"identifier", identifier,
"err", err,
)
return
}
req.Header.Set(proto.HeaderError, serverError.Error())
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
s.httpClient.Do(req.WithContext(ctx))
}
// addTunnels invokes addHost or addListener based on data from proto.Tunnel. If
// a tunnel cannot be added whole batch is reverted.
func (s *Server) addTunnels(tunnels map[string]*proto.Tunnel, identifier id.ID) error {
i := &RegistryItem{
Hosts: []*HostAuth{},
Listeners: []net.Listener{},
}
var err error
for name, t := range tunnels {
switch t.Protocol {
case proto.HTTP:
i.Hosts = append(i.Hosts, &HostAuth{t.Host, NewAuth(t.Auth)})
case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX:
var l net.Listener
l, err = net.Listen(t.Protocol, t.Addr)
if err != nil {
goto rollback
}
s.logger.Log(
"level", 2,
"action", "open listener",
"identifier", identifier,
"addr", l.Addr(),
)
i.Listeners = append(i.Listeners, l)
default:
err = fmt.Errorf("unsupported protocol for tunnel %s: %s", name, t.Protocol)
goto rollback
}
}
err = s.set(i, identifier)
if err != nil {
goto rollback
}
for _, l := range i.Listeners {
go s.listen(l, identifier)
}
return nil
rollback:
for _, l := range i.Listeners {
l.Close()
}
return err
}
// Unsubscribe removes client from registry, disconnects client if already
// connected and returns it's RegistryItem.
func (s *Server) Unsubscribe(identifier id.ID) *RegistryItem {
s.connPool.DeleteConn(identifier)
return s.registry.Unsubscribe(identifier)
}
func (s *Server) listen(l net.Listener, identifier id.ID) {
addr := l.Addr().String()
for {
conn, err := l.Accept()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
s.logger.Log(
"level", 2,
"action", "listener closed",
"identifier", identifier,
"addr", addr,
)
return
}
s.logger.Log(
"level", 0,
"msg", "accept connection failed",
"identifier", identifier,
"addr", addr,
"err", err,
)
continue
}
msg := &proto.ControlMessage{
Action: proto.ActionProxy,
Protocol: l.Addr().Network(),
ForwardedFor: conn.RemoteAddr().String(),
ForwardedBy: l.Addr().String(),
}
go func() {
if err := s.proxyConn(identifier, conn, msg); err != nil {
s.logger.Log(
"level", 0,
"msg", "proxy error",
"identifier", identifier,
"ctrlMsg", msg,
"err", err,
)
}
}()
}
}
// ServeHTTP proxies http connection to the client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
resp, err := s.RoundTrip(r)
if err == errUnauthorised {
w.Header().Set("WWW-Authenticate", "Basic realm=\"User Visible Realm\"")
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if err != nil {
s.logger.Log(
"level", 0,
"action", "round trip failed",
"addr", r.RemoteAddr,
"url", r.URL,
"err", err,
)
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
if resp.Body != nil {
transfer(w, resp.Body, log.NewContext(s.logger).With(
"dir", "client to user",
"dst", r.RemoteAddr,
"src", r.Host,
))
}
}
// RoundTrip is http.RoundTriper implementation.
func (s *Server) RoundTrip(r *http.Request) (*http.Response, error) {
msg := &proto.ControlMessage{
Action: proto.ActionProxy,
Protocol: proto.HTTP,
ForwardedFor: r.RemoteAddr,
ForwardedBy: r.Host,
}
identifier, auth, ok := s.Subscriber(r.Host)
if !ok {
return nil, errClientNotSubscribed
}
if auth != nil {
user, password, _ := r.BasicAuth()
if auth.User != user || auth.Password != password {
return nil, errUnauthorised
}
r.Header.Del("Authorization")
}
return s.proxyHTTP(identifier, r, msg)
}
func (s *Server) proxyConn(identifier id.ID, conn net.Conn, msg *proto.ControlMessage) error {
s.logger.Log(
"level", 2,
"action", "proxy",
"identifier", identifier,
"ctrlMsg", msg,
)
defer conn.Close()
pr, pw := io.Pipe()
defer pr.Close()
defer pw.Close()
req, err := s.connectRequest(identifier, msg, pr)
if err != nil {
return err
}
done := make(chan struct{})
go func() {
transfer(pw, conn, log.NewContext(s.logger).With(
"dir", "user to client",
"dst", identifier,
"src", conn.RemoteAddr(),
))
close(done)
}()
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("io error: %s", err)
}
transfer(conn, resp.Body, log.NewContext(s.logger).With(
"dir", "client to user",
"dst", conn.RemoteAddr(),
"src", identifier,
))
<-done
s.logger.Log(
"level", 2,
"action", "proxy done",
"identifier", identifier,
"ctrlMsg", msg,
)
return nil
}
func (s *Server) proxyHTTP(identifier id.ID, r *http.Request, msg *proto.ControlMessage) (*http.Response, error) {
s.logger.Log(
"level", 2,
"action", "proxy",
"identifier", identifier,
"ctrlMsg", msg,
)
pr, pw := io.Pipe()
defer pr.Close()
defer pw.Close()
req, err := s.connectRequest(identifier, msg, pr)
if err != nil {
return nil, fmt.Errorf("proxy request error: %s", err)
}
go func() {
cw := &countWriter{pw, 0}
err := r.Write(cw)
if err != nil {
s.logger.Log(
"level", 0,
"msg", "proxy error",
"identifier", identifier,
"ctrlMsg", msg,
"err", err,
)
}
s.logger.Log(
"level", 3,
"action", "transferred",
"identifier", identifier,
"bytes", cw.count,
"dir", "user to client",
"dst", r.Host,
"src", r.RemoteAddr,
)
if r.Body != nil {
r.Body.Close()
}
}()
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("io error: %s", err)
}
s.logger.Log(
"level", 2,
"action", "proxy done",
"identifier", identifier,
"ctrlMsg", msg,
"status code", resp.StatusCode,
)
return resp, nil
}
// connectRequest creates HTTP request to client with a given identifier having
// control message and data input stream, output data stream results from
// response the created request.
func (s *Server) connectRequest(identifier id.ID, msg *proto.ControlMessage, r io.Reader) (*http.Request, error) {
req, err := http.NewRequest(http.MethodPut, s.connPool.URL(identifier), r)
if err != nil {
return nil, fmt.Errorf("could not create request: %s", err)
}
msg.Update(req.Header)
return req, nil
}
// Addr returns network address clients connect to.
func (s *Server) Addr() string {
if s.listener == nil {
return ""
}
return s.listener.Addr().String()
}
// Stop closes the server.
func (s *Server) Stop() {
s.logger.Log(
"level", 1,
"action", "stop",
)
if s.listener != nil {
s.listener.Close()
}
}

145
vendor/github.com/mmatczuk/go-http-tunnel/tcpproxy.go generated vendored Normal file
View File

@ -0,0 +1,145 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"fmt"
"io"
"net"
"github.com/mmatczuk/go-http-tunnel/log"
"github.com/mmatczuk/go-http-tunnel/proto"
)
// TCPProxy forwards TCP streams.
type TCPProxy struct {
// localAddr specifies default TCP address of the local server.
localAddr string
// localAddrMap specifies mapping from ControlMessage ForwardedBy to
// local server address, keys may contain host and port, only host or
// only port. The order of precedence is the following
// * host and port
// * port
// * host
localAddrMap map[string]string
// logger is the proxy logger.
logger log.Logger
}
// NewTCPProxy creates new direct TCPProxy, everything will be proxied to
// localAddr.
func NewTCPProxy(localAddr string, logger log.Logger) *TCPProxy {
if localAddr == "" {
panic("missing localAddr")
}
if logger == nil {
logger = log.NewNopLogger()
}
return &TCPProxy{
localAddr: localAddr,
logger: logger,
}
}
// NewMultiTCPProxy creates a new dispatching TCPProxy, connections may go to
// different backends based on localAddrMap.
func NewMultiTCPProxy(localAddrMap map[string]string, logger log.Logger) *TCPProxy {
if localAddrMap == nil {
panic("missing localAddrMap")
}
if logger == nil {
logger = log.NewNopLogger()
}
return &TCPProxy{
localAddrMap: localAddrMap,
logger: logger,
}
}
// Proxy is a ProxyFunc.
func (p *TCPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
switch msg.Protocol {
case proto.TCP, proto.TCP4, proto.TCP6:
// ok
default:
p.logger.Log(
"level", 0,
"msg", "unsupported protocol",
"ctrlMsg", msg,
)
return
}
target := p.localAddrFor(msg.ForwardedBy)
if target == "" {
p.logger.Log(
"level", 1,
"msg", "no target",
"ctrlMsg", msg,
)
return
}
local, err := net.DialTimeout("tcp", target, DefaultTimeout)
if err != nil {
p.logger.Log(
"level", 0,
"msg", "dial failed",
"target", target,
"ctrlMsg", msg,
"err", err,
)
return
}
done := make(chan struct{})
go func() {
transfer(flushWriter{w}, local, log.NewContext(p.logger).With(
"dst", msg.ForwardedBy,
"src", target,
))
close(done)
}()
transfer(local, r, log.NewContext(p.logger).With(
"dst", target,
"src", msg.ForwardedBy,
))
<-done
}
func (p *TCPProxy) localAddrFor(hostPort string) string {
if p.localAddrMap == nil {
return p.localAddr
}
// try hostPort
if addr := p.localAddrMap[hostPort]; addr != "" {
return addr
}
// try port
host, port, _ := net.SplitHostPort(hostPort)
if addr := p.localAddrMap[port]; addr != "" {
return addr
}
// try 0.0.0.0:port
if addr := p.localAddrMap[fmt.Sprintf("0.0.0.0:%s", port)]; addr != "" {
return addr
}
// try host
if addr := p.localAddrMap[host]; addr != "" {
return addr
}
return p.localAddr
}

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIJAKbvDXgcAiNEMA0GCSqGSIb3DQEBCwUAMFUxCzAJBgNV
BAYTAlBMMRQwEgYDVQQIDAtNYXpvd2llY2tpZTEPMA0GA1UEBwwGV2Fyc2F3MREw
DwYDVQQKDAhDb2RpTGltZTEMMAoGA1UECwwDUm5EMB4XDTE2MDYwNzA2MzkxOVoX
DTE2MDcwNzA2MzkxOVowVTELMAkGA1UEBhMCUEwxFDASBgNVBAgMC01hem93aWVj
a2llMQ8wDQYDVQQHDAZXYXJzYXcxETAPBgNVBAoMCENvZGlMaW1lMQwwCgYDVQQL
DANSbkQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOXU4I9ytB8BaX
4kc93aox1hs0lh85cOHLdr+jSN1U+e/FYjtGX5t/cLx2zYBYbkdpLc0tUO4oAVcb
mqTtnlpxA6kDQWsCZlvJ3Cxc7ayo4we4ojGMcn2zDpoD902UUV80wWzIvyYFyVAW
0dvEN3YKQpqLEf3pqaMxRCYsidRDaDkRn/id7ugoIJ3fhb6ea2WAllVTHTzJR8tz
rHSsYrcvS938bpn2ZprR32z6FMTzUZe3LnuhQUVvDILDp3QY9ycgtiA1maH47YkT
E42pCft7VtoqQFAt4vWPCbhWqFapu635az7y+zF6CWM8q3n+d4I+X9JqrhkRBSfQ
u2njQb6DAgMBAAGjUDBOMB0GA1UdDgQWBBTZGAHyp2CyGP+kFwxR+zDQqn+bVDAf
BgNVHSMEGDAWgBTZGAHyp2CyGP+kFwxR+zDQqn+bVDAMBgNVHRMEBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQAFkhH2f9vMETDvMPq9b4RUdUNq8p+3rvYnw1gA8mja
PzXdy9e+YCI5hI9OY/uMJjfbTPwTYESPu8+YZNKqHfeQwk7dkhrX3FQplr4SAMdO
s+ztddCFvVHcDbaAm89CCXBgWjruG/tV/pZbrfOXt6vfAtJHvD07vnKK3PqSD4v0
SnahMx1MWlmVeAOT54TVAqxeFspo6F9eAihw3rje8bnRvwzVaTH6QgXf+Ks6uoE0
OPSiqHYpfwkl8WSlla0XUceMm7c15RgrXHQTNqBL6JA9uTuAODr4ga/7Cua2sJyG
Pe8wp7oz5djAnierAs8eJQ2Sa9CmNEFkoIYNu92nH3kM
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOXU4I9ytB8BaX
4kc93aox1hs0lh85cOHLdr+jSN1U+e/FYjtGX5t/cLx2zYBYbkdpLc0tUO4oAVcb
mqTtnlpxA6kDQWsCZlvJ3Cxc7ayo4we4ojGMcn2zDpoD902UUV80wWzIvyYFyVAW
0dvEN3YKQpqLEf3pqaMxRCYsidRDaDkRn/id7ugoIJ3fhb6ea2WAllVTHTzJR8tz
rHSsYrcvS938bpn2ZprR32z6FMTzUZe3LnuhQUVvDILDp3QY9ycgtiA1maH47YkT
E42pCft7VtoqQFAt4vWPCbhWqFapu635az7y+zF6CWM8q3n+d4I+X9JqrhkRBSfQ
u2njQb6DAgMBAAECggEBAMO2Ra3HDCVq12KQXVRVB3ZgQkjrHw3Q+rOGGVV4Y0CW
EUm3UdP6FHUWrAZX+yLi46LipzYVDOiv7LbnQQeCKPAJsp69ygjqnp6gywoO9rLt
LYNzf15drszESlj8j3zcd1iHIO56KktOk0AxIyXCG5a7d+nw1Ehoc7bjlPikdsS9
PBum9dw67Nl8XB0QpdXjDtSleHPvqT8PbAyDVr0nSBeulNh5PzJMvRtkcguTn6p/
y+LreatjR+XzgfRf+qtvYcrTXq1D99OOUKqSKGwgHpFbN7VcqbxOA5CalBf3NbzG
PYe4JSP8fwtLcDUj54GDrgX4vvZ8JlCqdAu8HUCaBfECgYEA+Mw92Olmx8OmJVLS
RURXlcMUuRe+5z9CHYfvjavdobcsyQ1xOlZJqm0wlEr3PIVAdvDKb9ErpDDtX/LW
NOL6ojz3d+XM6Um786Bn3B1LbDgcxwpSKGqvColspHIs0QnpvZ4s/dRX0WWq0b0S
NjpMFZgqlkOBIknX5P25bPIN7AUCgYEA1Faaja437+/thNetNbG4yHOBtGD25d4C
kblLGD6QvkD1q06BHm9WpVkivltIWuiP8cB0QVynwFv4Cmy0v63AsUw1kb8N2gYd
xXxpkPLRu+EvDGRocCJU6QJol9Lfldo5ir7pScbgUjM3nWIGdbBFW7jv5SzjUQae
odo7QpJbjucCgYBK9D0tvCNay3aih/ERLSW12K/Fk4HP6R7iBrIE3GJI9gZoC8Sw
7o4C6iJYir0xXnOtYZ2bUkjzjkn1PhOKm1cmyXdEh9bT8YLOQuUHS0wNrln9HP7j
bkCNzBkO8dbOo03n8l9bmT1buGVeCrgR3j5NwyoRWwTsb5K7SjUyvTm0gQKBgHRr
NznO92RaC8P17EWwNzvP+KFJOJU3b/ktunqEcx+cxhUyaaCiMsNdZ6suqTEOqT1G
43aismbJBenRSBh/z1JmEkjik1miWNhaKhcKyutTv1PwCULRz/QhGe+D8opap4nm
ukl0/LCU3D0x7ZDBIIX1k7H3NnrKQldDK5KIZCKpAoGBAOjcaDtzpnv07gkaOcZH
KarZMIrgDgN25K/WmYYSY1UC4Xu/pAK+8dva4mvxEELuYPqkhXIhMRVJ9dbCox2x
Nzlu/KIwyQYaRyZgQfrv1nssXuFfxdY8i8zTteRV0awuiOMS36a16sYQCChZp8p3
PkBuxPbRBLmPg/VkAJE/KbQ8
-----END PRIVATE KEY-----

View File

@ -0,0 +1,52 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Automatically generated by MockGen. DO NOT EDIT!
// Source: github.com/mmatczuk/go-http-tunnel (interfaces: Backoff)
package tunnelmock
import (
gomock "github.com/golang/mock/gomock"
time "time"
)
// Mock of Backoff interface
type MockBackoff struct {
ctrl *gomock.Controller
recorder *_MockBackoffRecorder
}
// Recorder for MockBackoff (not exported)
type _MockBackoffRecorder struct {
mock *MockBackoff
}
func NewMockBackoff(ctrl *gomock.Controller) *MockBackoff {
mock := &MockBackoff{ctrl: ctrl}
mock.recorder = &_MockBackoffRecorder{mock}
return mock
}
func (_m *MockBackoff) EXPECT() *_MockBackoffRecorder {
return _m.recorder
}
func (_m *MockBackoff) NextBackOff() time.Duration {
ret := _m.ctrl.Call(_m, "NextBackOff")
ret0, _ := ret[0].(time.Duration)
return ret0
}
func (_mr *_MockBackoffRecorder) NextBackOff() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "NextBackOff")
}
func (_m *MockBackoff) Reset() {
_m.ctrl.Call(_m, "Reset")
}
func (_mr *_MockBackoffRecorder) Reset() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Reset")
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Automatically generated by MockGen. DO NOT EDIT!
// Source: github.com/mmatczuk/go-http-tunnel/log (interfaces: Logger)
package tunnelmock
import (
gomock "github.com/golang/mock/gomock"
)
// Mock of Logger interface
type MockLogger struct {
ctrl *gomock.Controller
recorder *_MockLoggerRecorder
}
// Recorder for MockLogger (not exported)
type _MockLoggerRecorder struct {
mock *MockLogger
}
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &_MockLoggerRecorder{mock}
return mock
}
func (_m *MockLogger) EXPECT() *_MockLoggerRecorder {
return _m.recorder
}
func (_m *MockLogger) Log(_param0 ...interface{}) error {
_s := []interface{}{}
_s = append(_s, _param0...)
ret := _m.ctrl.Call(_m, "Log", _s...)
ret0, _ := ret[0].(error)
return ret0
}
func (_mr *_MockLoggerRecorder) Log(arg0 ...interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Log", arg0...)
}

78
vendor/github.com/mmatczuk/go-http-tunnel/utils.go generated vendored Normal file
View File

@ -0,0 +1,78 @@
// Copyright (C) 2017 Michał Matczuk
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tunnel
import (
"io"
"net/http"
"github.com/mmatczuk/go-http-tunnel/log"
)
type closeWriter interface {
CloseWrite() error
}
type closeReader interface {
CloseRead() error
}
func transfer(dst io.Writer, src io.ReadCloser, logger log.Logger) {
n, err := io.Copy(dst, src)
if err != nil {
logger.Log(
"level", 2,
"msg", "copy error",
"err", err,
)
}
if d, ok := dst.(closeWriter); ok {
d.CloseWrite()
}
if s, ok := src.(closeReader); ok {
s.CloseRead()
} else {
src.Close()
}
logger.Log(
"level", 3,
"action", "transferred",
"bytes", n,
)
}
func copyHeader(dst, src http.Header) {
for k, v := range src {
vv := make([]string, len(v))
copy(vv, v)
dst[k] = vv
}
}
type countWriter struct {
w io.Writer
count int64
}
func (cw *countWriter) Write(p []byte) (n int, err error) {
n, err = cw.w.Write(p)
cw.count += int64(n)
return
}
type flushWriter struct {
w io.Writer
}
func (fw flushWriter) Write(p []byte) (n int, err error) {
n, err = fw.w.Write(p)
if f, ok := fw.w.(http.Flusher); ok {
f.Flush()
}
return
}