dep: add go-http-tunnel
This commit is contained in:
parent
e8dbca1d99
commit
00819e8dc1
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -0,0 +1,15 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.x
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- moreutils
|
||||
|
||||
install:
|
||||
- make get-tools
|
||||
- make get-deps
|
||||
|
||||
script:
|
||||
- make
|
|
@ -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
|
|
@ -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"
|
|
@ -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.
|
|
@ -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
|
|
@ -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!
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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% | - | - |
|
||||
|
||||
|
||||
|
BIN
vendor/github.com/mmatczuk/go-http-tunnel/benchmark/constload.png
generated
vendored
Normal file
BIN
vendor/github.com/mmatczuk/go-http-tunnel/benchmark/constload.png
generated
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 137 KiB |
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
115
vendor/github.com/mmatczuk/go-http-tunnel/cmd/tunnel/normalize_test.go
generated
vendored
Normal file
115
vendor/github.com/mmatczuk/go-http-tunnel/cmd/tunnel/normalize_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
|
@ -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
|
|
@ -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")
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -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...)
|
||||
}
|
31
vendor/github.com/mmatczuk/go-http-tunnel/log/filterlogger_test.go
generated
vendored
Normal file
31
vendor/github.com/mmatczuk/go-http-tunnel/log/filterlogger_test.go
generated
vendored
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
80
vendor/github.com/mmatczuk/go-http-tunnel/proto/controlmsg_test.go
generated
vendored
Normal file
80
vendor/github.com/mmatczuk/go-http-tunnel/proto/controlmsg_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 ®istry{
|
||||
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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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")
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue