Compare commits
119 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 | ||
|
|
a911f7fb65 | ||
|
|
ea088b69f2 | ||
|
|
2fff31ff61 | ||
|
|
a9f659216e | ||
|
|
264ac352f7 | ||
|
|
d0b9161993 | ||
|
|
fd394cb336 | ||
|
|
8f6637c784 | ||
|
|
eb60b72aa4 | ||
|
|
ea1b1ed632 | ||
|
|
63c70c4996 | ||
|
|
20364f6de0 | ||
|
|
d914e26055 | ||
|
|
f5a24b30e8 | ||
|
|
bfbfdd9ee9 | ||
|
|
3d17d52659 | ||
|
|
b4082e7f89 | ||
|
|
151433e2fd | ||
|
|
eef7b6b755 | ||
|
|
816f5e8882 | ||
|
|
8bfebdc277 | ||
|
|
4e20bd9db8 | ||
|
|
d2ec1ec080 | ||
|
|
8017c0653b | ||
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,5 +4,6 @@ pkg/
|
||||
src/code.google.com
|
||||
src/github.com
|
||||
src/bitbucket.org
|
||||
src/launchpad.net
|
||||
src/ngrok/client/assets/assets_release.go
|
||||
src/ngrok/server/assets/assets_release.go
|
||||
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,4 +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
|
||||
|
||||
@@ -3,3 +3,4 @@ Alan Shreve (inconshreveable)
|
||||
Kyle Conroy (kyleconroy)
|
||||
Caleb Spare (cespare)
|
||||
Stephen Huenneke (skastel)
|
||||
Nick Presta (nickpresta)
|
||||
|
||||
@@ -31,11 +31,4 @@ are only available by creating an account on ngrok.com. If you need them, [creat
|
||||
|
||||
|
||||
## Developing on ngrok
|
||||
### Compiling ngrok
|
||||
|
||||
git clone git@github.com:inconshreveable/ngrok.git
|
||||
cd ngrok && make
|
||||
bin/ngrok [LOCAL PORT]
|
||||
|
||||
### Further Reading
|
||||
[ngrok developer's guide](docs/DEVELOPMENT.md)
|
||||
|
||||
@@ -23,7 +23,14 @@
|
||||
background-color: #ff9999;
|
||||
background-color: #000000;
|
||||
color:white;
|
||||
|
||||
}
|
||||
.path {
|
||||
width: 300px;
|
||||
}
|
||||
.wrapped {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -48,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>
|
||||
@@ -57,7 +69,7 @@
|
||||
<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>
|
||||
@@ -76,12 +88,12 @@
|
||||
</div>
|
||||
<div class="span4">
|
||||
<i class="icon-user"></i> IP
|
||||
<span style="margin-left: 8px;" class="muted">{{Txn.Req.Header['X-Real-Ip'][0]}}</span>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -1,4 +1,30 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ 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.
|
||||
@@ -36,15 +38,25 @@ 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 tunnel.ngrok.me
|
||||
127.0.0.1 test.ngrok.me
|
||||
|
||||
Run ngrokd with the following options:
|
||||
|
||||
./bin/ngrokd -domain ngrok.me
|
||||
|
||||
Run ngrok with the following options
|
||||
Create an ngrok configuration file, "debug.yml" with the following contents:
|
||||
|
||||
./bin/ngrok -server=ngrok.me:4443 -subdomain=tunnel -log=ngrok.log 8080
|
||||
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!
|
||||
|
||||
@@ -52,18 +64,23 @@ This will get you setup with an ngrok client talking to an ngrok server all loca
|
||||
## Network protocol and tunneling
|
||||
At a high level, ngrok's tunneling works as follows:
|
||||
|
||||
### Setup
|
||||
### 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 a registration message.
|
||||
1. The server registers the tunnel and then sends a registration ACK message with information about the created tunnel.
|
||||
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 examing 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 proxy request message to the client over the control connection.
|
||||
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 proxy registration message over the proxy connection so the server can associate it to the public connection that caused the request for the proxy connection to be initiated.
|
||||
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 port that was specified at startup. This is called the *Private Connection*.
|
||||
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
|
||||
@@ -81,7 +98,7 @@ The message length is sent as a 64-bit little endian integer.
|
||||
The definitions and shared protocol routines lives under _src/ngrok/msg_
|
||||
|
||||
#### src/ngrok/msg/msg.go
|
||||
All of the different message types (Reg, PxyReq, Ping, etc) are defined here. This is a good place to go to understand exactly what messages are sent between the client and server.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package client
|
||||
|
||||
/*
|
||||
Functions for reading and writing the auth token from the user's
|
||||
home directory.
|
||||
*/
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"ngrok/log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
currentAuthToken string
|
||||
authTokenFile string
|
||||
)
|
||||
|
||||
func initAuth() {
|
||||
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", err.Error())
|
||||
} else {
|
||||
homeDir = user.HomeDir
|
||||
}
|
||||
|
||||
authTokenFile = path.Join(homeDir, ".ngrok")
|
||||
|
||||
log.Debug("Reading auth token from file %s", authTokenFile)
|
||||
tokenBytes, err := ioutil.ReadFile(authTokenFile)
|
||||
|
||||
if err == nil {
|
||||
currentAuthToken = string(tokenBytes)
|
||||
} else {
|
||||
log.Warn("Failed to read ~/.ngrok for auth token: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Load the auth token from file
|
||||
func LoadAuthToken() string {
|
||||
once.Do(initAuth)
|
||||
return currentAuthToken
|
||||
}
|
||||
|
||||
// Save the auth token to file
|
||||
func SaveAuthToken(token string) {
|
||||
if token == "" || token == LoadAuthToken() || 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())
|
||||
}
|
||||
}
|
||||
@@ -1,108 +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
|
||||
webport int
|
||||
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 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")
|
||||
@@ -110,7 +74,7 @@ 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",
|
||||
@@ -119,40 +83,46 @@ func parseArgs() *Options {
|
||||
|
||||
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, -1 to disable")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return &Options{
|
||||
server: *server,
|
||||
httpAuth: *httpAuth,
|
||||
subdomain: *subdomain,
|
||||
localaddr: parseLocalAddr(),
|
||||
protocol: parseProtocol(*protocol),
|
||||
webport: *webport,
|
||||
opts = &Options{
|
||||
config: *config,
|
||||
logto: *logto,
|
||||
httpauth: *httpauth,
|
||||
subdomain: *subdomain,
|
||||
protocol: *protocol,
|
||||
authtoken: *authtoken,
|
||||
hostname: *hostname,
|
||||
command: flag.Arg(0),
|
||||
}
|
||||
|
||||
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"}
|
||||
)
|
||||
@@ -2,300 +2,37 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"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
|
||||
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>
|
||||
`
|
||||
)
|
||||
|
||||
/**
|
||||
* 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", tlsConfig)
|
||||
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)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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", tlsConfig)
|
||||
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
|
||||
s.publicUrl = regAck.Url
|
||||
conn.Info("Tunnel established at %v", s.GetPublicUrl())
|
||||
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)
|
||||
|
||||
// set up auth token
|
||||
if opts.authtoken == "" {
|
||||
opts.authtoken = LoadAuthToken()
|
||||
// read configuration file
|
||||
config, err := LoadConfiguration(opts)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// init client state
|
||||
s := &State{
|
||||
status: "connecting",
|
||||
|
||||
// unique client id
|
||||
id: util.RandIdOrPanic(8),
|
||||
|
||||
// command-line options
|
||||
opts: opts,
|
||||
|
||||
// metrics
|
||||
metrics: NewClientMetrics(),
|
||||
// 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)
|
||||
|
||||
switch opts.protocol {
|
||||
case "http":
|
||||
s.protocol = proto.NewHttp()
|
||||
case "tcp":
|
||||
s.protocol = proto.NewTcp()
|
||||
}
|
||||
|
||||
// init ui
|
||||
ctl := ui.NewController()
|
||||
if opts.webport != -1 {
|
||||
web.NewWebView(ctl, s, opts.webport)
|
||||
}
|
||||
|
||||
if opts.logto != "stdout" {
|
||||
term.New(ctl, s)
|
||||
}
|
||||
|
||||
go reconnectingControl(s, ctl)
|
||||
go autoUpdate(s, ctl, opts.authtoken)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package mvc
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
@@ -14,16 +14,34 @@ const (
|
||||
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
|
||||
GetUpdate() UpdateStatus
|
||||
GetPublicUrl() string
|
||||
GetLocalAddr() string
|
||||
GetStatus() string
|
||||
GetProtocol() proto.Protocol
|
||||
GetWebPort() int
|
||||
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,44 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/proto"
|
||||
"ngrok/version"
|
||||
)
|
||||
|
||||
// client state
|
||||
type State struct {
|
||||
id string
|
||||
publicUrl string
|
||||
serverVersion string
|
||||
update ui.UpdateStatus
|
||||
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) 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) GetUpdate() ui.UpdateStatus { return s.update }
|
||||
func (s State) GetPublicUrl() string { return s.publicUrl }
|
||||
|
||||
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
|
||||
}
|
||||
@@ -4,42 +4,34 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"ngrok/client/assets"
|
||||
)
|
||||
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) {
|
||||
pool := x509.NewCertPool()
|
||||
|
||||
ngrokRootCrt, err := assets.ReadAsset("assets/client/tls/ngrokroot.crt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, certPath := range rootCertPaths {
|
||||
rootCrt, err := assets.ReadAsset(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snakeoilCaCrt, err := assets.ReadAsset("assets/client/tls/snakeoilca.crt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, b := range [][]byte{ngrokRootCrt, snakeoilCaCrt} {
|
||||
pemBlock, _ := pem.Decode(b)
|
||||
pemBlock, _ := pem.Decode(rootCrt)
|
||||
if pemBlock == nil {
|
||||
panic("Bad PEM data")
|
||||
return nil, fmt.Errorf("Bad PEM data")
|
||||
}
|
||||
|
||||
certs, err := x509.ParseCertificates(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool.AddCert(certs[0])
|
||||
}
|
||||
|
||||
tlsConfig = &tls.Config{
|
||||
return &tls.Config{
|
||||
RootCAs: pool,
|
||||
ServerName: "tls.ngrok.com",
|
||||
}
|
||||
ServerName: "ngrokd.ngrok.com",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
)
|
||||
|
||||
// no auto-updating in debug mode
|
||||
func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
func autoUpdate(state mvc.State, token string) {
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
update "github.com/inconshreveable/go-update"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
checkEndpoint = "https://dl.ngrok.com/update/check"
|
||||
)
|
||||
|
||||
func progressWatcher(s *State, ctl *ui.Controller, progress chan int, complete chan int) {
|
||||
func progressWatcher(s mvc.State, progress chan int, complete chan int) {
|
||||
for {
|
||||
select {
|
||||
case pct, ok := <-progress:
|
||||
@@ -26,22 +26,20 @@ func progressWatcher(s *State, ctl *ui.Controller, progress chan int, complete c
|
||||
close(complete)
|
||||
return
|
||||
} else if pct == 100 {
|
||||
s.update = ui.UpdateInstalling
|
||||
ctl.Update(s)
|
||||
s.SetUpdateStatus(mvc.UpdateInstalling)
|
||||
close(complete)
|
||||
return
|
||||
} else {
|
||||
if pct%25 == 0 {
|
||||
log.Info("Downloading update %d%% complete", pct)
|
||||
}
|
||||
s.update = ui.UpdateStatus(pct)
|
||||
ctl.Update(s)
|
||||
s.SetUpdateStatus(mvc.UpdateStatus(pct))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
func autoUpdate(s mvc.State, token string) {
|
||||
tryAgain := true
|
||||
|
||||
params := make(url.Values)
|
||||
@@ -75,7 +73,7 @@ func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
download := update.NewDownload(updateUrl)
|
||||
downloadComplete := make(chan int)
|
||||
|
||||
go progressWatcher(s, ctl, download.Progress, downloadComplete)
|
||||
go progressWatcher(s, download.Progress, downloadComplete)
|
||||
|
||||
log.Info("Trying to update . . .")
|
||||
err, errRecover := download.GetAndUpdate()
|
||||
@@ -84,7 +82,6 @@ func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
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())
|
||||
@@ -99,20 +96,19 @@ func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
resp.Body.Close()
|
||||
|
||||
// tell the user to update manually
|
||||
s.update = ui.UpdateAvailable
|
||||
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.update = ui.UpdateAvailable
|
||||
s.SetUpdateStatus(mvc.UpdateAvailable)
|
||||
} else {
|
||||
// tell the user the update is ready
|
||||
log.Info("Update ready!")
|
||||
s.update = ui.UpdateReady
|
||||
s.SetUpdateStatus(mvc.UpdateReady)
|
||||
}
|
||||
}
|
||||
|
||||
ctl.Update(s)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 +65,16 @@ func (v *TermView) Render() {
|
||||
v.Printf(v.w-len(quitMsg), 0, quitMsg)
|
||||
|
||||
// new version message
|
||||
updateStatus := v.state.GetUpdate()
|
||||
updateStatus := state.GetUpdateStatus()
|
||||
var updateMsg string
|
||||
switch updateStatus {
|
||||
case ui.UpdateNone:
|
||||
case mvc.UpdateNone:
|
||||
updateMsg = ""
|
||||
case ui.UpdateInstalling:
|
||||
case mvc.UpdateInstalling:
|
||||
updateMsg = "ngrok is updating"
|
||||
case ui.UpdateReady:
|
||||
case mvc.UpdateReady:
|
||||
updateMsg = "ngrok has updated: restart ngrok for the new version"
|
||||
case ui.UpdateAvailable:
|
||||
case mvc.UpdateAvailable:
|
||||
updateMsg = "new version available at https://ngrok.com"
|
||||
default:
|
||||
pct := float64(updateStatus) / 100.0
|
||||
@@ -111,50 +98,65 @@ func (v *TermView) Render() {
|
||||
}
|
||||
|
||||
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", 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())
|
||||
if v.state.GetWebPort() == -1 {
|
||||
webAddr = "disabled"
|
||||
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, 6, "%-30s%s", "Web Interface", webAddr)
|
||||
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
|
||||
|
||||
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()
|
||||
@@ -163,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -22,6 +22,7 @@ type SerializedTxn struct {
|
||||
Id string
|
||||
Duration int64
|
||||
Start int64
|
||||
ConnCtx mvc.ConnectionContext
|
||||
*proto.HttpTxn `json:"-"`
|
||||
Req SerializedRequest
|
||||
Resp SerializedResponse
|
||||
@@ -55,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 {
|
||||
@@ -73,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 {
|
||||
@@ -153,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,
|
||||
@@ -178,10 +172,11 @@ func (whv *WebHttpView) updateHttp() {
|
||||
Body: body,
|
||||
Binary: !utf8.Valid(rawReq),
|
||||
},
|
||||
Start: htxn.Start.Unix(),
|
||||
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
|
||||
@@ -189,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{
|
||||
@@ -206,43 +201,46 @@ 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 {
|
||||
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: reqBytes}
|
||||
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) {
|
||||
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)
|
||||
@@ -251,8 +249,8 @@ func (h *WebHttpView) register() {
|
||||
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)
|
||||
@@ -266,3 +264,6 @@ func (h *WebHttpView) register() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (whv *WebHttpView) Shutdown() {
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/garyburd/go-websocket/websocket"
|
||||
"net/http"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -14,34 +13,38 @@ import (
|
||||
)
|
||||
|
||||
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,17 +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) {
|
||||
buf, err := assets.ReadAsset(path.Join("assets", "client", r.URL.Path[1:]))
|
||||
if err != nil {
|
||||
log.Warn("Error serving static file: %s", err.Error())
|
||||
wv.Warn("Error serving static file: %s", err.Error())
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
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
|
||||
@@ -75,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,14 +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["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{}
|
||||
@@ -28,45 +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 {
|
||||
}
|
||||
|
||||
// Sent by a client or server over the control channel to indicate
|
||||
// it received a Ping.
|
||||
type Pong struct {
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -84,7 +87,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
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)
|
||||
|
||||
@@ -94,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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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,11 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,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")
|
||||
|
||||
@@ -60,10 +67,13 @@ 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
|
||||
}
|
||||
@@ -73,7 +83,7 @@ func httpHandler(tcpConn net.Conn) {
|
||||
conn.Debug("Found hostname %s in request", host)
|
||||
|
||||
// multiplex to find the right backend host
|
||||
tunnel := tunnels.Get("http://" + host)
|
||||
tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host))
|
||||
if tunnel == nil {
|
||||
conn.Info("No tunnel found for hostname %s", host)
|
||||
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
|
||||
@@ -83,11 +93,15 @@ func httpHandler(tcpConn net.Conn) {
|
||||
// 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.regMsg.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
|
||||
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,128 +1,141 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"crypto/tls"
|
||||
"math/rand"
|
||||
"ngrok/conn"
|
||||
log "ngrok/log"
|
||||
"ngrok/msg"
|
||||
"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 (
|
||||
proxyAddr string
|
||||
tunnels *TunnelRegistry
|
||||
registryCacheSize uint64 = 1024 * 1024 // 1 MB
|
||||
domain string
|
||||
publicPort int
|
||||
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 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")
|
||||
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()
|
||||
}
|
||||
}()
|
||||
|
||||
flag.Parse()
|
||||
// set logging prefix
|
||||
pxyConn.SetType("pxy")
|
||||
|
||||
return &Options{
|
||||
publicPort: *publicPort,
|
||||
tunnelPort: *tunnelPort,
|
||||
proxyPort: *proxyPort,
|
||||
domain: *domain,
|
||||
logto: *logto,
|
||||
// look up the control connection for this proxy
|
||||
pxyConn.Info("Registering new proxy for %s", regPxy.ClientId)
|
||||
ctl := controlRegistry.Get(regPxy.ClientId)
|
||||
|
||||
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", tlsConfig)
|
||||
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", tlsConfig)
|
||||
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()
|
||||
domain = opts.domain
|
||||
publicPort = opts.publicPort
|
||||
opts = parseArgs()
|
||||
|
||||
// init logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
// init tunnel registry
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
// init tunnel/control registry
|
||||
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
|
||||
tunnels = NewTunnelRegistry(registryCacheSize, registryCacheFile)
|
||||
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
|
||||
controlRegistry = NewControlRegistry()
|
||||
|
||||
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})
|
||||
// start listeners
|
||||
listeners = make(map[string]*conn.Listener)
|
||||
|
||||
// wait forever
|
||||
done := make(chan int)
|
||||
<-done
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func init() {
|
||||
keenApiKey := os.Getenv("KEEN_API_KEY")
|
||||
|
||||
if keenApiKey != "" {
|
||||
metrics = NewKeenIoMetrics()
|
||||
metrics = NewKeenIoMetrics(60 * time.Second)
|
||||
} else {
|
||||
metrics = NewLocalMetrics(30 * time.Second)
|
||||
}
|
||||
@@ -94,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":
|
||||
@@ -105,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":
|
||||
@@ -154,9 +154,9 @@ func (m *LocalMetrics) Report() {
|
||||
}
|
||||
}
|
||||
|
||||
type KeenIoRequest struct {
|
||||
Path string
|
||||
Body []byte
|
||||
type KeenIoMetric struct {
|
||||
Collection string
|
||||
Event interface{}
|
||||
}
|
||||
|
||||
type KeenIoMetrics struct {
|
||||
@@ -164,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("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{})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -198,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)
|
||||
}
|
||||
@@ -217,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
|
||||
@@ -236,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) {
|
||||
@@ -265,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
|
||||
@@ -281,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}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
|
||||
registry := &TunnelRegistry{
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
affinity: cache.NewLRUCache(cacheSize),
|
||||
Logger: log.NewPrefixLogger("registry"),
|
||||
Logger: log.NewPrefixLogger("registry", "tun"),
|
||||
}
|
||||
|
||||
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
|
||||
@@ -42,14 +42,13 @@ func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
|
||||
var urlobj cacheUrl
|
||||
gob.Register(urlobj)
|
||||
|
||||
// try to load and then periodically save the affinity cache to file, if specified
|
||||
if cacheFile != "" {
|
||||
// load cache entries from file
|
||||
err := registry.affinity.LoadItemsFromFile(cacheFile)
|
||||
if err != nil {
|
||||
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
|
||||
}
|
||||
|
||||
// save cache periodically to file
|
||||
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
|
||||
} else {
|
||||
registry.Info("No affinity cache specified")
|
||||
@@ -93,10 +92,10 @@ func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
|
||||
|
||||
func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) {
|
||||
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
|
||||
clientId := t.regMsg.ClientId
|
||||
clientId := t.ctl.id
|
||||
|
||||
ipKey := fmt.Sprintf("client-ip-%s:%s", t.regMsg.Protocol, clientIp)
|
||||
idKey := fmt.Sprintf("client-id-%s:%s", t.regMsg.Protocol, clientId)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -160,3 +159,49 @@ func (r *TunnelRegistry) Get(url string) *Tunnel {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,40 +4,40 @@ import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"ngrok/server/assets"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
readOrBytes := func(envVar string, default_path string) []byte {
|
||||
f := os.Getenv(envVar)
|
||||
if f == "" {
|
||||
b, err := assets.ReadAsset(default_path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
} else {
|
||||
if b, err := ioutil.ReadFile(f); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
crt := readOrBytes("TLS_CRT_FILE", "assets/server/tls/snakeoil.crt")
|
||||
key := readOrBytes("TLS_KEY_FILE", "assets/server/tls/snakeoil.key")
|
||||
cert, err := tls.X509KeyPair(crt, key)
|
||||
var (
|
||||
crt []byte
|
||||
key []byte
|
||||
cert tls.Certificate
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/version"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,12 +16,19 @@ import (
|
||||
"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
|
||||
@@ -35,9 +42,6 @@ type Tunnel struct {
|
||||
// control connection
|
||||
ctl *Control
|
||||
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// logger
|
||||
log.Logger
|
||||
|
||||
@@ -45,107 +49,128 @@ type Tunnel struct {
|
||||
closing int32
|
||||
}
|
||||
|
||||
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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{
|
||||
regMsg: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
proxies: make(chan conn.Conn),
|
||||
Logger: log.NewPrefixLogger(),
|
||||
req: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
}
|
||||
|
||||
failReg := func(err error) {
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch t.regMsg.Protocol {
|
||||
proto := t.req.Protocol
|
||||
switch proto {
|
||||
case "tcp":
|
||||
var port int = 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
|
||||
}
|
||||
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 := tunnels.GetCachedRegistration(t)
|
||||
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)
|
||||
// continue with zero
|
||||
port = 0
|
||||
} 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
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port})
|
||||
bindTcp(0)
|
||||
return
|
||||
|
||||
// If we failed with a custom port, try with a random one
|
||||
if err != nil && port != 0 {
|
||||
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
|
||||
}
|
||||
|
||||
// we tried to bind with a random port and failed (no more ports available?)
|
||||
if err != nil {
|
||||
failReg(t.ctl.conn.Error("Error binding TCP listener: %v", err))
|
||||
case "http", "https":
|
||||
l, ok := listeners[proto]
|
||||
if !ok {
|
||||
err = fmt.Errorf("Not listeneing for %s connections", proto)
|
||||
return
|
||||
}
|
||||
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port)
|
||||
|
||||
// register it
|
||||
if err = tunnels.RegisterAndCache(t.url, t); err != nil {
|
||||
// This should never be possible because the OS will
|
||||
// only assign available ports to us.
|
||||
t.listener.Close()
|
||||
failReg(fmt.Errorf("TCP listener bound, but failed to register %s", t.url))
|
||||
if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
|
||||
case "http":
|
||||
vhost := os.Getenv("VHOST")
|
||||
if vhost == "" {
|
||||
vhost = fmt.Sprintf("%s:%d", domain, publicPort)
|
||||
}
|
||||
|
||||
// Canonicalize virtual host on default port 80
|
||||
if strings.HasSuffix(vhost, ":80") {
|
||||
vhost = vhost[0 : len(vhost)-3]
|
||||
}
|
||||
|
||||
if strings.TrimSpace(t.regMsg.Hostname) != "" {
|
||||
t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
|
||||
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
|
||||
t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, vhost)
|
||||
}
|
||||
|
||||
vhost = strings.ToLower(vhost)
|
||||
t.url = strings.ToLower(t.url)
|
||||
|
||||
if t.url != "" {
|
||||
if err := tunnels.Register(t.url, t); err != nil {
|
||||
failReg(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
t.url, err = tunnels.RegisterRepeat(func() string {
|
||||
return fmt.Sprintf("http://%x.%s", rand.Int31(), vhost)
|
||||
}, t)
|
||||
|
||||
if err != nil {
|
||||
failReg(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.Version != version.Proto {
|
||||
failReg(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version))
|
||||
default:
|
||||
err = fmt.Errorf("Protocol %s is not supported", proto)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,35 +179,31 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
|
||||
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)
|
||||
}
|
||||
@@ -191,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() {
|
||||
@@ -234,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 = "23"
|
||||
Proto = "2"
|
||||
Major = "1"
|
||||
Minor = "6"
|
||||
)
|
||||
|
||||
func MajorMinor() string {
|
||||
|
||||
Reference in New Issue
Block a user