Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c00a3fd8b | ||
|
|
7a9f972030 | ||
|
|
264fbe83fe | ||
|
|
3f8e4b8e0b | ||
|
|
5ddfa6232c | ||
|
|
cda0e2c050 | ||
|
|
69733b1e3c | ||
|
|
92569be139 | ||
|
|
c72868cbf5 | ||
|
|
e58d35972d | ||
|
|
98902cff73 | ||
|
|
a269effc05 | ||
|
|
ed2abcaa95 | ||
|
|
ea93073174 | ||
|
|
f2009d7f52 | ||
|
|
f956a32d64 | ||
|
|
85723df89b | ||
|
|
1860b0a1e1 | ||
|
|
f20d426690 | ||
|
|
d0c3f9cd03 | ||
|
|
148702929b | ||
|
|
3af3e39b20 | ||
|
|
ef9aa6251d | ||
|
|
4ce53025e1 | ||
|
|
46dd79a949 | ||
|
|
d08b89020f | ||
|
|
39bebc5e25 | ||
|
|
5335f85202 | ||
|
|
0134667055 | ||
|
|
9de09f3f1d | ||
|
|
0ca93d33aa | ||
|
|
0468760901 | ||
|
|
c47241cf3a | ||
|
|
54b2056c12 | ||
|
|
1d93622aec | ||
|
|
c7c9a2d07a | ||
|
|
f136b8486e | ||
|
|
7fc854b620 | ||
|
|
3d8f09ba08 | ||
|
|
ed5729bd63 | ||
|
|
ce2b352574 | ||
|
|
179a76b8cf | ||
|
|
8d044e722c | ||
|
|
5608df7ec5 | ||
|
|
7bf4547911 | ||
|
|
7b25fe80e0 | ||
|
|
97fcb417ac | ||
|
|
22537c0136 | ||
|
|
11dfbc7045 | ||
|
|
824a4b10ab | ||
|
|
13be54d4e7 | ||
|
|
15e017d285 | ||
|
|
917d636f3a | ||
|
|
16615300f5 | ||
|
|
1fa6bb644e | ||
|
|
92e125b137 | ||
|
|
280901df87 | ||
|
|
b32d551830 | ||
|
|
055ca06302 | ||
|
|
b3eb99f457 | ||
|
|
ca72fcb05c | ||
|
|
f17bc20337 | ||
|
|
7c1977c30f | ||
|
|
e4ce075701 | ||
|
|
a5d4279cca | ||
|
|
b488fe6bd7 | ||
|
|
00ae5d77db | ||
|
|
83af364b1d | ||
|
|
83dc742d38 | ||
|
|
937f665e51 | ||
|
|
a911f7fb65 | ||
|
|
cf5bd551be | ||
|
|
1bf705e4ee | ||
|
|
ea088b69f2 | ||
|
|
3655bd8027 | ||
|
|
2fff31ff61 | ||
|
|
a9f659216e | ||
|
|
a29febde5c | ||
|
|
677ea6ab95 | ||
|
|
23fd33b344 | ||
|
|
264ac352f7 | ||
|
|
a2cf6b70c1 | ||
|
|
d0b9161993 | ||
|
|
8d7081e747 | ||
|
|
fd394cb336 | ||
|
|
9373eb8f4d | ||
|
|
8339a76cc1 | ||
|
|
8f6637c784 | ||
|
|
8054e64ad7 | ||
|
|
eb60b72aa4 | ||
|
|
4d08291813 | ||
|
|
ea1b1ed632 | ||
|
|
bfd7e64d9f | ||
|
|
63c70c4996 | ||
|
|
20364f6de0 | ||
|
|
aa99cc1cd4 | ||
|
|
d914e26055 | ||
|
|
01167cb136 | ||
|
|
f5a24b30e8 | ||
|
|
bfbfdd9ee9 | ||
|
|
b157045a37 | ||
|
|
bf7a36f03e | ||
|
|
c5b7e331da | ||
|
|
e6141ac6ac | ||
|
|
0754d30550 | ||
|
|
3d17d52659 | ||
|
|
51e8ca782b | ||
|
|
999f1063d0 | ||
|
|
61dd957018 | ||
|
|
f88378a8da | ||
|
|
0115a63898 | ||
|
|
53e28ef0c1 | ||
|
|
b4082e7f89 | ||
|
|
151433e2fd | ||
|
|
77e3f140ca | ||
|
|
eef7b6b755 | ||
|
|
fb0c343161 | ||
|
|
7032606103 | ||
|
|
aea0524bb5 | ||
|
|
078bbb1fdc | ||
|
|
fbcde8774b | ||
|
|
816f5e8882 | ||
|
|
bd3356071e | ||
|
|
b63f9bc718 | ||
|
|
aa4543b824 | ||
|
|
896afc0141 | ||
|
|
4f4bd7bd06 | ||
|
|
7fb02bb768 | ||
|
|
6a2b9c31b1 | ||
|
|
07c2fdc693 | ||
|
|
b275aa7123 | ||
|
|
10a80333da | ||
|
|
09953ac19a | ||
|
|
081a1f7fde | ||
|
|
26038dbf1b | ||
|
|
8bfebdc277 | ||
|
|
4e20bd9db8 | ||
|
|
d2ec1ec080 | ||
|
|
8017c0653b | ||
|
|
452ba7a575 | ||
|
|
167cf25dae | ||
|
|
9f8bd8adc3 | ||
|
|
4db6e7143d | ||
|
|
65411b54bd | ||
|
|
213eb595a6 | ||
|
|
160bf79cb5 | ||
|
|
95ffd273f2 | ||
|
|
063389d36b | ||
|
|
514c16c91f | ||
|
|
f6a3742a74 | ||
|
|
d09037b0ff | ||
|
|
0c970983e2 | ||
|
|
a72e0fd7c7 | ||
|
|
41865cb20d | ||
|
|
66a919e63b | ||
|
|
569013e691 | ||
|
|
bf6ce69203 | ||
|
|
030b436d62 | ||
|
|
5b27861135 | ||
|
|
aa3fa9a08b | ||
|
|
53a940ec18 | ||
|
|
e5840c1e5d | ||
|
|
f1766e5884 | ||
|
|
404e70c1eb | ||
|
|
421b365483 | ||
|
|
c072be702c | ||
|
|
0f09e5feae | ||
|
|
35d8fc20ff | ||
|
|
492182497a | ||
|
|
0d49d7223d | ||
|
|
b136b8f1aa | ||
|
|
86c49329c0 | ||
|
|
e200e75da4 | ||
|
|
235e5e1a03 | ||
|
|
b1ecf6b2b7 | ||
|
|
d4421470c6 | ||
|
|
33e6abd696 | ||
|
|
4823077f97 | ||
|
|
885e29abde | ||
|
|
d704c5b2c6 | ||
|
|
6a7027cc64 | ||
|
|
26e9190fac | ||
|
|
c0b7bf6429 | ||
|
|
a51160d372 | ||
|
|
9f8d36a6a3 | ||
|
|
6ae24bc776 | ||
|
|
7825238a5f | ||
|
|
54751ce313 | ||
|
|
e0ff5ad359 | ||
|
|
733452c53c | ||
|
|
0b7ceda198 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,6 +3,7 @@ bin/
|
||||
pkg/
|
||||
src/code.google.com
|
||||
src/github.com
|
||||
src/ngrok/client/views/web/static/*.html.go
|
||||
src/ngrok/client/views/web/static/*.css.go
|
||||
src/ngrok/client/views/web/static/*.js.go
|
||||
src/bitbucket.org
|
||||
src/launchpad.net
|
||||
src/ngrok/client/assets/assets_release.go
|
||||
src/ngrok/server/assets/assets_release.go
|
||||
|
||||
22
.travis.yml
Normal file
22
.travis.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
script: make release-all
|
||||
install: true
|
||||
after_success:
|
||||
- sudo pip install boto
|
||||
- ls -lr bin/*
|
||||
- NGROK="bin/${GOOS}_${GOARCH}/ngrok" NGROKD="bin/${GOOS}_${GOARCH}/ngrokd" VERSION=$(GOOS="" GOARCH="" GOPATH=$(pwd) go run build/version.go) python build/travis.py
|
||||
before_script:
|
||||
- pushd /home/travis/.gvm/gos/go1.1.1/src
|
||||
- ./make.bash
|
||||
- popd
|
||||
env:
|
||||
global:
|
||||
- secure: Fd5zHi58jx8lsPDv4tkRFzXSY0KnPJZuZ+LvnRcpX4+3xJsuZU6moOfrOcGqDOm7/SqZRVZRKZapE772+8sXNKPmwSXHRsZEsUgqxdehFzlVP4PQN5efOdI/quO2ibwVpZ6Idze5pelZburALd7/VbfHCTB/0P0WDMNvfHuFPQg=
|
||||
- secure: Yg+y996B4S7zHXO8j6JrRbgMf6yilHGWv6I+7oZf02d8IHYtAb6A9DveX/q+v24O8Q9WzXRU4ZIaG5nJksVwb19qcy4vpaUbvx00COi8tg0l9hdKIotrzAs+i8q5h0xUifE1iuqYJw39vYU/9vC/727GTvOfRpBCr+edcpc9uy0=
|
||||
- secure: CMF8ZfcoZK8AhFrmo6ljwP4ulYOSI9Pftp02rfNcM7LYPzU5HxFmcR7M4+3Qt0HAWxYrReozNQZYrV0s5jqIO0H4LZeHxgi7EHTam1BelhmHxL6VBMoR1mTi53bErWNhpoc0qyCnWMpnzNMObwtBVm9vjacm4NJMWqQjte/0Kdw=
|
||||
matrix:
|
||||
- GOOS=linux GOARCH=arm
|
||||
- GOOS=linux GOARCH=386
|
||||
- GOOS=windows GOARCH=386
|
||||
- GOOS=darwin GOARCH=amd64
|
||||
go:
|
||||
- 1.1
|
||||
6
CONTRIBUTORS
Normal file
6
CONTRIBUTORS
Normal file
@@ -0,0 +1,6 @@
|
||||
Contributors to ngrok, both large and small:
|
||||
Alan Shreve (inconshreveable)
|
||||
Kyle Conroy (kyleconroy)
|
||||
Caleb Spare (cespare)
|
||||
Stephen Huenneke (skastel)
|
||||
Nick Presta (nickpresta)
|
||||
44
Makefile
44
Makefile
@@ -1,49 +1,39 @@
|
||||
.PHONY: default server client deps clean all release-client release-server bindata
|
||||
BUILDTAGS=
|
||||
.PHONY: default server client deps fmt clean all release-client release-server release-all client-assets server-assets
|
||||
export GOPATH:=$(shell pwd)
|
||||
|
||||
default: all
|
||||
|
||||
deps:
|
||||
go get -d -v ngrok/...
|
||||
go get -tags '$(BUILDTAGS)' -d -v ngrok/...
|
||||
|
||||
server: deps
|
||||
go install -tags '$(BUILDTAGS)' main/ngrokd
|
||||
go install -tags '$(BUILDTAGS)' ngrok/main/ngrokd
|
||||
|
||||
fmt:
|
||||
go fmt ngrok/...
|
||||
|
||||
client: deps
|
||||
go install -tags '$(BUILDTAGS)' main/ngrok
|
||||
go install -tags '$(BUILDTAGS)' ngrok/main/ngrok
|
||||
|
||||
client-assets:
|
||||
go get github.com/inconshreveable/go-bindata
|
||||
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
|
||||
bin/go-bindata -o src/ngrok/client/assets assets/client
|
||||
|
||||
server-assets:
|
||||
go get github.com/inconshreveable/go-bindata
|
||||
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
|
||||
bin/go-bindata -o src/ngrok/server/assets assets/server
|
||||
|
||||
release-client: BUILDTAGS=release
|
||||
release-client: bindata-client client
|
||||
release-client: client-assets client
|
||||
|
||||
release-server: BUILDTAGS=release
|
||||
release-server: server
|
||||
release-server: server-assets server
|
||||
|
||||
release-all: release-client release-server
|
||||
|
||||
certs:
|
||||
go get github.com/inconshreveable/go-bindata
|
||||
./bin/go-bindata -i assets/tls/snakeoil.crt -o src/ngrok/server/tls/snakeoil.crt.go -m -p tls -f snakeoilCrt
|
||||
./bin/go-bindata -i assets/tls/snakeoil.key -o src/ngrok/server/tls/snakeoil.key.go -m -p tls -f snakeoilKey
|
||||
./bin/go-bindata -i assets/tls/snakeoilca.crt -o src/ngrok/client/tls/snakeoilca.crt.go -m -p tls -f snakeoilCaCrt
|
||||
./bin/go-bindata -i assets/tls/ngrokroot.crt -o src/ngrok/client/tls/ngrokroot.crt.go -m -p tls -f ngrokRootCrt
|
||||
|
||||
bindata-client:
|
||||
go get github.com/inconshreveable/go-bindata
|
||||
./bin/go-bindata -b release -i assets/page.html -o src/ngrok/client/views/web/static/page.html.go -m -p static -f PageHtml
|
||||
./bin/go-bindata -b release -i assets/highlight.min.css -o src/ngrok/client/views/web/static/highlight.css.go -m -p static -f HighlightCss
|
||||
./bin/go-bindata -b release -i assets/highlight.min.js -o src/ngrok/client/views/web/static/highlight.js.go -m -p static -f HighlightJs
|
||||
./bin/go-bindata -b release -i assets/bootstrap.min.css -o src/ngrok/client/views/web/static/bootstrap.css.go -m -p static -f BootstrapCss
|
||||
./bin/go-bindata -b release -i assets/jquery-1.9.1.min.js -o src/ngrok/client/views/web/static/jquery-1.9.1.js.go -m -p static -f JqueryJs
|
||||
./bin/go-bindata -b release -i assets/vkbeautify.js -o src/ngrok/client/views/web/static/vkbeautify.js.go -m -p static -f VkBeautifyJs
|
||||
./bin/go-bindata -b release -i assets/angular.js -o src/ngrok/client/views/web/static/angular.js.go -m -p static -f AngularJs
|
||||
./bin/go-bindata -b release -i assets/ngrok.js -o src/ngrok/client/views/web/static/ngrok.js.go -m -p static -f NgrokJs
|
||||
./bin/go-bindata -b release -i assets/base64.js -o src/ngrok/client/views/web/static/base64.js.go -m -p static -f Base64Js
|
||||
|
||||
all: fmt client server
|
||||
|
||||
clean:
|
||||
go clean ngrok/...
|
||||
go clean -i -r ngrok/...
|
||||
|
||||
36
README.md
36
README.md
@@ -1,28 +1,34 @@
|
||||
# ngrok ([https://ngrok.com](https://ngrok.com))
|
||||
# ngrok - Introspected tunnels to localhost ([homepage](https://ngrok.com))
|
||||
### "I want to securely expose a web server to the internet and capture all traffic for detailed inspection and replay"
|
||||

|
||||
|
||||
## What is ngrok?
|
||||
ngrok is a tool that makes it easier to develop networked services (HTTP/TCP)
|
||||
It is a man-in-the-middle proxy server that you run locally on your development box. It allows you to do the following things:
|
||||
ngrok is a reverse proxy that creates a secure tunnel between from a public endpoint to a locally running web service.
|
||||
ngrok captures and analyzes all traffic over the tunnel for later inspection and replay.
|
||||
|
||||
- Inspect all http requests/responses sent to/from the proxied application
|
||||
- Replay any previously observed http request to the proxied application
|
||||
- Expose a local http server to the internet on a subdomain of ngrok.com
|
||||
- Expose a local tcp server to the internet on a random port on ngrok.com
|
||||
## What can I do with ngrok?
|
||||
- Expose any http service behind a NAT or firewall to the internet on a subdomain of ngrok.com
|
||||
- Expose any tcp service behind a NAT or firewall to the internet on a random port of ngrok.com
|
||||
- Inspect all http requests/resposes that are transmitted over the tunnel
|
||||
- Replay any request that was transmitted over the tunnel
|
||||
|
||||
|
||||
## What is ngrok useful for?
|
||||
- Temporarily sharing a website that is only running on your development machine
|
||||
- Demoing an app at a hackathon without deploying
|
||||
- Developing any services which consume webhooks (HTTP callbacks) by allowing you to replay those requests
|
||||
- Debugging and understanding any web service by inspecting the HTTP traffic
|
||||
- Running networked services on machines that are firewalled off from the internet
|
||||
|
||||
|
||||
## Downloading and installing ngrok
|
||||
ngrok has _no_ runtime dependencies. Just download a single binary for your platform and run it.
|
||||
ngrok has _no_ runtime dependencies. Just download a single binary for your platform and run it. Some premium features
|
||||
are only available by creating an account on ngrok.com. If you need them, [create an account on ngrok.com](https://ngrok.com/signup).
|
||||
|
||||
- [Linux](https://dl.ngrok.com/linux_386/ngrok.zip)
|
||||
- [Mac OSX](https://dl.ngrok.com/darwin_386/ngrok.zip)
|
||||
- [Windows](https://dl.ngrok.com/windows_386/ngrok.zip)
|
||||
|
||||
|
||||
## Compiling ngrok
|
||||
Binaries get placed in ./bin
|
||||
|
||||
git clone git@github.com:inconshreveable/ngrok.git
|
||||
cd ngrok && make
|
||||
bin/ngrok [LOCAL PORT]
|
||||
|
||||
## Developing on ngrok
|
||||
[ngrok developer's guide](docs/DEVELOPMENT.md)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ngrok</title>
|
||||
<link href="/static/highlight.min.css" rel="stylesheet">
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="/static/highlight.min.js"></script>
|
||||
<script src="/static/vkbeautify.js"></script>
|
||||
<script src="/static/jquery-1.9.1.min.js"></script>
|
||||
<script src="/static/angular.js"></script>
|
||||
<script src="/static/base64.js"></script>
|
||||
<script src="/static/ngrok.js"></script>
|
||||
<link href="/static/css/highlight.min.css" rel="stylesheet">
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="/static/js/highlight.min.js"></script>
|
||||
<script src="/static/js/vkbeautify.js"></script>
|
||||
<script src="/static/js/jquery-1.9.1.min.js"></script>
|
||||
<script src="/static/js/jquery.timeago.js"></script>
|
||||
<script src="/static/js/angular.js"></script>
|
||||
<script src="/static/js/angular-sanitize.min.js"></script>
|
||||
<script src="/static/js/base64.js"></script>
|
||||
<script src="/static/js/ngrok.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.data = JSON.parse({% . %});
|
||||
</script>
|
||||
@@ -17,11 +19,18 @@
|
||||
table.params { font-size: 12px; font-family: Courier, monospace; }
|
||||
.txn-selector tr { cursor: pointer; }
|
||||
.txn-selector tr:hover { background-color: #ddd; }
|
||||
tr.selected, tr.selected:hover {
|
||||
background-color: #ff9999;
|
||||
tr.selected, tr.selected:hover {
|
||||
background-color: #ff9999;
|
||||
background-color: #000000;
|
||||
color:white;
|
||||
|
||||
}
|
||||
.path {
|
||||
width: 300px;
|
||||
}
|
||||
.wrapped {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -46,7 +55,12 @@
|
||||
<div class="span6 offset3">
|
||||
<div class="well" style="padding: 20px 50px;">
|
||||
<h4>No requests to display yet</h4>
|
||||
<p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
|
||||
<hr />
|
||||
<h5>To get started, make a request to one of your tunnel URLs:</h5>
|
||||
<ul>
|
||||
<li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,15 +69,31 @@
|
||||
<h4>All Requests</h4>
|
||||
<table class="table txn-selector">
|
||||
<tr ng-controller="TxnNavItem" ng-class="{'selected':isActive()}" ng-repeat="txn in txns" ng-click="makeActive()">
|
||||
<td>{{ txn.Req.MethodPath }}</td>
|
||||
<td class="wrapped"><div class="path">{{ txn.Req.MethodPath }}</div></td>
|
||||
<td>{{ txn.Resp.Status }}</td>
|
||||
<td><span class="pull-right">{{ txn.Duration }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<div class="span6" ng-controller="HttpTxn" ng-show="!!Txn">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<span title="{{ISO8601(Txn.Start)}}" class="muted">
|
||||
{{TimeFormat(Txn.Start)}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<i class="icon-time"></i> Duration
|
||||
<span style="margin-left: 8px;" class="muted">{{Txn.Duration}}</span>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<i class="icon-user"></i> IP
|
||||
<span style="margin-left: 8px;" class="muted">{{Txn.ConnCtx.ClientAddr.split(":")[0]}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div ng-show="!!Req" ng-controller="HttpRequest">
|
||||
<h3>{{ Req.MethodPath }}</h3>
|
||||
<h3 class="wrapped">{{ Req.MethodPath }}</h3>
|
||||
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
|
||||
</div>
|
||||
|
||||
BIN
assets/client/static/img/glyphicons-halflings.png
Normal file
BIN
assets/client/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
13
assets/client/static/js/angular-sanitize.min.js
vendored
Normal file
13
assets/client/static/js/angular-sanitize.min.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
AngularJS v1.1.5
|
||||
(c) 2010-2012 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(I,h){'use strict';function i(a){var d={},a=a.split(","),c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function z(a,d){function c(a,b,c,f){b=h.lowercase(b);if(m[b])for(;e.last()&&n[e.last()];)g("",e.last());o[b]&&e.last()==b&&g("",b);(f=p[b]||!!f)||e.push(b);var j={};c.replace(A,function(a,b,d,c,g){j[b]=k(d||c||g||"")});d.start&&d.start(b,j,f)}function g(a,b){var c=0,g;if(b=h.lowercase(b))for(c=e.length-1;c>=0;c--)if(e[c]==b)break;if(c>=0){for(g=e.length-1;g>=c;g--)d.end&&d.end(e[g]);e.length=
|
||||
c}}var b,f,e=[],j=a;for(e.last=function(){return e[e.length-1]};a;){f=!0;if(!e.last()||!q[e.last()]){if(a.indexOf("<\!--")===0)b=a.indexOf("--\>"),b>=0&&(d.comment&&d.comment(a.substring(4,b)),a=a.substring(b+3),f=!1);else if(B.test(a)){if(b=a.match(r))a=a.substring(b[0].length),b[0].replace(r,g),f=!1}else if(C.test(a)&&(b=a.match(s)))a=a.substring(b[0].length),b[0].replace(s,c),f=!1;f&&(b=a.indexOf("<"),f=b<0?a:a.substring(0,b),a=b<0?"":a.substring(b),d.chars&&d.chars(k(f)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+
|
||||
e.last()+"[^>]*>","i"),function(a,b){b=b.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(b));return""}),g("",e.last());if(a==j)throw"Parse Error: "+a;j=a}g()}function k(a){l.innerHTML=a.replace(/</g,"<");return l.innerText||l.textContent||""}function t(a){return a.replace(/&/g,"&").replace(F,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function u(a){var d=!1,c=h.bind(a,a.push);return{start:function(a,b,f){a=h.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]==
|
||||
!0&&(c("<"),c(a),h.forEach(b,function(a,b){var d=h.lowercase(b);if(G[d]==!0&&(w[d]!==!0||a.match(H)))c(" "),c(b),c('="'),c(t(a)),c('"')}),c(f?"/>":">"))},end:function(a){a=h.lowercase(a);!d&&v[a]==!0&&(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||c(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^</,B=/^<\s*\//,D=/<\!--(.*?)--\>/g,
|
||||
E=/<!\[CDATA\[(.*?)]]\>/g,H=/^((ftp|https?):\/\/|mailto:|tel:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=h.extend({},y,x),m=h.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=h.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
|
||||
q=i("script,style"),v=h.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=h.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");h.module("ngSanitize",[]).value("$sanitize",function(a){var d=[];
|
||||
z(a,u(d));return d.join("")});h.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,c,g){c.addClass("ng-binding").data("$binding",g.ngBindHtml);d.$watch(g.ngBindHtml,function(b){b=a(b);c.html(b||"")})}}]);h.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(c,g){if(!c)return c;var b,f=c,e=[],j=u(e),i,k,l={};if(h.isDefined(g))l.target=g;for(;b=f.match(a);)i=
|
||||
b[0],b[2]==b[3]&&(i="mailto:"+i),k=b.index,j.chars(f.substr(0,k)),l.href=i,j.start("a",l),j.chars(b[0].replace(d,"")),j.end("a"),f=f.substring(k+b[0].length);j.chars(f);return e.join("")}})})(window,window.angular);
|
||||
193
assets/client/static/js/jquery.timeago.js
Normal file
193
assets/client/static/js/jquery.timeago.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Timeago is a jQuery plugin that makes it easy to support automatically
|
||||
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
||||
*
|
||||
* @name timeago
|
||||
* @version 1.3.0
|
||||
* @requires jQuery v1.2.3+
|
||||
* @author Ryan McGeary
|
||||
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* For usage and examples, visit:
|
||||
* http://timeago.yarp.com/
|
||||
*
|
||||
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
||||
*/
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
$.timeago = function(timestamp) {
|
||||
if (timestamp instanceof Date) {
|
||||
return inWords(timestamp);
|
||||
} else if (typeof timestamp === "string") {
|
||||
return inWords($.timeago.parse(timestamp));
|
||||
} else if (typeof timestamp === "number") {
|
||||
return inWords(new Date(timestamp));
|
||||
} else {
|
||||
return inWords($.timeago.datetime(timestamp));
|
||||
}
|
||||
};
|
||||
var $t = $.timeago;
|
||||
|
||||
$.extend($.timeago, {
|
||||
settings: {
|
||||
refreshMillis: 60000,
|
||||
allowFuture: false,
|
||||
localeTitle: false,
|
||||
cutoff: 0,
|
||||
strings: {
|
||||
prefixAgo: null,
|
||||
prefixFromNow: null,
|
||||
suffixAgo: "ago",
|
||||
suffixFromNow: "from now",
|
||||
seconds: "less than a minute",
|
||||
minute: "about a minute",
|
||||
minutes: "%d minutes",
|
||||
hour: "about an hour",
|
||||
hours: "about %d hours",
|
||||
day: "a day",
|
||||
days: "%d days",
|
||||
month: "about a month",
|
||||
months: "%d months",
|
||||
year: "about a year",
|
||||
years: "%d years",
|
||||
wordSeparator: " ",
|
||||
numbers: []
|
||||
}
|
||||
},
|
||||
inWords: function(distanceMillis) {
|
||||
var $l = this.settings.strings;
|
||||
var prefix = $l.prefixAgo;
|
||||
var suffix = $l.suffixAgo;
|
||||
if (this.settings.allowFuture) {
|
||||
if (distanceMillis < 0) {
|
||||
prefix = $l.prefixFromNow;
|
||||
suffix = $l.suffixFromNow;
|
||||
}
|
||||
}
|
||||
|
||||
var seconds = Math.abs(distanceMillis) / 1000;
|
||||
var minutes = seconds / 60;
|
||||
var hours = minutes / 60;
|
||||
var days = hours / 24;
|
||||
var years = days / 365;
|
||||
|
||||
function substitute(stringOrFunction, number) {
|
||||
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
||||
var value = ($l.numbers && $l.numbers[number]) || number;
|
||||
return string.replace(/%d/i, value);
|
||||
}
|
||||
|
||||
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
||||
seconds < 90 && substitute($l.minute, 1) ||
|
||||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
||||
minutes < 90 && substitute($l.hour, 1) ||
|
||||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
||||
hours < 42 && substitute($l.day, 1) ||
|
||||
days < 30 && substitute($l.days, Math.round(days)) ||
|
||||
days < 45 && substitute($l.month, 1) ||
|
||||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
||||
years < 1.5 && substitute($l.year, 1) ||
|
||||
substitute($l.years, Math.round(years));
|
||||
|
||||
var separator = $l.wordSeparator || "";
|
||||
if ($l.wordSeparator === undefined) { separator = " "; }
|
||||
return $.trim([prefix, words, suffix].join(separator));
|
||||
},
|
||||
parse: function(iso8601) {
|
||||
var s = $.trim(iso8601);
|
||||
s = s.replace(/\.\d+/,""); // remove milliseconds
|
||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||
return new Date(s);
|
||||
},
|
||||
datetime: function(elem) {
|
||||
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
||||
return $t.parse(iso8601);
|
||||
},
|
||||
isTime: function(elem) {
|
||||
// jQuery's `is()` doesn't play well with HTML5 in IE
|
||||
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
||||
}
|
||||
});
|
||||
|
||||
// functions that can be called via $(el).timeago('action')
|
||||
// init is default when no action is given
|
||||
// functions are called with context of a single element
|
||||
var functions = {
|
||||
init: function(){
|
||||
var refresh_el = $.proxy(refresh, this);
|
||||
refresh_el();
|
||||
var $s = $t.settings;
|
||||
if ($s.refreshMillis > 0) {
|
||||
setInterval(refresh_el, $s.refreshMillis);
|
||||
}
|
||||
},
|
||||
update: function(time){
|
||||
$(this).data('timeago', { datetime: $t.parse(time) });
|
||||
refresh.apply(this);
|
||||
},
|
||||
updateFromDOM: function(){
|
||||
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
|
||||
refresh.apply(this);
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.timeago = function(action, options) {
|
||||
var fn = action ? functions[action] : functions.init;
|
||||
if(!fn){
|
||||
throw new Error("Unknown function name '"+ action +"' for timeago");
|
||||
}
|
||||
// each over objects here and call the requested function
|
||||
this.each(function(){
|
||||
fn.call(this, options);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
var data = prepareData(this);
|
||||
var $s = $t.settings;
|
||||
|
||||
if (!isNaN(data.datetime)) {
|
||||
if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) {
|
||||
$(this).text(inWords(data.datetime));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function prepareData(element) {
|
||||
element = $(element);
|
||||
if (!element.data("timeago")) {
|
||||
element.data("timeago", { datetime: $t.datetime(element) });
|
||||
var text = $.trim(element.text());
|
||||
if ($t.settings.localeTitle) {
|
||||
element.attr("title", element.data('timeago').datetime.toLocaleString());
|
||||
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
||||
element.attr("title", text);
|
||||
}
|
||||
}
|
||||
return element.data("timeago");
|
||||
}
|
||||
|
||||
function inWords(date) {
|
||||
return $t.inWords(distance(date));
|
||||
}
|
||||
|
||||
function distance(date) {
|
||||
return (new Date().getTime() - date.getTime());
|
||||
}
|
||||
|
||||
// fix for IE6 suckage
|
||||
document.createElement("abbr");
|
||||
document.createElement("time");
|
||||
}));
|
||||
@@ -1,4 +1,4 @@
|
||||
var ngrok = angular.module("ngrok", []);
|
||||
var ngrok = angular.module("ngrok", ["ngSanitize"]);
|
||||
|
||||
var hexRepr = function(bytes) {
|
||||
var buf = [];
|
||||
@@ -47,7 +47,7 @@ ngrok.factory("txnSvc", function() {
|
||||
body.exists = body.Length > 0;
|
||||
body.hasError = !!body.Error;
|
||||
|
||||
body.syntaxClass = {
|
||||
var syntaxClass = {
|
||||
"text/xml": "xml",
|
||||
"application/xml": "xml",
|
||||
"text/html": "xml",
|
||||
@@ -63,12 +63,12 @@ ngrok.factory("txnSvc", function() {
|
||||
} else {
|
||||
body.Text = Base64.decode(body.Text).text;
|
||||
}
|
||||
|
||||
|
||||
// prettify
|
||||
var transform = {
|
||||
"xml": "xml",
|
||||
"json": "json"
|
||||
}[body.syntaxClass];
|
||||
}[syntaxClass];
|
||||
|
||||
if (!body.hasError && !!transform) {
|
||||
try {
|
||||
@@ -79,6 +79,13 @@ ngrok.factory("txnSvc", function() {
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!!syntaxClass) {
|
||||
body.Text = hljs.highlight(syntaxClass, body.Text).value;
|
||||
} else {
|
||||
// highlight.js doesn't have a 'plaintext' syntax, so we'll just copy its escaping function.
|
||||
body.Text = body.Text.replace(/&/gm, '&').replace(/</gm, '<').replace(/>/gm, '>');
|
||||
}
|
||||
};
|
||||
|
||||
var processReq = function(req) {
|
||||
@@ -214,7 +221,7 @@ ngrok.directive({
|
||||
"onbtnclick": "&"
|
||||
},
|
||||
replace: true,
|
||||
template: '' +
|
||||
template: '' +
|
||||
'<ul class="nav nav-pills">' +
|
||||
'<li ng-repeat="tab in tabNames" ng-class="{\'active\': isTab(tab)}">' +
|
||||
'<a href="" ng-click="setTab(tab)">{{tab}}</a>' +
|
||||
@@ -247,7 +254,7 @@ ngrok.directive({
|
||||
'</h6>' +
|
||||
'' +
|
||||
'<div ng-show="!body.isForm && !body.binary">' +
|
||||
'<pre ng-show="body.exists"><code ng-class="body.syntaxClass">{{ body.Text }}</code></pre>' +
|
||||
'<pre ng-show="body.exists"><code ng-bind-html="body.Text"></code></pre>' +
|
||||
'</div>' +
|
||||
'' +
|
||||
'<div ng-show="body.isForm">' +
|
||||
@@ -259,13 +266,6 @@ ngrok.directive({
|
||||
|
||||
link: function($scope, $elem) {
|
||||
$scope.$watch(function() { return $scope.body; }, function() {
|
||||
$code = $elem.find("code");
|
||||
// if we highlight when the code is empty, hljs manipulates the dom in a
|
||||
// a bad way that causes angular to fail
|
||||
if (!!$code.text()) {
|
||||
hljs.highlightBlock($code.get(0));
|
||||
}
|
||||
|
||||
if ($scope.body && $scope.body.ErrorOffset > -1) {
|
||||
var offset = $scope.body.ErrorOffset;
|
||||
|
||||
@@ -303,7 +303,7 @@ ngrok.directive({
|
||||
|
||||
ngrok.controller({
|
||||
"HttpTxns": function($scope, txnSvc) {
|
||||
$scope.publicUrl = window.data.UiState.Url;
|
||||
$scope.tunnels = window.data.UiState.Tunnels;
|
||||
$scope.txns = txnSvc.all();
|
||||
|
||||
if (!!window.WebSocket) {
|
||||
@@ -317,7 +317,7 @@ ngrok.controller({
|
||||
txnSvc.add(message.data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = function(err) {
|
||||
console.log("Web socket error:" + err);
|
||||
};
|
||||
@@ -347,7 +347,7 @@ ngrok.controller({
|
||||
$scope.$watch(function() { return txnSvc.active() }, setReq);
|
||||
},
|
||||
|
||||
"HttpResponse": function($scope, $element, $timeout, txnSvc) {
|
||||
"HttpResponse": function($scope, $element, txnSvc) {
|
||||
var setResp = function() {
|
||||
var txn = txnSvc.active();
|
||||
if (!!txn && txn.Resp) {
|
||||
@@ -365,4 +365,27 @@ ngrok.controller({
|
||||
txnSvc.active($scope.txn);
|
||||
};
|
||||
},
|
||||
|
||||
"HttpTxn": function($scope, txnSvc, $timeout) {
|
||||
var setTxn = function() {
|
||||
$scope.Txn = txnSvc.active();
|
||||
};
|
||||
|
||||
$scope.ISO8601 = function(ts) {
|
||||
if (!!ts) {
|
||||
return new Date(ts * 1000).toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.TimeFormat = function(ts) {
|
||||
if (!!ts) {
|
||||
return $.timeago($scope.ISO8601(ts));
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch(function() { return txnSvc.active() }, setTxn);
|
||||
|
||||
// this causes angular to update the timestamps
|
||||
setInterval(function() { $scope.$apply(function() {}); }, 30000);
|
||||
},
|
||||
});
|
||||
22
build/travis.py
Executable file
22
build/travis.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
import os, os.path, boto.s3.connection
|
||||
|
||||
access_key = os.getenv("AWS_ACCESS_KEY")
|
||||
secret_key = os.getenv("AWS_SECRET_KEY")
|
||||
bucket = os.getenv("BUCKET")
|
||||
version = os.getenv("VERSION")
|
||||
goos = os.getenv("GOOS")
|
||||
|
||||
s3 = boto.s3.connection.S3Connection(access_key, secret_key)
|
||||
bucket = s3.get_bucket(bucket)
|
||||
|
||||
for envpath in ["NGROK", "NGROKD"]:
|
||||
file_path = os.getenv(envpath)
|
||||
if goos == "windows":
|
||||
file_path += ".exe"
|
||||
dir_path, name = os.path.split(file_path)
|
||||
_, platform = os.path.split(dir_path)
|
||||
key_name = "%s/%s/%s" % (platform, version, name)
|
||||
key = bucket.new_key(key_name)
|
||||
key.set_contents_from_filename(file_path)
|
||||
10
build/version.go
Normal file
10
build/version.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ngrok/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(version.MajorMinor())
|
||||
}
|
||||
77
docs/CHANGELOG.md
Normal file
77
docs/CHANGELOG.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Changelog
|
||||
## 1.6 - 10/25/2013
|
||||
- BUGFIX: Fixed a goroutine/memory leak in ngrok/proto's parsing of http traffic
|
||||
- IMPROVEMENT: The web inspection API can now be disabled again by setting inspect_addr: disabled in the config file
|
||||
|
||||
## 1.5 - 10/20/2013
|
||||
- FEATURE: Added support a "remote_port" configuration parameter that lets you request a specific remote port for TCP tunnels
|
||||
- IMPROVEMENT: Upload instructions on crash reports are displayed after the dump where it is more likely to be seen
|
||||
- IMPROVEMENT: Improvements to ngrok's logging for easier debugging
|
||||
- IMPROVEMENT: Batch metric reporting to Keen to not be limited by the speed of their API at high request loads
|
||||
- IMPROVEMENT: Added additional safety to ensure the server doesn't crash on panics()
|
||||
- BUGFIX: Fixed an issue with prefetching tunnel connections that could hang tunnel connections when behind an aggresive NAT
|
||||
- BUGFIX: Fixed a race condition where ngrokd could send back a different message instead of AuthResp first
|
||||
- BUGFIX: Fixed an issue where under some circumstances, reconnecting would fail and tell the client the tunnels were still in use
|
||||
- BUGFIX: Fixed an issue where a race-condition with handling pings could cause a tunnel to hang forever and stop handling requests
|
||||
|
||||
## 1.4 - 09/27/2013
|
||||
- BUGFIX: Fixed an issue where long URL paths were not truncated in the terminal UI
|
||||
- BUGFIX: Fixed an issue where long URL paths ruined the web UI's formatting
|
||||
- BUGFIX: Fixed an issue where authtokens would not be remembered if an existing configuration file didn't exist
|
||||
|
||||
## 0.23 - 09/06/2013
|
||||
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay
|
||||
|
||||
## 0.22 - 09/04/2013
|
||||
- FEATURE: ngrok now tunnels websocket requests
|
||||
|
||||
## 0.21 - 08/17/2013
|
||||
- IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1
|
||||
|
||||
## 0.20 - 08/17/2013
|
||||
- BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop
|
||||
|
||||
## 0.19 - 08/17/2013
|
||||
- BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check
|
||||
- BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up
|
||||
|
||||
## 0.18 - 08/15/2013
|
||||
- BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons
|
||||
- BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80
|
||||
- BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request
|
||||
- IMPROVEMENT: ngrok can now indicate manual updates again
|
||||
- IMPROVEMENT: ngrok can now supports update channels
|
||||
- IMPROVEMENT: ngrok can now detect some updates that will fail before downloading
|
||||
|
||||
## 0.17 - 07/30/2013
|
||||
- BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol
|
||||
|
||||
## 0.16 - 07/30/2013
|
||||
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error
|
||||
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error
|
||||
- BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests
|
||||
- BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file
|
||||
- BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file
|
||||
- BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified.
|
||||
- BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first
|
||||
- IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs
|
||||
- IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles
|
||||
|
||||
## 0.15 - 07/27/2013
|
||||
- FEATURE: ngrok can now update itself automatically
|
||||
|
||||
## 0.14 - 07/03/2013
|
||||
- BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux
|
||||
- BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens
|
||||
|
||||
## 0.13 - 07/02/2013
|
||||
- FEATURE: -hostname switch on client allows you to run tunnels over custom domains (requires you CNAME your DNS)
|
||||
- IMPROVEMENT: ngrok client UI now shows the client IP address for a request
|
||||
- IMPROVEMENT: ngrok client UI now shows how long ago a request was made (uservoice request 4127487)
|
||||
- IMPROVEMENT: ngrokd now uses and LRU cache for tunnel affinity data
|
||||
- IMPROVEMENT: ngrokd can now save and restore its tunnel affinity cache to a file to preserve across restarts
|
||||
|
||||
## 0.12 - 06/30/2013
|
||||
- IMPROVEMENT: Improved developer documentation
|
||||
- IMPROVEMENT: Simplified build process with custom version of go-bindata that compiles assets into binary releases
|
||||
- BUGFIX: Github issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes.
|
||||
123
docs/DEVELOPMENT.md
Normal file
123
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Developer's guide to ngrok
|
||||
|
||||
|
||||
## Components
|
||||
The ngrok project is composed of two components, the ngrok client (ngrok) and the ngrok server (ngrokd).
|
||||
The ngrok client is the more complicated piece because it has UIs for displaying saved requests and responses.
|
||||
|
||||
## Compiling
|
||||
|
||||
git clone git@github.com:inconshreveable/ngrok.git
|
||||
cd ngrok && make
|
||||
bin/ngrok [LOCAL PORT]
|
||||
|
||||
There are Makefile targets for compiling just the client or server.
|
||||
|
||||
make client
|
||||
make server
|
||||
|
||||
**NB: You must compile with Go 1.1+!**
|
||||
|
||||
### Compiling release versions
|
||||
Both the client and the server contain static asset files.
|
||||
These include TLS/SSL certificates and the html/css/js for the client's web interface.
|
||||
The release versions embed all of this data into the binaries themselves, whereas the debug versions read these files from the filesystem.
|
||||
|
||||
*You should always develop on debug versions so that you don't have to recompile when testing changes in the static assets.*
|
||||
|
||||
There are Makefile targets for compiling the client and server for releases:
|
||||
|
||||
make release-client
|
||||
make release-server
|
||||
make release-all
|
||||
|
||||
|
||||
## Developing locally
|
||||
The strategy I use for developing on ngrok is to do the following:
|
||||
|
||||
Add the following lines to /etc/hosts:
|
||||
|
||||
127.0.0.1 ngrok.me
|
||||
127.0.0.1 test.ngrok.me
|
||||
|
||||
Run ngrokd with the following options:
|
||||
|
||||
./bin/ngrokd -domain ngrok.me
|
||||
|
||||
Create an ngrok configuration file, "debug.yml" with the following contents:
|
||||
|
||||
server_addr: ngrok.me:4443
|
||||
tunnels:
|
||||
test:
|
||||
proto:
|
||||
http: 8080
|
||||
|
||||
|
||||
Then run ngrok with either of these commands:
|
||||
|
||||
./bin/ngrok -config=debug.yml -log=ngrok.log start test
|
||||
./bin/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080
|
||||
|
||||
This will get you setup with an ngrok client talking to an ngrok server all locally under your control. Happy hacking!
|
||||
|
||||
|
||||
## Network protocol and tunneling
|
||||
At a high level, ngrok's tunneling works as follows:
|
||||
|
||||
### Connection Setup and Authentication
|
||||
1. The client initiates a long-lived TCP connection to the server over which they will pass JSON instruction messages. This connection is called the *Control Connection*.
|
||||
1. After the connection is established, the client sends an *Auth* message with authentication and version information.
|
||||
1. The server validates the client's *Auth* message and sends an *AuthResp* message indicating either success or failure.
|
||||
|
||||
### Tunnel creation
|
||||
1. The client may then ask the server to create tunnels for it by sending *ReqTunnel* messages.
|
||||
1. When the server receives a *ReqTunnel* message, it will send 1 or more *NewTunnel* messages that indicate successful tunnel creation or indicate failure.
|
||||
|
||||
### Tunneling connections
|
||||
1. When the server receives a new public connection, it locates the approriate tunnel by examining the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*.
|
||||
1. The server sends a *ReqProxy* message to the client over the control connection.
|
||||
1. The client initiates a new TCP connection to the server called a *Proxy Connection*.
|
||||
1. The client sends a *RegProxy* message over the proxy connection so the server can associate it to a control connection (and thus the tunnels it's responsible for).
|
||||
1. The server sends a *StartProxy* message over the proxy connection with metadata information about the connection (the client IP and name of the tunnel).
|
||||
1. The server begins copying the traffic byte-for-byte from the public connection to the proxy connection and vice-versa.
|
||||
1. The client opens a connection to the local address configured for that tunnel. This is called the *Private Connection*.
|
||||
1. The client begins copying the traffic byte-for-byte from the proxied connection to the private connection and vice-versa.
|
||||
|
||||
### Detecting dead tunnels
|
||||
1. In order to determine whether a tunnel is still alive, the client periodically sends Ping messages over the control connection to the server, which replies with Pong messages.
|
||||
1. When a tunnel is detected to be dead, the server will clean up all of that tunnel's state and the client will attempt to reconnect and establish a new tunnel.
|
||||
|
||||
### Wire format
|
||||
Messages are sent over the wire as netstrings of the form:
|
||||
|
||||
<message length><message payload>
|
||||
|
||||
The message length is sent as a 64-bit little endian integer.
|
||||
|
||||
### Code
|
||||
The definitions and shared protocol routines lives under _src/ngrok/msg_
|
||||
|
||||
#### src/ngrok/msg/msg.go
|
||||
All of the different message types (Auth, AuthResp, ReqTunnel, RegProxy, StartProxy, etc) are defined here and their fields documented. This is a good place to go to understand exactly what messages are sent between the client and server.
|
||||
|
||||
## ngrokd - the server
|
||||
### Code
|
||||
Code for the server lives under src/ngrok/server
|
||||
|
||||
### Entry point
|
||||
The ngrokd entry point is in _src/ngrok/server/main.go_.
|
||||
There is a stub at _src/ngrok/main/ngrokd/ngrokd.go_ for the purposes of creating a properly named binary and being in its own "main" package to comply with go's build system.
|
||||
|
||||
## ngrok - the client
|
||||
### Code
|
||||
Code for the client lives under src/ngrok/client
|
||||
|
||||
### Entry point
|
||||
The ngrok entry point is in _src/ngrok/client/main.go_.
|
||||
There is a stub at _src/ngrok/main/ngrok/ngrok.go_ for the purposes of creating a properly named binary and being in its own "main" package to comply with go's build system.
|
||||
|
||||
## Static assets
|
||||
The html and javascript code for the ngrok web interface as well as other static assets like TLS/SSL certificates live under the top-level _assets_ directory.
|
||||
|
||||
## Beyond
|
||||
More documentation can be found in the comments of the code itself.
|
||||
63
docs/SELFHOSTING.md
Normal file
63
docs/SELFHOSTING.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# How to run your own ngrokd server
|
||||
|
||||
Running your own ngrok server is really easy! The instructions below will guide you along your way!
|
||||
|
||||
## 1. Get an SSL certificate
|
||||
ngrok provides secure tunnels via TLS, so you'll need an SSL certificate. Assuming you want to create
|
||||
tunnels on *.example.com, buy a wildcard SSL certificate for *.example.com.
|
||||
|
||||
## 2. Modify your DNS
|
||||
You need to use the DNS management tools given to you by your provider to create an A
|
||||
record which points *.example.com to the IP address of the server where you will run ngrokd.
|
||||
|
||||
## 3. Compile it
|
||||
You can compile an ngrokd server with the following command:
|
||||
|
||||
make release-server
|
||||
|
||||
Make sure you compile it with the GOOS/GOARCH environment variables set to the platform of
|
||||
your target server. Then copy the binary over to your server.
|
||||
|
||||
## 4. Run the server
|
||||
You'll run the server with the following command.
|
||||
|
||||
|
||||
./ngrokd -tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt" -domain="example.com"
|
||||
|
||||
### Specifying your TLS certificate and key
|
||||
ngrok only makes TLS-encrypted connections. When you run ngrokd, you'll need to instruct it
|
||||
where to find your TLS certificate and private key. Specify the paths with the following switches:
|
||||
|
||||
-tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt"
|
||||
|
||||
### Setting the server's domain
|
||||
When you run your own ngrokd server, you need to tell ngrokd the domain it's running on so that it
|
||||
knows what URLs to issue to clients.
|
||||
|
||||
-domain="example.com"
|
||||
|
||||
## 5. Configure the client
|
||||
In order to connect with a client, you'll need to set two options in ngrok's configuration file.
|
||||
The ngrok configuration file is a simple YAML file that is read from ~/.ngrok by default. You may specify
|
||||
a custom configuration file path with the -config switch. Your config file must contain the following two
|
||||
options.
|
||||
|
||||
server_addr: example.com:4443
|
||||
trust_host_root_certs: true
|
||||
|
||||
Subsitute the address of your ngrokd server for "example.com:4443". The "trust_host_root_certs" parameter instructs
|
||||
ngrok to trust the root certificates on your computer when establishing TLS connections to the server. By default, ngrok
|
||||
only trusts the root certificate for ngrok.com.
|
||||
|
||||
## 6. Connect with a client
|
||||
Then, just run ngrok as usual to connect securely to your own ngrokd server!
|
||||
|
||||
ngrok 80
|
||||
|
||||
## FAQ
|
||||
#### Do I really need a wildcard certificate?
|
||||
If you don't need to run https tunnels, then you don't need a wildcard cert.
|
||||
|
||||
#### I don't want to pay for an SSL certificate, can I use a self-signed one?
|
||||
Yes, it's possible to use a self-signed certificate, but you'll need to recompile ngrok with your signing CA.
|
||||
|
||||
245
src/ngrok/cache/lru.go
vendored
Normal file
245
src/ngrok/cache/lru.go
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
// Copyright 2012, Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The implementation borrows heavily from SmallLRUCache (originally by Nathan
|
||||
// Schrenk). The object maintains a doubly-linked list of elements in the
|
||||
// When an element is accessed it is promoted to the head of the list, and when
|
||||
// space is needed the element at the tail of the list (the least recently used
|
||||
// element) is evicted.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LRUCache struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// list & table of *entry objects
|
||||
list *list.List
|
||||
table map[string]*list.Element
|
||||
|
||||
// Our current size, in bytes. Obviously a gross simplification and low-grade
|
||||
// approximation.
|
||||
size uint64
|
||||
|
||||
// How many bytes we are limiting the cache to.
|
||||
capacity uint64
|
||||
}
|
||||
|
||||
// Values that go into LRUCache need to satisfy this interface.
|
||||
type Value interface {
|
||||
Size() int
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Key string
|
||||
Value Value
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
key string
|
||||
value Value
|
||||
size int
|
||||
time_accessed time.Time
|
||||
}
|
||||
|
||||
func NewLRUCache(capacity uint64) *LRUCache {
|
||||
return &LRUCache{
|
||||
list: list.New(),
|
||||
table: make(map[string]*list.Element),
|
||||
capacity: capacity,
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Get(key string) (v Value, ok bool) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
element := lru.table[key]
|
||||
if element == nil {
|
||||
return nil, false
|
||||
}
|
||||
lru.moveToFront(element)
|
||||
return element.Value.(*entry).value, true
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Set(key string, value Value) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
if element := lru.table[key]; element != nil {
|
||||
lru.updateInplace(element, value)
|
||||
} else {
|
||||
lru.addNew(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) SetIfAbsent(key string, value Value) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
if element := lru.table[key]; element != nil {
|
||||
lru.moveToFront(element)
|
||||
} else {
|
||||
lru.addNew(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Delete(key string) bool {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
element := lru.table[key]
|
||||
if element == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lru.list.Remove(element)
|
||||
delete(lru.table, key)
|
||||
lru.size -= uint64(element.Value.(*entry).size)
|
||||
return true
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Clear() {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
lru.list.Init()
|
||||
lru.table = make(map[string]*list.Element)
|
||||
lru.size = 0
|
||||
}
|
||||
|
||||
func (lru *LRUCache) SetCapacity(capacity uint64) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
lru.capacity = capacity
|
||||
lru.checkCapacity()
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Stats() (length, size, capacity uint64, oldest time.Time) {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
if lastElem := lru.list.Back(); lastElem != nil {
|
||||
oldest = lastElem.Value.(*entry).time_accessed
|
||||
}
|
||||
return uint64(lru.list.Len()), lru.size, lru.capacity, oldest
|
||||
}
|
||||
|
||||
func (lru *LRUCache) StatsJSON() string {
|
||||
if lru == nil {
|
||||
return "{}"
|
||||
}
|
||||
l, s, c, o := lru.Stats()
|
||||
return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"OldestAccess\": \"%v\"}", l, s, c, o)
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Keys() []string {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
keys := make([]string, 0, lru.list.Len())
|
||||
for e := lru.list.Front(); e != nil; e = e.Next() {
|
||||
keys = append(keys, e.Value.(*entry).key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (lru *LRUCache) Items() []Item {
|
||||
lru.mu.Lock()
|
||||
defer lru.mu.Unlock()
|
||||
|
||||
items := make([]Item, 0, lru.list.Len())
|
||||
for e := lru.list.Front(); e != nil; e = e.Next() {
|
||||
v := e.Value.(*entry)
|
||||
items = append(items, Item{Key: v.key, Value: v.value})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (lru *LRUCache) SaveItems(w io.Writer) error {
|
||||
items := lru.Items()
|
||||
encoder := gob.NewEncoder(w)
|
||||
return encoder.Encode(items)
|
||||
}
|
||||
|
||||
func (lru *LRUCache) SaveItemsToFile(path string) error {
|
||||
if wr, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer wr.Close()
|
||||
return lru.SaveItems(wr)
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) LoadItems(r io.Reader) error {
|
||||
items := make([]Item, 0)
|
||||
decoder := gob.NewDecoder(r)
|
||||
if err := decoder.Decode(&items); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lru.mu.Lock()
|
||||
lru.mu.Unlock()
|
||||
for _, item := range items {
|
||||
// XXX: copied from Set()
|
||||
if element := lru.table[item.Key]; element != nil {
|
||||
lru.updateInplace(element, item.Value)
|
||||
} else {
|
||||
lru.addNew(item.Key, item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lru *LRUCache) LoadItemsFromFile(path string) error {
|
||||
if rd, err := os.Open(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer rd.Close()
|
||||
return lru.LoadItems(rd)
|
||||
}
|
||||
}
|
||||
|
||||
func (lru *LRUCache) updateInplace(element *list.Element, value Value) {
|
||||
valueSize := value.Size()
|
||||
sizeDiff := valueSize - element.Value.(*entry).size
|
||||
element.Value.(*entry).value = value
|
||||
element.Value.(*entry).size = valueSize
|
||||
lru.size += uint64(sizeDiff)
|
||||
lru.moveToFront(element)
|
||||
lru.checkCapacity()
|
||||
}
|
||||
|
||||
func (lru *LRUCache) moveToFront(element *list.Element) {
|
||||
lru.list.MoveToFront(element)
|
||||
element.Value.(*entry).time_accessed = time.Now()
|
||||
}
|
||||
|
||||
func (lru *LRUCache) addNew(key string, value Value) {
|
||||
newEntry := &entry{key, value, value.Size(), time.Now()}
|
||||
element := lru.list.PushFront(newEntry)
|
||||
lru.table[key] = element
|
||||
lru.size += uint64(newEntry.size)
|
||||
lru.checkCapacity()
|
||||
}
|
||||
|
||||
func (lru *LRUCache) checkCapacity() {
|
||||
// Partially duplicated from Delete
|
||||
for lru.size > lru.capacity {
|
||||
delElem := lru.list.Back()
|
||||
delValue := delElem.Value.(*entry)
|
||||
lru.list.Remove(delElem)
|
||||
delete(lru.table, delValue.key)
|
||||
lru.size -= uint64(delValue.size)
|
||||
}
|
||||
}
|
||||
11
src/ngrok/client/assets/assets_debug.go
Normal file
11
src/ngrok/client/assets/assets_debug.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !release
|
||||
|
||||
package assets
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func ReadAsset(name string) ([]byte, error) {
|
||||
return ioutil.ReadFile(name)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"ngrok/log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
)
|
||||
|
||||
/*
|
||||
Functions for reading and writing the auth token from the user's
|
||||
home directory.
|
||||
*/
|
||||
var (
|
||||
currentAuthToken string
|
||||
authTokenFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
log.Warn("Failed to get user's home directory: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
authTokenFile = path.Join(user.HomeDir, ".ngrok")
|
||||
tokenBytes, err := ioutil.ReadFile(authTokenFile)
|
||||
|
||||
if err == nil {
|
||||
currentAuthToken = string(tokenBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func SaveAuthToken(token string) {
|
||||
if token == "" || token == currentAuthToken || authTokenFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
perms := os.FileMode(0644)
|
||||
err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
|
||||
if err != nil {
|
||||
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func LoadAuthToken() string {
|
||||
return currentAuthToken
|
||||
}
|
||||
@@ -1,117 +1,72 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/version"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
PORT_OUT_OF_RANGE error = errors.New("Port number must be between 1 and 65535")
|
||||
)
|
||||
const usage1 string = `Usage: %s [OPTIONS] <local port or address>
|
||||
Options:
|
||||
`
|
||||
|
||||
const usage2 string = `
|
||||
Examples:
|
||||
ngrok 80
|
||||
ngrok -subdomain=example 8080
|
||||
ngrok -proto=tcp 22
|
||||
ngrok -hostname="example.com" -httpauth="user:password" 10.0.0.1
|
||||
|
||||
|
||||
Advanced usage: ngrok [OPTIONS] <command> [command args] [...]
|
||||
Commands:
|
||||
ngrok start [tunnel] [...] Start tunnels by name from config file
|
||||
ngrok help Print help
|
||||
ngrok version Print ngrok version
|
||||
|
||||
Examples:
|
||||
ngrok start www api blog pubsub
|
||||
ngrok -log=stdout -config=ngrok.yml start ssh
|
||||
ngrok version
|
||||
|
||||
`
|
||||
|
||||
type Options struct {
|
||||
server string
|
||||
httpAuth string
|
||||
hostname string
|
||||
localaddr string
|
||||
protocol string
|
||||
url string
|
||||
subdomain string
|
||||
historySize int
|
||||
webport int
|
||||
logto string
|
||||
authtoken string
|
||||
config string
|
||||
logto string
|
||||
authtoken string
|
||||
httpauth string
|
||||
hostname string
|
||||
protocol string
|
||||
subdomain string
|
||||
command string
|
||||
args []string
|
||||
}
|
||||
|
||||
func fail(msg string, args ...interface{}) {
|
||||
fmt.Printf(msg+"\n", args...)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func parsePort(portString string) (err error) {
|
||||
var port int
|
||||
if port, err = strconv.Atoi(portString); err != nil {
|
||||
return err
|
||||
func parseArgs() (opts *Options, err error) {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, usage1, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, usage2)
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
return PORT_OUT_OF_RANGE
|
||||
}
|
||||
config := flag.String(
|
||||
"config",
|
||||
"",
|
||||
"Path to ngrok configuration file. (default: $HOME/.ngrok)")
|
||||
|
||||
return
|
||||
}
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"none",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
// Local address could be a port of a host:port string
|
||||
// we always return a host:port string from this function or fail
|
||||
func parseLocalAddr() string {
|
||||
if flag.NArg() == 0 {
|
||||
fail("LOCAL not specified, specify a port number or host:port connection string")
|
||||
}
|
||||
|
||||
if flag.NArg() > 1 {
|
||||
fail("Only one LOCAL may be specified, not %d", flag.NArg())
|
||||
}
|
||||
|
||||
addr := flag.Arg(0)
|
||||
|
||||
// try to parse as a port number
|
||||
if err := parsePort(addr); err == nil {
|
||||
return fmt.Sprintf("127.0.0.1:%s", addr)
|
||||
} else if err == PORT_OUT_OF_RANGE {
|
||||
fail("%s is not in the valid port range 1-65535")
|
||||
}
|
||||
|
||||
// try to parse as a connection string
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
fail("%v", err)
|
||||
}
|
||||
|
||||
if parsePort(port) != nil {
|
||||
fail("'%s' is not a valid port number (1-65535)", port)
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
func parseProtocol(proto string) string {
|
||||
switch proto {
|
||||
case "http":
|
||||
fallthrough
|
||||
case "tcp":
|
||||
return proto
|
||||
default:
|
||||
fail("%s is not a valid protocol", proto)
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func parseAuthToken(token string) string {
|
||||
if token != "" {
|
||||
return token
|
||||
} else {
|
||||
return LoadAuthToken()
|
||||
}
|
||||
}
|
||||
|
||||
func parseArgs() *Options {
|
||||
authtoken := flag.String(
|
||||
"authtoken",
|
||||
"",
|
||||
"Authentication token for identifying a premium ngrok.com account")
|
||||
"Authentication token for identifying an ngrok.com account")
|
||||
|
||||
server := flag.String(
|
||||
"server",
|
||||
"ngrok.com:4443",
|
||||
"The remote ngrok server")
|
||||
|
||||
httpAuth := flag.String(
|
||||
httpauth := flag.String(
|
||||
"httpauth",
|
||||
"",
|
||||
"username:password HTTP basic auth creds protecting the public tunnel endpoint")
|
||||
@@ -119,43 +74,55 @@ func parseArgs() *Options {
|
||||
subdomain := flag.String(
|
||||
"subdomain",
|
||||
"",
|
||||
"Request a custom subdomain from the ngrok server. (HTTP mode only)")
|
||||
"Request a custom subdomain from the ngrok server. (HTTP only)")
|
||||
|
||||
hostname := flag.String(
|
||||
"hostname",
|
||||
"",
|
||||
"Request a custom hostname from the ngrok server. (HTTP only) (requires CNAME of your DNS)")
|
||||
|
||||
protocol := flag.String(
|
||||
"proto",
|
||||
"http",
|
||||
"The protocol of the traffic over the tunnel {'http', 'tcp'} (default: 'http')")
|
||||
|
||||
webport := flag.Int(
|
||||
"webport",
|
||||
4040,
|
||||
"The port on which the web interface is served")
|
||||
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"none",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
v := flag.Bool(
|
||||
"version",
|
||||
false,
|
||||
"Print ngrok version and exit")
|
||||
"http+https",
|
||||
"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *v {
|
||||
fmt.Println(version.MajorMinor())
|
||||
os.Exit(0)
|
||||
opts = &Options{
|
||||
config: *config,
|
||||
logto: *logto,
|
||||
httpauth: *httpauth,
|
||||
subdomain: *subdomain,
|
||||
protocol: *protocol,
|
||||
authtoken: *authtoken,
|
||||
hostname: *hostname,
|
||||
command: flag.Arg(0),
|
||||
}
|
||||
|
||||
return &Options{
|
||||
server: *server,
|
||||
httpAuth: *httpAuth,
|
||||
subdomain: *subdomain,
|
||||
localaddr: parseLocalAddr(),
|
||||
protocol: parseProtocol(*protocol),
|
||||
webport: *webport,
|
||||
logto: *logto,
|
||||
authtoken: parseAuthToken(*authtoken),
|
||||
switch opts.command {
|
||||
case "start":
|
||||
opts.args = flag.Args()[1:]
|
||||
case "version":
|
||||
fmt.Println(version.MajorMinor())
|
||||
os.Exit(0)
|
||||
case "help":
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
case "":
|
||||
err = fmt.Errorf("You must specify a local port to tunnel or an ngrok command.")
|
||||
return
|
||||
|
||||
default:
|
||||
if len(flag.Args()) > 1 {
|
||||
err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v",
|
||||
len(flag.Args()),
|
||||
flag.Args())
|
||||
return
|
||||
}
|
||||
|
||||
opts.command = "default"
|
||||
opts.args = flag.Args()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
266
src/ngrok/client/config.go
Normal file
266
src/ngrok/client/config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"launchpad.net/goyaml"
|
||||
"net"
|
||||
"net/url"
|
||||
"ngrok/log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
HttpProxy string `yaml:"http_proxy,omitempty"`
|
||||
ServerAddr string `yaml:"server_addr,omitempty"`
|
||||
InspectAddr string `yaml:"inspect_addr,omitempty"`
|
||||
TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"`
|
||||
AuthToken string `yaml:"auth_token,omitempty"`
|
||||
Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
|
||||
LogTo string `yaml:"-"`
|
||||
Path string `yaml:"-"`
|
||||
}
|
||||
|
||||
type TunnelConfiguration struct {
|
||||
Subdomain string `yaml:"subdomain,omitempty"`
|
||||
Hostname string `yaml:"hostname,omitempty"`
|
||||
Protocols map[string]string `yaml:"proto,omitempty"`
|
||||
HttpAuth string `yaml:"auth,omitempty"`
|
||||
RemotePort uint16 `yaml:"remote_port,omitempty"`
|
||||
}
|
||||
|
||||
func LoadConfiguration(opts *Options) (config *Configuration, err error) {
|
||||
configPath := opts.config
|
||||
if configPath == "" {
|
||||
configPath = defaultPath()
|
||||
}
|
||||
|
||||
log.Info("Reading configuration file %s", configPath)
|
||||
configBuf, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
// failure to read a configuration file is only a fatal error if
|
||||
// the user specified one explicitly
|
||||
if opts.config != "" {
|
||||
err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// deserialize/parse the config
|
||||
config = new(Configuration)
|
||||
if err = goyaml.Unmarshal(configBuf, &config); err != nil {
|
||||
err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// try to parse the old .ngrok format for backwards compatibility
|
||||
matched := false
|
||||
content := strings.TrimSpace(string(configBuf))
|
||||
if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
|
||||
return
|
||||
} else if matched {
|
||||
config = &Configuration{AuthToken: content}
|
||||
}
|
||||
|
||||
// set configuration defaults
|
||||
if config.ServerAddr == "" {
|
||||
config.ServerAddr = defaultServerAddr
|
||||
}
|
||||
|
||||
if config.InspectAddr == "" {
|
||||
config.InspectAddr = "127.0.0.1:4040"
|
||||
}
|
||||
|
||||
if config.HttpProxy == "" {
|
||||
config.HttpProxy = os.Getenv("http_proxy")
|
||||
}
|
||||
|
||||
// validate and normalize configuration
|
||||
if config.InspectAddr != "disabled" {
|
||||
if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if config.HttpProxy != "" {
|
||||
var proxyUrl *url.URL
|
||||
if proxyUrl, err = url.Parse(config.HttpProxy); err != nil {
|
||||
return
|
||||
} else {
|
||||
if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
|
||||
err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for name, t := range config.Tunnels {
|
||||
if t == nil || t.Protocols == nil || len(t.Protocols) == 0 {
|
||||
err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name)
|
||||
return
|
||||
}
|
||||
|
||||
for k, addr := range t.Protocols {
|
||||
tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
|
||||
if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateProtocol(k, tunnelName); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// use the name of the tunnel as the subdomain if none is specified
|
||||
if t.Hostname == "" && t.Subdomain == "" {
|
||||
// XXX: a crude heuristic, really we should be checking if the last part
|
||||
// is a TLD
|
||||
if len(strings.Split(name, ".")) > 1 {
|
||||
t.Hostname = name
|
||||
} else {
|
||||
t.Subdomain = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// override configuration with command-line options
|
||||
config.LogTo = opts.logto
|
||||
config.Path = configPath
|
||||
if opts.authtoken != "" {
|
||||
config.AuthToken = opts.authtoken
|
||||
}
|
||||
|
||||
switch opts.command {
|
||||
// start a single tunnel, the default, simple ngrok behavior
|
||||
case "default":
|
||||
config.Tunnels = make(map[string]*TunnelConfiguration)
|
||||
config.Tunnels["default"] = &TunnelConfiguration{
|
||||
Subdomain: opts.subdomain,
|
||||
Hostname: opts.hostname,
|
||||
HttpAuth: opts.httpauth,
|
||||
Protocols: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, proto := range strings.Split(opts.protocol, "+") {
|
||||
if err = validateProtocol(proto, "default"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start tunnels
|
||||
case "start":
|
||||
if len(opts.args) == 0 {
|
||||
err = fmt.Errorf("You must specify at least one tunnel to start")
|
||||
return
|
||||
}
|
||||
|
||||
requestedTunnels := make(map[string]bool)
|
||||
for _, arg := range opts.args {
|
||||
requestedTunnels[arg] = true
|
||||
|
||||
if _, ok := config.Tunnels[arg]; !ok {
|
||||
err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for name, _ := range config.Tunnels {
|
||||
if !requestedTunnels[name] {
|
||||
delete(config.Tunnels, name)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("Unknown command: %s", opts.command)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func defaultPath() string {
|
||||
user, err := user.Current()
|
||||
|
||||
// user.Current() does not work on linux when cross compilling because
|
||||
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
|
||||
homeDir := os.Getenv("HOME")
|
||||
if err != nil {
|
||||
log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
|
||||
} else {
|
||||
homeDir = user.HomeDir
|
||||
}
|
||||
|
||||
return path.Join(homeDir, ".ngrok")
|
||||
}
|
||||
|
||||
func normalizeAddress(addr string, propName string) (string, error) {
|
||||
// normalize port to address
|
||||
if _, err := strconv.Atoi(addr); err == nil {
|
||||
addr = ":" + addr
|
||||
}
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
|
||||
}
|
||||
|
||||
if tcpAddr.IP == nil {
|
||||
tcpAddr.IP = net.ParseIP("127.0.0.1")
|
||||
}
|
||||
|
||||
return tcpAddr.String(), nil
|
||||
}
|
||||
|
||||
func validateProtocol(proto, propName string) (err error) {
|
||||
switch proto {
|
||||
case "http", "https", "http+https", "tcp":
|
||||
default:
|
||||
err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func SaveAuthToken(configPath, authtoken string) (err error) {
|
||||
// empty configuration by default for the case that we can't read it
|
||||
c := new(Configuration)
|
||||
|
||||
// read the configuration
|
||||
oldConfigBytes, err := ioutil.ReadFile(configPath)
|
||||
if err == nil {
|
||||
// unmarshal if we successfully read the configuration file
|
||||
if err = goyaml.Unmarshal(oldConfigBytes, c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no need to save, the authtoken is already the correct value
|
||||
if c.AuthToken == authtoken {
|
||||
return
|
||||
}
|
||||
|
||||
// update auth token
|
||||
c.AuthToken = authtoken
|
||||
|
||||
// rewrite configuration
|
||||
newConfigBytes, err := goyaml.Marshal(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
|
||||
return
|
||||
}
|
||||
201
src/ngrok/client/controller.go
Normal file
201
src/ngrok/client/controller.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/client/views/term"
|
||||
"ngrok/client/views/web"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type command interface{}
|
||||
|
||||
type cmdQuit struct {
|
||||
// display this message after quit
|
||||
message string
|
||||
}
|
||||
|
||||
type cmdPlayRequest struct {
|
||||
// the tunnel to play this request over
|
||||
tunnel mvc.Tunnel
|
||||
|
||||
// the bytes of the request to issue
|
||||
payload []byte
|
||||
}
|
||||
|
||||
// The MVC Controller
|
||||
type Controller struct {
|
||||
// Controller logger
|
||||
log.Logger
|
||||
|
||||
// the model sends updates through this broadcast channel
|
||||
updates *util.Broadcast
|
||||
|
||||
// the model
|
||||
model mvc.Model
|
||||
|
||||
// the views
|
||||
views []mvc.View
|
||||
|
||||
// interal structure to issue commands to the controller
|
||||
cmds chan command
|
||||
|
||||
// internal structure to synchronize access to State object
|
||||
state chan mvc.State
|
||||
|
||||
// options
|
||||
config *Configuration
|
||||
}
|
||||
|
||||
// public interface
|
||||
func NewController() *Controller {
|
||||
ctl := &Controller{
|
||||
Logger: log.NewPrefixLogger("controller"),
|
||||
updates: util.NewBroadcast(),
|
||||
cmds: make(chan command),
|
||||
views: make([]mvc.View, 0),
|
||||
state: make(chan mvc.State),
|
||||
}
|
||||
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (ctl *Controller) State() mvc.State {
|
||||
return <-ctl.state
|
||||
}
|
||||
|
||||
func (ctl *Controller) Update(state mvc.State) {
|
||||
ctl.updates.In() <- state
|
||||
}
|
||||
|
||||
func (ctl *Controller) Updates() *util.Broadcast {
|
||||
return ctl.updates
|
||||
}
|
||||
|
||||
func (ctl *Controller) Shutdown(message string) {
|
||||
ctl.cmds <- cmdQuit{message: message}
|
||||
}
|
||||
|
||||
func (ctl *Controller) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
|
||||
ctl.cmds <- cmdPlayRequest{tunnel: tunnel, payload: payload}
|
||||
}
|
||||
|
||||
func (ctl *Controller) Go(fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
ctl.Error(err)
|
||||
ctl.Shutdown(err)
|
||||
}
|
||||
}()
|
||||
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// private functions
|
||||
func (ctl *Controller) doShutdown() {
|
||||
ctl.Info("Shutting down")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// wait for all of the views, plus the model
|
||||
wg.Add(len(ctl.views) + 1)
|
||||
|
||||
for _, v := range ctl.views {
|
||||
vClosure := v
|
||||
ctl.Go(func() {
|
||||
vClosure.Shutdown()
|
||||
wg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
ctl.Go(func() {
|
||||
ctl.model.Shutdown()
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (ctl *Controller) addView(v mvc.View) {
|
||||
ctl.views = append(ctl.views, v)
|
||||
}
|
||||
|
||||
func (ctl *Controller) GetWebInspectAddr() string {
|
||||
return ctl.config.InspectAddr
|
||||
}
|
||||
|
||||
func (ctl *Controller) Run(config *Configuration) {
|
||||
// Save the configuration
|
||||
ctl.config = config
|
||||
|
||||
// init the model
|
||||
model := newClientModel(config, ctl)
|
||||
ctl.model = model
|
||||
var state mvc.State = model
|
||||
|
||||
// init web ui
|
||||
var webView *web.WebView
|
||||
if config.InspectAddr != "disabled" {
|
||||
webView = web.NewWebView(ctl, config.InspectAddr)
|
||||
ctl.addView(webView)
|
||||
}
|
||||
|
||||
// init term ui
|
||||
var termView *term.TermView
|
||||
if config.LogTo != "stdout" {
|
||||
termView = term.NewTermView(ctl)
|
||||
ctl.addView(termView)
|
||||
}
|
||||
|
||||
for _, protocol := range model.GetProtocols() {
|
||||
switch p := protocol.(type) {
|
||||
case *proto.Http:
|
||||
if termView != nil {
|
||||
ctl.addView(termView.NewHttpView(p))
|
||||
}
|
||||
|
||||
if webView != nil {
|
||||
ctl.addView(webView.NewHttpView(p))
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
ctl.Go(func() { autoUpdate(state, config.AuthToken) })
|
||||
ctl.Go(ctl.model.Run)
|
||||
|
||||
updates := ctl.updates.Reg()
|
||||
defer ctl.updates.UnReg(updates)
|
||||
|
||||
done := make(chan int)
|
||||
for {
|
||||
select {
|
||||
case obj := <-ctl.cmds:
|
||||
switch cmd := obj.(type) {
|
||||
case cmdQuit:
|
||||
msg := cmd.message
|
||||
go func() {
|
||||
ctl.doShutdown()
|
||||
fmt.Println(msg)
|
||||
done <- 1
|
||||
}()
|
||||
|
||||
case cmdPlayRequest:
|
||||
ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) })
|
||||
}
|
||||
|
||||
case obj := <-updates:
|
||||
state = obj.(mvc.State)
|
||||
|
||||
case ctl.state <- state:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/ngrok/client/debug.go
Normal file
7
src/ngrok/client/debug.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !release
|
||||
|
||||
package client
|
||||
|
||||
var (
|
||||
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
|
||||
)
|
||||
@@ -1,331 +1,38 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"ngrok/client/tls"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/views/term"
|
||||
"ngrok/client/views/web"
|
||||
"ngrok/conn"
|
||||
"math/rand"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
pingInterval = 20 * time.Second
|
||||
maxPongLatency = 15 * time.Second
|
||||
versionCheckInterval = 6 * time.Hour
|
||||
versionEndpoint = "http://dl.ngrok.com/versions"
|
||||
BadGateway = `<html>
|
||||
<body style="background-color: #97a8b9">
|
||||
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
|
||||
<h2>Tunnel %s unavailable</h2>
|
||||
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
|
||||
`
|
||||
)
|
||||
|
||||
/**
|
||||
* Establishes and manages a tunnel proxy connection with the server
|
||||
*/
|
||||
func proxy(proxyAddr string, s *State, ctl *ui.Controller) {
|
||||
start := time.Now()
|
||||
remoteConn, err := conn.Dial(proxyAddr, "pxy", tls.Config)
|
||||
if err != nil {
|
||||
// XXX: What is the proper response here?
|
||||
// display something to the user?
|
||||
// retry?
|
||||
// reset control connection?
|
||||
log.Error("Failed to establish proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer remoteConn.Close()
|
||||
err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{Url: s.publicUrl})
|
||||
if err != nil {
|
||||
// XXX: What is the proper response here?
|
||||
// display something to the user?
|
||||
// retry?
|
||||
// reset control connection?
|
||||
log.Error("Failed to write RegProxyMsg: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
|
||||
if err != nil {
|
||||
remoteConn.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
|
||||
badGatewayBody := fmt.Sprintf(BadGateway, s.publicUrl, s.opts.localaddr, s.opts.localaddr)
|
||||
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
|
||||
Content-Type: text/html
|
||||
Content-Length: %d
|
||||
|
||||
%s`, len(badGatewayBody), badGatewayBody)))
|
||||
return
|
||||
}
|
||||
defer localConn.Close()
|
||||
|
||||
m := s.metrics
|
||||
m.proxySetupTimer.Update(time.Since(start))
|
||||
m.connMeter.Mark(1)
|
||||
ctl.Update(s)
|
||||
m.connTimer.Time(func() {
|
||||
localConn := s.protocol.WrapConn(localConn)
|
||||
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
|
||||
m.bytesIn.Update(bytesIn)
|
||||
m.bytesOut.Update(bytesOut)
|
||||
m.bytesInCount.Inc(bytesIn)
|
||||
m.bytesOutCount.Inc(bytesOut)
|
||||
})
|
||||
ctl.Update(s)
|
||||
}
|
||||
|
||||
func versionCheck(s *State, ctl *ui.Controller) {
|
||||
check := func() {
|
||||
resp, err := http.Get(versionEndpoint)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get version info %s: %v", versionEndpoint, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var payload struct {
|
||||
Client struct {
|
||||
Version string
|
||||
}
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
log.Warn("Failed to read version info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Client.Version != version.MajorMinor() {
|
||||
s.newVersion = payload.Client.Version
|
||||
ctl.Update(s)
|
||||
}
|
||||
}
|
||||
|
||||
// check immediately and then at a set interval
|
||||
check()
|
||||
for _ = range time.Tick(versionCheckInterval) {
|
||||
check()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Hearbeating to ensure our connection ngrokd is still live
|
||||
*/
|
||||
func heartbeat(lastPongAddr *int64, c conn.Conn) {
|
||||
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
|
||||
ping := time.NewTicker(pingInterval)
|
||||
pongCheck := time.NewTicker(time.Second)
|
||||
|
||||
defer func() {
|
||||
c.Close()
|
||||
ping.Stop()
|
||||
pongCheck.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pongCheck.C:
|
||||
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
|
||||
needPong := lastPong.Sub(lastPing) < 0
|
||||
pongLatency := time.Since(lastPing)
|
||||
|
||||
if needPong && pongLatency > maxPongLatency {
|
||||
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
|
||||
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
|
||||
return
|
||||
}
|
||||
|
||||
case <-ping.C:
|
||||
err := msg.WriteMsg(c, &msg.PingMsg{})
|
||||
if err != nil {
|
||||
c.Debug("Got error %v when writing PingMsg", err)
|
||||
return
|
||||
}
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reconnectingControl(s *State, ctl *ui.Controller) {
|
||||
// how long we should wait before we reconnect
|
||||
maxWait := 30 * time.Second
|
||||
wait := 1 * time.Second
|
||||
|
||||
for {
|
||||
control(s, ctl)
|
||||
|
||||
if s.status == "online" {
|
||||
wait = 1 * time.Second
|
||||
}
|
||||
|
||||
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
|
||||
time.Sleep(wait)
|
||||
// exponentially increase wait time
|
||||
wait = 2 * wait
|
||||
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
|
||||
s.status = "reconnecting"
|
||||
ctl.Update(s)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes and manages a tunnel control connection with the server
|
||||
*/
|
||||
func control(s *State, ctl *ui.Controller) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("control recovering from failure %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// establish control channel
|
||||
conn, err := conn.Dial(s.opts.server, "ctl", tls.Config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// register with the server
|
||||
err = msg.WriteMsg(conn, &msg.RegMsg{
|
||||
Protocol: s.opts.protocol,
|
||||
OS: runtime.GOOS,
|
||||
HttpAuth: s.opts.httpAuth,
|
||||
Hostname: s.opts.hostname,
|
||||
Subdomain: s.opts.subdomain,
|
||||
ClientId: s.id,
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
User: s.opts.authtoken,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var regAck msg.RegAckMsg
|
||||
if err = msg.ReadMsgInto(conn, ®Ack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if regAck.Error != "" {
|
||||
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
|
||||
ctl.Cmds <- ui.CmdQuit{Message: emsg}
|
||||
return
|
||||
}
|
||||
|
||||
// update UI state
|
||||
conn.Info("Tunnel established at %v", regAck.Url)
|
||||
s.publicUrl = regAck.Url
|
||||
s.status = "online"
|
||||
s.serverVersion = regAck.MmVersion
|
||||
ctl.Update(s)
|
||||
|
||||
SaveAuthToken(s.opts.authtoken)
|
||||
|
||||
// start the heartbeat
|
||||
lastPong := time.Now().UnixNano()
|
||||
go heartbeat(&lastPong, conn)
|
||||
|
||||
// main control loop
|
||||
for {
|
||||
var m msg.Message
|
||||
if m, err = msg.ReadMsg(conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch m.(type) {
|
||||
case *msg.ReqProxyMsg:
|
||||
go proxy(regAck.ProxyAddr, s, ctl)
|
||||
|
||||
case *msg.PongMsg:
|
||||
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Main() {
|
||||
// parse options
|
||||
opts := parseArgs()
|
||||
opts, err := parseArgs()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// set up logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
// init client state
|
||||
s := &State{
|
||||
status: "connecting",
|
||||
|
||||
// unique client id
|
||||
id: util.RandIdOrPanic(8),
|
||||
|
||||
// command-line options
|
||||
opts: opts,
|
||||
|
||||
// metrics
|
||||
metrics: NewClientMetrics(),
|
||||
// read configuration file
|
||||
config, err := LoadConfiguration(opts)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch opts.protocol {
|
||||
case "http":
|
||||
s.protocol = proto.NewHttp()
|
||||
case "tcp":
|
||||
s.protocol = proto.NewTcp()
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't securely seed the random number generator!")
|
||||
os.Exit(1)
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
// init ui
|
||||
ctl := ui.NewController()
|
||||
web.NewWebView(ctl, s, opts.webport)
|
||||
if opts.logto != "stdout" {
|
||||
term.New(ctl, s)
|
||||
}
|
||||
|
||||
go reconnectingControl(s, ctl)
|
||||
go versionCheck(s, ctl)
|
||||
|
||||
quitMessage := ""
|
||||
ctl.Wait.Add(1)
|
||||
go func() {
|
||||
defer ctl.Wait.Done()
|
||||
for {
|
||||
select {
|
||||
case obj := <-ctl.Cmds:
|
||||
switch cmd := obj.(type) {
|
||||
case ui.CmdQuit:
|
||||
quitMessage = cmd.Message
|
||||
ctl.DoShutdown()
|
||||
return
|
||||
case ui.CmdRequest:
|
||||
go func() {
|
||||
var localConn conn.Conn
|
||||
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
|
||||
return
|
||||
}
|
||||
//defer localConn.Close()
|
||||
localConn = s.protocol.WrapConn(localConn)
|
||||
localConn.Write(cmd.Payload)
|
||||
ioutil.ReadAll(localConn)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctl.Wait.Wait()
|
||||
fmt.Println(quitMessage)
|
||||
NewController().Run(config)
|
||||
}
|
||||
|
||||
428
src/ngrok/client/model.go
Normal file
428
src/ngrok/client/model.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultServerAddr = "ngrokd.ngrok.com:443"
|
||||
pingInterval = 20 * time.Second
|
||||
maxPongLatency = 15 * time.Second
|
||||
updateCheckInterval = 6 * time.Hour
|
||||
BadGateway = `<html>
|
||||
<body style="background-color: #97a8b9">
|
||||
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
|
||||
<h2>Tunnel %s unavailable</h2>
|
||||
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
|
||||
`
|
||||
)
|
||||
|
||||
type ClientModel struct {
|
||||
log.Logger
|
||||
|
||||
id string
|
||||
tunnels map[string]mvc.Tunnel
|
||||
serverVersion string
|
||||
metrics *ClientMetrics
|
||||
updateStatus mvc.UpdateStatus
|
||||
connStatus mvc.ConnStatus
|
||||
protoMap map[string]proto.Protocol
|
||||
protocols []proto.Protocol
|
||||
ctl mvc.Controller
|
||||
serverAddr string
|
||||
proxyUrl string
|
||||
authToken string
|
||||
tlsConfig *tls.Config
|
||||
tunnelConfig map[string]*TunnelConfiguration
|
||||
configPath string
|
||||
}
|
||||
|
||||
func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel {
|
||||
protoMap := make(map[string]proto.Protocol)
|
||||
protoMap["http"] = proto.NewHttp()
|
||||
protoMap["https"] = protoMap["http"]
|
||||
protoMap["tcp"] = proto.NewTcp()
|
||||
protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
|
||||
|
||||
// configure TLS
|
||||
var tlsConfig *tls.Config
|
||||
if config.TrustHostRootCerts {
|
||||
tlsConfig = &tls.Config{}
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ClientModel{
|
||||
Logger: log.NewPrefixLogger("client"),
|
||||
|
||||
// server address
|
||||
serverAddr: config.ServerAddr,
|
||||
|
||||
// proxy address
|
||||
proxyUrl: config.HttpProxy,
|
||||
|
||||
// auth token
|
||||
authToken: config.AuthToken,
|
||||
|
||||
// connection status
|
||||
connStatus: mvc.ConnConnecting,
|
||||
|
||||
// update status
|
||||
updateStatus: mvc.UpdateNone,
|
||||
|
||||
// metrics
|
||||
metrics: NewClientMetrics(),
|
||||
|
||||
// protocols
|
||||
protoMap: protoMap,
|
||||
|
||||
// protocol list
|
||||
protocols: protocols,
|
||||
|
||||
// open tunnels
|
||||
tunnels: make(map[string]mvc.Tunnel),
|
||||
|
||||
// controller
|
||||
ctl: ctl,
|
||||
|
||||
// tls configuration
|
||||
tlsConfig: tlsConfig,
|
||||
|
||||
// tunnel configuration
|
||||
tunnelConfig: config.Tunnels,
|
||||
|
||||
// config path
|
||||
configPath: config.Path,
|
||||
}
|
||||
}
|
||||
|
||||
// mvc.State interface
|
||||
func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols }
|
||||
func (c ClientModel) GetClientVersion() string { return version.MajorMinor() }
|
||||
func (c ClientModel) GetServerVersion() string { return c.serverVersion }
|
||||
func (c ClientModel) GetTunnels() []mvc.Tunnel {
|
||||
tunnels := make([]mvc.Tunnel, 0)
|
||||
for _, t := range c.tunnels {
|
||||
tunnels = append(tunnels, t)
|
||||
}
|
||||
return tunnels
|
||||
}
|
||||
func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus }
|
||||
func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus }
|
||||
|
||||
func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
|
||||
return c.metrics.connMeter, c.metrics.connTimer
|
||||
}
|
||||
|
||||
func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return c.metrics.bytesInCount, c.metrics.bytesIn
|
||||
}
|
||||
|
||||
func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return c.metrics.bytesOutCount, c.metrics.bytesOut
|
||||
}
|
||||
func (c ClientModel) SetUpdateStatus(updateStatus mvc.UpdateStatus) {
|
||||
c.updateStatus = updateStatus
|
||||
c.update()
|
||||
}
|
||||
|
||||
// mvc.Model interface
|
||||
func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
|
||||
var localConn conn.Conn
|
||||
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
|
||||
if err != nil {
|
||||
c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer localConn.Close()
|
||||
localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"})
|
||||
localConn.Write(payload)
|
||||
ioutil.ReadAll(localConn)
|
||||
}
|
||||
|
||||
func (c *ClientModel) Shutdown() {
|
||||
}
|
||||
|
||||
func (c *ClientModel) update() {
|
||||
c.ctl.Update(c)
|
||||
}
|
||||
|
||||
func (c *ClientModel) Run() {
|
||||
// how long we should wait before we reconnect
|
||||
maxWait := 30 * time.Second
|
||||
wait := 1 * time.Second
|
||||
|
||||
for {
|
||||
// run the control channel
|
||||
c.control()
|
||||
|
||||
// control oonly returns when a failure has occurred, so we're going to try to reconnect
|
||||
if c.connStatus == mvc.ConnOnline {
|
||||
wait = 1 * time.Second
|
||||
}
|
||||
|
||||
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
|
||||
time.Sleep(wait)
|
||||
// exponentially increase wait time
|
||||
wait = 2 * wait
|
||||
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
|
||||
c.connStatus = mvc.ConnReconnecting
|
||||
c.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Establishes and manages a tunnel control connection with the server
|
||||
func (c *ClientModel) control() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("control recovering from failure %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// establish control channel
|
||||
var (
|
||||
ctlConn conn.Conn
|
||||
err error
|
||||
)
|
||||
if c.proxyUrl == "" {
|
||||
// simple non-proxied case, just connect to the server
|
||||
ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
|
||||
} else {
|
||||
ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer ctlConn.Close()
|
||||
|
||||
// authenticate with the server
|
||||
auth := &msg.Auth{
|
||||
ClientId: c.id,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
User: c.authToken,
|
||||
}
|
||||
|
||||
if err = msg.WriteMsg(ctlConn, auth); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// wait for the server to authenticate us
|
||||
var authResp msg.AuthResp
|
||||
if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if authResp.Error != "" {
|
||||
emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error)
|
||||
c.ctl.Shutdown(emsg)
|
||||
return
|
||||
}
|
||||
|
||||
c.id = authResp.ClientId
|
||||
c.serverVersion = authResp.MmVersion
|
||||
c.Info("Authenticated with server, client id: %v", c.id)
|
||||
c.update()
|
||||
if err = SaveAuthToken(c.configPath, c.authToken); err != nil {
|
||||
c.Error("Failed to save auth token: %v", err)
|
||||
}
|
||||
|
||||
// request tunnels
|
||||
reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)
|
||||
for _, config := range c.tunnelConfig {
|
||||
// create the protocol list to ask for
|
||||
var protocols []string
|
||||
for proto, _ := range config.Protocols {
|
||||
protocols = append(protocols, proto)
|
||||
}
|
||||
|
||||
reqTunnel := &msg.ReqTunnel{
|
||||
ReqId: util.RandId(8),
|
||||
Protocol: strings.Join(protocols, "+"),
|
||||
Hostname: config.Hostname,
|
||||
Subdomain: config.Subdomain,
|
||||
HttpAuth: config.HttpAuth,
|
||||
RemotePort: config.RemotePort,
|
||||
}
|
||||
|
||||
// send the tunnel request
|
||||
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// save request id association so we know which local address
|
||||
// to proxy to later
|
||||
reqIdToTunnelConfig[reqTunnel.ReqId] = config
|
||||
}
|
||||
|
||||
// start the heartbeat
|
||||
lastPong := time.Now().UnixNano()
|
||||
c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) })
|
||||
|
||||
// main control loop
|
||||
for {
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(ctlConn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.ReqProxy:
|
||||
c.ctl.Go(c.proxy)
|
||||
|
||||
case *msg.Pong:
|
||||
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
|
||||
|
||||
case *msg.NewTunnel:
|
||||
if m.Error != "" {
|
||||
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error)
|
||||
c.Error(emsg)
|
||||
c.ctl.Shutdown(emsg)
|
||||
continue
|
||||
}
|
||||
|
||||
tunnel := mvc.Tunnel{
|
||||
PublicUrl: m.Url,
|
||||
LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
|
||||
Protocol: c.protoMap[m.Protocol],
|
||||
}
|
||||
|
||||
c.tunnels[tunnel.PublicUrl] = tunnel
|
||||
c.connStatus = mvc.ConnOnline
|
||||
c.Info("Tunnel established at %v", tunnel.PublicUrl)
|
||||
c.update()
|
||||
|
||||
default:
|
||||
ctlConn.Warn("Ignoring unknown control message %v ", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Establishes and manages a tunnel proxy connection with the server
|
||||
func (c *ClientModel) proxy() {
|
||||
var (
|
||||
remoteConn conn.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
if c.proxyUrl == "" {
|
||||
remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
|
||||
} else {
|
||||
remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("Failed to establish proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer remoteConn.Close()
|
||||
|
||||
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
|
||||
if err != nil {
|
||||
remoteConn.Error("Failed to write RegProxy: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var startPxy msg.StartProxy
|
||||
if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil {
|
||||
remoteConn.Error("Server failed to write StartProxy: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel, ok := c.tunnels[startPxy.Url]
|
||||
if !ok {
|
||||
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
|
||||
return
|
||||
}
|
||||
|
||||
// start up the private connection
|
||||
start := time.Now()
|
||||
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
|
||||
if err != nil {
|
||||
remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err)
|
||||
|
||||
if tunnel.Protocol.GetName() == "http" {
|
||||
// try to be helpful when you're in HTTP mode and a human might see the output
|
||||
badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr)
|
||||
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
|
||||
Content-Type: text/html
|
||||
Content-Length: %d
|
||||
|
||||
%s`, len(badGatewayBody), badGatewayBody)))
|
||||
}
|
||||
return
|
||||
}
|
||||
defer localConn.Close()
|
||||
|
||||
m := c.metrics
|
||||
m.proxySetupTimer.Update(time.Since(start))
|
||||
m.connMeter.Mark(1)
|
||||
c.update()
|
||||
m.connTimer.Time(func() {
|
||||
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr})
|
||||
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
|
||||
m.bytesIn.Update(bytesIn)
|
||||
m.bytesOut.Update(bytesOut)
|
||||
m.bytesInCount.Inc(bytesIn)
|
||||
m.bytesOutCount.Inc(bytesOut)
|
||||
})
|
||||
c.update()
|
||||
}
|
||||
|
||||
// Hearbeating to ensure our connection ngrokd is still live
|
||||
func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) {
|
||||
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
|
||||
ping := time.NewTicker(pingInterval)
|
||||
pongCheck := time.NewTicker(time.Second)
|
||||
|
||||
defer func() {
|
||||
conn.Close()
|
||||
ping.Stop()
|
||||
pongCheck.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pongCheck.C:
|
||||
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
|
||||
needPong := lastPong.Sub(lastPing) < 0
|
||||
pongLatency := time.Since(lastPing)
|
||||
|
||||
if needPong && pongLatency > maxPongLatency {
|
||||
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
|
||||
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
|
||||
return
|
||||
}
|
||||
|
||||
case <-ping.C:
|
||||
err := msg.WriteMsg(conn, &msg.Ping{})
|
||||
if err != nil {
|
||||
conn.Debug("Got error %v when writing PingMsg", err)
|
||||
return
|
||||
}
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ngrok/client/mvc/controller.go
Normal file
28
src/ngrok/client/mvc/controller.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"ngrok/util"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
// how the model communicates that it has changed state
|
||||
Update(State)
|
||||
|
||||
// instructs the controller to shut the app down
|
||||
Shutdown(message string)
|
||||
|
||||
// PlayRequest instructs the model to play requests
|
||||
PlayRequest(tunnel Tunnel, payload []byte)
|
||||
|
||||
// A channel of updates
|
||||
Updates() *util.Broadcast
|
||||
|
||||
// returns the current state
|
||||
State() State
|
||||
|
||||
// safe wrapper for running go-routines
|
||||
Go(fn func())
|
||||
|
||||
// the address where the web inspection interface is running
|
||||
GetWebInspectAddr() string
|
||||
}
|
||||
9
src/ngrok/client/mvc/model.go
Normal file
9
src/ngrok/client/mvc/model.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package mvc
|
||||
|
||||
type Model interface {
|
||||
Run()
|
||||
|
||||
Shutdown()
|
||||
|
||||
PlayRequest(tunnel Tunnel, payload []byte)
|
||||
}
|
||||
47
src/ngrok/client/mvc/state.go
Normal file
47
src/ngrok/client/mvc/state.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
)
|
||||
|
||||
type UpdateStatus int
|
||||
|
||||
const (
|
||||
UpdateNone = -1 * iota
|
||||
UpdateInstalling
|
||||
UpdateReady
|
||||
UpdateAvailable
|
||||
)
|
||||
|
||||
type ConnStatus int
|
||||
|
||||
const (
|
||||
ConnConnecting = iota
|
||||
ConnReconnecting
|
||||
ConnOnline
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
PublicUrl string
|
||||
Protocol proto.Protocol
|
||||
LocalAddr string
|
||||
}
|
||||
|
||||
type ConnectionContext struct {
|
||||
Tunnel Tunnel
|
||||
ClientAddr string
|
||||
}
|
||||
|
||||
type State interface {
|
||||
GetClientVersion() string
|
||||
GetServerVersion() string
|
||||
GetTunnels() []Tunnel
|
||||
GetProtocols() []proto.Protocol
|
||||
GetUpdateStatus() UpdateStatus
|
||||
GetConnStatus() ConnStatus
|
||||
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
|
||||
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
|
||||
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
|
||||
SetUpdateStatus(UpdateStatus)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package ui
|
||||
package mvc
|
||||
|
||||
type View interface {
|
||||
Render()
|
||||
Shutdown()
|
||||
}
|
||||
7
src/ngrok/client/release.go
Normal file
7
src/ngrok/client/release.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build release
|
||||
|
||||
package client
|
||||
|
||||
var (
|
||||
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"}
|
||||
)
|
||||
@@ -1,43 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
"ngrok/version"
|
||||
)
|
||||
|
||||
// client state
|
||||
type State struct {
|
||||
id string
|
||||
publicUrl string
|
||||
serverVersion string
|
||||
newVersion string
|
||||
protocol proto.Protocol
|
||||
opts *Options
|
||||
metrics *ClientMetrics
|
||||
|
||||
// just for UI purposes
|
||||
status string
|
||||
}
|
||||
|
||||
// implement client.ui.State
|
||||
func (s State) GetClientVersion() string { return version.MajorMinor() }
|
||||
func (s State) GetServerVersion() string { return s.serverVersion }
|
||||
func (s State) GetPublicUrl() string { return s.publicUrl }
|
||||
func (s State) GetLocalAddr() string { return s.opts.localaddr }
|
||||
func (s State) GetWebPort() int { return s.opts.webport }
|
||||
func (s State) GetStatus() string { return s.status }
|
||||
func (s State) GetProtocol() proto.Protocol { return s.protocol }
|
||||
func (s State) GetNewVersion() string { return s.newVersion }
|
||||
|
||||
func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
|
||||
return s.metrics.connMeter, s.metrics.connTimer
|
||||
}
|
||||
|
||||
func (s State) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return s.metrics.bytesInCount, s.metrics.bytesIn
|
||||
}
|
||||
|
||||
func (s State) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return s.metrics.bytesOutCount, s.metrics.bytesOut
|
||||
}
|
||||
37
src/ngrok/client/tls.go
Normal file
37
src/ngrok/client/tls.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"ngrok/client/assets"
|
||||
)
|
||||
|
||||
func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) {
|
||||
pool := x509.NewCertPool()
|
||||
|
||||
for _, certPath := range rootCertPaths {
|
||||
rootCrt, err := assets.ReadAsset(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(rootCrt)
|
||||
if pemBlock == nil {
|
||||
return nil, fmt.Errorf("Bad PEM data")
|
||||
}
|
||||
|
||||
certs, err := x509.ParseCertificates(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool.AddCert(certs[0])
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
RootCAs: pool,
|
||||
ServerName: "ngrokd.ngrok.com",
|
||||
}, nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _addTrustCrt = "" +
|
||||
"\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x74\x94\x5b\xcf\xaa\x38" +
|
||||
"\x14\x86\xef\xf9\x15\x73\x6f\x26\xa2\x82\xca\xc5\x5c\xb4\xa5\x40" +
|
||||
"\xd1\x02\xe5\x28\xdc\x71\x12\x04\xf1\x88\x16\xf9\xf5\xe3\xe7\xb7" +
|
||||
"\x33\xd9\x33\x7b\x76\x93\x26\xcd\xdb\x64\xad\xb7\x5d\xcf\x5a\x7f" +
|
||||
"\x7e\x2d\x88\x75\x62\xfd\x81\xb0\xeb\x13\x8d\x20\xe0\xe3\x8f\x2a" +
|
||||
"\x50\x42\xb0\xd5\x20\x04\x86\x65\x05\x38\x81\xa0\x7a\x6f\x1f\x58" +
|
||||
"\xb0\x6a\xaf\x75\x7b\xd0\x15\x2e\x42\xc0\x02\x0d\xa8\xf0\x49\xd9" +
|
||||
"\x9d\x23\x16\xab\x21\x63\x3a\xe6\xa6\xef\xfa\x38\x10\x28\x24\x3a" +
|
||||
"\x98\x05\x18\xd5\x74\xcb\x22\xb7\x0d\x0d\x73\x96\x2f\x58\xc5\x02" +
|
||||
"\x32\x98\x0d\x68\x61\x65\x85\x10\xdc\x7d\x23\xd0\xda\x44\x73\x5f" +
|
||||
"\xc5\xce\x12\x09\x0e\xa5\x42\x0f\x5f\x59\xa7\xdd\x05\xa2\xb9\x41" +
|
||||
"\x80\xa0\x9d\xec\xdc\x45\xb6\x30\x6f\xd4\x23\x9c\x80\x4f\x16\x15" +
|
||||
"\x0f\x47\x98\xe8\x6e\x90\x9f\xc2\xb1\x40\x50\x2b\x0d\xf7\x98\x77" +
|
||||
"\x72\x9d\x21\xa8\x32\x0f\x7a\xd9\x5c\x11\xdf\x0e\xa4\x9d\xea\x03" +
|
||||
"\x4e\xd5\x60\xa4\x2a\xe6\x96\x5a\x8d\xb6\x76\x7e\x6b\xe4\xbf\x1a" +
|
||||
"\xcf\x46\xbc\xa5\xa0\xfd\x38\x86\x35\x45\x81\x18\x0c\x82\xa6\x02" +
|
||||
"\xef\xdb\xe6\xd9\x47\xe2\xcf\x36\x35\x44\xbd\x98\x9b\xdf\x6e\xb6" +
|
||||
"\x78\x98\xfd\xaf\x1b\xe1\x6d\x27\x08\x35\x50\xf9\x5d\x28\x16\x73" +
|
||||
"\xe5\x95\x8e\xf8\x40\x11\xf8\x64\x01\x03\x4d\x7e\xfe\x17\x77\x57" +
|
||||
"\x8b\xc9\xce\x7c\xc4\x11\xaf\x98\x88\xab\xa0\x53\x9e\x42\xa1\xbe" +
|
||||
"\x2b\xf0\x7e\xb7\xca\x62\x73\x73\x4e\x48\xfd\xcc\x2d\xc0\x30\x84" +
|
||||
"\x0c\xa8\x55\x85\x1d\xf0\x75\xcf\xce\xe8\x7d\x86\x60\xbb\x5f\xe8" +
|
||||
"\x8d\xd3\xad\x2b\x80\xb7\xfe\xa9\xf2\x8f\xcf\x5e\x30\x56\xc3\x5d" +
|
||||
"\x5d\xcf\x67\x93\x83\x3d\x1f\xfb\x65\x86\x7d\x7b\x77\x41\x47\xba" +
|
||||
"\x4f\xec\xfd\x33\xb8\xae\xdb\x89\xa8\xea\x0f\xdb\x19\x27\x61\x1f" +
|
||||
"\x68\xb7\xe8\xf8\xea\x82\xe8\x8c\xb8\xb7\xbb\x65\xdb\xcb\x4e\x11" +
|
||||
"\x1e\xf4\x3a\xb5\xc6\xaa\x37\x9a\xa5\xcb\xd2\x19\x0f\xef\x7b\xee" +
|
||||
"\x8f\xd3\x33\xbd\xc8\xe2\xeb\x7e\x60\xa1\x7d\xd2\x77\x5c\x91\x4e" +
|
||||
"\xc9\x05\x38\x60\x79\x8f\xd3\x4b\xa9\x91\x49\x59\x2f\xb5\x6b\x60" +
|
||||
"\x8d\x3b\xa1\x6b\x97\x4f\x98\xd9\x5d\x9e\x78\x79\x9e\x59\x2c\x06" +
|
||||
"\x37\x03\xcb\xa2\x04\xa5\x18\x5d\xed\xee\x9c\x7a\x71\xdc\x6e\x7a" +
|
||||
"\x7a\xc7\xeb\xe6\x3a\x5e\x9c\xda\x6a\xf6\xe3\x65\x5a\xa7\xd1\x64" +
|
||||
"\x35\x13\xb7\x3b\x21\x15\xfd\x76\x58\x2e\x1e\x59\xa0\xed\xf3\xe3" +
|
||||
"\x65\x40\x6a\x39\x96\x51\xd4\x46\x29\x0a\xac\x69\x0e\xb6\x7c\x81" +
|
||||
"\xda\xd3\x36\x15\xd5\xfa\x35\x1f\xbc\xb3\x9b\xbb\xc5\xe6\x34\x5f" +
|
||||
"\xf4\x56\x86\x57\xd7\xd1\x12\xb0\xe8\x2d\x5e\xde\xb3\x60\x1c\x1c" +
|
||||
"\x27\x9d\x2e\xa7\xd1\x25\x26\x83\xbe\xb8\x8c\xb6\x13\x9e\xc2\x44" +
|
||||
"\xc9\xc5\xcb\x4c\x4c\x17\xe8\xd0\x1f\xfb\xde\x42\xd9\x10\xbd\x1e" +
|
||||
"\xc6\x73\xb5\x9a\x1c\x8b\x40\x09\x44\x21\x3a\xe4\x08\x70\x0c\x40" +
|
||||
"\x6a\xc3\x85\x8a\xe0\xdc\x07\xc5\x17\x17\x06\x93\xb0\xf6\x06\xfa" +
|
||||
"\x96\xcd\xe3\xb2\xf1\x44\xf3\xb9\x5f\x0e\x28\x09\x56\xdc\x56\x24" +
|
||||
"\xe4\x6f\x43\x78\xe6\x88\xc7\xaa\x10\xba\xa2\xf3\xee\x16\x15\x54" +
|
||||
"\x58\xa7\x60\xfd\x55\xff\x02\x73\x0c\xa7\x9c\x69\x14\x50\x08\xf6" +
|
||||
"\x6b\x5e\x25\x1f\xfa\x0a\xc2\x3d\xd8\xfa\x08\x36\x67\xf0\x4f\x60" +
|
||||
"\xe1\xd7\xc8\xd7\x3a\x5f\xba\x03\xd5\xd7\x03\x1a\x81\xf9\x4d\x69" +
|
||||
"\xec\x83\xa3\xa5\x51\x97\x71\x5c\x7d\xc8\xdc\x60\xde\x7f\xc8\x14" +
|
||||
"\x7e\xa0\x09\x59\x83\x3b\x8a\xd8\x77\x67\x0e\xb4\xf8\x1d\x81\xa1" +
|
||||
"\xe6\x32\x82\xe5\x63\x61\x14\xcf\xbc\xbb\x0f\x02\x69\x40\xf5\x9d" +
|
||||
"\x85\xfa\xfa\x6f\x5a\xf6\x3d\x31\x20\xd1\xcc\x67\xb6\xf0\xde\x54" +
|
||||
"\xe2\x7f\x51\x2b\xbc\x05\xed\x07\xb6\x6f\x54\x51\x26\x91\xe0\xc8" +
|
||||
"\xfb\xb8\x91\xaa\x49\x04\x2f\x9b\x82\x25\x87\x7c\x1e\xbb\x72\x55" +
|
||||
"\xb4\x65\x34\x30\x83\x8c\xc9\xb1\x59\xa9\x71\xb1\x7a\xdc\x59\x34" +
|
||||
"\x18\x42\x4c\x2c\xf7\xee\xb4\x2f\xa7\xdc\xaf\x95\x43\xec\x0f\x12" +
|
||||
"\x88\x2e\x99\x92\x4e\xc9\xde\x29\x8d\xce\x24\xc9\xed\xe0\x83\x7c" +
|
||||
"\x53\x37\xd1\x7a\xdd\xcb\xee\x60\x6d\xa2\x5e\x19\x26\xfe\x43\xe6" +
|
||||
"\x53\x97\xcb\xc2\x92\x73\x14\xb8\xac\x6f\x6e\x62\x24\x51\x63\xef" +
|
||||
"\x9e\x76\x27\x73\xb3\xb8\x2b\x78\x23\xd6\x89\xc5\xb1\x5e\x2e\x4f" +
|
||||
"\x2c\x9e\x79\x75\xe3\x6f\x16\x37\x1a\x04\x9b\xba\xec\x1c\x57\xbe" +
|
||||
"\x3d\xea\xc1\x7b\x22\xc1\xba\x49\xbe\x5a\xa6\x4a\xbc\x90\xe5\x72" +
|
||||
"\x99\x9b\x6a\x80\x6e\x69\x3f\x77\x0e\x77\x67\xae\x9c\x79\xca\xaa" +
|
||||
"\xd0\x9d\xe1\xdd\xec\xb4\x2c\x0e\x24\xaa\x42\x82\xe9\xba\x2b\x8b" +
|
||||
"\xf5\xd3\xf3\xe3\x6b\x82\x77\x42\x2e\x55\xd3\xb0\xbe\x0f\x36\x3c" +
|
||||
"\x88\x39\x9b\xa4\x63\x5e\xd9\xa7\xb3\xf4\xd0\x27\x3a\xed\x88\xb3" +
|
||||
"\x35\x46\x63\x70\xf1\xa8\x43\xc3\x32\x8b\x0e\x38\xc3\xf4\xa0\x68" +
|
||||
"\x12\xbc\x6d\x1f\x27\xea\x03\x39\x15\xba\x53\xeb\x10\x70\x7e\xcc" +
|
||||
"\x12\xb9\x31\x6b\x39\x6c\x2f\x7e\x5c\xd5\x45\x5a\x2a\x68\x3d\x48" +
|
||||
"\x8a\x5d\x57\xec\x2f\xe1\x33\xc0\xb1\xa5\xfe\x3a\xd4\xff\x0e\x00" +
|
||||
"\x00\xff\xff\x8b\x62\x79\xeb\xf1\x05\x00\x00"
|
||||
|
||||
// addTrustCrt returns raw, uncompressed file data.
|
||||
func addTrustCrt() []byte {
|
||||
var empty [0]byte
|
||||
sx := (*reflect.StringHeader)(unsafe.Pointer(&_addTrustCrt))
|
||||
b := empty[:]
|
||||
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bx.Data = sx.Data
|
||||
bx.Len = len(_addTrustCrt)
|
||||
bx.Cap = bx.Len
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
|
||||
if err != nil {
|
||||
panic("Decompression failed: " + err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
)
|
||||
|
||||
var (
|
||||
Config *tls.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
pool := x509.NewCertPool()
|
||||
for _, b := range [][]byte{ngrokRootCrt(), snakeoilCaCrt()} {
|
||||
pemBlock, _ := pem.Decode(b)
|
||||
if pemBlock == nil {
|
||||
panic("Bad PEM data")
|
||||
}
|
||||
|
||||
certs, err := x509.ParseCertificates(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pool.AddCert(certs[0])
|
||||
}
|
||||
|
||||
Config = &tls.Config{
|
||||
RootCAs: pool,
|
||||
ServerName: "tls.ngrok.com",
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _ngrokRootCrt = "" +
|
||||
"\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x74\x94\x5b\xcf\xaa\x38" +
|
||||
"\x14\x86\xef\xf9\x15\x73\x6f\x26\xa2\x82\xca\xc5\x5c\xb4\xa5\x40" +
|
||||
"\xd1\x02\xe5\x28\xdc\x71\x12\x04\xf1\x88\x16\xf9\xf5\xe3\xe7\xb7" +
|
||||
"\x33\xd9\x33\x7b\x76\x93\x26\xcd\xdb\x64\xad\xb7\x5d\xcf\x5a\x7f" +
|
||||
"\x7e\x2d\x88\x75\x62\xfd\x81\xb0\xeb\x13\x8d\x20\xe0\xe3\x8f\x2a" +
|
||||
"\x50\x42\xb0\xd5\x20\x04\x86\x65\x05\x38\x81\xa0\x7a\x6f\x1f\x58" +
|
||||
"\xb0\x6a\xaf\x75\x7b\xd0\x15\x2e\x42\xc0\x02\x0d\xa8\xf0\x49\xd9" +
|
||||
"\x9d\x23\x16\xab\x21\x63\x3a\xe6\xa6\xef\xfa\x38\x10\x28\x24\x3a" +
|
||||
"\x98\x05\x18\xd5\x74\xcb\x22\xb7\x0d\x0d\x73\x96\x2f\x58\xc5\x02" +
|
||||
"\x32\x98\x0d\x68\x61\x65\x85\x10\xdc\x7d\x23\xd0\xda\x44\x73\x5f" +
|
||||
"\xc5\xce\x12\x09\x0e\xa5\x42\x0f\x5f\x59\xa7\xdd\x05\xa2\xb9\x41" +
|
||||
"\x80\xa0\x9d\xec\xdc\x45\xb6\x30\x6f\xd4\x23\x9c\x80\x4f\x16\x15" +
|
||||
"\x0f\x47\x98\xe8\x6e\x90\x9f\xc2\xb1\x40\x50\x2b\x0d\xf7\x98\x77" +
|
||||
"\x72\x9d\x21\xa8\x32\x0f\x7a\xd9\x5c\x11\xdf\x0e\xa4\x9d\xea\x03" +
|
||||
"\x4e\xd5\x60\xa4\x2a\xe6\x96\x5a\x8d\xb6\x76\x7e\x6b\xe4\xbf\x1a" +
|
||||
"\xcf\x46\xbc\xa5\xa0\xfd\x38\x86\x35\x45\x81\x18\x0c\x82\xa6\x02" +
|
||||
"\xef\xdb\xe6\xd9\x47\xe2\xcf\x36\x35\x44\xbd\x98\x9b\xdf\x6e\xb6" +
|
||||
"\x78\x98\xfd\xaf\x1b\xe1\x6d\x27\x08\x35\x50\xf9\x5d\x28\x16\x73" +
|
||||
"\xe5\x95\x8e\xf8\x40\x11\xf8\x64\x01\x03\x4d\x7e\xfe\x17\x77\x57" +
|
||||
"\x8b\xc9\xce\x7c\xc4\x11\xaf\x98\x88\xab\xa0\x53\x9e\x42\xa1\xbe" +
|
||||
"\x2b\xf0\x7e\xb7\xca\x62\x73\x73\x4e\x48\xfd\xcc\x2d\xc0\x30\x84" +
|
||||
"\x0c\xa8\x55\x85\x1d\xf0\x75\xcf\xce\xe8\x7d\x86\x60\xbb\x5f\xe8" +
|
||||
"\x8d\xd3\xad\x2b\x80\xb7\xfe\xa9\xf2\x8f\xcf\x5e\x30\x56\xc3\x5d" +
|
||||
"\x5d\xcf\x67\x93\x83\x3d\x1f\xfb\x65\x86\x7d\x7b\x77\x41\x47\xba" +
|
||||
"\x4f\xec\xfd\x33\xb8\xae\xdb\x89\xa8\xea\x0f\xdb\x19\x27\x61\x1f" +
|
||||
"\x68\xb7\xe8\xf8\xea\x82\xe8\x8c\xb8\xb7\xbb\x65\xdb\xcb\x4e\x11" +
|
||||
"\x1e\xf4\x3a\xb5\xc6\xaa\x37\x9a\xa5\xcb\xd2\x19\x0f\xef\x7b\xee" +
|
||||
"\x8f\xd3\x33\xbd\xc8\xe2\xeb\x7e\x60\xa1\x7d\xd2\x77\x5c\x91\x4e" +
|
||||
"\xc9\x05\x38\x60\x79\x8f\xd3\x4b\xa9\x91\x49\x59\x2f\xb5\x6b\x60" +
|
||||
"\x8d\x3b\xa1\x6b\x97\x4f\x98\xd9\x5d\x9e\x78\x79\x9e\x59\x2c\x06" +
|
||||
"\x37\x03\xcb\xa2\x04\xa5\x18\x5d\xed\xee\x9c\x7a\x71\xdc\x6e\x7a" +
|
||||
"\x7a\xc7\xeb\xe6\x3a\x5e\x9c\xda\x6a\xf6\xe3\x65\x5a\xa7\xd1\x64" +
|
||||
"\x35\x13\xb7\x3b\x21\x15\xfd\x76\x58\x2e\x1e\x59\xa0\xed\xf3\xe3" +
|
||||
"\x65\x40\x6a\x39\x96\x51\xd4\x46\x29\x0a\xac\x69\x0e\xb6\x7c\x81" +
|
||||
"\xda\xd3\x36\x15\xd5\xfa\x35\x1f\xbc\xb3\x9b\xbb\xc5\xe6\x34\x5f" +
|
||||
"\xf4\x56\x86\x57\xd7\xd1\x12\xb0\xe8\x2d\x5e\xde\xb3\x60\x1c\x1c" +
|
||||
"\x27\x9d\x2e\xa7\xd1\x25\x26\x83\xbe\xb8\x8c\xb6\x13\x9e\xc2\x44" +
|
||||
"\xc9\xc5\xcb\x4c\x4c\x17\xe8\xd0\x1f\xfb\xde\x42\xd9\x10\xbd\x1e" +
|
||||
"\xc6\x73\xb5\x9a\x1c\x8b\x40\x09\x44\x21\x3a\xe4\x08\x70\x0c\x40" +
|
||||
"\x6a\xc3\x85\x8a\xe0\xdc\x07\xc5\x17\x17\x06\x93\xb0\xf6\x06\xfa" +
|
||||
"\x96\xcd\xe3\xb2\xf1\x44\xf3\xb9\x5f\x0e\x28\x09\x56\xdc\x56\x24" +
|
||||
"\xe4\x6f\x43\x78\xe6\x88\xc7\xaa\x10\xba\xa2\xf3\xee\x16\x15\x54" +
|
||||
"\x58\xa7\x60\xfd\x55\xff\x02\x73\x0c\xa7\x9c\x69\x14\x50\x08\xf6" +
|
||||
"\x6b\x5e\x25\x1f\xfa\x0a\xc2\x3d\xd8\xfa\x08\x36\x67\xf0\x4f\x60" +
|
||||
"\xe1\xd7\xc8\xd7\x3a\x5f\xba\x03\xd5\xd7\x03\x1a\x81\xf9\x4d\x69" +
|
||||
"\xec\x83\xa3\xa5\x51\x97\x71\x5c\x7d\xc8\xdc\x60\xde\x7f\xc8\x14" +
|
||||
"\x7e\xa0\x09\x59\x83\x3b\x8a\xd8\x77\x67\x0e\xb4\xf8\x1d\x81\xa1" +
|
||||
"\xe6\x32\x82\xe5\x63\x61\x14\xcf\xbc\xbb\x0f\x02\x69\x40\xf5\x9d" +
|
||||
"\x85\xfa\xfa\x6f\x5a\xf6\x3d\x31\x20\xd1\xcc\x67\xb6\xf0\xde\x54" +
|
||||
"\xe2\x7f\x51\x2b\xbc\x05\xed\x07\xb6\x6f\x54\x51\x26\x91\xe0\xc8" +
|
||||
"\xfb\xb8\x91\xaa\x49\x04\x2f\x9b\x82\x25\x87\x7c\x1e\xbb\x72\x55" +
|
||||
"\xb4\x65\x34\x30\x83\x8c\xc9\xb1\x59\xa9\x71\xb1\x7a\xdc\x59\x34" +
|
||||
"\x18\x42\x4c\x2c\xf7\xee\xb4\x2f\xa7\xdc\xaf\x95\x43\xec\x0f\x12" +
|
||||
"\x88\x2e\x99\x92\x4e\xc9\xde\x29\x8d\xce\x24\xc9\xed\xe0\x83\x7c" +
|
||||
"\x53\x37\xd1\x7a\xdd\xcb\xee\x60\x6d\xa2\x5e\x19\x26\xfe\x43\xe6" +
|
||||
"\x53\x97\xcb\xc2\x92\x73\x14\xb8\xac\x6f\x6e\x62\x24\x51\x63\xef" +
|
||||
"\x9e\x76\x27\x73\xb3\xb8\x2b\x78\x23\xd6\x89\xc5\xb1\x5e\x2e\x4f" +
|
||||
"\x2c\x9e\x79\x75\xe3\x6f\x16\x37\x1a\x04\x9b\xba\xec\x1c\x57\xbe" +
|
||||
"\x3d\xea\xc1\x7b\x22\xc1\xba\x49\xbe\x5a\xa6\x4a\xbc\x90\xe5\x72" +
|
||||
"\x99\x9b\x6a\x80\x6e\x69\x3f\x77\x0e\x77\x67\xae\x9c\x79\xca\xaa" +
|
||||
"\xd0\x9d\xe1\xdd\xec\xb4\x2c\x0e\x24\xaa\x42\x82\xe9\xba\x2b\x8b" +
|
||||
"\xf5\xd3\xf3\xe3\x6b\x82\x77\x42\x2e\x55\xd3\xb0\xbe\x0f\x36\x3c" +
|
||||
"\x88\x39\x9b\xa4\x63\x5e\xd9\xa7\xb3\xf4\xd0\x27\x3a\xed\x88\xb3" +
|
||||
"\x35\x46\x63\x70\xf1\xa8\x43\xc3\x32\x8b\x0e\x38\xc3\xf4\xa0\x68" +
|
||||
"\x12\xbc\x6d\x1f\x27\xea\x03\x39\x15\xba\x53\xeb\x10\x70\x7e\xcc" +
|
||||
"\x12\xb9\x31\x6b\x39\x6c\x2f\x7e\x5c\xd5\x45\x5a\x2a\x68\x3d\x48" +
|
||||
"\x8a\x5d\x57\xec\x2f\xe1\x33\xc0\xb1\xa5\xfe\x3a\xd4\xff\x0e\x00" +
|
||||
"\x00\xff\xff\x8b\x62\x79\xeb\xf1\x05\x00\x00"
|
||||
|
||||
// ngrokRootCrt returns raw, uncompressed file data.
|
||||
func ngrokRootCrt() []byte {
|
||||
var empty [0]byte
|
||||
sx := (*reflect.StringHeader)(unsafe.Pointer(&_ngrokRootCrt))
|
||||
b := empty[:]
|
||||
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bx.Data = sx.Data
|
||||
bx.Len = len(_ngrokRootCrt)
|
||||
bx.Cap = bx.Len
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
|
||||
if err != nil {
|
||||
panic("Decompression failed: " + err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _snakeoilCaCrt = "" +
|
||||
"\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x6c\x95\xc7\x0e\xab\xce" +
|
||||
"\x92\xc6\xf7\x3c\xc5\xec\xd1\x08\x0c\x26\x2d\x66\xd1\x34\x4d\x06" +
|
||||
"\x93\x31\xec\x8c\x03\x98\x9c\xd3\xd3\x8f\xff\xe7\x48\x23\xcd\xbd" +
|
||||
"\xb7\x57\xad\xaf\x5a\xbf\x2e\x95\xea\xab\xfa\xef\x7f\x8e\x88\x14" +
|
||||
"\xcd\xfe\x2f\x88\xbc\x40\x93\x35\x08\x02\xf4\x47\xc5\x2c\x4d\x93" +
|
||||
"\x65\x09\x42\xb0\x6e\x10\xba\xb0\xca\x6c\xd2\x53\x88\xee\xc2\x48" +
|
||||
"\xc0\x16\xf3\x6a\x28\xaa\xaf\x22\x6c\xa4\x08\xdc\x50\x06\x92\x68" +
|
||||
"\x59\xee\xb4\x41\x37\x91\x22\xd7\x55\xd0\xa6\x47\x58\x78\xa2\xc0" +
|
||||
"\xfa\xc1\xc1\x25\x44\x50\xb4\x0c\x97\x92\xa7\x47\x9c\xae\xcf\x86" +
|
||||
"\xe9\x93\x00\xf9\x96\x08\xfe\xc6\x0a\x4b\xcf\x9a\xd7\x91\x51\xd3" +
|
||||
"\x92\x50\xc2\x6c\x79\xee\x86\xf2\x44\xc2\x7e\x24\x49\x06\xd3\x60" +
|
||||
"\x36\x4c\xfb\x6c\x84\xd1\x6c\xec\x35\x0b\xc0\x5b\xde\xc8\xdd\x3a" +
|
||||
"\x01\x65\x49\xd6\x66\x9d\xe1\x6e\x07\xe9\xe3\xa7\x1d\x7f\x35\xf4" +
|
||||
"\x7f\x1a\x66\xa1\x6d\x87\x27\xd0\xc5\xdc\x8e\x44\x90\x04\xa0\x8e" +
|
||||
"\x02\xcb\xb3\x36\xf4\x37\x4d\x0d\x6d\xbd\x94\xc4\x7b\x9f\x36\xc2" +
|
||||
"\x91\x35\x75\x61\x79\xda\x86\xc0\x9f\x98\x81\xb6\x7a\xc1\x52\x5a" +
|
||||
"\x5f\x1f\x07\x53\x66\x14\xb9\xcb\x12\xf0\xff\x82\xac\x10\x1e\xdd" +
|
||||
"\xf2\xff\x52\xd6\x34\xa8\x95\xff\x5a\x17\x24\x63\x00\xdc\x20\xc8" +
|
||||
"\x79\xf0\xcf\x03\x98\x1b\xbf\x3b\x02\xac\x3b\x1e\xf7\xb7\x51\xf3" +
|
||||
"\x20\x8e\x1f\xc2\xa2\x50\x61\xe6\xdf\xfa\xae\x53\xb9\xeb\x69\x56" +
|
||||
"\xf7\x89\x96\xd3\x4f\x25\xe4\xc6\x3a\x54\x5f\xec\xbc\xdf\x5d\xe8" +
|
||||
"\x35\x73\xc8\x4a\x47\x4b\xe2\xb7\xc5\xa7\x27\x44\x8c\x5e\x93\x82" +
|
||||
"\xc9\x2f\x2b\x57\x21\x12\x69\x7e\x21\xc2\xcf\xcd\xeb\x2b\x62\x7d" +
|
||||
"\x96\x1a\xbe\x6e\x9b\x3b\x5d\x55\x7a\x64\xe8\x17\x98\x03\xaf\xea" +
|
||||
"\x96\xf1\x41\x69\x09\x31\x03\x5f\x78\xb7\x45\xa7\xe5\x92\x6b\x4d" +
|
||||
"\x82\xa4\x70\x2b\x3e\x79\x12\xe4\x7a\xc7\x4f\xe9\xb6\x35\x5e\xac" +
|
||||
"\x94\xb6\xe1\x0c\x57\x8a\x5a\x26\x59\x80\xba\xfc\x24\x30\xde\x4d" +
|
||||
"\xf0\x9b\xa4\x4b\xea\xd2\xf2\x51\x04\xaa\x81\xbe\x7f\x9e\xce\x3d" +
|
||||
"\x3f\x66\x55\x1b\xf6\xd5\xd7\xdb\x24\x97\xba\x45\x86\x00\x57\x82" +
|
||||
"\xc9\xe8\x09\x96\xf1\x99\xe7\x37\xf7\x6b\x6d\x74\xd3\x4c\xc5\x38" +
|
||||
"\x8e\x79\x06\x71\x01\xcb\x35\x69\x07\x36\x3f\x8f\xb1\xa2\xbd\xaf" +
|
||||
"\xf2\xca\x2e\x9a\xb2\x68\xfa\x67\xb6\xf4\x7d\xd1\x0f\x3d\xca\x3e" +
|
||||
"\x81\x3c\x0f\xb9\xa5\x04\x4d\xa9\xa6\xfb\xd7\x5c\x2d\xee\x75\x72" +
|
||||
"\x58\x49\x64\xcd\x78\x5e\x9d\xb5\x2d\x32\xd7\x4f\x53\xb3\x98\x56" +
|
||||
"\xe7\xd2\xf1\xcd\xde\x76\xb6\xd5\x77\x44\xd0\x31\xb3\xda\x13\xc1" +
|
||||
"\xc4\x66\x8c\xec\xda\x26\x27\xab\x7d\x73\x8b\x4c\xd0\xb9\xb4\xfc" +
|
||||
"\xba\x63\xd1\x78\x04\x65\xe7\x97\x5f\x44\xeb\xa6\xa4\xa4\x9a\xed" +
|
||||
"\x0a\x96\x8c\x9c\xdc\x39\x3d\xfe\x87\x1e\xe4\x8e\x60\xdf\xdc\x22" +
|
||||
"\x5e\x41\x52\x1b\x9d\xdb\x91\x17\xe3\xdc\xf1\xa4\x89\x36\xcf\x40" +
|
||||
"\xf3\x88\x45\x30\xf0\x52\xef\x59\xb3\x2c\x9e\x5b\xd5\xf3\x4e\x8e" +
|
||||
"\x47\x17\xb5\x79\x5e\x6a\x71\xbc\x5c\x5f\xfc\x02\x0a\xba\xfc\xe0" +
|
||||
"\xb8\xf1\x22\xa5\x32\x23\x6a\x4e\x82\x4e\x8f\x72\x5f\xdf\xd2\x24" +
|
||||
"\x28\xa1\x89\xa5\xac\xbf\x7f\xc5\xcd\xbd\x76\xc2\x0b\xb9\x8f\x97" +
|
||||
"\xa5\xe4\x15\x3d\xd7\x92\x0c\xc5\x69\x54\xbb\x81\xb3\x8f\x73\xbd" +
|
||||
"\xab\x0e\x29\xca\x94\x6a\x64\xbc\xe1\x21\x11\x19\x50\x5b\xdc\x52" +
|
||||
"\xf0\x60\x11\x63\x39\xc9\x9d\xcd\xad\x2f\xdb\x3c\x9e\x48\x78\x7f" +
|
||||
"\x24\xf5\x81\x07\x92\x83\x33\x52\x0a\x2d\x45\x12\xaa\xe6\x5e\xb9" +
|
||||
"\x89\x30\x10\xe7\x30\xac\xd6\x8c\x07\x38\x11\x88\x86\x50\x6f\xe1" +
|
||||
"\xd4\x7d\xa9\xf0\x89\xad\x02\xbd\xf9\x78\x60\x2f\x24\xfb\xf0\xba" +
|
||||
"\x7e\x70\x3a\x21\x49\x47\x9a\xaf\x1e\xf4\x6e\x38\xdf\x9b\xc0\x5e" +
|
||||
"\xfb\x8a\x12\x3b\xd9\x66\xb8\x9c\x63\x39\x85\xaf\x84\x20\x2b\x2a" +
|
||||
"\x71\xff\xce\xab\x1c\x42\x0c\xfc\xac\x01\x82\xff\xe0\xfd\x3f\x2d" +
|
||||
"\x8e\x80\xf7\x21\x8a\x28\x69\xe7\x33\xdc\xe4\x30\x77\xc7\x58\x22" +
|
||||
"\x6b\x22\x7c\x40\x31\x1f\xeb\x75\x8f\x20\x76\x84\x0f\x5e\x9b\x4a" +
|
||||
"\x7a\x7d\x9f\xa0\x90\xfd\x23\x6d\x67\x64\xe2\xc8\x94\x57\x7e\x5d" +
|
||||
"\xdd\x6c\x16\x15\x55\x25\x66\xd8\x72\x6c\xbc\x0c\xe5\x76\x2b\x23" +
|
||||
"\x93\xa2\x8f\xbd\x7a\xe8\xd3\xd8\x7a\x02\x8e\x05\x9e\x2d\xb7\xd1" +
|
||||
"\x5b\xe4\x2f\x0c\x87\x6b\x32\xab\x1a\xa7\x09\x4d\x42\xd5\xc0\xd7" +
|
||||
"\x25\xab\x8d\xa0\x6e\xbe\xf9\xfb\x34\x6d\x41\x95\x53\x57\x40\x12" +
|
||||
"\x82\xaf\x3e\x7b\x8d\x51\x00\x19\xd7\x41\x81\xb0\xab\x32\xe4\xcf" +
|
||||
"\xf0\x6b\x0b\x0d\xde\x98\x7c\x19\x2b\x74\x5e\xe2\xfe\x1d\x72\xda" +
|
||||
"\x33\xf2\xe9\x15\xac\xfe\x45\xe7\x8c\x93\xb4\xcf\xa0\xb8\xbc\x92" +
|
||||
"\xca\xb5\x63\xbd\x7e\x2c\x37\xca\x6d\x05\x26\x10\x04\xac\xaf\x2b" +
|
||||
"\xc7\x29\x78\xee\x48\x6f\xc2\x43\xc8\xf6\xfe\x2e\x38\xa1\x5e\x05" +
|
||||
"\xfa\x79\xdb\xc2\x2a\xd5\x7c\x2f\x45\xbf\x09\x45\xc2\xcf\xd4\x87" +
|
||||
"\xfd\x40\xf4\xc5\xb9\x04\xd1\xf7\xe0\x3a\xea\x33\xe2\x41\x87\xed" +
|
||||
"\x51\xf4\xa0\x1e\x46\x20\x5c\xa8\xb5\xfe\xf5\x41\xbc\x31\xdd\x80" +
|
||||
"\x0c\x6e\x3f\xf6\x60\x98\x92\x2d\xe2\x60\x58\x97\xbf\x22\x14\xbd" +
|
||||
"\xcf\x6d\xb7\xb4\xd8\x4e\x8d\x1e\xc2\x4a\x50\x0d\x95\xf4\x66\x01" +
|
||||
"\x23\x54\x77\x02\xf6\xe2\x7f\xab\xb4\x9b\xec\xf5\x65\xd1\x44\xb1" +
|
||||
"\xd2\x4f\x46\xf2\xc2\xdb\x66\x7c\x8d\x57\x96\xa7\xf0\x18\xdc\x0f" +
|
||||
"\x7f\xf7\xdf\xba\x67\x5d\xbe\xee\xfa\x54\x3b\x3e\xe4\x2b\xa4\x23" +
|
||||
"\x34\x63\xaf\xe6\x33\xb7\x38\x99\xab\xb4\xe7\x4c\x11\x49\xf1\x38" +
|
||||
"\x77\x97\xd8\x6d\x7c\x0c\x9f\xe7\xcf\x6d\xf6\x69\xb6\x03\x35\x4a" +
|
||||
"\xa6\x0f\x3e\x0a\x1d\x70\x7d\x4f\xb6\x17\x41\x3f\xc2\xef\x3b\x9e" +
|
||||
"\x02\xef\xca\x60\xbd\xf2\xdc\xec\xfe\x2e\x55\xf8\xf6\x1e\x5c\xa3" +
|
||||
"\x6a\xb7\xcc\xee\x8a\x07\xdd\x31\x97\xfb\x40\x5c\xda\xc2\x9b\x87" +
|
||||
"\x31\x74\x34\x94\xdf\x5a\x31\x16\x61\xc4\xaf\x55\x71\xb7\x0a\x21" +
|
||||
"\xca\xde\x78\x07\x1a\xcc\x6c\xb6\xb7\x7d\xb1\x46\xd6\x2a\x47\x35" +
|
||||
"\x7e\xbd\xa2\x16\xd7\x95\xa7\xc8\xf4\x38\xb0\xe2\x91\x3f\x11\x9e" +
|
||||
"\x15\xbd\x03\x41\xbb\xf4\xb2\x27\xf0\x27\xfd\xc9\x6e\xf5\xdb\x82" +
|
||||
"\xf1\xc0\xba\x78\x61\x39\x98\x15\x06\xc5\xa0\x7b\x37\xf5\xd1\xa8" +
|
||||
"\x9e\xa6\xd3\xda\xc9\x6f\x85\x36\x96\x1c\x32\x2a\x49\x34\x4c\xf4" +
|
||||
"\x41\x17\xb4\xfb\xb4\x28\x44\x2e\x9e\xbc\xd4\x83\x9d\xca\xf7\xfe" +
|
||||
"\x6a\xa0\xe6\xe6\xa8\x1e\xae\xd8\xd5\xb7\x96\x84\xd0\xab\xb4\xee" +
|
||||
"\xfe\x07\xfb\xb3\xed\x90\x2d\xfd\xfb\x06\xfc\xdf\x00\x00\x00\xff" +
|
||||
"\xff\x92\x24\xb8\x34\x1e\x07\x00\x00"
|
||||
|
||||
// snakeoilCaCrt returns raw, uncompressed file data.
|
||||
func snakeoilCaCrt() []byte {
|
||||
var empty [0]byte
|
||||
sx := (*reflect.StringHeader)(unsafe.Pointer(&_snakeoilCaCrt))
|
||||
b := empty[:]
|
||||
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bx.Data = sx.Data
|
||||
bx.Len = len(_snakeoilCaCrt)
|
||||
bx.Cap = bx.Len
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
|
||||
if err != nil {
|
||||
panic("Decompression failed: " + err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package ui
|
||||
|
||||
type Command interface{}
|
||||
|
||||
type CmdQuit struct {
|
||||
// display this message after quit
|
||||
Message string
|
||||
}
|
||||
|
||||
type CmdRequest struct {
|
||||
// the bytes of the request to issue
|
||||
Payload []byte
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/* The controller in the MVC
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
// the model sends updates through this broadcast channel
|
||||
Updates *util.Broadcast
|
||||
|
||||
// all views put any commands into this channel
|
||||
Cmds chan Command
|
||||
|
||||
// all threads may add themself to this to wait for clean shutdown
|
||||
Wait *sync.WaitGroup
|
||||
|
||||
// channel to signal shutdown
|
||||
Shutdown chan int
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
ctl := &Controller{
|
||||
Updates: util.NewBroadcast(),
|
||||
Cmds: make(chan Command),
|
||||
Wait: new(sync.WaitGroup),
|
||||
Shutdown: make(chan int),
|
||||
}
|
||||
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (ctl *Controller) Update(state State) {
|
||||
ctl.Updates.In() <- state
|
||||
}
|
||||
|
||||
func (ctl *Controller) DoShutdown() {
|
||||
close(ctl.Shutdown)
|
||||
}
|
||||
|
||||
func (ctl *Controller) IsShuttingDown() bool {
|
||||
select {
|
||||
case <-ctl.Shutdown:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
)
|
||||
|
||||
type State interface {
|
||||
GetClientVersion() string
|
||||
GetServerVersion() string
|
||||
GetNewVersion() string
|
||||
GetPublicUrl() string
|
||||
GetLocalAddr() string
|
||||
GetStatus() string
|
||||
GetProtocol() proto.Protocol
|
||||
GetWebPort() int
|
||||
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
|
||||
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
|
||||
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
|
||||
}
|
||||
11
src/ngrok/client/update_debug.go
Normal file
11
src/ngrok/client/update_debug.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !release,!autoupdate
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"ngrok/client/mvc"
|
||||
)
|
||||
|
||||
// no auto-updating in debug mode
|
||||
func autoUpdate(state mvc.State, token string) {
|
||||
}
|
||||
123
src/ngrok/client/update_release.go
Normal file
123
src/ngrok/client/update_release.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// +build release autoupdate
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
update "github.com/inconshreveable/go-update"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
updateEndpoint = "https://dl.ngrok.com/update"
|
||||
checkEndpoint = "https://dl.ngrok.com/update/check"
|
||||
)
|
||||
|
||||
func progressWatcher(s mvc.State, progress chan int, complete chan int) {
|
||||
for {
|
||||
select {
|
||||
case pct, ok := <-progress:
|
||||
if !ok {
|
||||
close(complete)
|
||||
return
|
||||
} else if pct == 100 {
|
||||
s.SetUpdateStatus(mvc.UpdateInstalling)
|
||||
close(complete)
|
||||
return
|
||||
} else {
|
||||
if pct%25 == 0 {
|
||||
log.Info("Downloading update %d%% complete", pct)
|
||||
}
|
||||
s.SetUpdateStatus(mvc.UpdateStatus(pct))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpdate(s mvc.State, token string) {
|
||||
tryAgain := true
|
||||
|
||||
params := make(url.Values)
|
||||
params.Add("version", version.MajorMinor())
|
||||
params.Add("os", runtime.GOOS)
|
||||
params.Add("arch", runtime.GOARCH)
|
||||
params.Add("user", token)
|
||||
|
||||
updateUrl := updateEndpoint + "?" + params.Encode()
|
||||
checkUrl := checkEndpoint + "?" + params.Encode()
|
||||
|
||||
update := func() {
|
||||
log.Info("Checking for update")
|
||||
available, err := update.NewDownload(checkUrl).Check()
|
||||
if err != nil {
|
||||
log.Error("Error while checking for update: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !available {
|
||||
log.Info("No update available")
|
||||
return
|
||||
}
|
||||
|
||||
// stop trying after a single download attempt
|
||||
// XXX: improve this so the we can:
|
||||
// 1. safely update multiple times
|
||||
// 2. only retry after a network connection failure
|
||||
tryAgain = false
|
||||
|
||||
download := update.NewDownload(updateUrl)
|
||||
downloadComplete := make(chan int)
|
||||
|
||||
go progressWatcher(s, download.Progress, downloadComplete)
|
||||
|
||||
log.Info("Trying to update . . .")
|
||||
err, errRecover := download.GetAndUpdate()
|
||||
<-downloadComplete
|
||||
|
||||
if err != nil {
|
||||
// log error to console
|
||||
log.Error("Error while updating ngrok: %v", err)
|
||||
if errRecover != nil {
|
||||
log.Error("Error while recovering from failed ngrok update, your binary may be missing: %v", errRecover.Error())
|
||||
params.Add("errorRecover", errRecover.Error())
|
||||
}
|
||||
|
||||
// log error to ngrok.com's servers for debugging purposes
|
||||
params.Add("error", err.Error())
|
||||
resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params)
|
||||
if err != nil {
|
||||
log.Error("Error while reporting update error: %v, %v", err, reportErr)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// tell the user to update manually
|
||||
s.SetUpdateStatus(mvc.UpdateAvailable)
|
||||
} else {
|
||||
if !download.Available {
|
||||
// this is the way the server tells us to update manually
|
||||
log.Info("Server wants us to update manually")
|
||||
s.SetUpdateStatus(mvc.UpdateAvailable)
|
||||
} else {
|
||||
// tell the user the update is ready
|
||||
log.Info("Update ready!")
|
||||
s.SetUpdateStatus(mvc.UpdateReady)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// try to update immediately and then at a set interval
|
||||
update()
|
||||
for _ = range time.Tick(updateCheckInterval) {
|
||||
if !tryAgain {
|
||||
break
|
||||
}
|
||||
update()
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,26 @@ package term
|
||||
|
||||
import (
|
||||
termbox "github.com/nsf/termbox-go"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
size = 10
|
||||
size = 10
|
||||
pathMaxLength = 25
|
||||
)
|
||||
|
||||
type HttpView struct {
|
||||
log.Logger
|
||||
*area
|
||||
|
||||
httpProto *proto.Http
|
||||
HttpRequests *util.Ring
|
||||
shutdown chan int
|
||||
flush chan int
|
||||
*area
|
||||
log.Logger
|
||||
termView *TermView
|
||||
}
|
||||
|
||||
func colorFor(status string) termbox.Attribute {
|
||||
@@ -33,19 +37,16 @@ func colorFor(status string) termbox.Attribute {
|
||||
return termbox.ColorWhite
|
||||
}
|
||||
|
||||
func NewHttp(proto *proto.Http, flush, shutdown chan int, x, y int) *HttpView {
|
||||
func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView {
|
||||
v := &HttpView{
|
||||
httpProto: proto,
|
||||
HttpRequests: util.NewRing(size),
|
||||
area: NewArea(x, y, 70, size+5),
|
||||
shutdown: shutdown,
|
||||
flush: flush,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
shutdown: make(chan int),
|
||||
termView: termView,
|
||||
Logger: log.NewPrefixLogger("view", "term", "http"),
|
||||
}
|
||||
v.AddLogPrefix("view")
|
||||
v.AddLogPrefix("term")
|
||||
v.AddLogPrefix("http")
|
||||
go v.Run()
|
||||
ctl.Go(v.Run)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -54,9 +55,6 @@ func (v *HttpView) Run() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-v.shutdown:
|
||||
return
|
||||
|
||||
case txn := <-updates:
|
||||
v.Debug("Got HTTP update")
|
||||
if txn.(*proto.HttpTxn).Resp == nil {
|
||||
@@ -73,10 +71,49 @@ func (v *HttpView) Render() {
|
||||
v.Printf(0, 1, "-------------")
|
||||
for i, obj := range v.HttpRequests.Slice() {
|
||||
txn := obj.(*proto.HttpTxn)
|
||||
v.Printf(0, 3+i, "%s %v", txn.Req.Method, txn.Req.URL.Path)
|
||||
path := truncatePath(txn.Req.URL.Path)
|
||||
v.Printf(0, 3+i, "%s %v", txn.Req.Method, path)
|
||||
if txn.Resp != nil {
|
||||
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
|
||||
}
|
||||
}
|
||||
v.flush <- 1
|
||||
v.termView.Flush()
|
||||
}
|
||||
|
||||
func (v *HttpView) Shutdown() {
|
||||
close(v.shutdown)
|
||||
}
|
||||
|
||||
func truncatePath(path string) string {
|
||||
// Truncate all long strings based on rune count
|
||||
if utf8.RuneCountInString(path) > pathMaxLength {
|
||||
path = string([]rune(path)[:pathMaxLength])
|
||||
}
|
||||
|
||||
// By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes.
|
||||
// Otherwise, we have a multi-byte string and need to calculate the size of each rune and
|
||||
// truncate manually.
|
||||
//
|
||||
// This is a workaround for a bug in termbox-go. Remove it when this issue is fixed:
|
||||
// https://github.com/nsf/termbox-go/pull/21
|
||||
if len(path) > pathMaxLength {
|
||||
out := make([]byte, pathMaxLength, pathMaxLength)
|
||||
length := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(path[length:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
// utf8.EncodeRune expects there to be enough room to store the full size of the rune
|
||||
if length+size <= pathMaxLength {
|
||||
utf8.EncodeRune(out[length:], r)
|
||||
length += size
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
path = string(out[:length])
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -1,76 +1,63 @@
|
||||
/*
|
||||
interactive terminal interface for local clients
|
||||
*/
|
||||
// interactive terminal interface for local clients
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
termbox "github.com/nsf/termbox-go"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TermView struct {
|
||||
ctl *ui.Controller
|
||||
ctl mvc.Controller
|
||||
updates chan interface{}
|
||||
flush chan int
|
||||
subviews []ui.View
|
||||
state ui.State
|
||||
shutdown chan int
|
||||
redraw *util.Broadcast
|
||||
subviews []mvc.View
|
||||
log.Logger
|
||||
*area
|
||||
}
|
||||
|
||||
func New(ctl *ui.Controller, state ui.State) *TermView {
|
||||
func NewTermView(ctl mvc.Controller) *TermView {
|
||||
// initialize terminal display
|
||||
termbox.Init()
|
||||
|
||||
// make sure ngrok doesn't quit until we've cleaned up
|
||||
ctl.Wait.Add(1)
|
||||
|
||||
w, _ := termbox.Size()
|
||||
|
||||
v := &TermView{
|
||||
ctl: ctl,
|
||||
updates: ctl.Updates.Reg(),
|
||||
updates: ctl.Updates().Reg(),
|
||||
redraw: util.NewBroadcast(),
|
||||
flush: make(chan int),
|
||||
subviews: make([]ui.View, 0),
|
||||
state: state,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
shutdown: make(chan int),
|
||||
Logger: log.NewPrefixLogger("view", "term"),
|
||||
area: NewArea(0, 0, w, 10),
|
||||
}
|
||||
|
||||
v.Logger.AddLogPrefix("view")
|
||||
v.Logger.AddLogPrefix("term")
|
||||
|
||||
switch p := state.GetProtocol().(type) {
|
||||
case *proto.Http:
|
||||
v.subviews = append(v.subviews, NewHttp(p, v.flush, ctl.Shutdown, 0, 10))
|
||||
default:
|
||||
}
|
||||
|
||||
v.Render()
|
||||
|
||||
go v.run()
|
||||
go v.input()
|
||||
ctl.Go(v.run)
|
||||
ctl.Go(v.input)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func colorForConn(status string) termbox.Attribute {
|
||||
func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) {
|
||||
switch status {
|
||||
case "connecting":
|
||||
return termbox.ColorCyan
|
||||
case "reconnecting":
|
||||
return termbox.ColorRed
|
||||
case "online":
|
||||
return termbox.ColorGreen
|
||||
case mvc.ConnConnecting:
|
||||
return "connecting", termbox.ColorCyan
|
||||
case mvc.ConnReconnecting:
|
||||
return "reconnecting", termbox.ColorRed
|
||||
case mvc.ConnOnline:
|
||||
return "online", termbox.ColorGreen
|
||||
}
|
||||
return termbox.ColorWhite
|
||||
return "unknown", termbox.ColorWhite
|
||||
}
|
||||
|
||||
func (v *TermView) Render() {
|
||||
func (v *TermView) draw() {
|
||||
state := v.ctl.State()
|
||||
|
||||
v.Clear()
|
||||
|
||||
// quit instructions
|
||||
@@ -78,54 +65,98 @@ func (v *TermView) Render() {
|
||||
v.Printf(v.w-len(quitMsg), 0, quitMsg)
|
||||
|
||||
// new version message
|
||||
newVersion := v.state.GetNewVersion()
|
||||
if newVersion != "" {
|
||||
newVersionMsg := fmt.Sprintf("new version available at http://ngrok.com")
|
||||
v.APrintf(termbox.ColorYellow, 30, 0, newVersionMsg)
|
||||
updateStatus := state.GetUpdateStatus()
|
||||
var updateMsg string
|
||||
switch updateStatus {
|
||||
case mvc.UpdateNone:
|
||||
updateMsg = ""
|
||||
case mvc.UpdateInstalling:
|
||||
updateMsg = "ngrok is updating"
|
||||
case mvc.UpdateReady:
|
||||
updateMsg = "ngrok has updated: restart ngrok for the new version"
|
||||
case mvc.UpdateAvailable:
|
||||
updateMsg = "new version available at https://ngrok.com"
|
||||
default:
|
||||
pct := float64(updateStatus) / 100.0
|
||||
const barLength = 25
|
||||
full := int(barLength * pct)
|
||||
bar := make([]byte, barLength+2)
|
||||
bar[0] = '['
|
||||
bar[barLength+1] = ']'
|
||||
for i := 0; i < 25; i++ {
|
||||
if i <= full {
|
||||
bar[i+1] = '#'
|
||||
} else {
|
||||
bar[i+1] = ' '
|
||||
}
|
||||
}
|
||||
updateMsg = "Downloading update: " + string(bar)
|
||||
}
|
||||
|
||||
if updateMsg != "" {
|
||||
v.APrintf(termbox.ColorYellow, 30, 0, updateMsg)
|
||||
}
|
||||
|
||||
v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
|
||||
statusStr, statusColor := connStatusRepr(state.GetConnStatus())
|
||||
v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr)
|
||||
|
||||
status := v.state.GetStatus()
|
||||
v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
|
||||
v.Printf(0, 3, "%-30s%s/%s", "Version", state.GetClientVersion(), state.GetServerVersion())
|
||||
var i int = 4
|
||||
for _, t := range state.GetTunnels() {
|
||||
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
|
||||
i++
|
||||
}
|
||||
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
|
||||
|
||||
v.Printf(0, 3, "%-30s%s/%s", "Version", v.state.GetClientVersion(), v.state.GetServerVersion())
|
||||
v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
|
||||
v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
|
||||
webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
|
||||
v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
|
||||
|
||||
connMeter, connTimer := v.state.GetConnectionMetrics()
|
||||
v.Printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
|
||||
connMeter, connTimer := state.GetConnectionMetrics()
|
||||
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())
|
||||
|
||||
msec := float64(time.Millisecond)
|
||||
v.Printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
|
||||
v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
|
||||
|
||||
termbox.Flush()
|
||||
}
|
||||
|
||||
func (v *TermView) run() {
|
||||
defer v.ctl.Wait.Done()
|
||||
defer close(v.shutdown)
|
||||
defer termbox.Close()
|
||||
|
||||
redraw := v.redraw.Reg()
|
||||
defer v.redraw.UnReg(redraw)
|
||||
|
||||
v.draw()
|
||||
for {
|
||||
v.Debug("Waiting for update")
|
||||
select {
|
||||
case <-v.flush:
|
||||
termbox.Flush()
|
||||
|
||||
case obj := <-v.updates:
|
||||
if obj != nil {
|
||||
v.state = obj.(ui.State)
|
||||
}
|
||||
v.Render()
|
||||
case <-v.updates:
|
||||
v.draw()
|
||||
|
||||
case <-v.ctl.Shutdown:
|
||||
case <-redraw:
|
||||
v.draw()
|
||||
|
||||
case <-v.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TermView) Shutdown() {
|
||||
v.shutdown <- 1
|
||||
<-v.shutdown
|
||||
}
|
||||
|
||||
func (v *TermView) Flush() {
|
||||
v.flush <- 1
|
||||
}
|
||||
|
||||
func (v *TermView) NewHttpView(p *proto.Http) *HttpView {
|
||||
return newTermHttpView(v.ctl, v, p, 0, 12)
|
||||
}
|
||||
|
||||
func (v *TermView) input() {
|
||||
for {
|
||||
ev := termbox.PollEvent()
|
||||
@@ -134,21 +165,14 @@ func (v *TermView) input() {
|
||||
switch ev.Key {
|
||||
case termbox.KeyCtrlC:
|
||||
v.Info("Got quit command")
|
||||
v.ctl.Cmds <- ui.CmdQuit{}
|
||||
v.ctl.Shutdown("")
|
||||
}
|
||||
|
||||
case termbox.EventResize:
|
||||
v.Info("Resize event, redrawing")
|
||||
// send nil to update channel to force re-rendering
|
||||
v.updates <- nil
|
||||
for _, sv := range v.subviews {
|
||||
sv.Render()
|
||||
}
|
||||
v.redraw.In() <- 1
|
||||
|
||||
case termbox.EventError:
|
||||
if v.ctl.IsShuttingDown() {
|
||||
return
|
||||
}
|
||||
panic(ev.Err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/views/web/static"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
type SerializedTxn struct {
|
||||
Id string
|
||||
Duration int64
|
||||
Start int64
|
||||
ConnCtx mvc.ConnectionContext
|
||||
*proto.HttpTxn `json:"-"`
|
||||
Req SerializedRequest
|
||||
Resp SerializedResponse
|
||||
@@ -54,17 +56,18 @@ type SerializedResponse struct {
|
||||
}
|
||||
|
||||
type WebHttpView struct {
|
||||
log.Logger
|
||||
|
||||
webview *WebView
|
||||
ctl *ui.Controller
|
||||
ctl mvc.Controller
|
||||
httpProto *proto.Http
|
||||
updates chan interface{}
|
||||
state chan SerializedUiState
|
||||
HttpRequests *util.Ring
|
||||
idToTxn map[string]*SerializedTxn
|
||||
}
|
||||
|
||||
type SerializedUiState struct {
|
||||
Url string
|
||||
Tunnels []mvc.Tunnel
|
||||
}
|
||||
|
||||
type SerializedPayload struct {
|
||||
@@ -72,20 +75,18 @@ type SerializedPayload struct {
|
||||
UiState SerializedUiState
|
||||
}
|
||||
|
||||
func NewWebHttpView(wv *WebView, ctl *ui.Controller, proto *proto.Http) *WebHttpView {
|
||||
w := &WebHttpView{
|
||||
func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView {
|
||||
whv := &WebHttpView{
|
||||
Logger: log.NewPrefixLogger("view", "web", "http"),
|
||||
webview: wv,
|
||||
ctl: ctl,
|
||||
httpProto: proto,
|
||||
idToTxn: make(map[string]*SerializedTxn),
|
||||
updates: ctl.Updates.Reg(),
|
||||
state: make(chan SerializedUiState),
|
||||
HttpRequests: util.NewRing(20),
|
||||
}
|
||||
go w.updateHttp()
|
||||
go w.updateUiState()
|
||||
w.register()
|
||||
return w
|
||||
ctl.Go(whv.updateHttp)
|
||||
whv.register()
|
||||
return whv
|
||||
}
|
||||
|
||||
type XMLDoc struct {
|
||||
@@ -111,21 +112,22 @@ func makeBody(h http.Header, body []byte) SerializedBody {
|
||||
if b.RawContentType != "" {
|
||||
b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0])
|
||||
switch b.ContentType {
|
||||
case "application/xml":
|
||||
case "text/xml":
|
||||
case "application/xml", "text/xml":
|
||||
err = xml.Unmarshal(body, new(XMLDoc))
|
||||
if err != nil {
|
||||
syntaxError := err.(*xml.SyntaxError)
|
||||
// xml syntax errors only give us a line number, so we
|
||||
// count to find an offset
|
||||
b.ErrorOffset = offsetForLine(syntaxError.Line)
|
||||
if syntaxError, ok := err.(*xml.SyntaxError); ok {
|
||||
// xml syntax errors only give us a line number, so we
|
||||
// count to find an offset
|
||||
b.ErrorOffset = offsetForLine(syntaxError.Line)
|
||||
}
|
||||
}
|
||||
|
||||
case "application/json":
|
||||
err = json.Unmarshal(body, new(json.RawMessage))
|
||||
if err != nil {
|
||||
syntaxError := err.(*json.SyntaxError)
|
||||
b.ErrorOffset = int(syntaxError.Offset)
|
||||
if syntaxError, ok := err.(*json.SyntaxError); ok {
|
||||
b.ErrorOffset = int(syntaxError.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
@@ -151,22 +153,16 @@ func (whv *WebHttpView) updateHttp() {
|
||||
|
||||
// we haven't processed this transaction yet if we haven't set the
|
||||
// user data
|
||||
if htxn.UserData == nil {
|
||||
id, err := util.RandId(8)
|
||||
if err != nil {
|
||||
log.Error("Failed to generate txn identifier for web storage: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if htxn.UserCtx == nil {
|
||||
rawReq, err := httputil.DumpRequestOut(htxn.Req.Request, true)
|
||||
if err != nil {
|
||||
log.Error("Failed to dump request: %v", err)
|
||||
whv.Error("Failed to dump request: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
body := makeBody(htxn.Req.Header, htxn.Req.BodyBytes)
|
||||
whtxn := &SerializedTxn{
|
||||
Id: id,
|
||||
Id: util.RandId(8),
|
||||
HttpTxn: htxn,
|
||||
Req: SerializedRequest{
|
||||
MethodPath: htxn.Req.Method + " " + htxn.Req.URL.Path,
|
||||
@@ -176,9 +172,11 @@ func (whv *WebHttpView) updateHttp() {
|
||||
Body: body,
|
||||
Binary: !utf8.Valid(rawReq),
|
||||
},
|
||||
Start: htxn.Start.Unix(),
|
||||
ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext),
|
||||
}
|
||||
|
||||
htxn.UserData = whtxn
|
||||
htxn.UserCtx = whtxn
|
||||
// XXX: unsafe map access from multiple go routines
|
||||
whv.idToTxn[whtxn.Id] = whtxn
|
||||
// XXX: use return value to delete from map so we don't leak memory
|
||||
@@ -186,11 +184,11 @@ func (whv *WebHttpView) updateHttp() {
|
||||
} else {
|
||||
rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
|
||||
if err != nil {
|
||||
log.Error("Failed to dump response: %v", err)
|
||||
whv.Error("Failed to dump response: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
txn := htxn.UserData.(*SerializedTxn)
|
||||
txn := htxn.UserCtx.(*SerializedTxn)
|
||||
body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes)
|
||||
txn.Duration = htxn.Duration.Nanoseconds()
|
||||
txn.Resp = SerializedResponse{
|
||||
@@ -203,48 +201,56 @@ func (whv *WebHttpView) updateHttp() {
|
||||
|
||||
payload, err := json.Marshal(txn)
|
||||
if err != nil {
|
||||
log.Error("Failed to serialized txn payload for websocket: %v", err)
|
||||
whv.Error("Failed to serialized txn payload for websocket: %v", err)
|
||||
}
|
||||
whv.webview.wsMessages.In() <- payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *WebHttpView) updateUiState() {
|
||||
var s SerializedUiState
|
||||
for {
|
||||
select {
|
||||
case obj := <-v.updates:
|
||||
uiState := obj.(ui.State)
|
||||
s.Url = uiState.GetPublicUrl()
|
||||
case v.state <- s:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebHttpView) register() {
|
||||
func (whv *WebHttpView) register() {
|
||||
http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
whv.Error("Replay failed: %v", err)
|
||||
http.Error(w, err, 500)
|
||||
}
|
||||
}()
|
||||
|
||||
r.ParseForm()
|
||||
txnid := r.Form.Get("txnid")
|
||||
if txn, ok := h.idToTxn[txnid]; ok {
|
||||
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
|
||||
if txn, ok := whv.idToTxn[txnid]; ok {
|
||||
reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.ctl.Cmds <- ui.CmdRequest{Payload: bodyBytes}
|
||||
whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes)
|
||||
w.Write([]byte(http.StatusText(200)))
|
||||
} else {
|
||||
// XXX: 400
|
||||
http.NotFound(w, r)
|
||||
http.Error(w, http.StatusText(400), 400)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(static.PageHtml())))
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
whv.Error("HTTP web view failed: %v", err)
|
||||
http.Error(w, err, 500)
|
||||
}
|
||||
}()
|
||||
|
||||
pageTmpl, err := assets.ReadAsset("assets/client/page.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
|
||||
|
||||
payloadData := SerializedPayload{
|
||||
Txns: h.HttpRequests.Slice(),
|
||||
UiState: <-h.state,
|
||||
Txns: whv.HttpRequests.Slice(),
|
||||
UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(payloadData)
|
||||
@@ -258,3 +264,6 @@ func (h *WebHttpView) register() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (whv *WebHttpView) Shutdown() {
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// +build !release
|
||||
|
||||
package static
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
var assetDir string
|
||||
|
||||
func init() {
|
||||
// find the directory with the assets.
|
||||
// this doesn't work if you:
|
||||
// 1. move the binary
|
||||
// 2. put ngrok in your PATH
|
||||
// but you shouldn't be doing either of these things while developng anyways
|
||||
var binPath string
|
||||
execPath := os.Args[0]
|
||||
if path.IsAbs(execPath) {
|
||||
binPath = execPath
|
||||
} else {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binPath = path.Join(wd, execPath)
|
||||
}
|
||||
assetDir = path.Join(path.Dir(path.Dir(binPath)), "assets")
|
||||
|
||||
// call all the functions on startup to make sure the files exist
|
||||
fns := []func() []byte{
|
||||
PageHtml,
|
||||
HighlightJs,
|
||||
HighlightCss,
|
||||
BootstrapCss,
|
||||
JqueryJs,
|
||||
VkBeautifyJs,
|
||||
AngularJs,
|
||||
NgrokJs,
|
||||
Base64Js,
|
||||
}
|
||||
for _, f := range fns {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func ReadFileOrPanic(p string) []byte {
|
||||
bytes, err := ioutil.ReadFile(path.Join(assetDir, p))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
func PageHtml() []byte { return ReadFileOrPanic("page.html") }
|
||||
func HighlightJs() []byte { return ReadFileOrPanic("highlight.min.js") }
|
||||
func HighlightCss() []byte { return ReadFileOrPanic("highlight.min.css") }
|
||||
func BootstrapCss() []byte { return ReadFileOrPanic("bootstrap.min.css") }
|
||||
func JqueryJs() []byte { return ReadFileOrPanic("jquery-1.9.1.min.js") }
|
||||
func VkBeautifyJs() []byte { return ReadFileOrPanic("vkbeautify.js") }
|
||||
func AngularJs() []byte { return ReadFileOrPanic("angular.js") }
|
||||
func NgrokJs() []byte { return ReadFileOrPanic("ngrok.js") }
|
||||
func Base64Js() []byte { return ReadFileOrPanic("base64.js") }
|
||||
@@ -1,12 +0,0 @@
|
||||
package static
|
||||
|
||||
var AssetMap = map[string]func() []byte{
|
||||
"jquery-1.9.1.min.js": JqueryJs,
|
||||
"bootstrap.min.css": BootstrapCss,
|
||||
"highlight.min.css": HighlightCss,
|
||||
"highlight.min.js": HighlightJs,
|
||||
"vkbeautify.js": VkBeautifyJs,
|
||||
"angular.js": AngularJs,
|
||||
"ngrok.js": NgrokJs,
|
||||
"base64.js": Base64Js,
|
||||
}
|
||||
@@ -2,46 +2,49 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/garyburd/go-websocket/websocket"
|
||||
"net/http"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/views/web/static"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"strings"
|
||||
"path"
|
||||
)
|
||||
|
||||
type WebView struct {
|
||||
log.Logger
|
||||
|
||||
ctl mvc.Controller
|
||||
|
||||
// messages sent over this broadcast are sent too all websocket connections
|
||||
wsMessages *util.Broadcast
|
||||
}
|
||||
|
||||
func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
|
||||
v := &WebView{
|
||||
func NewWebView(ctl mvc.Controller, addr string) *WebView {
|
||||
wv := &WebView{
|
||||
Logger: log.NewPrefixLogger("view", "web"),
|
||||
wsMessages: util.NewBroadcast(),
|
||||
ctl: ctl,
|
||||
}
|
||||
|
||||
switch p := state.GetProtocol().(type) {
|
||||
case *proto.Http:
|
||||
NewWebHttpView(v, ctl, p)
|
||||
}
|
||||
|
||||
// for now, always redirect to the http view
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/http/in", 302)
|
||||
})
|
||||
|
||||
// handle web socket connections
|
||||
http.HandleFunc("/_ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Upgrade(w, r.Header, nil, 1024, 1024)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed websocket upgrade", 400)
|
||||
log.Warn("Failed websocket upgrade: %v", err)
|
||||
wv.Warn("Failed websocket upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
msgs := v.wsMessages.Reg()
|
||||
defer v.wsMessages.UnReg(msgs)
|
||||
msgs := wv.wsMessages.Reg()
|
||||
defer wv.wsMessages.UnReg(msgs)
|
||||
for m := range msgs {
|
||||
err := conn.WriteMessage(websocket.OpText, m.([]byte))
|
||||
if err != nil {
|
||||
@@ -51,18 +54,25 @@ func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
|
||||
}
|
||||
})
|
||||
|
||||
// serve static assets
|
||||
http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
name := parts[len(parts)-1]
|
||||
fn, ok := static.AssetMap[name]
|
||||
if !ok {
|
||||
buf, err := assets.ReadAsset(path.Join("assets", "client", r.URL.Path[1:]))
|
||||
if err != nil {
|
||||
wv.Warn("Error serving static file: %s", err.Error())
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Write(fn())
|
||||
w.Write(buf)
|
||||
})
|
||||
|
||||
log.Info("Serving web interface on localhost:%d", port)
|
||||
go http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||
return v
|
||||
wv.Info("Serving web interface on %s", addr)
|
||||
wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
|
||||
return wv
|
||||
}
|
||||
|
||||
func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView {
|
||||
return newWebHttpView(wv.ctl, wv, proto)
|
||||
}
|
||||
|
||||
func (wv *WebView) Shutdown() {
|
||||
}
|
||||
|
||||
@@ -4,21 +4,27 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"ngrok/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
log.Logger
|
||||
Id() string
|
||||
SetType(string)
|
||||
CloseRead() error
|
||||
}
|
||||
|
||||
type tcpConn struct {
|
||||
type loggedConn struct {
|
||||
tcp *net.TCPConn
|
||||
net.Conn
|
||||
log.Logger
|
||||
id int32
|
||||
@@ -26,126 +32,207 @@ type tcpConn struct {
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
*net.TCPAddr
|
||||
net.Addr
|
||||
Conns chan Conn
|
||||
}
|
||||
|
||||
func wrapTcpConn(conn net.Conn, typ string) *tcpConn {
|
||||
c := &tcpConn{conn, log.NewPrefixLogger(), rand.Int31(), typ}
|
||||
c.AddLogPrefix(c.Id())
|
||||
return c
|
||||
func wrapConn(conn net.Conn, typ string) *loggedConn {
|
||||
switch c := conn.(type) {
|
||||
case *loggedConn:
|
||||
return c
|
||||
case *net.TCPConn:
|
||||
wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ}
|
||||
wrapped.AddLogPrefix(wrapped.Id())
|
||||
return wrapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Listen(addr *net.TCPAddr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
|
||||
func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
|
||||
// listen for incoming connections
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l = &Listener{
|
||||
TCPAddr: listener.Addr().(*net.TCPAddr),
|
||||
Conns: make(chan Conn),
|
||||
Addr: listener.Addr(),
|
||||
Conns: make(chan Conn),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
tcpConn, err := listener.AcceptTCP()
|
||||
rawConn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Error("Failed to accept new TCP connection of type %s: %v", typ, err)
|
||||
continue
|
||||
}
|
||||
|
||||
c := wrapTcpConn(tcpConn, typ)
|
||||
c := wrapConn(rawConn, typ)
|
||||
if tlsCfg != nil {
|
||||
c.Conn = tls.Server(c.Conn, tlsCfg)
|
||||
}
|
||||
c.Info("New connection from %v", tcpConn.RemoteAddr())
|
||||
c.Info("New connection from %v", c.RemoteAddr())
|
||||
l.Conns <- c
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func Wrap(conn net.Conn, typ string) *tcpConn {
|
||||
return wrapTcpConn(conn, typ)
|
||||
func Wrap(conn net.Conn, typ string) *loggedConn {
|
||||
return wrapConn(conn, typ)
|
||||
}
|
||||
|
||||
func Dial(addr, typ string, tlsCfg *tls.Config) (conn *tcpConn, err error) {
|
||||
var (
|
||||
tcpAddr *net.TCPAddr
|
||||
tcpConn net.Conn
|
||||
)
|
||||
|
||||
if tcpAddr, err = net.ResolveTCPAddr("tcp", addr); err != nil {
|
||||
func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
|
||||
var rawConn net.Conn
|
||||
if rawConn, err = net.Dial("tcp", addr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tcpConn, err = net.DialTCP("tcp", nil, tcpAddr); err != nil {
|
||||
return
|
||||
}
|
||||
conn = wrapConn(rawConn, typ)
|
||||
conn.Debug("New connection to: %v", rawConn.RemoteAddr())
|
||||
|
||||
if tlsCfg != nil {
|
||||
tcpConn = tls.Client(tcpConn, tlsCfg)
|
||||
conn.StartTLS(tlsCfg)
|
||||
}
|
||||
|
||||
conn = wrapTcpConn(tcpConn, typ)
|
||||
conn.Debug("New connection to: %v", tcpAddr)
|
||||
return conn, nil
|
||||
return
|
||||
}
|
||||
|
||||
func (c *tcpConn) Close() error {
|
||||
c.Debug("Closing")
|
||||
return c.Conn.Close()
|
||||
func DialHttpProxy(proxyUrl, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
|
||||
// parse the proxy address
|
||||
var parsedUrl *url.URL
|
||||
if parsedUrl, err = url.Parse(proxyUrl); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var proxyAuth string
|
||||
if parsedUrl.User != nil {
|
||||
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(parsedUrl.User.String()))
|
||||
}
|
||||
|
||||
var proxyTlsConfig *tls.Config
|
||||
switch parsedUrl.Scheme {
|
||||
case "http":
|
||||
proxyTlsConfig = nil
|
||||
case "https":
|
||||
proxyTlsConfig = new(tls.Config)
|
||||
default:
|
||||
err = fmt.Errorf("Proxy URL scheme must be http or https, got: %s", parsedUrl.Scheme)
|
||||
return
|
||||
}
|
||||
|
||||
// dial the proxy
|
||||
if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send an HTTP proxy CONNECT message
|
||||
req, err := http.NewRequest("CONNECT", "https://"+addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if proxyAuth != "" {
|
||||
req.Header.Set("Proxy-Authorization", proxyAuth)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ngrok)")
|
||||
req.Write(conn)
|
||||
|
||||
// read the proxy's response
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("Non-200 response from proxy server: %s", resp.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// upgrade to TLS
|
||||
conn.StartTLS(tlsCfg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *tcpConn) Id() string {
|
||||
func (c *loggedConn) StartTLS(tlsCfg *tls.Config) {
|
||||
c.Conn = tls.Client(c.Conn, tlsCfg)
|
||||
}
|
||||
|
||||
func (c *loggedConn) Close() (err error) {
|
||||
if err := c.Conn.Close(); err == nil {
|
||||
c.Debug("Closing")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *loggedConn) Id() string {
|
||||
return fmt.Sprintf("%s:%x", c.typ, c.id)
|
||||
}
|
||||
|
||||
func (c *loggedConn) SetType(typ string) {
|
||||
oldId := c.Id()
|
||||
c.typ = typ
|
||||
c.ClearLogPrefixes()
|
||||
c.AddLogPrefix(c.Id())
|
||||
c.Info("Renamed connection %s", oldId)
|
||||
}
|
||||
|
||||
func (c *loggedConn) CloseRead() error {
|
||||
// XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner
|
||||
// connection termination. Unfortunately, when I've tried that, I've observed
|
||||
// failures where the connection was closed *before* flushing its write buffer,
|
||||
// set with SetLinger() set properly (which it is by default).
|
||||
return c.tcp.CloseRead()
|
||||
}
|
||||
|
||||
func Join(c Conn, c2 Conn) (int64, int64) {
|
||||
done := make(chan error)
|
||||
var wait sync.WaitGroup
|
||||
|
||||
pipe := func(to Conn, from Conn, bytesCopied *int64) {
|
||||
defer to.Close()
|
||||
defer from.Close()
|
||||
defer wait.Done()
|
||||
|
||||
var err error
|
||||
*bytesCopied, err = io.Copy(to, from)
|
||||
if err != nil {
|
||||
from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err)
|
||||
done <- err
|
||||
} else {
|
||||
from.Debug("Copied %d bytes from to %s", *bytesCopied, to.Id())
|
||||
done <- nil
|
||||
from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id())
|
||||
}
|
||||
}
|
||||
|
||||
wait.Add(2)
|
||||
var fromBytes, toBytes int64
|
||||
go pipe(c, c2, &fromBytes)
|
||||
go pipe(c2, c, &toBytes)
|
||||
c.Info("Joined with connection %s", c2.Id())
|
||||
<-done
|
||||
c.Close()
|
||||
c2.Close()
|
||||
<-done
|
||||
wait.Wait()
|
||||
return fromBytes, toBytes
|
||||
}
|
||||
|
||||
type httpConn struct {
|
||||
*tcpConn
|
||||
*loggedConn
|
||||
reqBuf *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewHttp(conn net.Conn, typ string) *httpConn {
|
||||
return &httpConn{
|
||||
wrapTcpConn(conn, typ),
|
||||
wrapConn(conn, typ),
|
||||
bytes.NewBuffer(make([]byte, 0, 1024)),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *httpConn) ReadRequest() (*http.Request, error) {
|
||||
r := io.TeeReader(c.tcpConn, c.reqBuf)
|
||||
r := io.TeeReader(c.loggedConn, c.reqBuf)
|
||||
return http.ReadRequest(bufio.NewReader(r))
|
||||
}
|
||||
|
||||
func (c *tcpConn) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
func (c *loggedConn) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
// special case when we're reading from an http request where
|
||||
// we had to parse the request and consume bytes from the socket
|
||||
// and store them in a temporary request buffer
|
||||
|
||||
@@ -26,6 +26,7 @@ func LogTo(target string) {
|
||||
|
||||
type Logger interface {
|
||||
AddLogPrefix(string)
|
||||
ClearLogPrefixes()
|
||||
Debug(string, ...interface{})
|
||||
Info(string, ...interface{})
|
||||
Warn(string, ...interface{}) error
|
||||
@@ -37,8 +38,14 @@ type PrefixLogger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewPrefixLogger() Logger {
|
||||
return &PrefixLogger{Logger: &root}
|
||||
func NewPrefixLogger(prefixes ...string) Logger {
|
||||
logger := &PrefixLogger{Logger: &root}
|
||||
|
||||
for _, p := range prefixes {
|
||||
logger.AddLogPrefix(p)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func (pl *PrefixLogger) pfx(fmtstr string) interface{} {
|
||||
@@ -69,6 +76,10 @@ func (pl *PrefixLogger) AddLogPrefix(prefix string) {
|
||||
pl.prefix += "[" + prefix + "]"
|
||||
}
|
||||
|
||||
func (pl *PrefixLogger) ClearLogPrefixes() {
|
||||
pl.prefix = ""
|
||||
}
|
||||
|
||||
// we should never really use these . . . always prefer logging through a prefix logger
|
||||
func Debug(arg0 string, args ...interface{}) {
|
||||
root.Debug(arg0, args...)
|
||||
|
||||
@@ -11,16 +11,15 @@ func init() {
|
||||
TypeMap = make(map[string]reflect.Type)
|
||||
|
||||
t := func(obj interface{}) reflect.Type { return reflect.TypeOf(obj).Elem() }
|
||||
TypeMap["RegMsg"] = t((*RegMsg)(nil))
|
||||
TypeMap["RegAckMsg"] = t((*RegAckMsg)(nil))
|
||||
TypeMap["RegProxyMsg"] = t((*RegProxyMsg)(nil))
|
||||
TypeMap["ReqProxyMsg"] = t((*ReqProxyMsg)(nil))
|
||||
TypeMap["PingMsg"] = t((*PingMsg)(nil))
|
||||
TypeMap["PongMsg"] = t((*PongMsg)(nil))
|
||||
TypeMap["VerisonMsg"] = t((*VersionMsg)(nil))
|
||||
TypeMap["VersionRespMsg"] = t((*VersionRespMsg)(nil))
|
||||
TypeMap["MetricsMsg"] = t((*MetricsMsg)(nil))
|
||||
TypeMap["MetricsRespMsg"] = t((*MetricsRespMsg)(nil))
|
||||
TypeMap["Auth"] = t((*Auth)(nil))
|
||||
TypeMap["AuthResp"] = t((*AuthResp)(nil))
|
||||
TypeMap["ReqTunnel"] = t((*ReqTunnel)(nil))
|
||||
TypeMap["NewTunnel"] = t((*NewTunnel)(nil))
|
||||
TypeMap["RegProxy"] = t((*RegProxy)(nil))
|
||||
TypeMap["ReqProxy"] = t((*ReqProxy)(nil))
|
||||
TypeMap["StartProxy"] = t((*StartProxy)(nil))
|
||||
TypeMap["Ping"] = t((*Ping)(nil))
|
||||
TypeMap["Pong"] = t((*Pong)(nil))
|
||||
}
|
||||
|
||||
type Message interface{}
|
||||
@@ -30,52 +29,92 @@ type Envelope struct {
|
||||
Payload json.RawMessage
|
||||
}
|
||||
|
||||
type RegMsg struct {
|
||||
Version string
|
||||
MmVersion string
|
||||
Protocol string
|
||||
Hostname string
|
||||
Subdomain string
|
||||
ClientId string
|
||||
HttpAuth string
|
||||
// When a client opens a new control channel to the server
|
||||
// it must start by sending an Auth message.
|
||||
type Auth struct {
|
||||
Version string // protocol version
|
||||
MmVersion string // major/minor software version (informational only)
|
||||
User string
|
||||
Password string
|
||||
OS string
|
||||
Arch string
|
||||
ClientId string // empty for new sessions
|
||||
}
|
||||
|
||||
type RegAckMsg struct {
|
||||
// A server responds to an Auth message with an
|
||||
// AuthResp message over the control channel.
|
||||
//
|
||||
// If Error is not the empty string
|
||||
// the server has indicated it will not accept
|
||||
// the new session and will close the connection.
|
||||
//
|
||||
// The server response includes a unique ClientId
|
||||
// that is used to associate and authenticate future
|
||||
// proxy connections via the same field in RegProxy messages.
|
||||
type AuthResp struct {
|
||||
Version string
|
||||
MmVersion string
|
||||
Url string
|
||||
ProxyAddr string
|
||||
ClientId string
|
||||
Error string
|
||||
}
|
||||
|
||||
type RegProxyMsg struct {
|
||||
Url string
|
||||
// A client sends this message to the server over the control channel
|
||||
// to request a new tunnel be opened on the client's behalf.
|
||||
// ReqId is a random number set by the client that it can pull
|
||||
// from future NewTunnel's to correlate then to the requesting ReqTunnel.
|
||||
type ReqTunnel struct {
|
||||
ReqId string
|
||||
Protocol string
|
||||
|
||||
// http only
|
||||
Hostname string
|
||||
Subdomain string
|
||||
HttpAuth string
|
||||
|
||||
// tcp only
|
||||
RemotePort uint16
|
||||
}
|
||||
|
||||
type ReqProxyMsg struct {
|
||||
// When the server opens a new tunnel on behalf of
|
||||
// a client, it sends a NewTunnel message to notify the client.
|
||||
// ReqId is the ReqId from the corresponding ReqTunnel message.
|
||||
//
|
||||
// A client may receive *multiple* NewTunnel messages from a single
|
||||
// ReqTunnel. (ex. A client opens an https tunnel and the server
|
||||
// chooses to open an http tunnel of the same name as well)
|
||||
type NewTunnel struct {
|
||||
ReqId string
|
||||
Url string
|
||||
Protocol string
|
||||
Error string
|
||||
}
|
||||
|
||||
type PingMsg struct {
|
||||
// When the server wants to initiate a new tunneled connection, it sends
|
||||
// this message over the control channel to the client. When a client receives
|
||||
// this message, it must initiate a new proxy connection to the server.
|
||||
type ReqProxy struct {
|
||||
}
|
||||
|
||||
type PongMsg struct {
|
||||
// After a client receives a ReqProxy message, it opens a new
|
||||
// connection to the server and sends a RegProxy message.
|
||||
type RegProxy struct {
|
||||
ClientId string
|
||||
}
|
||||
|
||||
type VersionMsg struct {
|
||||
// This message is sent by the server to the client over a *proxy* connection before it
|
||||
// begins to send the bytes of the proxied request.
|
||||
type StartProxy struct {
|
||||
Url string // URL of the tunnel this connection connection is being proxied for
|
||||
ClientAddr string // Network address of the client initiating the connection to the tunnel
|
||||
}
|
||||
|
||||
type VersionRespMsg struct {
|
||||
Version string
|
||||
MmVersion string
|
||||
// A client or server may send this message periodically over
|
||||
// the control channel to request that the remote side acknowledge
|
||||
// its connection is still alive. The remote side must respond with a Pong.
|
||||
type Ping struct {
|
||||
}
|
||||
|
||||
type MetricsMsg struct {
|
||||
}
|
||||
|
||||
type MetricsRespMsg struct {
|
||||
Metrics string
|
||||
// Sent by a client or server over the control channel to indicate
|
||||
// it received a Ping.
|
||||
type Pong struct {
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"ngrok/conn"
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -24,11 +24,12 @@ type HttpResponse struct {
|
||||
}
|
||||
|
||||
type HttpTxn struct {
|
||||
Req *HttpRequest
|
||||
Resp *HttpResponse
|
||||
Start time.Time
|
||||
Duration time.Duration
|
||||
UserData interface{}
|
||||
Req *HttpRequest
|
||||
Resp *HttpResponse
|
||||
Start time.Time
|
||||
Duration time.Duration
|
||||
UserCtx interface{}
|
||||
ConnUserCtx interface{}
|
||||
}
|
||||
|
||||
type Http struct {
|
||||
@@ -55,15 +56,17 @@ func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) {
|
||||
|
||||
func (h *Http) GetName() string { return "http" }
|
||||
|
||||
func (h *Http) WrapConn(c conn.Conn) conn.Conn {
|
||||
func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
|
||||
tee := conn.NewTee(c)
|
||||
lastTxn := make(chan *HttpTxn)
|
||||
go h.readRequests(tee, lastTxn)
|
||||
go h.readRequests(tee, lastTxn, ctx)
|
||||
go h.readResponses(tee, lastTxn)
|
||||
return tee
|
||||
}
|
||||
|
||||
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) {
|
||||
defer close(lastTxn)
|
||||
|
||||
for {
|
||||
req, err := http.ReadRequest(tee.WriteBuffer())
|
||||
if err != nil {
|
||||
@@ -80,13 +83,11 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
tee.Warn("Failed to extract request body: %v", err)
|
||||
}
|
||||
|
||||
// net/http's ReadRequest doesn't properly create the req.URL
|
||||
// structure, which is needed to properly DumpRequest() later
|
||||
req.URL, err = url.Parse(req.RequestURI)
|
||||
req.URL.Host = req.Host
|
||||
// golang's ReadRequest/DumpRequestOut is broken. Fix up the request so it works later
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = req.Host
|
||||
|
||||
txn := &HttpTxn{Start: time.Now()}
|
||||
txn := &HttpTxn{Start: time.Now(), ConnUserCtx: connCtx}
|
||||
txn.Req = &HttpRequest{Request: req}
|
||||
txn.Req.BodyBytes, txn.Req.Body, err = extractBody(req.Body)
|
||||
|
||||
@@ -96,9 +97,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
}
|
||||
|
||||
func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
for {
|
||||
var err error
|
||||
txn := <-lastTxn
|
||||
for txn := range lastTxn {
|
||||
resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request)
|
||||
txn.Duration = time.Since(txn.Start)
|
||||
h.reqTimer.Update(txn.Duration)
|
||||
@@ -118,5 +117,29 @@ func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
}
|
||||
|
||||
h.Txns.In() <- txn
|
||||
|
||||
// XXX: remove web socket shim in favor of a real websocket protocol analyzer
|
||||
if txn.Req.Header.Get("Upgrade") == "websocket" {
|
||||
tee.Info("Upgrading to websocket")
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// shim for websockets
|
||||
// in order for websockets to work, we need to continue reading all of the
|
||||
// the bytes in the analyzer so that the joined connections will continue
|
||||
// sending bytes to each other
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
ioutil.ReadAll(tee.WriteBuffer())
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ioutil.ReadAll(tee.ReadBuffer())
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import (
|
||||
|
||||
type Protocol interface {
|
||||
GetName() string
|
||||
WrapConn(conn.Conn) conn.Conn
|
||||
WrapConn(conn.Conn, interface{}) conn.Conn
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ func NewTcp() *Tcp {
|
||||
|
||||
func (h *Tcp) GetName() string { return "tcp" }
|
||||
|
||||
func (h *Tcp) WrapConn(c conn.Conn) conn.Conn {
|
||||
func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
|
||||
return c
|
||||
}
|
||||
|
||||
11
src/ngrok/server/assets/assets_debug.go
Normal file
11
src/ngrok/server/assets/assets_debug.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !release
|
||||
|
||||
package assets
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func ReadAsset(name string) ([]byte, error) {
|
||||
return ioutil.ReadFile(name)
|
||||
}
|
||||
37
src/ngrok/server/cli.go
Normal file
37
src/ngrok/server/cli.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
httpAddr string
|
||||
httpsAddr string
|
||||
tunnelAddr string
|
||||
domain string
|
||||
tlsCrt string
|
||||
tlsKey string
|
||||
logto string
|
||||
}
|
||||
|
||||
func parseArgs() *Options {
|
||||
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 ngrok client")
|
||||
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
|
||||
tlsCrt := flag.String("tlsCrt", "", "Path to a TLS certificate file")
|
||||
tlsKey := flag.String("tlsKey", "", "Path to a TLS key file")
|
||||
logto := flag.String("log", "stdout", "Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
return &Options{
|
||||
httpAddr: *httpAddr,
|
||||
httpsAddr: *httpsAddr,
|
||||
tunnelAddr: *tunnelAddr,
|
||||
domain: *domain,
|
||||
tlsCrt: *tlsCrt,
|
||||
tlsKey: *tlsKey,
|
||||
logto: *logto,
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,241 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"ngrok/conn"
|
||||
"ngrok/msg"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pingTimeoutInterval = 30 * time.Second
|
||||
connReapInterval = 10 * time.Second
|
||||
controlWriteTimeout = 10 * time.Second
|
||||
proxyStaleDuration = 60 * time.Second
|
||||
proxyMaxPoolSize = 10
|
||||
)
|
||||
|
||||
type Control struct {
|
||||
// auth message
|
||||
auth *msg.Auth
|
||||
|
||||
// actual connection
|
||||
conn conn.Conn
|
||||
|
||||
// channels for communicating messages over the connection
|
||||
out chan (interface{})
|
||||
in chan (msg.Message)
|
||||
stop chan (msg.Message)
|
||||
// put a message in this channel to send it over
|
||||
// conn to the client
|
||||
out chan (msg.Message)
|
||||
|
||||
// heartbeat
|
||||
// read from this channel to get the next message sent
|
||||
// to us over conn by the client
|
||||
in chan (msg.Message)
|
||||
|
||||
// the last time we received a ping from the client - for heartbeats
|
||||
lastPing time.Time
|
||||
|
||||
// tunnel
|
||||
tun *Tunnel
|
||||
// all of the tunnels this control connection handles
|
||||
tunnels []*Tunnel
|
||||
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// identifier
|
||||
id string
|
||||
|
||||
// synchronizer for controlled shutdown of writer()
|
||||
writerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controlled shutdown of reader()
|
||||
readerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controlled shutdown of manager()
|
||||
managerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controller shutdown of entire Control
|
||||
shutdown *util.Shutdown
|
||||
}
|
||||
|
||||
func NewControl(conn conn.Conn) {
|
||||
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
|
||||
var err error
|
||||
|
||||
// create the object
|
||||
c := &Control{
|
||||
conn: conn,
|
||||
out: make(chan (interface{}), 1),
|
||||
in: make(chan (msg.Message), 1),
|
||||
stop: make(chan (msg.Message), 1),
|
||||
lastPing: time.Now(),
|
||||
auth: authMsg,
|
||||
conn: ctlConn,
|
||||
out: make(chan msg.Message),
|
||||
in: make(chan msg.Message),
|
||||
proxies: make(chan conn.Conn, 10),
|
||||
lastPing: time.Now(),
|
||||
writerShutdown: util.NewShutdown(),
|
||||
readerShutdown: util.NewShutdown(),
|
||||
managerShutdown: util.NewShutdown(),
|
||||
shutdown: util.NewShutdown(),
|
||||
}
|
||||
|
||||
go c.managerThread()
|
||||
go c.readThread()
|
||||
failAuth := func(e error) {
|
||||
_ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()})
|
||||
ctlConn.Close()
|
||||
}
|
||||
|
||||
// register the clientid
|
||||
c.id = authMsg.ClientId
|
||||
if c.id == "" {
|
||||
// it's a new session, assign an ID
|
||||
if c.id, err = util.SecureRandId(16); err != nil {
|
||||
failAuth(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// set logging prefix
|
||||
ctlConn.SetType("ctl")
|
||||
ctlConn.AddLogPrefix(c.id)
|
||||
|
||||
if authMsg.Version != version.Proto {
|
||||
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
|
||||
return
|
||||
}
|
||||
|
||||
// register the control
|
||||
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
|
||||
replaced.shutdown.WaitComplete()
|
||||
}
|
||||
|
||||
// start the writer first so that the following messages get sent
|
||||
go c.writer()
|
||||
|
||||
// Respond to authentication
|
||||
c.out <- &msg.AuthResp{
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
ClientId: c.id,
|
||||
}
|
||||
|
||||
// As a performance optimization, ask for a proxy connection up front
|
||||
c.out <- &msg.ReqProxy{}
|
||||
|
||||
// manage the connection
|
||||
go c.manager()
|
||||
go c.reader()
|
||||
go c.stopper()
|
||||
}
|
||||
|
||||
func (c *Control) managerThread() {
|
||||
reap := time.NewTicker(connReapInterval)
|
||||
// Register a new tunnel on this control connection
|
||||
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
|
||||
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
|
||||
tunnelReq := *rawTunnelReq
|
||||
tunnelReq.Protocol = proto
|
||||
|
||||
// all shutdown functionality in here
|
||||
c.conn.Debug("Registering new tunnel")
|
||||
t, err := NewTunnel(&tunnelReq, c)
|
||||
if err != nil {
|
||||
c.out <- &msg.NewTunnel{Error: err.Error()}
|
||||
if len(c.tunnels) == 0 {
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
// we're done
|
||||
return
|
||||
}
|
||||
|
||||
// add it to the list of tunnels
|
||||
c.tunnels = append(c.tunnels, t)
|
||||
|
||||
// acknowledge success
|
||||
c.out <- &msg.NewTunnel{
|
||||
Url: t.url,
|
||||
Protocol: proto,
|
||||
ReqId: rawTunnelReq.ReqId,
|
||||
}
|
||||
|
||||
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) manager() {
|
||||
// don't crash on panics
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
reap.Stop()
|
||||
c.conn.Close()
|
||||
|
||||
// shutdown the tunnel if it's open
|
||||
if c.tun != nil {
|
||||
c.tun.shutdown()
|
||||
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
// kill everything if the control manager stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that manager() has shutdown
|
||||
defer c.managerShutdown.Complete()
|
||||
|
||||
// reaping timer for detecting heartbeat failure
|
||||
reap := time.NewTicker(connReapInterval)
|
||||
defer reap.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case m := <-c.out:
|
||||
msg.WriteMsg(c.conn, m)
|
||||
|
||||
case <-reap.C:
|
||||
if time.Since(c.lastPing) > pingTimeoutInterval {
|
||||
c.conn.Info("Lost heartbeat")
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
case mRaw, ok := <-c.in:
|
||||
// c.in closes to indicate shutdown
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
case m := <-c.stop:
|
||||
if m != nil {
|
||||
msg.WriteMsg(c.conn, m)
|
||||
}
|
||||
return
|
||||
|
||||
case mRaw := <-c.in:
|
||||
switch m := mRaw.(type) {
|
||||
case *msg.RegMsg:
|
||||
c.conn.Info("Registering new tunnel")
|
||||
c.tun = newTunnel(m, c)
|
||||
case *msg.ReqTunnel:
|
||||
c.registerTunnel(m)
|
||||
|
||||
case *msg.PingMsg:
|
||||
case *msg.Ping:
|
||||
c.lastPing = time.Now()
|
||||
c.out <- &msg.PongMsg{}
|
||||
|
||||
case *msg.VersionMsg:
|
||||
c.out <- &msg.VersionRespMsg{
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
}
|
||||
c.out <- &msg.Pong{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) readThread() {
|
||||
func (c *Control) writer() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Info("Control::readThread failed with error %v: %s", err, debug.Stack())
|
||||
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
c.stop <- nil
|
||||
}()
|
||||
|
||||
// kill everything if the writer() stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that we've flushed all messages
|
||||
defer c.writerShutdown.Complete()
|
||||
|
||||
// write messages to the control channel
|
||||
for m := range c.out {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
|
||||
if err := msg.WriteMsg(c.conn, m); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) reader() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
// kill everything if the reader stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that we're done
|
||||
defer c.readerShutdown.Complete()
|
||||
|
||||
// read messages from the control channel
|
||||
for {
|
||||
if msg, err := msg.ReadMsg(c.conn); err != nil {
|
||||
@@ -115,7 +246,111 @@ func (c *Control) readThread() {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
// this can also panic during shutdown
|
||||
c.in <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) stopper() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.conn.Error("Failed to shut down control: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait until we're instructed to shutdown
|
||||
c.shutdown.WaitBegin()
|
||||
|
||||
// remove ourself from the control registry
|
||||
controlRegistry.Del(c.id)
|
||||
|
||||
// shutdown manager() so that we have no more work to do
|
||||
close(c.in)
|
||||
c.managerShutdown.WaitComplete()
|
||||
|
||||
// shutdown writer()
|
||||
close(c.out)
|
||||
c.writerShutdown.WaitComplete()
|
||||
|
||||
// close connection fully
|
||||
c.conn.Close()
|
||||
|
||||
// shutdown all of the tunnels
|
||||
for _, t := range c.tunnels {
|
||||
t.Shutdown()
|
||||
}
|
||||
|
||||
// shutdown all of the proxy connections
|
||||
close(c.proxies)
|
||||
for p := range c.proxies {
|
||||
p.Close()
|
||||
}
|
||||
|
||||
c.shutdown.Complete()
|
||||
c.conn.Info("Shutdown complete")
|
||||
}
|
||||
|
||||
func (c *Control) RegisterProxy(conn conn.Conn) {
|
||||
conn.AddLogPrefix(c.id)
|
||||
|
||||
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
|
||||
select {
|
||||
case c.proxies <- conn:
|
||||
conn.Info("Registered")
|
||||
default:
|
||||
conn.Info("Proxies buffer is full, discarding.")
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a proxy connection from the pool and return it
|
||||
// If not proxy connections are in the pool, request one
|
||||
// and wait until it is available
|
||||
// Returns an error if we couldn't get a proxy because it took too long
|
||||
// or the tunnel is closing
|
||||
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
|
||||
var ok bool
|
||||
|
||||
// get a proxy connection from the pool
|
||||
select {
|
||||
case proxyConn, ok = <-c.proxies:
|
||||
if !ok {
|
||||
err = fmt.Errorf("No proxy connections available, control is closing")
|
||||
return
|
||||
}
|
||||
default:
|
||||
// no proxy available in the pool, ask for one over the control channel
|
||||
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
|
||||
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case proxyConn, ok = <-c.proxies:
|
||||
if !ok {
|
||||
err = fmt.Errorf("No proxy connections available, control is closing")
|
||||
return
|
||||
}
|
||||
|
||||
case <-time.After(pingTimeoutInterval):
|
||||
err = fmt.Errorf("Timeout trying to get proxy connection")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Called when this control is replaced by another control
|
||||
// this can happen if the network drops out and the client reconnects
|
||||
// before the old tunnel has lost its heartbeat
|
||||
func (c *Control) Replaced(replacement *Control) {
|
||||
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
|
||||
|
||||
// set the control id to empty string so that when stopper()
|
||||
// calls registry.Del it won't delete the replacement
|
||||
c.id = ""
|
||||
|
||||
// tell the old one to shutdown
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,26 +31,31 @@ Bad Request
|
||||
`
|
||||
)
|
||||
|
||||
/**
|
||||
* Listens for new http connections from the public internet
|
||||
*/
|
||||
func httpListener(addr *net.TCPAddr) {
|
||||
// Listens for new http(s) connections from the public internet
|
||||
func startHttpListener(addr string, tlsCfg *tls.Config) (listener *conn.Listener) {
|
||||
// bind/listen for incoming connections
|
||||
listener, err := conn.Listen(addr, "pub", nil)
|
||||
if err != nil {
|
||||
var err error
|
||||
if listener, err = conn.Listen(addr, "pub", tlsCfg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Listening for public http connections on %v", listener.Port)
|
||||
for conn := range listener.Conns {
|
||||
go httpHandler(conn)
|
||||
proto := "http"
|
||||
if tlsCfg != nil {
|
||||
proto = "https"
|
||||
}
|
||||
|
||||
log.Info("Listening for public %s connections on %v", proto, listener.Addr.String())
|
||||
go func() {
|
||||
for conn := range listener.Conns {
|
||||
go httpHandler(conn, proto)
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new http connection from the public internet
|
||||
*/
|
||||
func httpHandler(tcpConn net.Conn) {
|
||||
// Handles a new http connection from the public internet
|
||||
func httpHandler(tcpConn net.Conn, proto string) {
|
||||
// wrap up the connection for logging
|
||||
conn := conn.NewHttp(tcpConn, "pub")
|
||||
|
||||
@@ -59,31 +67,41 @@ func httpHandler(tcpConn net.Conn) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Make sure we detect dead connections while we decide how to multiplex
|
||||
conn.SetDeadline(time.Now().Add(connReadTimeout))
|
||||
|
||||
// read out the http request
|
||||
req, err := conn.ReadRequest()
|
||||
if err != nil {
|
||||
conn.Warn("Failed to read valid http request: %v", err)
|
||||
conn.Warn("Failed to read valid %s request: %v", proto, err)
|
||||
conn.Write([]byte(BadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
// read out the Host header from the request
|
||||
host := strings.ToLower(req.Host)
|
||||
conn.Debug("Found hostname %s in request", host)
|
||||
|
||||
// multiplex to find the right backend host
|
||||
conn.Debug("Found hostname %s in request", req.Host)
|
||||
tunnel := tunnels.Get("http://" + req.Host)
|
||||
tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host))
|
||||
if tunnel == nil {
|
||||
conn.Info("No tunnel found for hostname %s", req.Host)
|
||||
conn.Write([]byte(fmt.Sprintf(NotFound, len(req.Host)+18, req.Host)))
|
||||
conn.Info("No tunnel found for hostname %s", host)
|
||||
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
|
||||
return
|
||||
}
|
||||
|
||||
// satisfy auth, if necessary
|
||||
conn.Debug("From client: %s", req.Header.Get("Authorization"))
|
||||
conn.Debug("To match: %s", tunnel.regMsg.HttpAuth)
|
||||
if req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
|
||||
conn.Info("Authentication failed")
|
||||
// If the client specified http auth and it doesn't match this request's auth
|
||||
// then fail the request with 401 Not Authorized and request the client reissue the
|
||||
// request with basic authdeny the request
|
||||
if tunnel.req.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.req.HttpAuth {
|
||||
conn.Info("Authentication failed: %s", req.Header.Get("Authorization"))
|
||||
conn.Write([]byte(NotAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
// dead connections will now be handled by tunnel heartbeating and the client
|
||||
conn.SetDeadline(time.Time{})
|
||||
|
||||
// let the tunnel handle the connection now
|
||||
tunnel.HandlePublicConnection(conn)
|
||||
}
|
||||
|
||||
@@ -1,127 +1,141 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"crypto/tls"
|
||||
"math/rand"
|
||||
"ngrok/conn"
|
||||
log "ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/server/tls"
|
||||
"regexp"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
publicPort int
|
||||
proxyPort int
|
||||
tunnelPort int
|
||||
domain string
|
||||
logto string
|
||||
}
|
||||
const (
|
||||
registryCacheSize uint64 = 1024 * 1024 // 1 MB
|
||||
connReadTimeout time.Duration = 10 * time.Second
|
||||
)
|
||||
|
||||
/* GLOBALS */
|
||||
// GLOBALS
|
||||
var (
|
||||
hostRegex *regexp.Regexp
|
||||
proxyAddr string
|
||||
tunnels *TunnelManager
|
||||
tunnelRegistry *TunnelRegistry
|
||||
controlRegistry *ControlRegistry
|
||||
|
||||
// XXX: kill these global variables - they're only used in tunnel.go for constructing forwarding URLs
|
||||
opts *Options
|
||||
listeners map[string]*conn.Listener
|
||||
)
|
||||
|
||||
func init() {
|
||||
hostRegex = regexp.MustCompile("[H|h]ost: ([^\\(\\);:,<>]+)\n")
|
||||
}
|
||||
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
|
||||
// fail gracefully if the proxy connection fails to register
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
pxyConn.Warn("Failed with error: %v", r)
|
||||
pxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
func parseArgs() *Options {
|
||||
publicPort := flag.Int("publicport", 80, "Public port")
|
||||
tunnelPort := flag.Int("tunnelport", 4443, "Tunnel port")
|
||||
proxyPort := flag.Int("proxyPort", 0, "Proxy port")
|
||||
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"stdout",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
// set logging prefix
|
||||
pxyConn.SetType("pxy")
|
||||
|
||||
flag.Parse()
|
||||
// look up the control connection for this proxy
|
||||
pxyConn.Info("Registering new proxy for %s", regPxy.ClientId)
|
||||
ctl := controlRegistry.Get(regPxy.ClientId)
|
||||
|
||||
return &Options{
|
||||
publicPort: *publicPort,
|
||||
tunnelPort: *tunnelPort,
|
||||
proxyPort: *proxyPort,
|
||||
domain: *domain,
|
||||
logto: *logto,
|
||||
if ctl == nil {
|
||||
panic("No client found for identifier: " + regPxy.ClientId)
|
||||
}
|
||||
|
||||
ctl.RegisterProxy(pxyConn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new control connections from tunnel clients
|
||||
*/
|
||||
func controlListener(addr *net.TCPAddr, domain string) {
|
||||
// Listen for incoming control and proxy connections
|
||||
// We listen for incoming control and proxy connections on the same port
|
||||
// for ease of deployment. The hope is that by running on port 443, using
|
||||
// TLS and running all connections over the same port, we can bust through
|
||||
// restrictive firewalls.
|
||||
func tunnelListener(addr string, tlsConfig *tls.Config) {
|
||||
// listen for incoming connections
|
||||
listener, err := conn.Listen(addr, "ctl", tls.Config)
|
||||
listener, err := conn.Listen(addr, "tun", tlsConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Listening for control connections on %d", listener.Port)
|
||||
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
|
||||
for c := range listener.Conns {
|
||||
NewControl(c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new proxy connections from tunnel clients
|
||||
*/
|
||||
func proxyListener(addr *net.TCPAddr, domain string) {
|
||||
listener, err := conn.Listen(addr, "pxy", tls.Config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// set global proxy addr variable
|
||||
proxyAddr = fmt.Sprintf("%s:%d", domain, listener.Port)
|
||||
log.Info("Listening for proxy connection on %d", listener.Port)
|
||||
for proxyConn := range listener.Conns {
|
||||
go func(conn conn.Conn) {
|
||||
// fail gracefully if the proxy connection dies
|
||||
go func(tunnelConn conn.Conn) {
|
||||
// don't crash on panics
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
conn.Warn("Failed with error: %v", r)
|
||||
conn.Close()
|
||||
tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
// read the proxy register message
|
||||
var regPxy msg.RegProxyMsg
|
||||
if err = msg.ReadMsgInto(conn, ®Pxy); err != nil {
|
||||
panic(err)
|
||||
tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout))
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
|
||||
tunnelConn.Warn("Failed to read message: %v", err)
|
||||
tunnelConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// look up the tunnel for this proxy
|
||||
conn.Info("Registering new proxy for %s", regPxy.Url)
|
||||
tunnel := tunnels.Get(regPxy.Url)
|
||||
if tunnel == nil {
|
||||
panic("No tunnel found for: " + regPxy.Url)
|
||||
}
|
||||
// don't timeout after the initial read, tunnel heartbeating will kill
|
||||
// dead connections
|
||||
tunnelConn.SetReadDeadline(time.Time{})
|
||||
|
||||
// register the proxy connection with the tunnel
|
||||
tunnel.RegisterProxy(conn)
|
||||
}(proxyConn)
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.Auth:
|
||||
NewControl(tunnelConn, m)
|
||||
|
||||
case *msg.RegProxy:
|
||||
NewProxy(tunnelConn, m)
|
||||
|
||||
default:
|
||||
tunnelConn.Close()
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func Main() {
|
||||
// parse options
|
||||
opts := parseArgs()
|
||||
opts = parseArgs()
|
||||
|
||||
// init logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
tunnels = NewTunnelManager(opts.domain)
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
go proxyListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.proxyPort}, opts.domain)
|
||||
go controlListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
|
||||
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.publicPort})
|
||||
// init tunnel/control registry
|
||||
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
|
||||
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
|
||||
controlRegistry = NewControlRegistry()
|
||||
|
||||
// wait forever
|
||||
done := make(chan int)
|
||||
<-done
|
||||
// start listeners
|
||||
listeners = make(map[string]*conn.Listener)
|
||||
|
||||
// load tls configuration
|
||||
tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// listen for http
|
||||
if opts.httpAddr != "" {
|
||||
listeners["http"] = startHttpListener(opts.httpAddr, nil)
|
||||
}
|
||||
|
||||
// listen for https
|
||||
if opts.httpsAddr != "" {
|
||||
listeners["https"] = startHttpListener(opts.httpsAddr, tlsConfig)
|
||||
}
|
||||
|
||||
// ngrok clients
|
||||
tunnelListener(opts.tunnelAddr, tlsConfig)
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
cache "github.com/pmylund/go-cache"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheDuration time.Duration = 24 * time.Hour
|
||||
cacheCleanupInterval time.Duration = time.Minute
|
||||
)
|
||||
|
||||
/**
|
||||
* TunnelManager: Manages a set of tunnels
|
||||
*/
|
||||
type TunnelManager struct {
|
||||
domain string
|
||||
tunnels map[string]*Tunnel
|
||||
idDomainAffinity *cache.Cache
|
||||
ipDomainAffinity *cache.Cache
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelManager(domain string) *TunnelManager {
|
||||
return &TunnelManager{
|
||||
domain: domain,
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
idDomainAffinity: cache.New(cacheDuration, cacheCleanupInterval),
|
||||
ipDomainAffinity: cache.New(cacheDuration, cacheCleanupInterval),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TunnelManager) Add(t *Tunnel) error {
|
||||
assignTunnel := func(url string) bool {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.tunnels[url] == nil {
|
||||
m.tunnels[url] = t
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
url := ""
|
||||
switch t.regMsg.Protocol {
|
||||
case "tcp":
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
url = fmt.Sprintf("tcp://%s:%d", m.domain, addr.Port)
|
||||
if !assignTunnel(url) {
|
||||
return t.Error("TCP at %s already registered!", url)
|
||||
}
|
||||
|
||||
case "http":
|
||||
if strings.TrimSpace(t.regMsg.Hostname) != "" {
|
||||
url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
|
||||
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
|
||||
url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, m.domain)
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
if !assignTunnel(url) {
|
||||
return t.Warn("The tunnel address %s is already registered!", url)
|
||||
}
|
||||
} else {
|
||||
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
|
||||
clientId := t.regMsg.ClientId
|
||||
|
||||
// try to give the same subdomain back if it's available
|
||||
subdomain := fmt.Sprintf("%x", rand.Int31())
|
||||
if lastDomain, ok := m.idDomainAffinity.Get(clientId); ok {
|
||||
t.Debug("Found affinity for subdomain %s with client id %s", subdomain, clientId)
|
||||
subdomain = lastDomain.(string)
|
||||
} else if lastDomain, ok = m.ipDomainAffinity.Get(clientIp); ok {
|
||||
t.Debug("Found affinity for subdomain %s with client ip %s", subdomain, clientIp)
|
||||
subdomain = lastDomain.(string)
|
||||
}
|
||||
|
||||
// pick one randomly
|
||||
for {
|
||||
url = fmt.Sprintf("http://%s.%s", subdomain, m.domain)
|
||||
if assignTunnel(url) {
|
||||
break
|
||||
} else {
|
||||
subdomain = fmt.Sprintf("%x", rand.Int31())
|
||||
}
|
||||
}
|
||||
|
||||
// save our choice so we can try to give clients back the same
|
||||
// tunnel later
|
||||
m.idDomainAffinity.Set(clientId, subdomain, 0)
|
||||
m.ipDomainAffinity.Set(clientIp, subdomain, 0)
|
||||
}
|
||||
|
||||
default:
|
||||
return t.Error("Unrecognized protocol type %s", t.regMsg.Protocol)
|
||||
}
|
||||
|
||||
t.url = url
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *TunnelManager) Del(url string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
delete(m.tunnels, url)
|
||||
}
|
||||
|
||||
func (m *TunnelManager) Get(url string) *Tunnel {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.tunnels[url]
|
||||
}
|
||||
@@ -19,12 +19,10 @@ func init() {
|
||||
keenApiKey := os.Getenv("KEEN_API_KEY")
|
||||
|
||||
if keenApiKey != "" {
|
||||
metrics = NewKeenIoMetrics()
|
||||
metrics = NewKeenIoMetrics(60 * time.Second)
|
||||
} else {
|
||||
metrics = NewLocalMetrics(30 * time.Second)
|
||||
}
|
||||
|
||||
metrics.AddLogPrefix("metrics")
|
||||
}
|
||||
|
||||
type Metrics interface {
|
||||
@@ -63,7 +61,7 @@ type LocalMetrics struct {
|
||||
|
||||
func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics {
|
||||
metrics := LocalMetrics{
|
||||
Logger: log.NewPrefixLogger(),
|
||||
Logger: log.NewPrefixLogger("metrics"),
|
||||
reportInterval: reportInterval,
|
||||
windowsCounter: gometrics.NewCounter(),
|
||||
linuxCounter: gometrics.NewCounter(),
|
||||
@@ -96,7 +94,7 @@ func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics {
|
||||
func (m *LocalMetrics) OpenTunnel(t *Tunnel) {
|
||||
m.tunnelMeter.Mark(1)
|
||||
|
||||
switch t.regMsg.OS {
|
||||
switch t.ctl.auth.OS {
|
||||
case "windows":
|
||||
m.windowsCounter.Inc(1)
|
||||
case "linux":
|
||||
@@ -107,7 +105,7 @@ func (m *LocalMetrics) OpenTunnel(t *Tunnel) {
|
||||
m.otherCounter.Inc(1)
|
||||
}
|
||||
|
||||
switch t.regMsg.Protocol {
|
||||
switch t.req.Protocol {
|
||||
case "tcp":
|
||||
m.tcpTunnelMeter.Mark(1)
|
||||
case "http":
|
||||
@@ -156,9 +154,9 @@ func (m *LocalMetrics) Report() {
|
||||
}
|
||||
}
|
||||
|
||||
type KeenIoRequest struct {
|
||||
Path string
|
||||
Body []byte
|
||||
type KeenIoMetric struct {
|
||||
Collection string
|
||||
Event interface{}
|
||||
}
|
||||
|
||||
type KeenIoMetrics struct {
|
||||
@@ -166,20 +164,54 @@ type KeenIoMetrics struct {
|
||||
ApiKey string
|
||||
ProjectToken string
|
||||
HttpClient http.Client
|
||||
Requests chan *KeenIoRequest
|
||||
Metrics chan *KeenIoMetric
|
||||
}
|
||||
|
||||
func NewKeenIoMetrics() *KeenIoMetrics {
|
||||
func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics {
|
||||
k := &KeenIoMetrics{
|
||||
Logger: log.NewPrefixLogger(),
|
||||
Logger: log.NewPrefixLogger("metrics"),
|
||||
ApiKey: os.Getenv("KEEN_API_KEY"),
|
||||
ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"),
|
||||
Requests: make(chan *KeenIoRequest, 100),
|
||||
Metrics: make(chan *KeenIoMetric, 1000),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for req := range k.Requests {
|
||||
k.AuthedRequest("POST", req.Path, bytes.NewReader(req.Body))
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
k.Error("KeenIoMetrics failed: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
batch := make(map[string][]interface{})
|
||||
batchTimer := time.Tick(batchInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case m := <-k.Metrics:
|
||||
list, ok := batch[m.Collection]
|
||||
if !ok {
|
||||
list = make([]interface{}, 0)
|
||||
}
|
||||
batch[m.Collection] = append(list, m.Event)
|
||||
|
||||
case <-batchTimer:
|
||||
// no metrics to report
|
||||
if len(batch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(batch)
|
||||
if err != nil {
|
||||
k.Error("Failed to serialize metrics payload: %v, %v", batch, err)
|
||||
} else {
|
||||
for key, val := range batch {
|
||||
k.Debug("Reporting %d metrics for %s", len(val), key)
|
||||
}
|
||||
|
||||
k.AuthedRequest("POST", "/events", bytes.NewReader(payload))
|
||||
}
|
||||
batch = make(map[string][]interface{})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -200,13 +232,15 @@ func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (
|
||||
req.ContentLength = int64(body.Len())
|
||||
}
|
||||
|
||||
requestStartAt := time.Now()
|
||||
resp, err = k.HttpClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
k.Error("Failed to send metric event to keen.io %v", err)
|
||||
} else {
|
||||
k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds())
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 201 {
|
||||
if resp.StatusCode != 200 {
|
||||
bytes, _ := ioutil.ReadAll(resp.Body)
|
||||
k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes)
|
||||
}
|
||||
@@ -219,7 +253,7 @@ func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) {
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) {
|
||||
buf, err := json.Marshal(struct {
|
||||
event := struct {
|
||||
Keen KeenStruct `json:"keen"`
|
||||
OS string
|
||||
ClientId string
|
||||
@@ -238,25 +272,21 @@ func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time,
|
||||
Keen: KeenStruct{
|
||||
Timestamp: start.UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
},
|
||||
OS: t.regMsg.OS,
|
||||
ClientId: t.regMsg.ClientId,
|
||||
Protocol: t.regMsg.Protocol,
|
||||
OS: t.ctl.auth.OS,
|
||||
ClientId: t.ctl.id,
|
||||
Protocol: t.req.Protocol,
|
||||
Url: t.url,
|
||||
User: t.regMsg.User,
|
||||
Version: t.regMsg.MmVersion,
|
||||
HttpAuth: t.regMsg.HttpAuth != "",
|
||||
Subdomain: t.regMsg.Subdomain != "",
|
||||
User: t.ctl.auth.User,
|
||||
Version: t.ctl.auth.MmVersion,
|
||||
HttpAuth: t.req.HttpAuth != "",
|
||||
Subdomain: t.req.Subdomain != "",
|
||||
TunnelDuration: time.Since(t.start).Seconds(),
|
||||
ConnectionDuration: time.Since(start).Seconds(),
|
||||
BytesIn: in,
|
||||
BytesOut: out,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
k.Error("Error serializing metric %v", err)
|
||||
} else {
|
||||
k.Requests <- &KeenIoRequest{Path: "/events/CloseConnection", Body: buf}
|
||||
}
|
||||
|
||||
k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event}
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) {
|
||||
@@ -267,7 +297,7 @@ type KeenStruct struct {
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
|
||||
buf, err := json.Marshal(struct {
|
||||
event := struct {
|
||||
Keen KeenStruct `json:"keen"`
|
||||
OS string
|
||||
ClientId string
|
||||
@@ -283,22 +313,17 @@ func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
|
||||
Keen: KeenStruct{
|
||||
Timestamp: t.start.UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
},
|
||||
OS: t.regMsg.OS,
|
||||
ClientId: t.regMsg.ClientId,
|
||||
Protocol: t.regMsg.Protocol,
|
||||
OS: t.ctl.auth.OS,
|
||||
ClientId: t.ctl.id,
|
||||
Protocol: t.req.Protocol,
|
||||
Url: t.url,
|
||||
User: t.regMsg.User,
|
||||
Version: t.regMsg.MmVersion,
|
||||
User: t.ctl.auth.User,
|
||||
Version: t.ctl.auth.MmVersion,
|
||||
//Reason: reason,
|
||||
Duration: time.Since(t.start).Seconds(),
|
||||
HttpAuth: t.regMsg.HttpAuth != "",
|
||||
Subdomain: t.regMsg.Subdomain != "",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
k.Error("Error serializing metric %v", err)
|
||||
return
|
||||
} else {
|
||||
k.Requests <- &KeenIoRequest{Path: "/events/CloseTunnel", Body: buf}
|
||||
HttpAuth: t.req.HttpAuth != "",
|
||||
Subdomain: t.req.Subdomain != "",
|
||||
}
|
||||
|
||||
k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event}
|
||||
}
|
||||
|
||||
207
src/ngrok/server/registry.go
Normal file
207
src/ngrok/server/registry.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/cache"
|
||||
"ngrok/log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheSaveInterval time.Duration = 10 * time.Minute
|
||||
)
|
||||
|
||||
type cacheUrl string
|
||||
|
||||
func (url cacheUrl) Size() int {
|
||||
return len(url)
|
||||
}
|
||||
|
||||
// TunnelRegistry maps a tunnel URL to Tunnel structures
|
||||
type TunnelRegistry struct {
|
||||
tunnels map[string]*Tunnel
|
||||
affinity *cache.LRUCache
|
||||
log.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
|
||||
registry := &TunnelRegistry{
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
affinity: cache.NewLRUCache(cacheSize),
|
||||
Logger: log.NewPrefixLogger("registry", "tun"),
|
||||
}
|
||||
|
||||
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
|
||||
// to encode or decode any non-primitive types that haven't been "registered"
|
||||
// with it. Since we store cacheUrl objects, we need to register them here first
|
||||
// for the encoding/decoding to work
|
||||
var urlobj cacheUrl
|
||||
gob.Register(urlobj)
|
||||
|
||||
// try to load and then periodically save the affinity cache to file, if specified
|
||||
if cacheFile != "" {
|
||||
err := registry.affinity.LoadItemsFromFile(cacheFile)
|
||||
if err != nil {
|
||||
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
|
||||
}
|
||||
|
||||
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
|
||||
} else {
|
||||
registry.Info("No affinity cache specified")
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// Spawns a goroutine the periodically saves the cache to a file.
|
||||
func (r *TunnelRegistry) SaveCacheThread(path string, interval time.Duration) {
|
||||
go func() {
|
||||
r.Info("Saving affinity cache to %s every %s", path, interval.String())
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
|
||||
r.Debug("Saving affinity cache")
|
||||
err := r.affinity.SaveItemsToFile(path)
|
||||
if err != nil {
|
||||
r.Error("Failed to save affinity cache: %v", err)
|
||||
} else {
|
||||
r.Info("Saved affinity cache")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Register a tunnel with a specific url, returns an error
|
||||
// if a tunnel is already registered at that url
|
||||
func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.tunnels[url] != nil {
|
||||
return fmt.Errorf("The tunnel %s is already registered.", url)
|
||||
}
|
||||
|
||||
r.tunnels[url] = t
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) {
|
||||
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
|
||||
clientId := t.ctl.id
|
||||
|
||||
ipKey := fmt.Sprintf("client-ip-%s:%s", t.req.Protocol, clientIp)
|
||||
idKey := fmt.Sprintf("client-id-%s:%s", t.req.Protocol, clientId)
|
||||
return ipKey, idKey
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) GetCachedRegistration(t *Tunnel) (url string) {
|
||||
ipCacheKey, idCacheKey := r.cacheKeys(t)
|
||||
|
||||
// check cache for ID first, because we prefer that over IP which might
|
||||
// not be specific to a user because of NATs
|
||||
if v, ok := r.affinity.Get(idCacheKey); ok {
|
||||
url = string(v.(cacheUrl))
|
||||
t.Debug("Found registry affinity %s for %s", url, idCacheKey)
|
||||
} else if v, ok := r.affinity.Get(ipCacheKey); ok {
|
||||
url = string(v.(cacheUrl))
|
||||
t.Debug("Found registry affinity %s for %s", url, ipCacheKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) RegisterAndCache(url string, t *Tunnel) (err error) {
|
||||
if err = r.Register(url, t); err == nil {
|
||||
// we successfully assigned a url, cache it
|
||||
ipCacheKey, idCacheKey := r.cacheKeys(t)
|
||||
r.affinity.Set(ipCacheKey, cacheUrl(url))
|
||||
r.affinity.Set(idCacheKey, cacheUrl(url))
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Register a tunnel with the following process:
|
||||
// Consult the affinity cache to try to assign a previously used tunnel url if possible
|
||||
// Generate new urls repeatedly with the urlFn and register until one is available.
|
||||
func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) (string, error) {
|
||||
url := r.GetCachedRegistration(t)
|
||||
if url == "" {
|
||||
url = urlFn()
|
||||
}
|
||||
|
||||
maxAttempts := 5
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
if err := r.RegisterAndCache(url, t); err != nil {
|
||||
// pick a new url and try again
|
||||
url = urlFn()
|
||||
} else {
|
||||
// we successfully assigned a url, we're done
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts)
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) Del(url string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
delete(r.tunnels, url)
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) Get(url string) *Tunnel {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.tunnels[url]
|
||||
}
|
||||
|
||||
// ControlRegistry maps a client ID to Control structures
|
||||
type ControlRegistry struct {
|
||||
controls map[string]*Control
|
||||
log.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewControlRegistry() *ControlRegistry {
|
||||
return &ControlRegistry{
|
||||
controls: make(map[string]*Control),
|
||||
Logger: log.NewPrefixLogger("registry", "ctl"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Get(clientId string) *Control {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.controls[clientId]
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
oldCtl = r.controls[clientId]
|
||||
if oldCtl != nil {
|
||||
oldCtl.Replaced(ctl)
|
||||
}
|
||||
|
||||
r.controls[clientId] = ctl
|
||||
r.Info("Registered control with id %s", clientId)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Del(clientId string) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if r.controls[clientId] == nil {
|
||||
return fmt.Errorf("No control found for client id: %s", clientId)
|
||||
} else {
|
||||
r.Info("Removed control registry id %s", clientId)
|
||||
delete(r.controls, clientId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
43
src/ngrok/server/tls.go
Normal file
43
src/ngrok/server/tls.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"ngrok/server/assets"
|
||||
)
|
||||
|
||||
func LoadTLSConfig(crtPath string, keyPath string) (tlsConfig *tls.Config, err error) {
|
||||
fileOrAsset := func(path string, default_path string) ([]byte, error) {
|
||||
loadFn := ioutil.ReadFile
|
||||
if path == "" {
|
||||
loadFn = assets.ReadAsset
|
||||
path = default_path
|
||||
}
|
||||
|
||||
return loadFn(path)
|
||||
}
|
||||
|
||||
var (
|
||||
crt []byte
|
||||
key []byte
|
||||
cert tls.Certificate
|
||||
)
|
||||
|
||||
if crt, err = fileOrAsset(crtPath, "assets/server/tls/snakeoil.crt"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cert, err = tls.X509KeyPair(crt, key); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
Config *tls.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
readOrBytes := func(envVar string, defaultFile func() []byte) []byte {
|
||||
f := os.Getenv(envVar)
|
||||
if f == "" {
|
||||
return defaultFile()
|
||||
} else {
|
||||
if b, err := ioutil.ReadFile(f); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crt := readOrBytes("TLS_CRT_FILE", snakeoilCrt)
|
||||
key := readOrBytes("TLS_KEY_FILE", snakeoilKey)
|
||||
cert, err := tls.X509KeyPair(crt, key)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Config = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _snakeoilCrt = "" +
|
||||
"\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x64\x55\xc7\x0e\xab\xda" +
|
||||
"\x96\x9c\xf3\x15\x3d\x47\x2d\x32\xb6\x87\xc0\x26\xb3\xc9\x06\xc3" +
|
||||
"\x8c\x9c\xb3\x31\x98\xaf\x6f\x9f\x73\xa5\xd6\xd3\x7d\xcc\x56\x2d" +
|
||||
"\xa9\xa8\x5d\xaa\xd2\xfa\xdf\x3f\x1f\x2f\xca\xaa\xf9\x3f\x82\xe8" +
|
||||
"\xfa\xaa\xa4\x0a\x9c\x2f\xfe\x45\x11\xa8\xaa\x12\x00\x82\xc0\x7d" +
|
||||
"\x1c\x81\x73\xc4\x03\x38\x91\xa6\x4f\xb1\x5a\x7f\x32\xf3\x37\x4b" +
|
||||
"\xbc\xc3\x1d\x3e\x10\x0d\xc8\x75\x32\x47\x3c\x45\xbe\x86\x42\x10" +
|
||||
"\xc0\x53\xbc\x38\x97\xaf\xcc\x00\xe1\xb9\xca\x17\x3a\xb3\x4e\xe5" +
|
||||
"\x7e\x48\x29\x6d\x4f\x42\xf1\x14\x5b\xce\xf9\xb3\xe4\xb9\xc9\x17" +
|
||||
"\x42\x66\xcc\x86\xc7\x6a\x0c\xe6\x27\xf5\xc5\x27\xe4\xd5\xbf\x44" +
|
||||
"\xdc\xe9\x18\x7a\xc3\xec\x48\x4c\x69\x9f\xe4\xcb\xb4\x29\x89\x1f" +
|
||||
"\x4a\x9d\x99\xd0\x87\x87\xd9\x72\x17\x04\xce\x17\x5e\xdc\x11\xfe" +
|
||||
"\xc1\xda\xbf\xd8\xf9\xff\x58\xcb\x43\xe8\x6c\x87\xe0\x44\x00\x09" +
|
||||
"\x1c\x47\x16\x0f\x2d\x78\x5e\xa2\x0f\x7f\xcf\xfc\xc3\x2e\xf0\x50" +
|
||||
"\x77\x48\x69\x4b\xc2\xf8\x93\x0d\xcc\x1c\xf9\xa2\x07\x79\xee\x9f" +
|
||||
"\x5d\x0d\xb5\x74\xc8\xbf\x29\xb9\xed\x11\xf9\x78\x23\xd0\x75\x0e" +
|
||||
"\xb1\x8a\xc0\x8f\x08\x48\xdc\xb6\x18\xc3\x7f\x4a\xfe\x79\xd3\xa8" +
|
||||
"\xff\xf6\x85\xff\xf9\x02\xaa\x4a\xb5\xb9\x9f\x77\x08\x57\x4d\xc2" +
|
||||
"\x6f\xe0\x39\x73\xda\x5f\x02\x7b\x68\x49\x66\xab\x52\x24\x36\xba" +
|
||||
"\x2a\xcb\x75\xdf\x78\xf5\xb9\x7d\x89\x3b\x61\x68\x38\x4b\xc7\x3e" +
|
||||
"\xa0\x38\x17\xfa\xcd\xc3\x32\xad\xcd\xed\x94\x97\x8f\x4c\x10\x17" +
|
||||
"\x02\x22\x36\xc1\xd8\xd8\x33\xa5\xa9\x46\x23\x1b\x0c\x29\xd9\x33" +
|
||||
"\x5e\xe7\x5a\x51\x39\xe8\x7c\x5d\x58\x05\xda\x17\x5e\x3f\xa0\x10" +
|
||||
"\x16\x27\xa4\x7f\x32\xe7\x08\xf7\x4b\x71\xe9\x1e\x3d\xa2\x84\xba" +
|
||||
"\xad\x55\xa8\x05\xf4\xfb\x91\x10\xd8\xb1\x3b\x96\x8e\xa2\x03\xf6" +
|
||||
"\x8a\xa8\x23\x4b\xe3\xca\x68\x49\x4a\x54\x32\xf9\x9b\xf6\xd8\x29" +
|
||||
"\x6b\x5d\x52\x24\x8b\x6c\x7e\x56\x6d\xc2\x68\xb7\xf2\x0c\x04\x6b" +
|
||||
"\xfd\x79\xc6\x73\x10\xea\xcf\x41\xe7\x67\x5f\xa1\xed\x1d\xb0\x69" +
|
||||
"\xd7\xdf\x17\xdd\xe1\x6c\xa2\xd8\x7b\x36\xf6\x50\x79\xc5\xfa\x5e" +
|
||||
"\x8a\xb4\x70\x39\xaa\x9c\xf2\x00\x15\xd6\x18\x4b\x44\xf4\x88\xb0" +
|
||||
"\x1b\xd8\xb7\x62\x20\x4d\xa7\x7f\x8f\xbd\x74\x14\x4c\xe0\x11\x46" +
|
||||
"\x39\xcd\xc1\xdb\xa2\x0a\xd2\x64\xca\x4f\x21\x3e\xf3\xb4\x22\x23" +
|
||||
"\xb0\xdd\x8a\x8f\x19\x05\x3a\x4a\x34\x78\xd7\x4e\xbf\xdf\x52\x00" +
|
||||
"\x79\xa7\x43\x5d\x10\x94\xe5\x60\x69\xb2\x97\x67\xa6\x64\xc4\xd9" +
|
||||
"\xbe\xe5\x4d\x63\x6e\xc7\x76\x1f\x0b\xdb\xa0\x8b\x09\xfd\x48\x3b" +
|
||||
"\x60\x9c\x36\x7b\x88\x5e\x79\xed\xa3\x57\x0c\x78\x2d\x30\x9c\x56" +
|
||||
"\x49\xc8\xba\x85\x97\x1a\x6d\xeb\xed\xb9\x35\xbb\x2e\xd3\x95\x93" +
|
||||
"\x03\xe2\x66\xa0\x60\xaa\x02\x61\x71\xb3\x3c\x7a\xed\xd5\x6a\x8b" +
|
||||
"\x9a\x65\x36\xf7\x48\xa6\x0a\xd7\xb5\x31\xe7\xd1\xf5\x2f\xda\x0e" +
|
||||
"\xbf\x37\x0b\x89\xdb\x9b\xfe\xc2\x4e\x7e\xfd\x78\x8c\x5c\x00\x73" +
|
||||
"\x59\x41\xb0\xd9\x06\x1e\x92\xb4\xa4\x61\x98\xb7\x6f\x37\xde\x93" +
|
||||
"\xf0\x32\x63\x80\x17\x2d\x6f\x40\xc7\x55\x49\xc8\x6e\x83\xa3\xa7" +
|
||||
"\x97\x50\xc3\xe3\x97\x03\xed\x08\x2d\x65\x60\x1b\x01\x1f\x94\x23" +
|
||||
"\xcf\xa7\xcb\x36\x72\x54\xb0\x31\x78\xb3\xa3\xcb\x76\x41\x13\x46" +
|
||||
"\x58\x51\x4f\x82\xd4\x2c\x2e\x57\xf5\x91\x85\x0f\xb5\xfe\xd5\x3e" +
|
||||
"\x93\x6f\xb5\x0b\x8b\xf8\x7c\x14\xcd\xd8\xdd\xc8\xd2\xa5\x5c\x9e" +
|
||||
"\x9d\xca\x02\x6f\x8c\xc9\xd2\xcc\x5f\x3a\xf8\x36\x2b\xf4\x2c\x9f" +
|
||||
"\xd6\x89\xbd\x22\xa0\x92\x2c\xf7\x29\xdd\x33\x63\x39\x51\x46\x69" +
|
||||
"\x0a\x26\x67\x05\xdd\x45\xaa\xd1\x24\x98\x4d\x3b\x64\x5d\xfd\x92" +
|
||||
"\xb7\xb2\x62\x0d\xee\x6a\x08\x39\xdc\x85\x2b\x26\x3a\x65\x69\x77" +
|
||||
"\xa7\xa6\xa6\xa0\xdc\x79\x7d\x94\xca\x2d\x37\x9e\xa3\xc4\x55\xbf" +
|
||||
"\x66\x70\xff\x54\x1f\xf9\x57\xf7\xc1\xdf\x58\x83\xa4\xe8\x49\xd4" +
|
||||
"\x73\xef\x86\xc0\x9c\xa6\xc0\x67\x3b\x5d\x2c\x43\xd3\x53\x78\x58" +
|
||||
"\xa7\xb7\x65\x59\x09\x56\x54\x9e\x18\xf4\x9c\x14\x19\x07\x09\x9e" +
|
||||
"\xfc\xe2\x27\x24\x7f\x77\x41\xac\xaa\x5d\x7a\x33\xf6\x4e\xb9\xaf" +
|
||||
"\x1b\x97\x3f\xef\xfc\xd5\x65\xe4\xd7\xcd\xdb\x89\x2b\x45\x08\x33" +
|
||||
"\x8d\xc3\x4a\x38\x1f\x7b\xf2\x52\x99\x6c\x4f\xf4\x00\x69\xf2\x58" +
|
||||
"\x9a\xcd\xe7\xd7\x45\x75\xe6\x29\x63\xc2\xf8\xce\x84\xcf\x71\xc5" +
|
||||
"\x33\x8d\x61\x15\x6a\x04\xfa\x63\xb1\x41\x4c\xf2\xda\xc0\x61\x36" +
|
||||
"\x74\x59\x6b\xae\xdd\xc3\x3d\x64\x74\xdd\x3d\xe3\x29\x34\xc8\xf7" +
|
||||
"\xa8\x24\xa9\x36\x7b\x1b\xce\x76\x4c\x3f\x3e\x9b\x34\x60\x0e\xce" +
|
||||
"\xd1\xda\x60\xcc\xf1\x2b\xe1\xdc\x37\x43\x5d\x66\x6a\x36\x8a\x4f" +
|
||||
"\x48\x95\x6f\x63\x0f\x83\x50\xd4\xb9\x4b\xcb\x49\x72\x7c\xd6\x45" +
|
||||
"\x66\x1e\x0b\xa2\xe2\x6e\xa1\xf2\xb0\x1e\x89\x41\xf5\xa8\x71\x4f" +
|
||||
"\x2c\xfc\xa6\xb9\x10\xdd\x71\x4b\x37\xed\xd3\xaa\xce\xb0\x12\x6f" +
|
||||
"\xcf\xb1\x6a\xc2\xbd\x7f\xcd\xa3\xaa\x70\xaf\xe8\xf4\x03\xe8\xdc" +
|
||||
"\x03\x84\x50\xa3\xfd\xfd\x79\xa4\x91\x86\xa1\x3e\xf7\x7d\xe8\xe7" +
|
||||
"\x95\xd1\x0e\x45\xe9\x28\xb3\xfc\xb2\x75\x97\x05\xae\xb5\x24\x02" +
|
||||
"\x37\x47\xa5\x6b\x7c\xc7\x4e\xeb\x67\xa8\x7a\x53\x4b\x12\x45\xac" +
|
||||
"\x11\x75\x8a\xc4\x34\x9f\x0a\xcc\x85\x45\xf7\x9f\x50\x98\x4c\x38" +
|
||||
"\x27\x48\xb8\x33\xf4\xe3\xab\x7f\xe3\xd1\xda\xb5\x5d\x16\x3b\xfb" +
|
||||
"\x52\xac\xd7\x2c\xb0\x11\x3c\x28\xf2\xf3\xeb\x9e\x2b\x66\x0c\xf3" +
|
||||
"\x7a\x3c\x1d\x88\x0c\x41\x3a\x82\xa4\xa6\xcf\xaf\x0d\xd1\xaf\xa9" +
|
||||
"\x56\xd8\xae\x2b\x81\x1f\x69\x2c\x4a\xfb\xef\x77\x28\xce\x84\x26" +
|
||||
"\xbb\x89\x4d\x8a\x56\xb4\xe2\x5f\xb9\xa0\x5f\x92\xb4\xea\xaa\xad" +
|
||||
"\x90\xa0\xe7\x66\x88\x8c\x66\xb9\xb8\x28\x2c\xcf\x47\x18\x7b\xd4" +
|
||||
"\xc8\x4a\x7b\x68\xcb\xe1\x13\xf0\xdf\x82\x2c\xf3\xd3\x6a\xc3\x87" +
|
||||
"\x76\x64\x18\xaa\x01\x97\xe2\x37\x8f\x65\x8c\xfe\xb2\xd7\x8d\x15" +
|
||||
"\xee\x7e\x66\x8e\x23\x40\xc4\x3c\xd4\xe8\xae\x8f\xae\xb0\xdb\xbd" +
|
||||
"\xfc\xd9\x04\x84\xf8\x4a\x37\x9e\xd8\x3c\x55\x1f\x9e\x7b\x4e\x96" +
|
||||
"\xf4\x47\xdb\xad\xa5\xe7\xb7\xca\xbb\x63\x83\x7f\xb6\x1d\x41\x52" +
|
||||
"\xfb\x2b\x31\x1f\x57\x62\xb2\x08\xf7\xc8\x60\xe8\xa8\x24\x4c\xa7" +
|
||||
"\x54\xc7\x15\x71\xed\x3a\xdc\xf9\x9a\xfe\x3b\xf0\xf5\x7d\x10\xc5" +
|
||||
"\x6e\xc2\x32\xb2\x7b\x6f\xe3\xbd\x5f\x73\x2d\xd3\x17\xa1\x70\x1e" +
|
||||
"\xa2\x7e\xcc\xec\x8a\x1b\xc3\x7b\x46\xfe\x1e\x38\xd1\x04\xff\x7d" +
|
||||
"\xf4\xfe\x2f\x00\x00\xff\xff\x82\xb7\xc1\xb2\x11\x07\x00\x00"
|
||||
|
||||
// snakeoilCrt returns raw, uncompressed file data.
|
||||
func snakeoilCrt() []byte {
|
||||
var empty [0]byte
|
||||
sx := (*reflect.StringHeader)(unsafe.Pointer(&_snakeoilCrt))
|
||||
b := empty[:]
|
||||
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bx.Data = sx.Data
|
||||
bx.Len = len(_snakeoilCrt)
|
||||
bx.Cap = bx.Len
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
|
||||
if err != nil {
|
||||
panic("Decompression failed: " + err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _snakeoilKey = "" +
|
||||
"\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x6c\x97\xb7\xb2\x84\x48" +
|
||||
"\x94\x6d\x7d\xbe\xa2\x7d\xe2\x05\x5a\x19\xcf\x40\x6b\xad\xf1\x0a" +
|
||||
"\x5d\x68\x4d\xc1\xd7\xcf\xed\x8e\x31\x27\xdd\xb4\x72\xe5\x8e\x73" +
|
||||
"\xd6\xfe\x7f\xff\x1e\x4e\x94\x55\xeb\x1f\xcf\x67\xff\x71\x3c\x35" +
|
||||
"\x62\x03\xf1\x1f\x5d\x4c\xff\xbb\x01\x4c\x55\xd5\x74\x56\xe5\x58" +
|
||||
"\x56\xe7\xd9\x46\x64\xd1\x2f\x51\x18\x1b\x3b\x2c\x37\xdd\x44\x8d" +
|
||||
"\xaf\xce\x4d\xfa\x91\x63\x4d\x57\xe4\xd7\x48\xde\x70\x9f\x82\xad" +
|
||||
"\x1d\x4c\xa7\xe0\xc4\x9f\x6d\xc0\x38\x0c\x90\x3f\xd9\x2d\x4b\xbb" +
|
||||
"\x79\x5d\x2d\x89\x7a\xd8\xae\x54\x70\x2a\xfa\xe6\x54\xd3\x6b\x4a" +
|
||||
"\x43\x70\xf2\x25\x78\x20\x39\x81\x9c\x1c\x07\x64\x6d\xab\xf6\x15" +
|
||||
"\xf8\xd6\x32\x4a\x7c\xbf\xcf\x7c\x0e\x9d\x07\x58\xb4\xbf\x06\x68" +
|
||||
"\x58\xa6\x33\x3d\x0a\x38\x6e\x6e\x2f\xb7\x25\x10\x4f\xb0\xf8\x46" +
|
||||
"\x91\x39\x53\x76\x35\xf7\x3b\x46\xf6\x74\xf2\xc2\x2d\xef\x5c\x3b" +
|
||||
"\x13\x48\x4c\x47\x6f\x21\xd6\x39\x45\xc1\x7d\x04\x3a\xa8\x95\x39" +
|
||||
"\xf5\x02\x2d\x7b\x9c\x3c\xd8\x4a\x17\x3f\x9d\xe5\xc1\xac\x1b\x10" +
|
||||
"\x77\x96\xd3\x4f\x9e\x79\x61\x59\x28\x22\xc9\x64\x19\x0c\xfc\x73" +
|
||||
"\x81\x71\x18\x35\xc3\x67\xe3\x39\xac\x54\x9d\xf2\x03\x28\xd0\x16" +
|
||||
"\x75\xdf\x7a\xbd\x6d\xf2\x47\xe6\x29\xc2\xc7\x68\x15\x26\x1c\x35" +
|
||||
"\x44\x46\x78\x80\xdf\x21\x86\xa9\x92\x4a\xb1\x01\x64\x70\xcf\x43" +
|
||||
"\x4e\x21\x6b\xec\xf7\x20\x69\xa4\x0d\x37\x2a\x36\x7c\x1f\xb0\x3b" +
|
||||
"\x0f\x76\x0a\x1b\x39\x3f\x12\x95\x14\x84\xc0\x1c\x2b\x01\x49\x77" +
|
||||
"\xf9\x26\xb2\x0d\x7f\xee\x69\xba\xf8\xf7\x29\x71\xfa\x6a\xc9\x8e" +
|
||||
"\xa4\x63\xdc\x19\x78\xeb\x85\x3d\x0d\xb2\xc9\x52\x23\x73\x1f\x10" +
|
||||
"\x8d\x9e\x1d\xd9\xf8\xfc\xe5\x66\xfb\x7d\xae\xc3\x7f\x74\x7c\xce" +
|
||||
"\xbf\x1c\x07\x3b\xc9\x7e\xe1\xf6\x97\x0b\xf5\x45\xfa\x21\x6d\x45" +
|
||||
"\xf2\x3b\xed\xf6\x38\x6a\xfc\x9a\xbc\x24\x24\x11\x62\x04\xd4\x8f" +
|
||||
"\x80\xba\x61\x72\x63\x27\x46\x67\x5f\x6a\x48\x94\x4f\xc6\xe8\x33" +
|
||||
"\xdc\x44\xd7\xdd\x8a\x6f\xfa\xf2\xf2\xbc\x09\x27\x88\xd1\xc9\x77" +
|
||||
"\x97\xd4\xc4\x63\xde\xde\xd2\xbe\x2b\xec\xb4\x23\x07\x85\x99\x6c" +
|
||||
"\x04\x00\x25\x69\x5b\x95\xc3\x3c\x3b\x71\x29\x5e\xe5\xab\x6a\xf8" +
|
||||
"\x69\xcd\x21\x68\x67\xd2\x07\x86\xab\x10\xfd\xee\x4c\x67\x32\xa2" +
|
||||
"\xad\x65\x1d\x43\xca\x8d\x1a\xeb\x8b\xc8\xf3\x51\x43\xf9\x1f\x71" +
|
||||
"\x53\x47\x00\x6c\x4d\xd2\xde\x16\x53\x6a\xbf\x13\x74\x1f\xbf\x73" +
|
||||
"\x01\x17\xdf\xed\x66\x4b\x2f\xa7\x8c\x46\x90\x62\x76\x74\x7d\xff" +
|
||||
"\xa9\x84\xd3\x6c\x31\x29\x84\x77\xb9\x6e\x4f\x7b\x12\xf7\xb9\x29" +
|
||||
"\x54\x5f\x69\x00\x35\x60\xb3\xba\xc8\x7c\x81\x25\xe3\x3d\xb1\xe9" +
|
||||
"\xdc\x19\xf9\x01\x0a\xf3\x2e\xd8\x46\x98\x7d\x70\xc3\x9a\x62\xb7" +
|
||||
"\x5a\x6d\x82\x57\xaa\x36\x02\x71\x71\x2d\x22\xf0\x07\x1f\x7e\x11" +
|
||||
"\xf2\xec\x2d\xb2\x00\xeb\xfe\x1b\x61\x2e\x27\xbd\x93\x36\xe0\xe6" +
|
||||
"\x08\xb9\x09\x53\x67\xc5\xac\xb1\xd8\xd1\x21\x9e\xae\x8c\x36\x58" +
|
||||
"\xdf\x2d\x55\x63\x2c\x96\xd2\xbb\xc5\x51\x73\x8f\x47\x8c\xad\x6c" +
|
||||
"\x94\xb4\x7c\xdf\x05\x6c\xd9\xe3\x93\xdd\xfe\xc3\xac\xe5\x60\x77" +
|
||||
"\xc3\x47\x66\x1a\x21\xb2\x59\x7c\xc0\x8d\xcf\xcc\xa9\xdd\x97\x23" +
|
||||
"\x3b\x18\x2f\x52\x94\x2b\x40\x03\x86\xea\x98\x47\x31\xd2\x16\xcc" +
|
||||
"\x0d\xdb\x35\xc9\x83\x81\x55\xed\x55\x49\x29\x82\xb2\x5e\xa4\x53" +
|
||||
"\x5c\x71\x1f\xf9\x09\x38\x24\x1f\x61\x16\x46\xc3\x45\x75\xb0\xa1" +
|
||||
"\xcb\x74\x0e\xa7\x10\xa3\x74\xac\x37\x51\x6b\x7e\x43\xba\xd6\x56" +
|
||||
"\x4a\xc5\xb7\x72\xde\x8a\x00\xde\xf4\x07\xc1\x75\xd4\x19\xd2\x54" +
|
||||
"\x4b\x75\x0a\x7b\x2f\x3d\x09\x95\xc3\x0d\x57\x7d\x0a\xc3\x39\xd4" +
|
||||
"\x9c\xbe\x59\x1a\x10\xa1\x23\x5f\x7a\x07\x17\x9c\x5c\xa9\x5d\xa9" +
|
||||
"\x4e\xda\x77\x90\x72\x3d\x62\x80\x7f\x2b\x6c\xd0\xb5\x40\x98\xf4" +
|
||||
"\xdc\xe0\x98\xae\x17\x7a\x5a\xe5\x11\xb5\xc9\x3b\x03\xae\x4d\x35" +
|
||||
"\x81\x6f\x5c\x7e\x61\xc5\xd3\x6a\x1c\x0e\x88\xef\x2c\x9f\xff\xfe" +
|
||||
"\x14\x24\x85\x13\x3f\x3e\x25\x09\xdc\xa4\x6b\xae\x22\xa4\x59\xd5" +
|
||||
"\x1e\xd4\xd2\xfa\x5b\xbd\x37\xe3\xee\x40\x43\xc0\x5f\x9f\xf4\xbe" +
|
||||
"\x34\x4a\x6d\xc6\x98\x45\xfd\x1b\xc5\x63\x7e\x3f\x27\x6c\x28\x64" +
|
||||
"\x70\xf4\xf2\xa8\x5c\xf5\x38\x91\x10\xa0\xd2\xea\x38\xa8\xde\xa3" +
|
||||
"\xc8\x76\x18\x29\xbb\xc8\x7a\x74\x41\x7d\x04\xa6\xfc\x34\xb0\x91" +
|
||||
"\x27\x63\xf7\xcc\x43\xfd\x9d\x68\x61\xbe\x11\x0c\x86\x64\xac\x0d" +
|
||||
"\x92\x74\x12\xc4\xe0\x47\x92\x3d\xe2\x05\x00\xd7\x92\xbd\x20\xa6" +
|
||||
"\xd7\xf9\xbb\xa4\xb5\x4f\x52\xdd\x63\x4f\xe2\x44\x90\xcb\x65\x99" +
|
||||
"\xb3\x4c\x57\x0b\xff\x82\xed\xe1\xa2\xb6\x9d\xda\xa2\x6e\x25\xb4" +
|
||||
"\xfe\x7b\x08\x12\xdd\x92\xa6\xa4\x09\xf3\x09\x80\x2a\x30\x70\x61" +
|
||||
"\x6c\x21\x52\xa3\xa2\x23\x96\xf1\xc9\x42\x65\x7a\x99\x3a\xc2\x68" +
|
||||
"\x18\xc6\xa5\xeb\x57\x94\xdc\x51\x32\xa4\x42\x87\x7b\xed\x56\x4e" +
|
||||
"\x18\x5f\xb4\xcf\xba\x29\x06\x78\x2a\x4a\x49\x04\x80\x6e\xd7\x0e" +
|
||||
"\xbb\xc1\x4b\x98\x57\x27\xba\x47\x25\x53\xb4\x8d\xd0\xb2\x42\x28" +
|
||||
"\x22\x10\x9d\x3a\xad\xcf\xef\xca\x59\x9a\xb8\x35\xe3\x87\xc0\x67" +
|
||||
"\x91\xb8\xea\x1a\xb2\xc8\x06\xbf\x84\x37\xa6\x20\x17\xd0\xc2\x1f" +
|
||||
"\x16\xe2\x08\x1a\x5d\x50\x5e\x96\x16\x79\x56\x27\xbd\x96\xf4\x2b" +
|
||||
"\xcc\xd3\x03\x9d\x2a\x21\xcb\xdc\xa7\x19\x53\x3e\x54\x9e\x7b\xb5" +
|
||||
"\x7e\xee\x7f\xc9\x75\x45\x16\x52\x60\x93\xd7\xd2\x08\x88\x3c\xcf" +
|
||||
"\x38\xeb\xac\x7f\xb1\x7a\xe1\xc1\x3b\x00\x8b\x79\xcd\x9b\xf3\xde" +
|
||||
"\x02\x34\x5f\x22\xee\xb4\xca\xeb\x11\xf4\x54\xed\xf3\xc2\x17\x0e" +
|
||||
"\x35\xb2\x76\x1b\x42\x16\x04\xe2\xe5\x79\x1b\x85\x61\x00\x7a\xa4" +
|
||||
"\xb3\xa4\xd6\x03\x87\x95\xe3\x68\xd7\xad\x73\x24\xab\x0e\x67\x6e" +
|
||||
"\x7f\xaf\x63\x4d\x85\xf3\xd5\x9e\x25\x67\xc2\xef\x75\x52\xb2\x8c" +
|
||||
"\x98\x56\xa8\x68\x61\x55\x48\x07\x7d\xaa\x41\x91\x32\xd8\x0e\x84" +
|
||||
"\x8d\xc7\x45\x3b\xbf\x18\x4d\xe8\x50\xbe\x44\x8e\x88\xb9\x9c\x28" +
|
||||
"\xf7\x49\x5c\x68\x17\x4d\xba\x4c\x79\x7f\x3c\x70\xe9\xf3\xa3\x8c" +
|
||||
"\x18\x86\x0b\x75\x31\x86\x2a\x1b\xf9\x64\x2e\xa0\x6e\x0f\x42\x13" +
|
||||
"\xf8\xfe\xd2\xc0\x24\x36\x22\x8c\x13\xae\x1d\x10\x86\x34\xea\x13" +
|
||||
"\x8c\xbb\x60\x9c\x7f\x72\x23\x55\xc1\x2d\xd1\x3f\xe4\x3e\x67\x6a" +
|
||||
"\x74\x2f\xa7\xde\xbe\xcb\x15\x26\x50\xde\xc9\x1e\x3e\x13\x9a\x6b" +
|
||||
"\xdf\x00\xa2\xc2\x89\x8e\x60\xcf\xd6\x4a\xbd\xc8\xaa\x11\x89\x4c" +
|
||||
"\x0a\x27\x7f\x9f\x76\xd4\xfc\xd6\xf9\xf8\x2a\x7a\x0a\x67\xf9\x83" +
|
||||
"\xb3\xbe\x3e\x43\xd5\x44\x42\xe9\x5a\xab\x35\xde\x3b\x36\xf8\x18" +
|
||||
"\xa3\x1d\x03\x09\x35\x88\xf0\x6a\x35\x14\x31\xec\xde\xff\x12\xc6" +
|
||||
"\x82\x62\x6d\xae\xef\xbe\x33\x1f\xdc\xbe\x37\x98\xfb\x34\x43\x65" +
|
||||
"\xe7\xe1\x14\x98\xd2\xdb\xd3\x79\xb3\x54\xa3\x55\xbe\x5f\x9c\xaa" +
|
||||
"\x64\x80\xe2\x09\x0a\x8d\x7f\xb4\xbc\x93\x49\x63\xe5\x36\x4a\x0f" +
|
||||
"\x8b\x08\x3b\x02\xa5\x87\x90\x77\x1d\x18\x79\x3b\xcc\xf8\xca\x5f" +
|
||||
"\x07\x36\xb1\x6c\xc3\x5b\x1a\x61\x6b\xe3\xaa\x8f\xd9\x34\x77\xf2" +
|
||||
"\x39\x7a\xe0\x56\x34\xbb\x3c\x9c\x90\xcf\xd6\x6b\xca\x87\x4b\xb7" +
|
||||
"\x55\x1c\x66\x26\x79\x9a\x43\x01\xc4\xa6\x5a\x81\xd6\x55\xe9\xf1" +
|
||||
"\xcc\xc0\xea\x09\xbf\x94\xfc\x77\x9b\x73\x99\xd5\x76\xe2\x6d\x0a" +
|
||||
"\xda\x6a\x1b\x20\x46\x2b\x3b\x4d\xd5\xef\xcb\xf8\xb0\x96\x6b\xda" +
|
||||
"\xaf\x2c\xfc\x81\x33\x55\xe2\x72\x4d\xfd\x2f\x70\x1d\x81\xf2\xe8" +
|
||||
"\x58\x17\x12\x98\x78\xe6\x29\xd1\x74\x7e\xd7\xda\xdf\x0a\xca\xb1" +
|
||||
"\xa7\x7d\x2e\xfa\x0b\x2c\x89\xa2\x64\x19\xe3\xea\xe2\xdf\x83\x28" +
|
||||
"\x32\xb4\x41\x51\xd6\x5f\xba\x99\xb0\x3b\xd6\xcf\x96\x50\x92\x05" +
|
||||
"\x53\xe6\x47\xff\x96\xcd\x9b\x1c\x8e\x9a\x9f\x74\x5d\xe2\xe1\x9b" +
|
||||
"\x0c\x10\xf7\x87\xa4\x00\xba\x98\x95\xe1\xb4\xd8\x47\xa2\x0c\xe6" +
|
||||
"\xdc\x85\xd3\x19\x4e\xaa\xee\x24\x94\x9d\x70\xf9\x43\x3d\x52\xca" +
|
||||
"\x66\xa5\x5d\x81\x9a\xff\x00\x73\x1c\x1f\xf0\x4c\x98\x58\x9d\xcd" +
|
||||
"\xdc\xd9\x92\xe6\x33\x20\x94\x33\xdb\xfb\x13\x2b\x1a\x7f\x34\xe6" +
|
||||
"\x11\xfd\x35\x3e\x58\xc1\x38\xae\x20\x5e\x7f\x91\x27\x95\xd9\xa8" +
|
||||
"\x8a\x14\x5a\xa0\x54\x54\xad\xf3\x6d\xd9\xa7\xd6\x6b\xc4\xe8\x30" +
|
||||
"\x12\xb0\x2b\x80\x48\x0d\xd4\x1e\x5e\x6b\x8c\x8c\xf0\xf7\xe0\x8a" +
|
||||
"\x8b\x42\xbe\x9b\xbb\x52\x5c\x3e\xdd\x79\x8b\xab\x76\xb2\x99\xe5" +
|
||||
"\xbc\xca\xf9\xdc\x1d\xab\x34\x74\xd9\xce\xac\x82\xd5\xcc\xf5\x4f" +
|
||||
"\x0f\x59\x4e\xfd\x86\x98\x0e\x2c\xa5\xb8\xa4\x8b\x25\xf8\x9e\x9d" +
|
||||
"\x3d\xa1\x51\xe6\xf4\xc0\x1c\xe7\x49\x68\x5e\x75\x17\x67\xdb\xb8" +
|
||||
"\x4d\x35\xa0\x7d\x8f\x6e\xf6\x6b\xd2\xdc\x02\x9e\x0b\xa5\x9f\x29" +
|
||||
"\x37\x4e\x8f\xab\xf1\x55\x38\x03\x15\x4c\x3c\xd0\x5e\xd3\xc8\x7e" +
|
||||
"\x81\x8d\xbc\x2d\xdc\x6b\x1c\xf7\x57\x7c\xdf\x9f\x84\x16\x90\xb4" +
|
||||
"\x4e\x93\x7c\xff\xa4\x0b\xcb\x30\x6e\xeb\xa3\x91\xa8\xf2\x66\xae" +
|
||||
"\x32\x68\x80\xd9\xa4\x7f\xcd\x81\x07\xb0\x3b\x99\x9d\x5c\xd2\xf6" +
|
||||
"\x9f\x94\x7d\x64\x8d\x7a\x1d\x14\x2d\x39\x41\x6e\x2c\x7c\x8e\xcc" +
|
||||
"\x89\xbf\xf7\x05\x63\xf5\xd0\xa2\x4f\xba\xa4\xc7\x2e\x1c\x84\xf2" +
|
||||
"\xcb\x64\x0a\x11\xf0\x64\x22\xbd\x5c\x00\x10\x2f\x2b\xb1\xf3\x9f" +
|
||||
"\xa5\x49\x7c\x37\x24\x63\x01\x9b\x75\x04\x33\xe4\x97\x4b\xd3\x27" +
|
||||
"\x81\x3f\x56\xb0\x40\x93\x8b\x3f\x86\x57\x4c\x94\x5a\x8f\x7b\x9b" +
|
||||
"\x0a\xad\x0c\x5e\xf3\x54\xf7\xba\x64\x1e\x33\x80\xd0\x08\xd3\x28" +
|
||||
"\x9f\x8f\xfc\x8b\xcd\x43\x0a\xa5\x1a\x8c\x6d\x33\x25\x53\x9d\xe1" +
|
||||
"\x6e\x2a\x4e\x65\x5f\x57\xb4\x38\x31\xd6\x91\xb3\x10\x3e\x6c\x29" +
|
||||
"\x25\x5a\xe1\xcb\x3c\x9e\x84\xbc\x8e\x08\x72\x5d\x20\x0c\x1b\x6a" +
|
||||
"\x9c\xaf\xbf\x81\xcf\x9d\xa7\xb2\xd0\xd3\x22\x0a\x6e\x1b\x84\x76" +
|
||||
"\x68\xc9\xb0\x6d\x81\x38\x9f\xa7\x19\x56\xea\x79\x7c\xb0\x59\x54" +
|
||||
"\x29\xd6\xa7\x90\x71\xda\x47\xd6\x5b\xcc\x1c\x03\x39\x81\xf0\x1b" +
|
||||
"\x06\xfb\x11\x58\x2b\x93\xfa\x4d\xaf\x82\xa9\xd4\xdc\x15\xcf\xca" +
|
||||
"\xb2\x53\xcc\xa7\xc7\x21\x12\x5f\xae\xc2\xeb\x28\x7d\x71\x45\x50" +
|
||||
"\x42\x57\x75\xe6\x86\x6a\xb0\xfb\xb2\x09\xde\x53\x12\xc3\x80\xdc" +
|
||||
"\xd1\x15\x46\x67\x85\x76\xc5\x2a\x8c\x8f\xce\x2c\xc0\x0e\x13\x7f" +
|
||||
"\x8c\x16\xf2\x71\x7c\x01\x3d\x71\xe8\x42\xe6\x90\xb5\x21\x79\x57" +
|
||||
"\x03\x1d\xad\x71\x28\x9e\x3a\x7a\x84\x77\x6b\xc1\x86\xd5\x1c\xe0" +
|
||||
"\x4d\x57\xf2\x64\x93\xe2\xb5\xee\x1a\x0a\x25\xa8\x23\x67\x5e\xa3" +
|
||||
"\xd1\x4f\x84\xbe\x70\x74\x8b\x7c\xd3\x88\x1c\xab\x35\x1f\x54\xcf" +
|
||||
"\x91\xf9\xb2\x0c\xd3\xa5\xa3\x73\x44\x0e\x91\xe6\xec\xfe\x64\x04" +
|
||||
"\xa0\x0d\xa2\xe5\x05\x77\xe9\x6c\xb2\x33\x25\x84\x77\x60\x76\x47" +
|
||||
"\x0e\xb0\x30\x91\xbb\x2c\xa3\x17\x59\x94\x17\xc3\x71\x75\x34\x75" +
|
||||
"\x88\xa7\x00\xab\x96\x9f\x1a\xac\x72\x8e\x24\x82\x77\x27\xf4\x88" +
|
||||
"\x18\x40\xa8\xee\x9b\x9b\xa8\x96\x4a\xbd\xc7\x78\x1a\xcf\xb3\x74" +
|
||||
"\x01\x82\xa8\x7d\xff\xb2\x20\xd2\xfe\x46\x79\xd8\x2b\x32\x55\x2c" +
|
||||
"\x03\xe1\x1a\xf9\x6b\x6b\x6c\x2c\x53\xf4\x9f\x29\x7f\xad\x35\x13" +
|
||||
"\x1f\x1e\x06\xbc\x3d\xd5\x9d\x4f\x49\x56\x70\x35\xa5\x87\x73\xa5" +
|
||||
"\xc3\xc4\xa7\x1b\x45\xa5\xb8\x6e\x91\x63\x48\xd8\x07\x66\xbf\xab" +
|
||||
"\x5c\x39\x38\x08\x96\xa1\xf9\x4e\x06\x8a\x5c\x39\x7a\x8f\x7f\x66" +
|
||||
"\xab\x18\x7f\x86\x62\xde\x1f\xf2\xa6\x6a\x35\x49\x2d\xd6\x3e\xf9" +
|
||||
"\x1e\x96\xcd\x2a\x7d\x7d\x25\x0b\xa8\x2c\x7d\x8e\x30\x6c\xbf\xdf" +
|
||||
"\x20\xca\x95\xec\xfc\xbc\x9a\x47\x6a\x8f\x47\x3d\x35\x56\x4e\x5d" +
|
||||
"\xef\xd4\x5e\xa4\x54\xc0\x6a\xec\xfa\xdf\xbe\xc9\x9f\xf8\xf3\xe7" +
|
||||
"\xd5\x24\x52\x5b\x59\x5c\x5b\xb8\xd4\x18\xe6\x18\x0a\x2f\x6f\x59" +
|
||||
"\xc2\xcb\xec\x52\x02\x33\xda\x88\xb8\x1f\x71\x06\x85\x73\xa2\x84" +
|
||||
"\x9b\xf6\xf7\xff\x0f\xfc\x57\x39\x44\x4b\xf8\xbf\xab\xc8\xff\x04" +
|
||||
"\x00\x00\xff\xff\x14\x22\x96\x1d\xab\x0c\x00\x00"
|
||||
|
||||
// snakeoilKey returns raw, uncompressed file data.
|
||||
func snakeoilKey() []byte {
|
||||
var empty [0]byte
|
||||
sx := (*reflect.StringHeader)(unsafe.Pointer(&_snakeoilKey))
|
||||
b := empty[:]
|
||||
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bx.Data = sx.Data
|
||||
bx.Len = len(_snakeoilKey)
|
||||
bx.Cap = bx.Len
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
|
||||
if err != nil {
|
||||
panic("Decompression failed: " + err.Error())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -3,21 +3,32 @@ package server
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/version"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultPortMap = map[string]int{
|
||||
"http": 80,
|
||||
"https": 443,
|
||||
"smtp": 25,
|
||||
}
|
||||
|
||||
/**
|
||||
* Tunnel: A control connection, metadata and proxy connections which
|
||||
* route public traffic to a firewalled endpoint.
|
||||
*/
|
||||
type Tunnel struct {
|
||||
regMsg *msg.RegMsg
|
||||
// request that opened the tunnel
|
||||
req *msg.ReqTunnel
|
||||
|
||||
// time when the tunnel was opened
|
||||
start time.Time
|
||||
@@ -31,9 +42,6 @@ type Tunnel struct {
|
||||
// control connection
|
||||
ctl *Control
|
||||
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// logger
|
||||
log.Logger
|
||||
|
||||
@@ -41,74 +49,161 @@ type Tunnel struct {
|
||||
closing int32
|
||||
}
|
||||
|
||||
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
|
||||
t = &Tunnel{
|
||||
regMsg: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
proxies: make(chan conn.Conn),
|
||||
Logger: log.NewPrefixLogger(),
|
||||
// Common functionality for registering virtually hosted protocols
|
||||
func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
|
||||
vhost := os.Getenv("VHOST")
|
||||
if vhost == "" {
|
||||
vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort)
|
||||
}
|
||||
|
||||
switch t.regMsg.Protocol {
|
||||
// Canonicalize virtual host by removing default port (e.g. :80 on HTTP)
|
||||
defaultPort, ok := defaultPortMap[protocol]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find default port for protocol %s", protocol)
|
||||
}
|
||||
|
||||
defaultPortSuffix := fmt.Sprintf(":%d", defaultPort)
|
||||
if strings.HasSuffix(vhost, defaultPortSuffix) {
|
||||
vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)]
|
||||
}
|
||||
|
||||
// Canonicalize by always using lower-case
|
||||
vhost = strings.ToLower(vhost)
|
||||
|
||||
// Register for specific hostname
|
||||
hostname := strings.ToLower(strings.TrimSpace(t.req.Hostname))
|
||||
if hostname != "" {
|
||||
t.url = fmt.Sprintf("%s://%s", protocol, hostname)
|
||||
return tunnelRegistry.Register(t.url, t)
|
||||
}
|
||||
|
||||
// Register for specific subdomain
|
||||
subdomain := strings.ToLower(strings.TrimSpace(t.req.Subdomain))
|
||||
if subdomain != "" {
|
||||
t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost)
|
||||
return tunnelRegistry.Register(t.url, t)
|
||||
}
|
||||
|
||||
// Register for random URL
|
||||
t.url, err = tunnelRegistry.RegisterRepeat(func() string {
|
||||
return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost)
|
||||
}, t)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new tunnel from a registration message received
|
||||
// on a control channel
|
||||
func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
|
||||
t = &Tunnel{
|
||||
req: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
}
|
||||
|
||||
proto := t.req.Protocol
|
||||
switch proto {
|
||||
case "tcp":
|
||||
var err error
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
|
||||
bindTcp := func(port int) error {
|
||||
if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil {
|
||||
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
|
||||
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"}
|
||||
// register it
|
||||
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
|
||||
// This should never be possible because the OS will
|
||||
// only assign available ports to us.
|
||||
t.listener.Close()
|
||||
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
|
||||
return err
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
// use the custom remote port you asked for
|
||||
if t.req.RemotePort != 0 {
|
||||
bindTcp(int(t.req.RemotePort))
|
||||
return
|
||||
}
|
||||
|
||||
// try to return to you the same port you had before
|
||||
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
|
||||
if cachedUrl != "" {
|
||||
var port int
|
||||
parts := strings.Split(cachedUrl, ":")
|
||||
portPart := parts[len(parts)-1]
|
||||
port, err = strconv.Atoi(portPart)
|
||||
if err != nil {
|
||||
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
|
||||
} else {
|
||||
// we have a valid, cached port, let's try to bind with it
|
||||
if bindTcp(port) != nil {
|
||||
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
|
||||
} else {
|
||||
// success, we're done
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bind for TCP connections
|
||||
bindTcp(0)
|
||||
return
|
||||
|
||||
case "http", "https":
|
||||
l, ok := listeners[proto]
|
||||
if !ok {
|
||||
err = fmt.Errorf("Not listeneing for %s connections", proto)
|
||||
return
|
||||
}
|
||||
|
||||
if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
if err := tunnels.Add(t); err != nil {
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprint(err)}
|
||||
err = fmt.Errorf("Protocol %s is not supported", proto)
|
||||
return
|
||||
}
|
||||
|
||||
if m.Version != version.Proto {
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprintf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)}
|
||||
}
|
||||
|
||||
// pre-encode the http basic auth for fast comparisons later
|
||||
if m.HttpAuth != "" {
|
||||
m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth))
|
||||
}
|
||||
|
||||
t.ctl.conn.AddLogPrefix(t.Id())
|
||||
t.AddLogPrefix(t.Id())
|
||||
t.Info("Registered new tunnel")
|
||||
t.ctl.out <- &msg.RegAckMsg{
|
||||
Url: t.url,
|
||||
ProxyAddr: fmt.Sprintf("%s", proxyAddr),
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
}
|
||||
t.Info("Registered new tunnel on: %s", t.ctl.conn.Id())
|
||||
|
||||
metrics.OpenTunnel(t)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Tunnel) shutdown() {
|
||||
func (t *Tunnel) Shutdown() {
|
||||
t.Info("Shutting down")
|
||||
|
||||
// mark that we're shutting down
|
||||
atomic.StoreInt32(&t.closing, 1)
|
||||
|
||||
// if we have a public listener (this is a raw TCP tunnel, shut it down
|
||||
// if we have a public listener (this is a raw TCP tunnel), shut it down
|
||||
if t.listener != nil {
|
||||
t.listener.Close()
|
||||
}
|
||||
|
||||
// remove ourselves from the tunnel registry
|
||||
tunnels.Del(t.url)
|
||||
tunnelRegistry.Del(t.url)
|
||||
|
||||
// XXX: shut down all of the proxy connections?
|
||||
// let the control connection know we're shutting down
|
||||
// currently, only the control connection shuts down tunnels,
|
||||
// so it doesn't need to know about it
|
||||
// t.ctl.stoptunnel <- t
|
||||
|
||||
metrics.CloseTunnel(t)
|
||||
}
|
||||
@@ -117,9 +212,7 @@ func (t *Tunnel) Id() string {
|
||||
return t.url
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new public tcp connections from the internet.
|
||||
*/
|
||||
// Listens for new public tcp connections from the internet.
|
||||
func (t *Tunnel) listenTcp(listener *net.TCPListener) {
|
||||
for {
|
||||
defer func() {
|
||||
@@ -160,20 +253,47 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
|
||||
startTime := time.Now()
|
||||
metrics.OpenConnection(t, publicConn)
|
||||
|
||||
t.Debug("Requesting new proxy connection")
|
||||
t.ctl.out <- &msg.ReqProxyMsg{}
|
||||
var proxyConn conn.Conn
|
||||
var err error
|
||||
for i := 0; i < (2 * proxyMaxPoolSize); i++ {
|
||||
// get a proxy connection
|
||||
if proxyConn, err = t.ctl.GetProxy(); err != nil {
|
||||
t.Warn("Failed to get proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer proxyConn.Close()
|
||||
t.Info("Got proxy connection %s", proxyConn.Id())
|
||||
proxyConn.AddLogPrefix(t.Id())
|
||||
|
||||
proxyConn := <-t.proxies
|
||||
t.Info("Returning proxy connection %s", proxyConn.Id())
|
||||
// tell the client we're going to start using this proxy connection
|
||||
startPxyMsg := &msg.StartProxy{
|
||||
Url: t.url,
|
||||
ClientAddr: publicConn.RemoteAddr().String(),
|
||||
}
|
||||
|
||||
defer proxyConn.Close()
|
||||
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
|
||||
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i)
|
||||
proxyConn.Close()
|
||||
} else {
|
||||
// success
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// give up
|
||||
publicConn.Error("Too many failures starting proxy connection")
|
||||
return
|
||||
}
|
||||
|
||||
// To reduce latency handling tunnel connections, we employ the following curde heuristic:
|
||||
// Whenever we take a proxy connection from the pool, replace it with a new one
|
||||
util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} })
|
||||
|
||||
// no timeouts while connections are joined
|
||||
proxyConn.SetDeadline(time.Time{})
|
||||
|
||||
// join the public and proxy connections
|
||||
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
|
||||
|
||||
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)
|
||||
}
|
||||
|
||||
func (t *Tunnel) RegisterProxy(conn conn.Conn) {
|
||||
t.Info("Registered proxy connection %s", conn.Id())
|
||||
conn.AddLogPrefix(t.Id())
|
||||
t.proxies <- conn
|
||||
}
|
||||
|
||||
33
src/ngrok/util/errors.go
Normal file
33
src/ngrok/util/errors.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const crashMessage = `panic: %v
|
||||
|
||||
%s
|
||||
|
||||
Oh noes! ngrok crashed!
|
||||
|
||||
Please submit the stack trace and any relevant information to:
|
||||
github.com/inconshreveable/ngrok/issues`
|
||||
|
||||
func MakePanicTrace(err interface{}) string {
|
||||
stackBuf := make([]byte, 4096)
|
||||
n := runtime.Stack(stackBuf, false)
|
||||
return fmt.Sprintf(crashMessage, err, stackBuf[:n])
|
||||
}
|
||||
|
||||
// Runs the given function and converts any panic encountered while doing so
|
||||
// into an error. Useful for sending to channels that will close
|
||||
func PanicToError(fn func()) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Panic: %v", r)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return
|
||||
}
|
||||
@@ -2,11 +2,32 @@ package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
)
|
||||
|
||||
// create a random identifier for this client
|
||||
func RandId(idlen int) (id string, err error) {
|
||||
func RandomSeed() (seed int64, err error) {
|
||||
err = binary.Read(rand.Reader, binary.LittleEndian, &seed)
|
||||
return
|
||||
}
|
||||
|
||||
// creates a random identifier of the specified length
|
||||
func RandId(idlen int) string {
|
||||
b := make([]byte, idlen)
|
||||
var randVal uint32
|
||||
for i := 0; i < idlen; i++ {
|
||||
byteIdx := i % 4
|
||||
if byteIdx == 0 {
|
||||
randVal = mrand.Uint32()
|
||||
}
|
||||
b[i] = byte((randVal >> (8 * uint(byteIdx))) & 0xFF)
|
||||
}
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// like RandId, but uses a crypto/rand for secure random identifiers
|
||||
func SecureRandId(idlen int) (id string, err error) {
|
||||
b := make([]byte, idlen)
|
||||
n, err := rand.Read(b)
|
||||
|
||||
@@ -23,8 +44,8 @@ func RandId(idlen int) (id string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func RandIdOrPanic(idlen int) string {
|
||||
id, err := RandId(idlen)
|
||||
func SecureRandIdOrPanic(idlen int) string {
|
||||
id, err := SecureRandId(idlen)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
43
src/ngrok/util/shutdown.go
Normal file
43
src/ngrok/util/shutdown.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A small utility class for managing controlled shutdowns
|
||||
type Shutdown struct {
|
||||
sync.Mutex
|
||||
inProgress bool
|
||||
begin chan int // closed when the shutdown begins
|
||||
complete chan int // closed when the shutdown completes
|
||||
}
|
||||
|
||||
func NewShutdown() *Shutdown {
|
||||
return &Shutdown{
|
||||
begin: make(chan int),
|
||||
complete: make(chan int),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shutdown) Begin() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.inProgress == true {
|
||||
return
|
||||
} else {
|
||||
s.inProgress = true
|
||||
close(s.begin)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shutdown) WaitBegin() {
|
||||
<-s.begin
|
||||
}
|
||||
|
||||
func (s *Shutdown) Complete() {
|
||||
close(s.complete)
|
||||
}
|
||||
|
||||
func (s *Shutdown) WaitComplete() {
|
||||
<-s.complete
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Proto = "1"
|
||||
Major = "0"
|
||||
Minor = "11"
|
||||
Proto = "2"
|
||||
Major = "1"
|
||||
Minor = "6"
|
||||
)
|
||||
|
||||
func MajorMinor() string {
|
||||
|
||||
Reference in New Issue
Block a user