109 Commits
1.0 ... travis

Author SHA1 Message Date
Alan Shreve
2c00a3fd8b Merge branch 'master' into travis 2013-10-25 18:13:13 +03:00
Alan Shreve
7a9f972030 add some more entries to the changelog 2013-10-25 18:12:56 +03:00
Alan Shreve
264fbe83fe bump version 2013-10-25 17:44:38 +03:00
Alan Shreve
3f8e4b8e0b fix a goroutine leak in ngrok's parsing of http transactions 2013-10-25 17:42:52 +03:00
Alan Shreve
5ddfa6232c fix a bug handling 'disabled' value for the inspection interface configuration 2013-10-25 13:06:51 +03:00
Alan Shreve
cda0e2c050 fix a bug handling 'disabled' value for the inspection interface configuration 2013-10-22 01:20:39 +03:00
Alan Shreve
69733b1e3c Merge branch 'master' into travis 2013-10-20 16:19:00 +03:00
Alan Shreve
92569be139 bump version 2013-10-20 15:46:57 +03:00
Alan Shreve
c72868cbf5 start cross-compiling to OS X on this branch as well 2013-10-20 15:39:25 +03:00
Alan Shreve
e58d35972d fix an issue where aggressive NATs or proxy connections pooled for too long could hang the tunneling of public connections 2013-10-15 17:27:45 +03:00
Alan Shreve
98902cff73 add support for specifying a remote tcp port 2013-10-13 14:32:23 +02:00
Alan Shreve
a269effc05 Revert "remove ip-based caching so that it's possible to reliably generate new urls"
This reverts commit f2009d7f52.
2013-10-13 14:26:31 +02:00
Alan Shreve
ed2abcaa95 support disabling the web inspection interface with 'disabled' 2013-10-13 14:04:52 +02:00
Alan Shreve
ea93073174 show error reporting instructions after crash dump 2013-10-08 12:16:05 +02:00
Alan Shreve
f2009d7f52 remove ip-based caching so that it's possible to reliably generate new urls 2013-10-08 12:13:26 +02:00
Alan Shreve
f956a32d64 improve connection logging 2013-10-08 12:04:29 +02:00
Alan Shreve
85723df89b improve metrics logging 2013-10-02 09:44:31 +02:00
Alan Shreve
1860b0a1e1 batch metric reporting to keen to improve performance at high request loads 2013-10-02 00:25:05 +02:00
Alan Shreve
f20d426690 fix developers guide debug command 2013-09-30 11:23:01 +02:00
Alan Shreve
d0c3f9cd03 stop using CloseRead() until I figure out linger/flush issues 2013-09-30 09:56:44 +02:00
Alan Shreve
148702929b switch back to Close() calls since CloseRead() seems to cause sockets not to flush their writes 2013-09-30 01:54:23 +02:00
Alan Shreve
3af3e39b20 Recover any errors while handling tunnel connections. Fix a race condition that could cause the server to send its initial auth message out of order or even on a closed channel and crash the server 2013-09-29 22:34:23 +02:00
Alan Shreve
ef9aa6251d safely shutdown connections by using TCPConn.CloseRead() so that they don't throw errors 2013-09-29 00:21:37 +02:00
Alan Shreve
4ce53025e1 go fmt 2013-09-29 00:20:22 +02:00
Alan Shreve
46dd79a949 fix a critical bug which could cause tunnels to freeze indefinitely and never clean up resources. fix a bug where a client trying to reconnect on an intermittent connection would be told that its tunnels were already registered by another user 2013-09-28 19:31:09 +02:00
Alan Shreve
d08b89020f update development documentation to reflect changes to the ngrok protocol 2013-09-28 11:58:59 +02:00
Alan Shreve
39bebc5e25 make note in developer guide that you must compile with Go1.1+ 2013-09-27 22:14:29 +02:00
Alan Shreve
5335f85202 Merge branch 'master' into travis 2013-09-27 21:27:56 +02:00
Alan Shreve
0134667055 bump version 2013-09-27 21:26:06 +02:00
Alan Shreve
9de09f3f1d fix a bug where your auth token couldn't be saved to a configuration file if no configuration file existed 2013-09-27 21:24:50 +02:00
Alan Shreve
0ca93d33aa Merge branch 'master' of github.com:inconshreveable/ngrok 2013-09-27 17:02:45 +02:00
Alan Shreve
0468760901 add comment around removing the workaround when the bug is fixed in termbox-go 2013-09-27 17:02:10 +02:00
Nick Presta
c47241cf3a Refactoring to support unicode. 2013-09-26 23:15:37 -04:00
Nick Presta
54b2056c12 Refactoring to remove cleverness. 2013-09-26 21:49:44 -04:00
Nick Presta
1d93622aec Adding myself to CONTRIBUTORS 2013-09-26 15:07:45 -04:00
inconshreveable
c7c9a2d07a Merge pull request #35 from NickPresta/fix-wrapping-web
Adding word wrapping to path for web dashboard.
2013-09-26 12:06:06 -07:00
Nick Presta
f136b8486e Adding word wrapping to path for web dashboard.
* Tested in latest versions of Chrome, Firefox, Safari.
2013-09-26 02:22:32 -04:00
Nick Presta
7fc854b620 Limiting the length of the request's path. 2013-09-26 01:29:07 -04:00
Alan Shreve
3d8f09ba08 fix up docs 2013-09-24 23:24:39 +02:00
Alan Shreve
ed5729bd63 Merge branch 'master' into travis 2013-09-22 17:40:17 +02:00
Alan Shreve
ce2b352574 bump version 2013-09-22 17:21:29 +02:00
Alan Shreve
179a76b8cf re-add support to write the configuration file when the server validates a new authtoken works 2013-09-22 17:21:05 +02:00
Alan Shreve
8d044e722c improve config validation, parse the tunnel name as a subdomain or hostname if neither is specified 2013-09-22 00:50:14 +03:00
Alan Shreve
5608df7ec5 parse old v0 configuration files for backwards-compat 2013-09-21 22:28:14 +03:00
Alan Shreve
7bf4547911 go fmt 2013-09-21 21:25:23 +03:00
Alan Shreve
7b25fe80e0 connect through to an HTTPS endpoint in the hopes that it will eliminate possible errors from proxies trying to parse the http request 2013-09-21 21:24:43 +03:00
Alan Shreve
97fcb417ac rename proxy_url configuration variable 2013-09-20 19:41:50 +03:00
Alan Shreve
22537c0136 add bug reporting instructions on crash 2013-09-20 19:41:31 +03:00
Alan Shreve
11dfbc7045 update self-hosting document 2013-09-20 19:40:52 +03:00
Alan Shreve
824a4b10ab add support for a yaml-based ngrok configuration file. add full support for running multiple configured tunnels from the configuration file. modify CLI to support ngrok commands. remove snakeoil CA from release builds of ngrok client. add support for dialing to an HTTP proxy via HTTPS. Use http_proxy environment variable when none is specified. ngrokd now honors the new protocol and proplery populates ReqId in NewTunnel messages. ngrokd TLS configuration options are now command line switches instead of environment variables. 2013-09-20 16:12:00 +03:00
Alan Shreve
13be54d4e7 make sure every new tunnel connection and every new http connection has a read/write timeouts until handled by a tunnel. fix a bug where a slow tunnel or proxy connection could block all others. fix a bug where sending a non-expected valid ngrok protocol message over a new connection could leak a connection 2013-09-16 13:12:44 +03:00
Alan Shreve
15e017d285 add support for proxy authentication 2013-09-13 21:28:56 +03:00
Alan Shreve
917d636f3a bump version 2013-09-11 13:15:34 +03:00
Alan Shreve
16615300f5 add support for tunneling through http proxies 2013-09-11 13:13:59 +03:00
Alan Shreve
1fa6bb644e add support for registering multiple protocols in a single tunnel request message 2013-09-10 21:43:06 +03:00
Alan Shreve
92e125b137 fix a bug that would cause the client to hang when shutting down abnormally 2013-09-10 20:18:30 +03:00
Alan Shreve
280901df87 modernize the ngrok protocol to make more sense in the context of multiple tunnels. separate out authentication and tunnel registration 2013-09-10 17:53:13 +03:00
Alan Shreve
b32d551830 go fmt 2013-09-10 11:39:13 +03:00
Alan Shreve
055ca06302 fix autoupdate to work with the new API, go fmt 2013-09-10 11:35:08 +03:00
Alan Shreve
b3eb99f457 update the default ngrokd endpoint 2013-09-09 18:27:58 +03:00
Alan Shreve
ca72fcb05c allow ngrokd to bind to specific interfaces 2013-09-09 15:50:30 +03:00
Alan Shreve
00ae5d77db Merge branch 'master' into travis 2013-09-06 22:59:15 +03:00
Alan Shreve
a911f7fb65 Merge branch 'master' into travis 2013-09-04 18:43:36 +03:00
Alan Shreve
a9f659216e Merge branch 'master' into travis 2013-08-17 21:54:29 +02:00
Alan Shreve
264ac352f7 Merge branch 'master' into travis 2013-08-17 21:25:50 +02:00
Alan Shreve
d0b9161993 Merge branch 'master' into travis 2013-08-17 12:19:41 +02:00
Alan Shreve
fd394cb336 Merge branch 'master' into travis 2013-08-17 12:10:33 +02:00
Alan Shreve
8f6637c784 Merge branch 'master' into travis 2013-08-17 11:17:21 +02:00
Alan Shreve
eb60b72aa4 Merge branch 'master' into travis 2013-08-17 11:16:33 +02:00
Alan Shreve
63c70c4996 don't cross-compile osx anymore 2013-08-16 01:58:59 +02:00
Alan Shreve
20364f6de0 Merge branch 'master' into travis 2013-08-15 13:10:19 +02:00
Alan Shreve
d914e26055 Merge branch 'master' into travis 2013-08-15 12:42:28 +02:00
Alan Shreve
f5a24b30e8 Merge branch 'master' into travis 2013-08-15 12:41:48 +02:00
Alan Shreve
bfbfdd9ee9 cross-compile for darwin/amd64 2013-08-15 12:41:42 +02:00
Alan Shreve
b4082e7f89 fix merge conflicts 2013-07-30 16:20:21 +02:00
Alan Shreve
151433e2fd merge in master 2013-07-30 16:08:18 +02:00
Alan Shreve
eef7b6b755 merge in branch master 2013-07-30 15:27:15 +02:00
Alan Shreve
816f5e8882 Merge branch 'master' into travis 2013-07-28 00:44:43 +02:00
Alan Shreve
8bfebdc277 remove unecessary package step 2013-07-23 15:40:36 +01:00
Alan Shreve
4e20bd9db8 fix build script 2013-07-18 19:08:07 +01:00
Alan Shreve
d2ec1ec080 fix windows build with exe extension 2013-07-18 18:24:55 +01:00
Alan Shreve
8017c0653b figure out why windows isn't building 2013-07-18 18:11:56 +01:00
Alan Shreve
167cf25dae remove debugging prints 2013-07-18 02:46:32 +01:00
Alan Shreve
9f8bd8adc3 make sure we run the verison command in native platform 2013-07-18 02:35:52 +01:00
Alan Shreve
4db6e7143d fix how we pass in environment variables 2013-07-18 02:29:08 +01:00
Alan Shreve
65411b54bd more travis debugging 2013-07-18 02:05:55 +01:00
Alan Shreve
213eb595a6 touch to get travis to build 2013-07-18 01:58:47 +01:00
Alan Shreve
160bf79cb5 pass env variables to travis 2013-07-18 01:45:00 +01:00
Alan Shreve
95ffd273f2 try to debug travis build problems 2013-07-18 01:09:47 +01:00
Alan Shreve
063389d36b new travis upload strategy 2013-07-18 00:56:32 +01:00
Alan Shreve
514c16c91f fix host target 2013-07-17 01:13:09 +01:00
Alan Shreve
f6a3742a74 set env vars separately 2013-07-17 00:59:10 +01:00
Alan Shreve
d09037b0ff run after_success explicitly 2013-07-17 00:50:19 +01:00
Alan Shreve
0c970983e2 one more attempt at fixing after_success 2013-07-17 00:43:32 +01:00
Alan Shreve
a72e0fd7c7 fix after_success 2013-07-17 00:27:56 +01:00
Alan Shreve
41865cb20d fix after_success 2013-07-17 00:21:47 +01:00
Alan Shreve
66a919e63b always install go-bindata for the current architecture 2013-07-17 00:07:51 +01:00
Alan Shreve
569013e691 fix for bootstrap 2013-07-16 23:49:16 +01:00
Alan Shreve
bf6ce69203 bootstrap for cross-compile 2013-07-16 23:46:35 +01:00
Alan Shreve
030b436d62 find make.bash 2013-07-16 23:37:44 +01:00
Alan Shreve
5b27861135 add cross-compilation targets 2013-07-16 23:30:03 +01:00
Alan Shreve
aa3fa9a08b continue trying to get travis to build 2013-07-16 23:26:19 +01:00
Alan Shreve
53a940ec18 try to get travis to build again 2013-07-16 23:16:42 +01:00
Alan Shreve
e5840c1e5d fix travis branch specifier 2013-07-16 23:10:11 +01:00
Alan Shreve
f1766e5884 test travis configuration 2013-07-16 22:57:38 +01:00
Alan Shreve
404e70c1eb add travis upload step 2013-07-16 22:53:39 +01:00
Alan Shreve
421b365483 add cross-compilation for windows and linux/arm 2013-07-16 20:50:49 +01:00
Alan Shreve
c072be702c make sure go-bindata tool is installed 2013-07-16 20:34:01 +01:00
Alan Shreve
0f09e5feae add travis integration 2013-07-16 20:28:33 +01:00
44 changed files with 1575 additions and 799 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -3,3 +3,4 @@ Alan Shreve (inconshreveable)
Kyle Conroy (kyleconroy)
Caleb Spare (cespare)
Stephen Huenneke (skastel)
Nick Presta (nickpresta)

View File

@@ -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)

View File

@@ -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>
@@ -62,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>
@@ -86,7 +93,7 @@
</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>

22
build/travis.py Executable file
View 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
View File

@@ -0,0 +1,10 @@
package main
import (
"fmt"
"ngrok/version"
)
func main() {
fmt.Print(version.MajorMinor())
}

View File

@@ -1,4 +1,24 @@
# 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

View File

@@ -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
View 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.

View File

@@ -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())
}
}

View File

@@ -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
View 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
}

View File

@@ -6,7 +6,6 @@ import (
"ngrok/client/views/term"
"ngrok/client/views/web"
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"sync"
@@ -48,7 +47,7 @@ type Controller struct {
state chan mvc.State
// options
opts *Options
config *Configuration
}
// public interface
@@ -127,29 +126,29 @@ func (ctl *Controller) addView(v mvc.View) {
ctl.views = append(ctl.views, v)
}
func (ctl *Controller) GetWebViewPort() int {
return ctl.opts.webport
func (ctl *Controller) GetWebInspectAddr() string {
return ctl.config.InspectAddr
}
func (ctl *Controller) Run(opts *Options) {
// Save the options
ctl.opts = opts
func (ctl *Controller) Run(config *Configuration) {
// Save the configuration
ctl.config = config
// init the model
model := newClientModel(ctl)
model := newClientModel(config, ctl)
ctl.model = model
var state mvc.State = model
// init web ui
var webView *web.WebView
if opts.webport != -1 {
webView = web.NewWebView(ctl, opts.webport)
if config.InspectAddr != "disabled" {
webView = web.NewWebView(ctl, config.InspectAddr)
ctl.addView(webView)
}
// init term ui
var termView *term.TermView
if opts.logto != "stdout" {
if config.LogTo != "stdout" {
termView = term.NewTermView(ctl)
ctl.addView(termView)
}
@@ -168,34 +167,24 @@ func (ctl *Controller) Run(opts *Options) {
}
}
ctl.Go(func() { autoUpdate(ctl, opts.authtoken) })
reg := &msg.RegMsg{
Protocol: opts.protocol,
Hostname: opts.hostname,
Subdomain: opts.subdomain,
HttpAuth: opts.httpAuth,
User: opts.authtoken,
}
ctl.Go(func() { ctl.model.Run(opts.server, opts.authtoken, ctl, reg, opts.localaddr) })
quitMessage := ""
defer func() {
ctl.doShutdown()
fmt.Println(quitMessage)
}()
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:
quitMessage = cmd.message
return
msg := cmd.message
go func() {
ctl.doShutdown()
fmt.Println(msg)
done <- 1
}()
case cmdPlayRequest:
ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) })
@@ -205,6 +194,8 @@ func (ctl *Controller) Run(opts *Options) {
state = obj.(mvc.State)
case ctl.state <- state:
case <-done:
return
}
}
}

View File

@@ -0,0 +1,7 @@
// +build !release
package client
var (
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
)

View File

@@ -1,29 +1,38 @@
package client
import (
"fmt"
"math/rand"
"ngrok/log"
"ngrok/util"
"os"
)
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)
}
// seed random number generator
seed, err := util.RandomSeed()
if err != nil {
log.Error("Couldn't securely seed the random number generator!")
fmt.Printf("Couldn't securely seed the random number generator!")
os.Exit(1)
}
rand.Seed(seed)
NewController().Run(opts)
NewController().Run(config)
}

View File

@@ -1,6 +1,7 @@
package client
import (
"crypto/tls"
"fmt"
metrics "github.com/inconshreveable/go-metrics"
"io/ioutil"
@@ -13,11 +14,13 @@ import (
"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
@@ -42,21 +45,42 @@ type ClientModel struct {
protocols []proto.Protocol
ctl mvc.Controller
serverAddr string
proxyUrl string
authToken string
tlsConfig *tls.Config
tunnelConfig map[string]*TunnelConfiguration
configPath string
}
func newClientModel(ctl mvc.Controller) *ClientModel {
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"),
// unique client id
id: util.RandIdOrPanic(8),
// server address
serverAddr: config.ServerAddr,
// proxy address
proxyUrl: config.HttpProxy,
// auth token
authToken: config.AuthToken,
// connection status
connStatus: mvc.ConnConnecting,
@@ -78,6 +102,15 @@ func newClientModel(ctl mvc.Controller) *ClientModel {
// controller
ctl: ctl,
// tls configuration
tlsConfig: tlsConfig,
// tunnel configuration
tunnelConfig: config.Tunnels,
// config path
configPath: config.Path,
}
}
@@ -106,6 +139,10 @@ func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
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) {
@@ -129,21 +166,16 @@ func (c *ClientModel) update() {
c.ctl.Update(c)
}
func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, reg *msg.RegMsg, localaddr string) {
c.serverAddr = serverAddr
c.authToken = authToken
c.ctl = ctl
c.reconnectingControl(reg, localaddr)
}
func (c *ClientModel) reconnectingControl(reg *msg.RegMsg, localaddr string) {
func (c *ClientModel) Run() {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
c.control(reg, localaddr)
// 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
}
@@ -159,7 +191,7 @@ func (c *ClientModel) reconnectingControl(reg *msg.RegMsg, localaddr string) {
}
// Establishes and manages a tunnel control connection with the server
func (c *ClientModel) control(reg *msg.RegMsg, localaddr string) {
func (c *ClientModel) control() {
defer func() {
if r := recover(); r != nil {
log.Error("control recovering from failure %v", r)
@@ -167,117 +199,161 @@ func (c *ClientModel) control(reg *msg.RegMsg, localaddr string) {
}()
// establish control channel
conn, err := conn.Dial(c.serverAddr, "ctl", tlsConfig)
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 conn.Close()
defer ctlConn.Close()
// register with the server
reg.OS = runtime.GOOS
reg.ClientId = c.id
reg.Version = version.Proto
reg.MmVersion = version.MajorMinor()
reg.User = c.authToken
// 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(conn, reg); err != nil {
if err = msg.WriteMsg(ctlConn, auth); err != nil {
panic(err)
}
// wait for the server to ack our register
var regAck msg.RegAckMsg
if err = msg.ReadMsgInto(conn, &regAck); err != nil {
// wait for the server to authenticate us
var authResp msg.AuthResp
if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil {
panic(err)
}
if regAck.Error != "" {
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
if authResp.Error != "" {
emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error)
c.ctl.Shutdown(emsg)
return
}
tunnel := mvc.Tunnel{
PublicUrl: regAck.Url,
LocalAddr: localaddr,
Protocol: c.protoMap[reg.Protocol],
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)
}
c.tunnels[tunnel.PublicUrl] = tunnel
// 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)
}
// update UI state
c.id = regAck.ClientId
c.Info("Tunnel established at %v", tunnel.PublicUrl)
c.connStatus = mvc.ConnOnline
c.serverVersion = regAck.MmVersion
c.update()
reqTunnel := &msg.ReqTunnel{
ReqId: util.RandId(8),
Protocol: strings.Join(protocols, "+"),
Hostname: config.Hostname,
Subdomain: config.Subdomain,
HttpAuth: config.HttpAuth,
RemotePort: config.RemotePort,
}
SaveAuthToken(c.authToken)
// 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, conn) })
c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) })
// main control loop
for {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(conn); err != nil {
if rawMsg, err = msg.ReadMsg(ctlConn); err != nil {
panic(err)
}
switch m := rawMsg.(type) {
case *msg.ReqProxyMsg:
case *msg.ReqProxy:
c.ctl.Go(c.proxy)
case *msg.PongMsg:
case *msg.Pong:
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
case *msg.RegAckMsg:
case *msg.NewTunnel:
if m.Error != "" {
c.Error("Server failed to allocate tunnel: %s", regAck.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: localaddr,
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:
conn.Warn("Ignoring unknown control message %v ", m)
ctlConn.Warn("Ignoring unknown control message %v ", m)
}
}
}
// Establishes and manages a tunnel proxy connection with the server
func (c *ClientModel) proxy() {
remoteConn, err := conn.Dial(c.serverAddr, "pxy", tlsConfig)
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.RegProxyMsg{ClientId: c.id})
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
if err != nil {
log.Error("Failed to write RegProxyMsg: %v", err)
remoteConn.Error("Failed to write RegProxy: %v", err)
return
}
// wait for the server to ack our register
var startPxyMsg msg.StartProxyMsg
if err = msg.ReadMsgInto(remoteConn, &startPxyMsg); err != nil {
log.Error("Server failed to write StartProxyMsg: %v", err)
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[startPxyMsg.Url]
tunnel, ok := c.tunnels[startPxy.Url]
if !ok {
c.Error("Couldn't find tunnel for proxy: %s", startPxyMsg.Url)
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
return
}
@@ -305,7 +381,7 @@ Content-Length: %d
m.connMeter.Mark(1)
c.update()
m.connTimer.Time(func() {
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxyMsg.ClientAddr})
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)
@@ -341,7 +417,7 @@ func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) {
}
case <-ping.C:
err := msg.WriteMsg(conn, &msg.PingMsg{})
err := msg.WriteMsg(conn, &msg.Ping{})
if err != nil {
conn.Debug("Got error %v when writing PingMsg", err)
return

View File

@@ -23,6 +23,6 @@ type Controller interface {
// safe wrapper for running go-routines
Go(fn func())
// the port where the web interface is running
GetWebViewPort() int
// the address where the web inspection interface is running
GetWebInspectAddr() string
}

View File

@@ -1,11 +1,7 @@
package mvc
import (
"ngrok/msg"
)
type Model interface {
Run(serverAddr, authToken string, ctl Controller, reg *msg.RegMsg, localaddr string)
Run()
Shutdown()

View File

@@ -43,4 +43,5 @@ type State interface {
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
SetUpdateStatus(UpdateStatus)
}

View File

@@ -0,0 +1,7 @@
// +build release
package client
var (
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"}
)

View File

@@ -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
}

View File

@@ -7,5 +7,5 @@ import (
)
// no auto-updating in debug mode
func autoUpdate(ctl mvc.Controller, token string) {
func autoUpdate(state mvc.State, token string) {
}

View File

@@ -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()
@@ -98,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
}

View File

@@ -6,10 +6,12 @@ import (
"ngrok/log"
"ngrok/proto"
"ngrok/util"
"unicode/utf8"
)
const (
size = 10
size = 10
pathMaxLength = 25
)
type HttpView struct {
@@ -69,7 +71,8 @@ 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)
}
@@ -80,3 +83,37 @@ func (v *HttpView) Render() {
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
}

View File

@@ -2,7 +2,6 @@
package term
import (
"fmt"
termbox "github.com/nsf/termbox-go"
"ngrok/client/mvc"
"ngrok/log"
@@ -108,11 +107,7 @@ func (v *TermView) draw() {
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
i++
}
webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
if v.ctl.GetWebViewPort() == -1 {
webAddr = "disabled"
}
v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
connMeter, connTimer := state.GetConnectionMetrics()
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())

View File

@@ -154,12 +154,6 @@ func (whv *WebHttpView) updateHttp() {
// we haven't processed this transaction yet if we haven't set the
// user data
if htxn.UserCtx == nil {
id, err := util.RandId(8)
if err != nil {
whv.Error("Failed to generate txn identifier for web storage: %v", err)
continue
}
rawReq, err := httputil.DumpRequestOut(htxn.Req.Request, true)
if err != nil {
whv.Error("Failed to dump request: %v", err)
@@ -168,7 +162,7 @@ func (whv *WebHttpView) updateHttp() {
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,

View File

@@ -2,7 +2,6 @@
package web
import (
"fmt"
"github.com/garyburd/go-websocket/websocket"
"net/http"
"ngrok/client/assets"
@@ -22,7 +21,7 @@ type WebView struct {
wsMessages *util.Broadcast
}
func NewWebView(ctl mvc.Controller, port int) *WebView {
func NewWebView(ctl mvc.Controller, addr string) *WebView {
wv := &WebView{
Logger: log.NewPrefixLogger("view", "web"),
wsMessages: util.NewBroadcast(),
@@ -66,8 +65,8 @@ func NewWebView(ctl mvc.Controller, port int) *WebView {
w.Write(buf)
})
wv.Info("Serving web interface on localhost:%d", port)
wv.ctl.Go(func() { http.ListenAndServe(fmt.Sprintf(":%d", port), nil) })
wv.Info("Serving web interface on %s", addr)
wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
return wv
}

View File

@@ -4,12 +4,15 @@ import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"ngrok/log"
"sync"
)
type Conn interface {
@@ -17,9 +20,11 @@ type Conn interface {
log.Logger
Id() string
SetType(string)
CloseRead() error
}
type tcpConn struct {
type loggedConn struct {
tcp *net.TCPConn
net.Conn
log.Logger
id int32
@@ -27,84 +32,148 @@ 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 *tcpConn) SetType(typ string) {
func (c *loggedConn) SetType(typ string) {
oldId := c.Id()
c.typ = typ
c.ClearLogPrefixes()
@@ -112,49 +181,58 @@ func (c *tcpConn) SetType(typ string) {
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

View File

@@ -11,15 +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["StartProxyMsg"] = t((*StartProxyMsg)(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{}
@@ -29,51 +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
Protocol string
Error string
ClientId string
Error string
}
type ReqProxyMsg struct {
// 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 RegProxyMsg 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
}
// 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 {
}
// 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 StartProxyMsg struct {
Url string
ClientAddr string
// 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 PingMsg struct {
// A client or server may send this message periodically over
// the control channel to request that the remote side acknowledge
// its connection is still alive. The remote side must respond with a Pong.
type Ping struct {
}
type PongMsg struct {
}
type VersionMsg struct {
}
type VersionRespMsg struct {
Version string
MmVersion string
// Sent by a client or server over the control channel to indicate
// it received a Ping.
type Pong struct {
}

View File

@@ -65,6 +65,8 @@ func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
}
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 {
@@ -95,9 +97,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interf
}
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)

View File

@@ -5,30 +5,33 @@ import (
)
type Options struct {
httpPort int
httpsPort int
tunnelPort int
httpAddr string
httpsAddr string
tunnelAddr string
domain string
tlsCrt string
tlsKey string
logto string
}
func parseArgs() *Options {
httpPort := flag.Int("httpPort", 80, "Public HTTP port, -1 to disable")
httpsPort := flag.Int("httpsPort", 443, "Public HTTPS port, -1 to disable")
tunnelPort := flag.Int("tunnelPort", 4443, "Port to which ngrok clients connect")
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")
logto := flag.String(
"log",
"stdout",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
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{
httpPort: *httpPort,
httpsPort: *httpsPort,
tunnelPort: *tunnelPort,
httpAddr: *httpAddr,
httpsAddr: *httpsAddr,
tunnelAddr: *tunnelAddr,
domain: *domain,
tlsCrt: *tlsCrt,
tlsKey: *tlsKey,
logto: *logto,
}
}

View File

@@ -8,16 +8,22 @@ import (
"ngrok/util"
"ngrok/version"
"runtime/debug"
"sync/atomic"
"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
@@ -29,11 +35,6 @@ type Control struct {
// to us over conn by the client
in chan (msg.Message)
// put a message in this channel to send it over
// conn to the client and then terminate this
// control connection and all of its tunnels
stop chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
@@ -43,170 +44,198 @@ type Control struct {
// proxy connections
proxies chan conn.Conn
// closing indicator
closing int32
// 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(ctlConn conn.Conn, regMsg *msg.RegMsg) {
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
// channels are buffered because we read and write to them
// from the same goroutine in managerThread()
c := &Control{
conn: ctlConn,
out: make(chan msg.Message, 5),
in: make(chan msg.Message, 5),
stop: make(chan msg.Message, 5),
proxies: make(chan conn.Conn, 10),
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(),
}
// assign the random id
serverId, err := util.RandId(8)
if err != nil {
c.stop <- &msg.RegAckMsg{Error: err.Error()}
failAuth := func(e error) {
_ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()})
ctlConn.Close()
}
c.id = fmt.Sprintf("%s-%s", regMsg.ClientId, serverId)
// register the control
err = controlRegistry.Add(c.id, c)
if err != nil {
c.stop <- &msg.RegAckMsg{Error: err.Error()}
// 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)
// register the first tunnel
c.in <- regMsg
// manage the connection
go c.managerThread()
go c.readThread()
}
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(regMsg *msg.RegMsg) {
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(regMsg, c)
if err != nil {
ack := &msg.RegAckMsg{Error: err.Error()}
if len(c.tunnels) == 0 {
// you can't fail your first tunnel registration
// terminate the control connection
c.stop <- ack
} else {
// inform client of failure
c.out <- ack
}
// we're done
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
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// register the control
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
replaced.shutdown.WaitComplete()
}
// acknowledge success
c.out <- &msg.RegAckMsg{
Url: t.url,
Protocol: regMsg.Protocol,
// 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,
}
if regMsg.Protocol == "http" {
httpsRegMsg := *regMsg
httpsRegMsg.Protocol = "https"
c.in <- &httpsRegMsg
// 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()
}
// 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
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) managerThread() {
reap := time.NewTicker(connReapInterval)
// all shutdown functionality in here
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())
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
}
// remove from the control registry
controlRegistry.Del(c.id)
// mark that we're shutting down
atomic.StoreInt32(&c.closing, 1)
// stop the reaping timer
reap.Stop()
// close the connection
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// we're safe to close(c.proxies) because c.closing
// protects us inside of RegisterProxy
close(c.proxies)
// shut down all of the proxy connections
for p := range c.proxies {
p.Close()
}
}()
// 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 m := <-c.stop:
if m != nil {
msg.WriteMsg(c.conn, m)
}
return
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 mRaw := <-c.in:
switch m := mRaw.(type) {
case *msg.RegMsg:
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 {
@@ -217,23 +246,60 @@ func (c *Control) readThread() {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}
func (c *Control) RegisterProxy(conn conn.Conn) {
if atomic.LoadInt32(&c.closing) == 1 {
c.conn.Debug("Can't register proxies for a control that is closing")
conn.Close()
return
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:
c.conn.Info("Registered proxy connection %s", conn.Id())
conn.Info("Registered")
default:
// c.proxies buffer is full, discard this one
conn.Info("Proxies buffer is full, discarding.")
conn.Close()
}
}
@@ -244,37 +310,47 @@ func (c *Control) RegisterProxy(conn conn.Conn) {
// 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) {
// initial timeout is zero to try to get a proxy connection without asking for one
timeout := time.NewTimer(0)
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
}
// get a proxy connection. if we timeout, request one over the control channel
for proxyConn == nil {
var ok bool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
continue
case <-timeout.C:
c.conn.Debug("Requesting new proxy connection")
// request a proxy connection
c.out <- &msg.ReqProxyMsg{}
// timeout after 1 second if we don't get one
timeout.Reset(1 * time.Second)
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
// To try to reduce latency hanndling tunnel connections, we employ
// the following curde heuristic:
// If the proxy connection pool is empty, request a new one.
// The idea is to always have at least one proxy connection available for immediate use.
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
// It should be a good start though.
if len(c.proxies) == 0 {
c.out <- &msg.ReqProxyMsg{}
}
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()
}

View File

@@ -7,6 +7,7 @@ import (
"ngrok/conn"
"ngrok/log"
"strings"
"time"
)
const (
@@ -31,10 +32,10 @@ Bad Request
)
// Listens for new http(s) connections from the public internet
func httpListener(addr *net.TCPAddr, tlsCfg *tls.Config) {
func startHttpListener(addr string, tlsCfg *tls.Config) (listener *conn.Listener) {
// bind/listen for incoming connections
listener, err := conn.Listen(addr, "pub", tlsCfg)
if err != nil {
var err error
if listener, err = conn.Listen(addr, "pub", tlsCfg); err != nil {
panic(err)
}
@@ -43,10 +44,14 @@ func httpListener(addr *net.TCPAddr, tlsCfg *tls.Config) {
proto = "https"
}
log.Info("Listening for public %s connections on %v", proto, listener.Port)
for conn := range listener.Conns {
go httpHandler(conn, proto)
}
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
@@ -62,6 +67,9 @@ func httpHandler(tcpConn net.Conn, proto string) {
}
}()
// 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 {
@@ -85,11 +93,15 @@ func httpHandler(tcpConn net.Conn, proto string) {
// 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)
}

View File

@@ -1,26 +1,33 @@
package server
import (
"crypto/tls"
"math/rand"
"net"
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"ngrok/util"
"os"
"runtime/debug"
"time"
)
const (
registryCacheSize uint64 = 1024 * 1024 // 1 MB
connReadTimeout time.Duration = 10 * time.Second
)
// GLOBALS
var (
opts *Options
tunnelRegistry *TunnelRegistry
controlRegistry *ControlRegistry
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 NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxyMsg) {
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
// fail gracefully if the proxy connection fails to register
defer func() {
if r := recover(); r != nil {
@@ -48,28 +55,46 @@ func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxyMsg) {
// 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 *net.TCPAddr, domain string) {
func tunnelListener(addr string, tlsConfig *tls.Config) {
// listen for incoming connections
listener, err := conn.Listen(addr, "tun", tlsConfig)
if err != nil {
panic(err)
}
log.Info("Listening for control and proxy connections on %d", listener.Port)
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
for c := range listener.Conns {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(c); err != nil {
c.Error("Failed to read message: %v", err)
c.Close()
}
go func(tunnelConn conn.Conn) {
// don't crash on panics
defer func() {
if r := recover(); r != nil {
tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack())
}
}()
switch m := rawMsg.(type) {
case *msg.RegMsg:
go NewControl(c, m)
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
}
case *msg.RegProxyMsg:
go NewProxy(c, m)
}
// don't timeout after the initial read, tunnel heartbeating will kill
// dead connections
tunnelConn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Auth:
NewControl(tunnelConn, m)
case *msg.RegProxy:
NewProxy(tunnelConn, m)
default:
tunnelConn.Close()
}
}(c)
}
}
@@ -92,20 +117,25 @@ func Main() {
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
controlRegistry = NewControlRegistry()
// ngrok clients
go tunnelListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
// start listeners
listeners = make(map[string]*conn.Listener)
// load tls configuration
tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
if err != nil {
panic(err)
}
// listen for http
if opts.httpPort != -1 {
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpPort}, nil)
if opts.httpAddr != "" {
listeners["http"] = startHttpListener(opts.httpAddr, nil)
}
// listen for https
if opts.httpsPort != -1 {
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpsPort}, tlsConfig)
if opts.httpsAddr != "" {
listeners["https"] = startHttpListener(opts.httpsAddr, tlsConfig)
}
// wait forever
done := make(chan int)
<-done
// ngrok clients
tunnelListener(opts.tunnelAddr, tlsConfig)
}

View File

@@ -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}
}

View File

@@ -92,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
}
@@ -180,16 +180,18 @@ func (r *ControlRegistry) Get(clientId string) *Control {
return r.controls[clientId]
}
func (r *ControlRegistry) Add(clientId string, ctl *Control) error {
func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) {
r.Lock()
defer r.Unlock()
if r.controls[clientId] == nil {
r.Info("Registered control with id %s", clientId)
r.controls[clientId] = ctl
return nil
} else {
return fmt.Errorf("Client with id %s already registered!", clientId)
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 {

View File

@@ -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
}

View File

@@ -8,7 +8,7 @@ import (
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"ngrok/util"
"os"
"strconv"
"strings"
@@ -27,7 +27,8 @@ var defaultPortMap = map[string]int{
* 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
@@ -68,17 +69,16 @@ func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
// Canonicalize by always using lower-case
vhost = strings.ToLower(vhost)
t.url = strings.ToLower(t.url)
// Register for specific hostname
hostname := strings.TrimSpace(t.regMsg.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.TrimSpace(t.regMsg.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)
@@ -94,74 +94,83 @@ func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
// Create a new tunnel from a registration message received
// on a control channel
func NewTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel, err error) {
func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
t = &Tunnel{
regMsg: m,
req: m,
start: time.Now(),
ctl: ctl,
Logger: log.NewPrefixLogger(),
}
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 := 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 {
err = 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 = 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)
if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil {
return
}
go t.listenTcp(t.listener)
case "http":
if err = registerVhost(t, "http", opts.httpPort); err != nil {
return
}
case "https":
if err = registerVhost(t, "https", opts.httpsPort); err != nil {
return
}
}
if m.Version != version.Proto {
err = 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
}
@@ -245,9 +254,8 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
metrics.OpenConnection(t, publicConn)
var proxyConn conn.Conn
var attempts int
var err error
for {
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)
@@ -258,24 +266,33 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
proxyConn.AddLogPrefix(t.Id())
// tell the client we're going to start using this proxy connection
startPxyMsg := &msg.StartProxyMsg{
startPxyMsg := &msg.StartProxy{
Url: t.url,
ClientAddr: publicConn.RemoteAddr().String(),
}
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
attempts += 1
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
if attempts > 3 {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
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)

33
src/ngrok/util/errors.go Normal file
View 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
}

View File

@@ -2,31 +2,32 @@ package util
import (
"crypto/rand"
"encoding/binary"
"fmt"
mrand "math/rand"
)
func RandomSeed() (int64, error) {
b := make([]byte, 8)
n, err := rand.Read(b)
if n != 8 {
return 0, fmt.Errorf("Only generated %d random bytes, %d requested", n, 8)
}
if err != nil {
return 0, err
}
var seed int64
var i uint
for i = 0; i < 8; i++ {
seed = seed | int64(b[i]<<(i*8))
}
return seed, nil
func RandomSeed() (seed int64, err error) {
err = binary.Read(rand.Reader, binary.LittleEndian, &seed)
return
}
// create a random identifier for this client
func RandId(idlen int) (id string, err error) {
// 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)
@@ -43,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)
}

View 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
}

View File

@@ -1,12 +0,0 @@
package util
import (
"fmt"
"runtime"
)
func MakePanicTrace(err interface{}) string {
stackBuf := make([]byte, 4096)
n := runtime.Stack(stackBuf, false)
return fmt.Sprintf("panic: %v\n\n%s", err, stackBuf[:n])
}

View File

@@ -7,7 +7,7 @@ import (
const (
Proto = "2"
Major = "1"
Minor = "0"
Minor = "6"
)
func MajorMinor() string {