171 Commits
0.13 ... 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
f17bc20337 bump version numbers for new code/protocol 2013-09-09 14:22:37 +03:00
Alan Shreve
7c1977c30f go fmt 2013-09-09 13:53:36 +03:00
Alan Shreve
e4ce075701 fix a small interface change after merge 2013-09-09 13:52:37 +03:00
Alan Shreve
a5d4279cca merge in master 2013-09-09 13:25:51 +03:00
Alan Shreve
b488fe6bd7 properly display client ip addresses without relying on X-Real-Ip to be set by a proxy 2013-09-09 12:50:45 +03:00
Alan Shreve
00ae5d77db Merge branch 'master' into travis 2013-09-06 22:59:15 +03:00
Alan Shreve
83af364b1d update changelog 2013-09-06 22:58:40 +03:00
Alan Shreve
83dc742d38 bump version 2013-09-06 22:53:25 +03:00
Alan Shreve
937f665e51 fix introspection and replay to properly include all necessary headers 2013-09-06 22:52:47 +03:00
Alan Shreve
a911f7fb65 Merge branch 'master' into travis 2013-09-04 18:43:36 +03:00
Alan Shreve
cf5bd551be bump version 2013-09-04 18:42:32 +03:00
Alan Shreve
1bf705e4ee add websocket tunnelling support 2013-09-04 18:41:32 +03:00
Alan Shreve
ea088b69f2 safely capture and display panics while rendering any pages in the web http view. safely print out stack traces from crashed goroutines without destroying the terminal 2013-08-29 21:05:55 +03:00
Alan Shreve
3655bd8027 make clean now cleans up all of the object files 2013-08-29 18:39:16 +03:00
Alan Shreve
2fff31ff61 major refactoring and cleanup to support multiple tunnels 2013-08-29 18:38:32 +03:00
Alan Shreve
a9f659216e Merge branch 'master' into travis 2013-08-17 21:54:29 +02:00
Alan Shreve
a29febde5c indicate in the terminal UI when the web UI is disabled 2013-08-17 21:51:47 +02:00
Alan Shreve
677ea6ab95 update changelog 2013-08-17 21:51:22 +02:00
Alan Shreve
23fd33b344 bump version 2013-08-17 21:26:15 +02:00
Alan Shreve
264ac352f7 Merge branch 'master' into travis 2013-08-17 21:25:50 +02:00
Alan Shreve
a2cf6b70c1 allow the web interface to be disabled 2013-08-17 21:22:17 +02:00
Alan Shreve
d0b9161993 Merge branch 'master' into travis 2013-08-17 12:19:41 +02:00
Alan Shreve
8d7081e747 bump version 2013-08-17 12:19:19 +02:00
Alan Shreve
fd394cb336 Merge branch 'master' into travis 2013-08-17 12:10:33 +02:00
Alan Shreve
9373eb8f4d fix a variable shadowing bug 2013-08-17 12:09:49 +02:00
Alan Shreve
8339a76cc1 don't continue after a download error 2013-08-17 11:33:44 +02:00
Alan Shreve
8f6637c784 Merge branch 'master' into travis 2013-08-17 11:17:21 +02:00
Alan Shreve
8054e64ad7 bump version 2013-08-17 11:17:09 +02:00
Alan Shreve
eb60b72aa4 Merge branch 'master' into travis 2013-08-17 11:16:33 +02:00
Alan Shreve
4d08291813 fix a bug which causes updates to be tried without delay after the first update interval 2013-08-17 11:15:52 +02:00
Alan Shreve
ea1b1ed632 incremental progress on supporting multiple active tunnels in the ngrok client 2013-08-17 11:09:43 +02:00
Alan Shreve
bfd7e64d9f fix race condition in logging initialization. go fmt 2013-08-16 21:11:44 +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
aa99cc1cd4 send user id in update request 2013-08-15 13:09:23 +02:00
Alan Shreve
d914e26055 Merge branch 'master' into travis 2013-08-15 12:42:28 +02:00
Alan Shreve
01167cb136 bump version 2013-08-15 12:42:18 +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
b157045a37 modify auto-updating to use the new API and check first so the server can signal manual updates as necessary 2013-08-15 12:14:14 +02:00
Alan Shreve
bf7a36f03e virtual host names are now case-insensitive 2013-08-13 15:11:44 +02:00
Alan Shreve
c5b7e331da Fix a bug which crashed the client when replaying requests 2013-08-13 12:46:46 +02:00
Alan Shreve
e6141ac6ac Merge branch 'master' of github.com:inconshreveable/ngrok 2013-08-04 18:33:08 +02:00
inconshreveable
0754d30550 Merge pull request #17 from jzs/jzs
Fixed copy paste error
2013-08-04 08:09:59 -07:00
Alan Shreve
3d17d52659 Major ngrokd server refactor improving:
No longer fail to handle a public connection if a proxy request goes unanswered by requesting additional proxy connections
Buffer tunnel proxies channel so we allow a pool of available connections to be used
Don't tie up goroutines if proxy connection pool are full
Allow a single control connection to manage multiple tunnels
Fix a security issue where anyone could could register a proxy for any tunnel
Natively handle serving HTTPS traffic
Multiplex a single port for handling control and proxy connections in order to evade corporate firewalls
Tunnels now shut down all of their resources gracefully
2013-08-03 22:00:06 +02:00
jzs
51e8ca782b Fixed copy paste error 2013-08-03 20:47:17 +02:00
Alan Shreve
999f1063d0 update changelog 2013-08-03 17:57:32 +02:00
Alan Shreve
61dd957018 correctly handle virtual hosting on non default ports and report the correct public URL back to the client without modifying the protocol. add the VHOST environment variable for more flexibility around how virtual hosting is implemented 2013-08-03 17:37:28 +02:00
Alan Shreve
f88378a8da Merge branch 'master' of https://github.com/skastel/ngrok into publicport 2013-08-01 21:26:33 +02:00
Stephen Huenneke
0115a63898 Adding Stephen Huenneke to contributors. 2013-07-30 10:29:51 -07:00
Stephen Huenneke
53e28ef0c1 Fixing formatting so that we're faithfully passing on port information. 2013-07-30 10:27:29 -07: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
77e3f140ca separate cache keys for each protocol 2013-07-30 16:06:35 +02:00
Alan Shreve
eef7b6b755 merge in branch master 2013-07-30 15:27:15 +02:00
Alan Shreve
fb0c343161 bump version 2013-07-30 15:25:45 +02:00
Alan Shreve
7032606103 Fixed an issues where the affinity cache could not be serialized/deserialized or saved/loaded from disk. Added better logging around affinity cache load/save. TCP tunnel urls are now handled in the affinity cache and will attempt to return to you the same port you had previously. 2013-07-30 15:24:08 +02:00
Stephen Huenneke
aea0524bb5 Fixing over-rides of public port so that they don't result in mis-matched host names due to Request.Host including the port in the returned hostname. 2013-07-29 16:00:24 -07:00
Alan Shreve
078bbb1fdc fix makefile to get bindata dependency properly 2013-07-28 01:01:18 +02:00
Alan Shreve
fbcde8774b add basic travis.yml 2013-07-28 00:51:50 +02:00
Alan Shreve
816f5e8882 Merge branch 'master' into travis 2013-07-28 00:44:43 +02:00
Alan Shreve
bd3356071e Merge branch 'autoupdate' 2013-07-28 00:14:04 +02:00
Alan Shreve
b63f9bc718 Merge branch 'master' of github.com:inconshreveable/ngrok 2013-07-28 00:13:51 +02:00
Alan Shreve
aa4543b824 use new version of go-update. report update error statistics. bump version 2013-07-27 23:58:22 +02:00
Alan Shreve
896afc0141 add contributors file 2013-07-26 14:00:31 +02:00
inconshreveable
4f4bd7bd06 Merge pull request #10 from cespare/7-xml-syntax-crash
XML and JSON crash
2013-07-26 04:58:17 -07:00
inconshreveable
7fb02bb768 Merge pull request #9 from cespare/8-response-summary-fix
Response summary fix
2013-07-26 04:57:05 -07:00
Alan Shreve
6a2b9c31b1 fix a bug where ngrokd would reject http requests with an authorization header if the client had not specified any authentication necessary 2013-07-26 13:32:47 +02:00
Caleb Spare
07c2fdc693 Handle 'application/xml' the same as 'text/xml'
(Go does not fallthrough switch statements.)
2013-07-24 01:54:31 -07:00
Caleb Spare
b275aa7123 Fix some unsafe type assertions.
Fixes issue #7.
2013-07-24 01:53:35 -07:00
Caleb Spare
10a80333da Do the code highlighting where the bodies are processed.
This fixes an issue where highlight.js was removing a bound element
out from underneath angular, which breaks the updates of the body
elements for requests and responses.

Fixes issue #8.
2013-07-24 01:22:59 -07:00
Caleb Spare
09953ac19a Add the angular-sanitize plugin. 2013-07-24 01:16:09 -07:00
Alan Shreve
081a1f7fde fix update to use ssl, specify the right parameters 2013-07-23 16:04:11 +01:00
Alan Shreve
26038dbf1b set correct update endpoint 2013-07-23 15:41:54 +01: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
452ba7a575 fix download completion ui handling, add some better logging 2013-07-18 18:11:18 +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
Alan Shreve
35d8fc20ff initial implementation of auto-updating in ngrok 2013-07-16 10:37:22 +01:00
Alan Shreve
492182497a better comments for auth 2013-07-10 14:34:09 -07:00
Alan Shreve
0d49d7223d bump version 2013-07-03 00:18:11 -07:00
Alan Shreve
b136b8f1aa fix an issue where logging during authtoken reading was never reported. fix an issue where ngrok could not find the user's home directory on linux when cross-compiled from darwin 2013-07-03 00:16:16 -07:00
55 changed files with 2957 additions and 1270 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

22
.travis.yml Normal file
View File

@@ -0,0 +1,22 @@
script: make release-all
install: true
after_success:
- sudo pip install boto
- ls -lr bin/*
- NGROK="bin/${GOOS}_${GOARCH}/ngrok" NGROKD="bin/${GOOS}_${GOARCH}/ngrokd" VERSION=$(GOOS="" GOARCH="" GOPATH=$(pwd) go run build/version.go) python build/travis.py
before_script:
- pushd /home/travis/.gvm/gos/go1.1.1/src
- ./make.bash
- popd
env:
global:
- secure: Fd5zHi58jx8lsPDv4tkRFzXSY0KnPJZuZ+LvnRcpX4+3xJsuZU6moOfrOcGqDOm7/SqZRVZRKZapE772+8sXNKPmwSXHRsZEsUgqxdehFzlVP4PQN5efOdI/quO2ibwVpZ6Idze5pelZburALd7/VbfHCTB/0P0WDMNvfHuFPQg=
- secure: Yg+y996B4S7zHXO8j6JrRbgMf6yilHGWv6I+7oZf02d8IHYtAb6A9DveX/q+v24O8Q9WzXRU4ZIaG5nJksVwb19qcy4vpaUbvx00COi8tg0l9hdKIotrzAs+i8q5h0xUifE1iuqYJw39vYU/9vC/727GTvOfRpBCr+edcpc9uy0=
- secure: CMF8ZfcoZK8AhFrmo6ljwP4ulYOSI9Pftp02rfNcM7LYPzU5HxFmcR7M4+3Qt0HAWxYrReozNQZYrV0s5jqIO0H4LZeHxgi7EHTam1BelhmHxL6VBMoR1mTi53bErWNhpoc0qyCnWMpnzNMObwtBVm9vjacm4NJMWqQjte/0Kdw=
matrix:
- GOOS=linux GOARCH=arm
- GOOS=linux GOARCH=386
- GOOS=windows GOARCH=386
- GOOS=darwin GOARCH=amd64
go:
- 1.1

6
CONTRIBUTORS Normal file
View File

@@ -0,0 +1,6 @@
Contributors to ngrok, both large and small:
Alan Shreve (inconshreveable)
Kyle Conroy (kyleconroy)
Caleb Spare (cespare)
Stephen Huenneke (skastel)
Nick Presta (nickpresta)

View File

@@ -1,5 +1,4 @@
.PHONY: default server client deps fmt clean all release-client release-server release-all client-assets server-assets
BUILDTAGS=
export GOPATH:=$(shell pwd)
default: all
@@ -18,10 +17,12 @@ client: deps
client-assets:
go get github.com/inconshreveable/go-bindata
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
bin/go-bindata -o src/ngrok/client/assets assets/client
server-assets:
go get github.com/inconshreveable/go-bindata
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
bin/go-bindata -o src/ngrok/server/assets assets/server
release-client: BUILDTAGS=release
@@ -35,4 +36,4 @@ release-all: release-client release-server
all: fmt client server
clean:
go clean ngrok/...
go clean -i -r ngrok/...

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

@@ -8,6 +8,7 @@
<script src="/static/js/jquery-1.9.1.min.js"></script>
<script src="/static/js/jquery.timeago.js"></script>
<script src="/static/js/angular.js"></script>
<script src="/static/js/angular-sanitize.min.js"></script>
<script src="/static/js/base64.js"></script>
<script src="/static/js/ngrok.js"></script>
<script type="text/javascript">
@@ -18,11 +19,18 @@
table.params { font-size: 12px; font-family: Courier, monospace; }
.txn-selector tr { cursor: pointer; }
.txn-selector tr:hover { background-color: #ddd; }
tr.selected, tr.selected:hover {
background-color: #ff9999;
tr.selected, tr.selected:hover {
background-color: #ff9999;
background-color: #000000;
color:white;
}
.path {
width: 300px;
}
.wrapped {
word-wrap: break-word;
word-break: break-word;
overflow: hidden;
}
</style>
</head>
@@ -47,7 +55,12 @@
<div class="span6 offset3">
<div class="well" style="padding: 20px 50px;">
<h4>No requests to display yet</h4>
<p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
<hr />
<h5>To get started, make a request to one of your tunnel URLs:</h5>
<ul>
<li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
</ul>
</p>
</div>
</div>
</div>
@@ -56,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>
@@ -70,17 +83,17 @@
</span>
</div>
<div class="span4">
<i class="icon-time"></i> Duration
<i class="icon-time"></i> Duration
<span style="margin-left: 8px;" class="muted">{{Txn.Duration}}</span>
</div>
<div class="span4">
<i class="icon-user"></i> IP
<span style="margin-left: 8px;" class="muted">{{Txn.Req.Header['X-Real-Ip'][0]}}</span>
<i class="icon-user"></i> IP
<span style="margin-left: 8px;" class="muted">{{Txn.ConnCtx.ClientAddr.split(":")[0]}}</span>
</div>
</div>
<hr />
<div ng-show="!!Req" ng-controller="HttpRequest">
<h3>{{ Req.MethodPath }}</h3>
<h3 class="wrapped">{{ Req.MethodPath }}</h3>
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
</div>

View File

@@ -0,0 +1,13 @@
/*
AngularJS v1.1.5
(c) 2010-2012 Google, Inc. http://angularjs.org
License: MIT
*/
(function(I,h){'use strict';function i(a){var d={},a=a.split(","),c;for(c=0;c<a.length;c++)d[a[c]]=!0;return d}function z(a,d){function c(a,b,c,f){b=h.lowercase(b);if(m[b])for(;e.last()&&n[e.last()];)g("",e.last());o[b]&&e.last()==b&&g("",b);(f=p[b]||!!f)||e.push(b);var j={};c.replace(A,function(a,b,d,c,g){j[b]=k(d||c||g||"")});d.start&&d.start(b,j,f)}function g(a,b){var c=0,g;if(b=h.lowercase(b))for(c=e.length-1;c>=0;c--)if(e[c]==b)break;if(c>=0){for(g=e.length-1;g>=c;g--)d.end&&d.end(e[g]);e.length=
c}}var b,f,e=[],j=a;for(e.last=function(){return e[e.length-1]};a;){f=!0;if(!e.last()||!q[e.last()]){if(a.indexOf("<\!--")===0)b=a.indexOf("--\>"),b>=0&&(d.comment&&d.comment(a.substring(4,b)),a=a.substring(b+3),f=!1);else if(B.test(a)){if(b=a.match(r))a=a.substring(b[0].length),b[0].replace(r,g),f=!1}else if(C.test(a)&&(b=a.match(s)))a=a.substring(b[0].length),b[0].replace(s,c),f=!1;f&&(b=a.indexOf("<"),f=b<0?a:a.substring(0,b),a=b<0?"":a.substring(b),d.chars&&d.chars(k(f)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+
e.last()+"[^>]*>","i"),function(a,b){b=b.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(b));return""}),g("",e.last());if(a==j)throw"Parse Error: "+a;j=a}g()}function k(a){l.innerHTML=a.replace(/</g,"&lt;");return l.innerText||l.textContent||""}function t(a){return a.replace(/&/g,"&amp;").replace(F,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function u(a){var d=!1,c=h.bind(a,a.push);return{start:function(a,b,f){a=h.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]==
!0&&(c("<"),c(a),h.forEach(b,function(a,b){var d=h.lowercase(b);if(G[d]==!0&&(w[d]!==!0||a.match(H)))c(" "),c(b),c('="'),c(t(a)),c('"')}),c(f?"/>":">"))},end:function(a){a=h.lowercase(a);!d&&v[a]==!0&&(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||c(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^</,B=/^<\s*\//,D=/<\!--(.*?)--\>/g,
E=/<!\[CDATA\[(.*?)]]\>/g,H=/^((ftp|https?):\/\/|mailto:|tel:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=h.extend({},y,x),m=h.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=h.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
q=i("script,style"),v=h.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=h.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");h.module("ngSanitize",[]).value("$sanitize",function(a){var d=[];
z(a,u(d));return d.join("")});h.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,c,g){c.addClass("ng-binding").data("$binding",g.ngBindHtml);d.$watch(g.ngBindHtml,function(b){b=a(b);c.html(b||"")})}}]);h.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(c,g){if(!c)return c;var b,f=c,e=[],j=u(e),i,k,l={};if(h.isDefined(g))l.target=g;for(;b=f.match(a);)i=
b[0],b[2]==b[3]&&(i="mailto:"+i),k=b.index,j.chars(f.substr(0,k)),l.href=i,j.start("a",l),j.chars(b[0].replace(d,"")),j.end("a"),f=f.substring(k+b[0].length);j.chars(f);return e.join("")}})})(window,window.angular);

View File

@@ -1,4 +1,4 @@
var ngrok = angular.module("ngrok", []);
var ngrok = angular.module("ngrok", ["ngSanitize"]);
var hexRepr = function(bytes) {
var buf = [];
@@ -47,7 +47,7 @@ ngrok.factory("txnSvc", function() {
body.exists = body.Length > 0;
body.hasError = !!body.Error;
body.syntaxClass = {
var syntaxClass = {
"text/xml": "xml",
"application/xml": "xml",
"text/html": "xml",
@@ -63,12 +63,12 @@ ngrok.factory("txnSvc", function() {
} else {
body.Text = Base64.decode(body.Text).text;
}
// prettify
var transform = {
"xml": "xml",
"json": "json"
}[body.syntaxClass];
}[syntaxClass];
if (!body.hasError && !!transform) {
try {
@@ -79,6 +79,13 @@ ngrok.factory("txnSvc", function() {
} catch (e) {
}
}
if (!!syntaxClass) {
body.Text = hljs.highlight(syntaxClass, body.Text).value;
} else {
// highlight.js doesn't have a 'plaintext' syntax, so we'll just copy its escaping function.
body.Text = body.Text.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}
};
var processReq = function(req) {
@@ -214,7 +221,7 @@ ngrok.directive({
"onbtnclick": "&"
},
replace: true,
template: '' +
template: '' +
'<ul class="nav nav-pills">' +
'<li ng-repeat="tab in tabNames" ng-class="{\'active\': isTab(tab)}">' +
'<a href="" ng-click="setTab(tab)">{{tab}}</a>' +
@@ -247,7 +254,7 @@ ngrok.directive({
'</h6>' +
'' +
'<div ng-show="!body.isForm && !body.binary">' +
'<pre ng-show="body.exists"><code ng-class="body.syntaxClass">{{ body.Text }}</code></pre>' +
'<pre ng-show="body.exists"><code ng-bind-html="body.Text"></code></pre>' +
'</div>' +
'' +
'<div ng-show="body.isForm">' +
@@ -259,13 +266,6 @@ ngrok.directive({
link: function($scope, $elem) {
$scope.$watch(function() { return $scope.body; }, function() {
$code = $elem.find("code");
// if we highlight when the code is empty, hljs manipulates the dom in a
// a bad way that causes angular to fail
if (!!$code.text()) {
hljs.highlightBlock($code.get(0));
}
if ($scope.body && $scope.body.ErrorOffset > -1) {
var offset = $scope.body.ErrorOffset;
@@ -303,7 +303,7 @@ ngrok.directive({
ngrok.controller({
"HttpTxns": function($scope, txnSvc) {
$scope.publicUrl = window.data.UiState.Url;
$scope.tunnels = window.data.UiState.Tunnels;
$scope.txns = txnSvc.all();
if (!!window.WebSocket) {
@@ -317,7 +317,7 @@ ngrok.controller({
txnSvc.add(message.data);
});
};
ws.onerror = function(err) {
console.log("Web socket error:" + err);
};

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,13 +1,77 @@
# Changelog
## 1.6 - 10/25/2013
- BUGFIX: Fixed a goroutine/memory leak in ngrok/proto's parsing of http traffic
- IMPROVEMENT: The web inspection API can now be disabled again by setting inspect_addr: disabled in the config file
## 1.5 - 10/20/2013
- FEATURE: Added support a "remote_port" configuration parameter that lets you request a specific remote port for TCP tunnels
- IMPROVEMENT: Upload instructions on crash reports are displayed after the dump where it is more likely to be seen
- IMPROVEMENT: Improvements to ngrok's logging for easier debugging
- IMPROVEMENT: Batch metric reporting to Keen to not be limited by the speed of their API at high request loads
- IMPROVEMENT: Added additional safety to ensure the server doesn't crash on panics()
- BUGFIX: Fixed an issue with prefetching tunnel connections that could hang tunnel connections when behind an aggresive NAT
- BUGFIX: Fixed a race condition where ngrokd could send back a different message instead of AuthResp first
- BUGFIX: Fixed an issue where under some circumstances, reconnecting would fail and tell the client the tunnels were still in use
- BUGFIX: Fixed an issue where a race-condition with handling pings could cause a tunnel to hang forever and stop handling requests
## 1.4 - 09/27/2013
- BUGFIX: Fixed an issue where long URL paths were not truncated in the terminal UI
- BUGFIX: Fixed an issue where long URL paths ruined the web UI's formatting
- BUGFIX: Fixed an issue where authtokens would not be remembered if an existing configuration file didn't exist
## 0.23 - 09/06/2013
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay
## 0.22 - 09/04/2013
- FEATURE: ngrok now tunnels websocket requests
## 0.21 - 08/17/2013
- IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1
## 0.20 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop
## 0.19 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check
- BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up
## 0.18 - 08/15/2013
- BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons
- BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80
- BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request
- IMPROVEMENT: ngrok can now indicate manual updates again
- IMPROVEMENT: ngrok can now supports update channels
- IMPROVEMENT: ngrok can now detect some updates that will fail before downloading
## 0.17 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol
## 0.16 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error
- BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests
- BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file
- BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file
- BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified.
- BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first
- IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs
- IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles
## 0.15 - 07/27/2013
- FEATURE: ngrok can now update itself automatically
## 0.14 - 07/03/2013
- BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux
- BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens
## 0.13 - 07/02/2013
- FEATURE -hostname switch on client allows you to run tunnels over custom domains (requires you CNAME your DNS)
- IMPROVEMENT ngrok client UI now shows the client IP address for a request
- IMPROVEMENT ngrok client UI now shows how long ago a request was made (uservoice request 4127487)
- IMPROVEMENT ngrokd now uses and LRU cache for tunnel affinity data
- IMPROVEMENT ngrokd can now save and restore its tunnel affinity cache to a file to preserve across restarts
- FEATURE: -hostname switch on client allows you to run tunnels over custom domains (requires you CNAME your DNS)
- IMPROVEMENT: ngrok client UI now shows the client IP address for a request
- IMPROVEMENT: ngrok client UI now shows how long ago a request was made (uservoice request 4127487)
- IMPROVEMENT: ngrokd now uses and LRU cache for tunnel affinity data
- IMPROVEMENT: ngrokd can now save and restore its tunnel affinity cache to a file to preserve across restarts
## 0.12 - 06/30/2013
- IMPROVEMENT Improved developer documentation
- IMPROVEMENT Simplified build process with custom version of go-bindata that compiles assets into binary releases
- BUGFIX Github issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes.
- IMPROVEMENT: Improved developer documentation
- IMPROVEMENT: Simplified build process with custom version of go-bindata that compiles assets into binary releases
- BUGFIX: Github issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes.

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
@@ -93,7 +110,7 @@ There is a stub at _src/ngrok/main/ngrokd/ngrokd.go_ for the purposes of creatin
## ngrok - the client
### Code
Code for the server lives under src/ngrok/client
Code for the client lives under src/ngrok/client
### Entry point
The ngrok entry point is in _src/ngrok/client/main.go_.

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

@@ -167,17 +167,12 @@ func (lru *LRUCache) Items() []Item {
func (lru *LRUCache) SaveItems(w io.Writer) error {
items := lru.Items()
for _, v := range items {
gob.Register(v)
}
encoder := gob.NewEncoder(w)
return encoder.Encode(items)
}
func (lru *LRUCache) SaveItemsToFile(path string) error {
if wr, err := os.Open(path); err != nil {
if wr, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
return err
} else {
defer wr.Close()
@@ -188,7 +183,7 @@ func (lru *LRUCache) SaveItemsToFile(path string) error {
func (lru *LRUCache) LoadItems(r io.Reader) error {
items := make([]Item, 0)
decoder := gob.NewDecoder(r)
if err := decoder.Decode(items); err != nil {
if err := decoder.Decode(&items); err != nil {
return err
}

View File

@@ -1,49 +0,0 @@
package client
import (
"io/ioutil"
"ngrok/log"
"os"
"os/user"
"path"
)
/*
Functions for reading and writing the auth token from the user's
home directory.
*/
var (
currentAuthToken string
authTokenFile string
)
func init() {
user, err := user.Current()
if err != nil {
log.Warn("Failed to get user's home directory: %s", err.Error())
return
}
authTokenFile = path.Join(user.HomeDir, ".ngrok")
tokenBytes, err := ioutil.ReadFile(authTokenFile)
if err == nil {
currentAuthToken = string(tokenBytes)
}
}
func SaveAuthToken(token string) {
if token == "" || token == currentAuthToken || authTokenFile == "" {
return
}
perms := os.FileMode(0644)
err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
if err != nil {
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
}
}
func LoadAuthToken() string {
return currentAuthToken
}

View File

@@ -1,116 +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 parseAuthToken(token string) string {
if token != "" {
return token
} else {
return LoadAuthToken()
}
}
func parseArgs() *Options {
authtoken := flag.String(
"authtoken",
"",
"Authentication token for identifying a premium ngrok.com account")
"Authentication token for identifying an ngrok.com account")
server := flag.String(
"server",
"ngrok.com:4443",
"The remote ngrok server")
httpAuth := flag.String(
httpauth := flag.String(
"httpauth",
"",
"username:password HTTP basic auth creds protecting the public tunnel endpoint")
@@ -118,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",
@@ -127,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")
logto := flag.String(
"log",
"none",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
v := flag.Bool(
"version",
false,
"Print ngrok version and exit")
"http+https",
"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
flag.Parse()
if *v {
fmt.Println(version.MajorMinor())
os.Exit(0)
opts = &Options{
config: *config,
logto: *logto,
httpauth: *httpauth,
subdomain: *subdomain,
protocol: *protocol,
authtoken: *authtoken,
hostname: *hostname,
command: flag.Arg(0),
}
return &Options{
server: *server,
httpAuth: *httpAuth,
subdomain: *subdomain,
localaddr: parseLocalAddr(),
protocol: parseProtocol(*protocol),
webport: *webport,
logto: *logto,
authtoken: parseAuthToken(*authtoken),
hostname: *hostname,
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

@@ -0,0 +1,201 @@
package client
import (
"fmt"
"ngrok/client/mvc"
"ngrok/client/views/term"
"ngrok/client/views/web"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
"sync"
)
type command interface{}
type cmdQuit struct {
// display this message after quit
message string
}
type cmdPlayRequest struct {
// the tunnel to play this request over
tunnel mvc.Tunnel
// the bytes of the request to issue
payload []byte
}
// The MVC Controller
type Controller struct {
// Controller logger
log.Logger
// the model sends updates through this broadcast channel
updates *util.Broadcast
// the model
model mvc.Model
// the views
views []mvc.View
// interal structure to issue commands to the controller
cmds chan command
// internal structure to synchronize access to State object
state chan mvc.State
// options
config *Configuration
}
// public interface
func NewController() *Controller {
ctl := &Controller{
Logger: log.NewPrefixLogger("controller"),
updates: util.NewBroadcast(),
cmds: make(chan command),
views: make([]mvc.View, 0),
state: make(chan mvc.State),
}
return ctl
}
func (ctl *Controller) State() mvc.State {
return <-ctl.state
}
func (ctl *Controller) Update(state mvc.State) {
ctl.updates.In() <- state
}
func (ctl *Controller) Updates() *util.Broadcast {
return ctl.updates
}
func (ctl *Controller) Shutdown(message string) {
ctl.cmds <- cmdQuit{message: message}
}
func (ctl *Controller) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
ctl.cmds <- cmdPlayRequest{tunnel: tunnel, payload: payload}
}
func (ctl *Controller) Go(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
ctl.Error(err)
ctl.Shutdown(err)
}
}()
fn()
}()
}
// private functions
func (ctl *Controller) doShutdown() {
ctl.Info("Shutting down")
var wg sync.WaitGroup
// wait for all of the views, plus the model
wg.Add(len(ctl.views) + 1)
for _, v := range ctl.views {
vClosure := v
ctl.Go(func() {
vClosure.Shutdown()
wg.Done()
})
}
ctl.Go(func() {
ctl.model.Shutdown()
wg.Done()
})
wg.Wait()
}
func (ctl *Controller) addView(v mvc.View) {
ctl.views = append(ctl.views, v)
}
func (ctl *Controller) GetWebInspectAddr() string {
return ctl.config.InspectAddr
}
func (ctl *Controller) Run(config *Configuration) {
// Save the configuration
ctl.config = config
// init the model
model := newClientModel(config, ctl)
ctl.model = model
var state mvc.State = model
// init web ui
var webView *web.WebView
if config.InspectAddr != "disabled" {
webView = web.NewWebView(ctl, config.InspectAddr)
ctl.addView(webView)
}
// init term ui
var termView *term.TermView
if config.LogTo != "stdout" {
termView = term.NewTermView(ctl)
ctl.addView(termView)
}
for _, protocol := range model.GetProtocols() {
switch p := protocol.(type) {
case *proto.Http:
if termView != nil {
ctl.addView(termView.NewHttpView(p))
}
if webView != nil {
ctl.addView(webView.NewHttpView(p))
}
default:
}
}
ctl.Go(func() { autoUpdate(state, config.AuthToken) })
ctl.Go(ctl.model.Run)
updates := ctl.updates.Reg()
defer ctl.updates.UnReg(updates)
done := make(chan int)
for {
select {
case obj := <-ctl.cmds:
switch cmd := obj.(type) {
case cmdQuit:
msg := cmd.message
go func() {
ctl.doShutdown()
fmt.Println(msg)
done <- 1
}()
case cmdPlayRequest:
ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) })
}
case obj := <-updates:
state = obj.(mvc.State)
case ctl.state <- state:
case <-done:
return
}
}
}

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,330 +1,38 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"ngrok/client/ui"
"ngrok/client/views/term"
"ngrok/client/views/web"
"ngrok/conn"
"math/rand"
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"ngrok/version"
"runtime"
"sync/atomic"
"time"
"os"
)
const (
pingInterval = 20 * time.Second
maxPongLatency = 15 * time.Second
versionCheckInterval = 6 * time.Hour
versionEndpoint = "http://dl.ngrok.com/versions"
BadGateway = `<html>
<body style="background-color: #97a8b9">
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
<h2>Tunnel %s unavailable</h2>
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
`
)
/**
* Establishes and manages a tunnel proxy connection with the server
*/
func proxy(proxyAddr string, s *State, ctl *ui.Controller) {
start := time.Now()
remoteConn, err := conn.Dial(proxyAddr, "pxy", tlsConfig)
if err != nil {
// XXX: What is the proper response here?
// display something to the user?
// retry?
// reset control connection?
log.Error("Failed to establish proxy connection: %v", err)
return
}
defer remoteConn.Close()
err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{Url: s.publicUrl})
if err != nil {
// XXX: What is the proper response here?
// display something to the user?
// retry?
// reset control connection?
log.Error("Failed to write RegProxyMsg: %v", err)
return
}
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
if err != nil {
remoteConn.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
badGatewayBody := fmt.Sprintf(BadGateway, s.publicUrl, s.opts.localaddr, s.opts.localaddr)
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
Content-Type: text/html
Content-Length: %d
%s`, len(badGatewayBody), badGatewayBody)))
return
}
defer localConn.Close()
m := s.metrics
m.proxySetupTimer.Update(time.Since(start))
m.connMeter.Mark(1)
ctl.Update(s)
m.connTimer.Time(func() {
localConn := s.protocol.WrapConn(localConn)
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
m.bytesIn.Update(bytesIn)
m.bytesOut.Update(bytesOut)
m.bytesInCount.Inc(bytesIn)
m.bytesOutCount.Inc(bytesOut)
})
ctl.Update(s)
}
func versionCheck(s *State, ctl *ui.Controller) {
check := func() {
resp, err := http.Get(versionEndpoint)
if err != nil {
log.Warn("Failed to get version info %s: %v", versionEndpoint, err)
return
}
defer resp.Body.Close()
var payload struct {
Client struct {
Version string
}
}
err = json.NewDecoder(resp.Body).Decode(&payload)
if err != nil {
log.Warn("Failed to read version info: %v", err)
return
}
if payload.Client.Version != version.MajorMinor() {
s.newVersion = payload.Client.Version
ctl.Update(s)
}
}
// check immediately and then at a set interval
check()
for _ = range time.Tick(versionCheckInterval) {
check()
}
}
/*
* Hearbeating to ensure our connection ngrokd is still live
*/
func heartbeat(lastPongAddr *int64, c conn.Conn) {
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
ping := time.NewTicker(pingInterval)
pongCheck := time.NewTicker(time.Second)
defer func() {
c.Close()
ping.Stop()
pongCheck.Stop()
}()
for {
select {
case <-pongCheck.C:
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
needPong := lastPong.Sub(lastPing) < 0
pongLatency := time.Since(lastPing)
if needPong && pongLatency > maxPongLatency {
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
return
}
case <-ping.C:
err := msg.WriteMsg(c, &msg.PingMsg{})
if err != nil {
c.Debug("Got error %v when writing PingMsg", err)
return
}
lastPing = time.Now()
}
}
}
func reconnectingControl(s *State, ctl *ui.Controller) {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
control(s, ctl)
if s.status == "online" {
wait = 1 * time.Second
}
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
time.Sleep(wait)
// exponentially increase wait time
wait = 2 * wait
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
s.status = "reconnecting"
ctl.Update(s)
}
}
/**
* Establishes and manages a tunnel control connection with the server
*/
func control(s *State, ctl *ui.Controller) {
defer func() {
if r := recover(); r != nil {
log.Error("control recovering from failure %v", r)
}
}()
// establish control channel
conn, err := conn.Dial(s.opts.server, "ctl", tlsConfig)
if err != nil {
panic(err)
}
defer conn.Close()
// register with the server
err = msg.WriteMsg(conn, &msg.RegMsg{
Protocol: s.opts.protocol,
OS: runtime.GOOS,
HttpAuth: s.opts.httpAuth,
Hostname: s.opts.hostname,
Subdomain: s.opts.subdomain,
ClientId: s.id,
Version: version.Proto,
MmVersion: version.MajorMinor(),
User: s.opts.authtoken,
})
if err != nil {
panic(err)
}
// wait for the server to ack our register
var regAck msg.RegAckMsg
if err = msg.ReadMsgInto(conn, &regAck); err != nil {
panic(err)
}
if regAck.Error != "" {
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
ctl.Cmds <- ui.CmdQuit{Message: emsg}
return
}
// update UI state
conn.Info("Tunnel established at %v", regAck.Url)
s.publicUrl = regAck.Url
s.status = "online"
s.serverVersion = regAck.MmVersion
ctl.Update(s)
SaveAuthToken(s.opts.authtoken)
// start the heartbeat
lastPong := time.Now().UnixNano()
go heartbeat(&lastPong, conn)
// main control loop
for {
var m msg.Message
if m, err = msg.ReadMsg(conn); err != nil {
panic(err)
}
switch m.(type) {
case *msg.ReqProxyMsg:
go proxy(regAck.ProxyAddr, s, ctl)
case *msg.PongMsg:
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
}
}
}
func Main() {
// parse options
opts := parseArgs()
opts, err := parseArgs()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// set up logging
log.LogTo(opts.logto)
// init client state
s := &State{
status: "connecting",
// unique client id
id: util.RandIdOrPanic(8),
// command-line options
opts: opts,
// metrics
metrics: NewClientMetrics(),
// read configuration file
config, err := LoadConfiguration(opts)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
switch opts.protocol {
case "http":
s.protocol = proto.NewHttp()
case "tcp":
s.protocol = proto.NewTcp()
// seed random number generator
seed, err := util.RandomSeed()
if err != nil {
fmt.Printf("Couldn't securely seed the random number generator!")
os.Exit(1)
}
rand.Seed(seed)
// init ui
ctl := ui.NewController()
web.NewWebView(ctl, s, opts.webport)
if opts.logto != "stdout" {
term.New(ctl, s)
}
go reconnectingControl(s, ctl)
go versionCheck(s, ctl)
quitMessage := ""
ctl.Wait.Add(1)
go func() {
defer ctl.Wait.Done()
for {
select {
case obj := <-ctl.Cmds:
switch cmd := obj.(type) {
case ui.CmdQuit:
quitMessage = cmd.Message
ctl.DoShutdown()
return
case ui.CmdRequest:
go func() {
var localConn conn.Conn
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
if err != nil {
log.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
return
}
//defer localConn.Close()
localConn = s.protocol.WrapConn(localConn)
localConn.Write(cmd.Payload)
ioutil.ReadAll(localConn)
}()
}
}
}
}()
ctl.Wait.Wait()
fmt.Println(quitMessage)
NewController().Run(config)
}

428
src/ngrok/client/model.go Normal file
View File

@@ -0,0 +1,428 @@
package client
import (
"crypto/tls"
"fmt"
metrics "github.com/inconshreveable/go-metrics"
"io/ioutil"
"math"
"ngrok/client/mvc"
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"ngrok/version"
"runtime"
"strings"
"sync/atomic"
"time"
)
const (
defaultServerAddr = "ngrokd.ngrok.com:443"
pingInterval = 20 * time.Second
maxPongLatency = 15 * time.Second
updateCheckInterval = 6 * time.Hour
BadGateway = `<html>
<body style="background-color: #97a8b9">
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
<h2>Tunnel %s unavailable</h2>
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
`
)
type ClientModel struct {
log.Logger
id string
tunnels map[string]mvc.Tunnel
serverVersion string
metrics *ClientMetrics
updateStatus mvc.UpdateStatus
connStatus mvc.ConnStatus
protoMap map[string]proto.Protocol
protocols []proto.Protocol
ctl mvc.Controller
serverAddr string
proxyUrl string
authToken string
tlsConfig *tls.Config
tunnelConfig map[string]*TunnelConfiguration
configPath string
}
func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel {
protoMap := make(map[string]proto.Protocol)
protoMap["http"] = proto.NewHttp()
protoMap["https"] = protoMap["http"]
protoMap["tcp"] = proto.NewTcp()
protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
// configure TLS
var tlsConfig *tls.Config
if config.TrustHostRootCerts {
tlsConfig = &tls.Config{}
} else {
var err error
if tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
panic(err)
}
}
return &ClientModel{
Logger: log.NewPrefixLogger("client"),
// server address
serverAddr: config.ServerAddr,
// proxy address
proxyUrl: config.HttpProxy,
// auth token
authToken: config.AuthToken,
// connection status
connStatus: mvc.ConnConnecting,
// update status
updateStatus: mvc.UpdateNone,
// metrics
metrics: NewClientMetrics(),
// protocols
protoMap: protoMap,
// protocol list
protocols: protocols,
// open tunnels
tunnels: make(map[string]mvc.Tunnel),
// controller
ctl: ctl,
// tls configuration
tlsConfig: tlsConfig,
// tunnel configuration
tunnelConfig: config.Tunnels,
// config path
configPath: config.Path,
}
}
// mvc.State interface
func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols }
func (c ClientModel) GetClientVersion() string { return version.MajorMinor() }
func (c ClientModel) GetServerVersion() string { return c.serverVersion }
func (c ClientModel) GetTunnels() []mvc.Tunnel {
tunnels := make([]mvc.Tunnel, 0)
for _, t := range c.tunnels {
tunnels = append(tunnels, t)
}
return tunnels
}
func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus }
func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus }
func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
return c.metrics.connMeter, c.metrics.connTimer
}
func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
return c.metrics.bytesInCount, c.metrics.bytesIn
}
func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
return c.metrics.bytesOutCount, c.metrics.bytesOut
}
func (c ClientModel) SetUpdateStatus(updateStatus mvc.UpdateStatus) {
c.updateStatus = updateStatus
c.update()
}
// mvc.Model interface
func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
var localConn conn.Conn
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
if err != nil {
c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err)
return
}
defer localConn.Close()
localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"})
localConn.Write(payload)
ioutil.ReadAll(localConn)
}
func (c *ClientModel) Shutdown() {
}
func (c *ClientModel) update() {
c.ctl.Update(c)
}
func (c *ClientModel) Run() {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
// run the control channel
c.control()
// control oonly returns when a failure has occurred, so we're going to try to reconnect
if c.connStatus == mvc.ConnOnline {
wait = 1 * time.Second
}
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
time.Sleep(wait)
// exponentially increase wait time
wait = 2 * wait
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
c.connStatus = mvc.ConnReconnecting
c.update()
}
}
// Establishes and manages a tunnel control connection with the server
func (c *ClientModel) control() {
defer func() {
if r := recover(); r != nil {
log.Error("control recovering from failure %v", r)
}
}()
// establish control channel
var (
ctlConn conn.Conn
err error
)
if c.proxyUrl == "" {
// simple non-proxied case, just connect to the server
ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
} else {
ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
}
if err != nil {
panic(err)
}
defer ctlConn.Close()
// authenticate with the server
auth := &msg.Auth{
ClientId: c.id,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Version: version.Proto,
MmVersion: version.MajorMinor(),
User: c.authToken,
}
if err = msg.WriteMsg(ctlConn, auth); err != nil {
panic(err)
}
// wait for the server to authenticate us
var authResp msg.AuthResp
if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil {
panic(err)
}
if authResp.Error != "" {
emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error)
c.ctl.Shutdown(emsg)
return
}
c.id = authResp.ClientId
c.serverVersion = authResp.MmVersion
c.Info("Authenticated with server, client id: %v", c.id)
c.update()
if err = SaveAuthToken(c.configPath, c.authToken); err != nil {
c.Error("Failed to save auth token: %v", err)
}
// request tunnels
reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)
for _, config := range c.tunnelConfig {
// create the protocol list to ask for
var protocols []string
for proto, _ := range config.Protocols {
protocols = append(protocols, proto)
}
reqTunnel := &msg.ReqTunnel{
ReqId: util.RandId(8),
Protocol: strings.Join(protocols, "+"),
Hostname: config.Hostname,
Subdomain: config.Subdomain,
HttpAuth: config.HttpAuth,
RemotePort: config.RemotePort,
}
// send the tunnel request
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
panic(err)
}
// save request id association so we know which local address
// to proxy to later
reqIdToTunnelConfig[reqTunnel.ReqId] = config
}
// start the heartbeat
lastPong := time.Now().UnixNano()
c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) })
// main control loop
for {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(ctlConn); err != nil {
panic(err)
}
switch m := rawMsg.(type) {
case *msg.ReqProxy:
c.ctl.Go(c.proxy)
case *msg.Pong:
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
case *msg.NewTunnel:
if m.Error != "" {
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error)
c.Error(emsg)
c.ctl.Shutdown(emsg)
continue
}
tunnel := mvc.Tunnel{
PublicUrl: m.Url,
LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
Protocol: c.protoMap[m.Protocol],
}
c.tunnels[tunnel.PublicUrl] = tunnel
c.connStatus = mvc.ConnOnline
c.Info("Tunnel established at %v", tunnel.PublicUrl)
c.update()
default:
ctlConn.Warn("Ignoring unknown control message %v ", m)
}
}
}
// Establishes and manages a tunnel proxy connection with the server
func (c *ClientModel) proxy() {
var (
remoteConn conn.Conn
err error
)
if c.proxyUrl == "" {
remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
} else {
remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
}
if err != nil {
log.Error("Failed to establish proxy connection: %v", err)
return
}
defer remoteConn.Close()
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
if err != nil {
remoteConn.Error("Failed to write RegProxy: %v", err)
return
}
// wait for the server to ack our register
var startPxy msg.StartProxy
if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil {
remoteConn.Error("Server failed to write StartProxy: %v", err)
return
}
tunnel, ok := c.tunnels[startPxy.Url]
if !ok {
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
return
}
// start up the private connection
start := time.Now()
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
if err != nil {
remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err)
if tunnel.Protocol.GetName() == "http" {
// try to be helpful when you're in HTTP mode and a human might see the output
badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr)
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
Content-Type: text/html
Content-Length: %d
%s`, len(badGatewayBody), badGatewayBody)))
}
return
}
defer localConn.Close()
m := c.metrics
m.proxySetupTimer.Update(time.Since(start))
m.connMeter.Mark(1)
c.update()
m.connTimer.Time(func() {
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr})
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
m.bytesIn.Update(bytesIn)
m.bytesOut.Update(bytesOut)
m.bytesInCount.Inc(bytesIn)
m.bytesOutCount.Inc(bytesOut)
})
c.update()
}
// Hearbeating to ensure our connection ngrokd is still live
func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) {
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
ping := time.NewTicker(pingInterval)
pongCheck := time.NewTicker(time.Second)
defer func() {
conn.Close()
ping.Stop()
pongCheck.Stop()
}()
for {
select {
case <-pongCheck.C:
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
needPong := lastPong.Sub(lastPing) < 0
pongLatency := time.Since(lastPing)
if needPong && pongLatency > maxPongLatency {
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
return
}
case <-ping.C:
err := msg.WriteMsg(conn, &msg.Ping{})
if err != nil {
conn.Debug("Got error %v when writing PingMsg", err)
return
}
lastPing = time.Now()
}
}
}

View File

@@ -0,0 +1,28 @@
package mvc
import (
"ngrok/util"
)
type Controller interface {
// how the model communicates that it has changed state
Update(State)
// instructs the controller to shut the app down
Shutdown(message string)
// PlayRequest instructs the model to play requests
PlayRequest(tunnel Tunnel, payload []byte)
// A channel of updates
Updates() *util.Broadcast
// returns the current state
State() State
// safe wrapper for running go-routines
Go(fn func())
// the address where the web inspection interface is running
GetWebInspectAddr() string
}

View File

@@ -0,0 +1,9 @@
package mvc
type Model interface {
Run()
Shutdown()
PlayRequest(tunnel Tunnel, payload []byte)
}

View File

@@ -0,0 +1,47 @@
package mvc
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/proto"
)
type UpdateStatus int
const (
UpdateNone = -1 * iota
UpdateInstalling
UpdateReady
UpdateAvailable
)
type ConnStatus int
const (
ConnConnecting = iota
ConnReconnecting
ConnOnline
)
type Tunnel struct {
PublicUrl string
Protocol proto.Protocol
LocalAddr string
}
type ConnectionContext struct {
Tunnel Tunnel
ClientAddr string
}
type State interface {
GetClientVersion() string
GetServerVersion() string
GetTunnels() []Tunnel
GetProtocols() []proto.Protocol
GetUpdateStatus() UpdateStatus
GetConnStatus() ConnStatus
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
SetUpdateStatus(UpdateStatus)
}

View File

@@ -1,5 +1,5 @@
package ui
package mvc
type View interface {
Render()
Shutdown()
}

View File

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

View File

@@ -1,43 +0,0 @@
package client
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/proto"
"ngrok/version"
)
// client state
type State struct {
id string
publicUrl string
serverVersion string
newVersion string
protocol proto.Protocol
opts *Options
metrics *ClientMetrics
// just for UI purposes
status string
}
// implement client.ui.State
func (s State) GetClientVersion() string { return version.MajorMinor() }
func (s State) GetServerVersion() string { return s.serverVersion }
func (s State) GetPublicUrl() string { return s.publicUrl }
func (s State) GetLocalAddr() string { return s.opts.localaddr }
func (s State) GetWebPort() int { return s.opts.webport }
func (s State) GetStatus() string { return s.status }
func (s State) GetProtocol() proto.Protocol { return s.protocol }
func (s State) GetNewVersion() string { return s.newVersion }
func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
return s.metrics.connMeter, s.metrics.connTimer
}
func (s State) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
return s.metrics.bytesInCount, s.metrics.bytesIn
}
func (s State) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
return s.metrics.bytesOutCount, s.metrics.bytesOut
}

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

@@ -1,13 +0,0 @@
package ui
type Command interface{}
type CmdQuit struct {
// display this message after quit
Message string
}
type CmdRequest struct {
// the bytes of the request to issue
Payload []byte
}

View File

@@ -1,51 +0,0 @@
/* The controller in the MVC
*/
package ui
import (
"ngrok/util"
"sync"
)
type Controller struct {
// the model sends updates through this broadcast channel
Updates *util.Broadcast
// all views put any commands into this channel
Cmds chan Command
// all threads may add themself to this to wait for clean shutdown
Wait *sync.WaitGroup
// channel to signal shutdown
Shutdown chan int
}
func NewController() *Controller {
ctl := &Controller{
Updates: util.NewBroadcast(),
Cmds: make(chan Command),
Wait: new(sync.WaitGroup),
Shutdown: make(chan int),
}
return ctl
}
func (ctl *Controller) Update(state State) {
ctl.Updates.In() <- state
}
func (ctl *Controller) DoShutdown() {
close(ctl.Shutdown)
}
func (ctl *Controller) IsShuttingDown() bool {
select {
case <-ctl.Shutdown:
return true
default:
}
return false
}

View File

@@ -1,20 +0,0 @@
package ui
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/proto"
)
type State interface {
GetClientVersion() string
GetServerVersion() string
GetNewVersion() string
GetPublicUrl() string
GetLocalAddr() string
GetStatus() string
GetProtocol() proto.Protocol
GetWebPort() int
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
}

View File

@@ -0,0 +1,11 @@
// +build !release,!autoupdate
package client
import (
"ngrok/client/mvc"
)
// no auto-updating in debug mode
func autoUpdate(state mvc.State, token string) {
}

View File

@@ -0,0 +1,123 @@
// +build release autoupdate
package client
import (
update "github.com/inconshreveable/go-update"
"net/http"
"net/url"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/version"
"runtime"
"time"
)
const (
updateEndpoint = "https://dl.ngrok.com/update"
checkEndpoint = "https://dl.ngrok.com/update/check"
)
func progressWatcher(s mvc.State, progress chan int, complete chan int) {
for {
select {
case pct, ok := <-progress:
if !ok {
close(complete)
return
} else if pct == 100 {
s.SetUpdateStatus(mvc.UpdateInstalling)
close(complete)
return
} else {
if pct%25 == 0 {
log.Info("Downloading update %d%% complete", pct)
}
s.SetUpdateStatus(mvc.UpdateStatus(pct))
}
}
}
}
func autoUpdate(s mvc.State, token string) {
tryAgain := true
params := make(url.Values)
params.Add("version", version.MajorMinor())
params.Add("os", runtime.GOOS)
params.Add("arch", runtime.GOARCH)
params.Add("user", token)
updateUrl := updateEndpoint + "?" + params.Encode()
checkUrl := checkEndpoint + "?" + params.Encode()
update := func() {
log.Info("Checking for update")
available, err := update.NewDownload(checkUrl).Check()
if err != nil {
log.Error("Error while checking for update: %v", err)
return
}
if !available {
log.Info("No update available")
return
}
// stop trying after a single download attempt
// XXX: improve this so the we can:
// 1. safely update multiple times
// 2. only retry after a network connection failure
tryAgain = false
download := update.NewDownload(updateUrl)
downloadComplete := make(chan int)
go progressWatcher(s, download.Progress, downloadComplete)
log.Info("Trying to update . . .")
err, errRecover := download.GetAndUpdate()
<-downloadComplete
if err != nil {
// log error to console
log.Error("Error while updating ngrok: %v", err)
if errRecover != nil {
log.Error("Error while recovering from failed ngrok update, your binary may be missing: %v", errRecover.Error())
params.Add("errorRecover", errRecover.Error())
}
// log error to ngrok.com's servers for debugging purposes
params.Add("error", err.Error())
resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params)
if err != nil {
log.Error("Error while reporting update error: %v, %v", err, reportErr)
}
resp.Body.Close()
// tell the user to update manually
s.SetUpdateStatus(mvc.UpdateAvailable)
} else {
if !download.Available {
// this is the way the server tells us to update manually
log.Info("Server wants us to update manually")
s.SetUpdateStatus(mvc.UpdateAvailable)
} else {
// tell the user the update is ready
log.Info("Update ready!")
s.SetUpdateStatus(mvc.UpdateReady)
}
}
return
}
// try to update immediately and then at a set interval
update()
for _ = range time.Tick(updateCheckInterval) {
if !tryAgain {
break
}
update()
}
}

View File

@@ -2,22 +2,26 @@ package term
import (
termbox "github.com/nsf/termbox-go"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
"unicode/utf8"
)
const (
size = 10
size = 10
pathMaxLength = 25
)
type HttpView struct {
log.Logger
*area
httpProto *proto.Http
HttpRequests *util.Ring
shutdown chan int
flush chan int
*area
log.Logger
termView *TermView
}
func colorFor(status string) termbox.Attribute {
@@ -33,19 +37,16 @@ func colorFor(status string) termbox.Attribute {
return termbox.ColorWhite
}
func NewHttp(proto *proto.Http, flush, shutdown chan int, x, y int) *HttpView {
func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView {
v := &HttpView{
httpProto: proto,
HttpRequests: util.NewRing(size),
area: NewArea(x, y, 70, size+5),
shutdown: shutdown,
flush: flush,
Logger: log.NewPrefixLogger(),
shutdown: make(chan int),
termView: termView,
Logger: log.NewPrefixLogger("view", "term", "http"),
}
v.AddLogPrefix("view")
v.AddLogPrefix("term")
v.AddLogPrefix("http")
go v.Run()
ctl.Go(v.Run)
return v
}
@@ -54,9 +55,6 @@ func (v *HttpView) Run() {
for {
select {
case <-v.shutdown:
return
case txn := <-updates:
v.Debug("Got HTTP update")
if txn.(*proto.HttpTxn).Resp == nil {
@@ -73,10 +71,49 @@ func (v *HttpView) Render() {
v.Printf(0, 1, "-------------")
for i, obj := range v.HttpRequests.Slice() {
txn := obj.(*proto.HttpTxn)
v.Printf(0, 3+i, "%s %v", txn.Req.Method, txn.Req.URL.Path)
path := truncatePath(txn.Req.URL.Path)
v.Printf(0, 3+i, "%s %v", txn.Req.Method, path)
if txn.Resp != nil {
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
}
}
v.flush <- 1
v.termView.Flush()
}
func (v *HttpView) Shutdown() {
close(v.shutdown)
}
func truncatePath(path string) string {
// Truncate all long strings based on rune count
if utf8.RuneCountInString(path) > pathMaxLength {
path = string([]rune(path)[:pathMaxLength])
}
// By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes.
// Otherwise, we have a multi-byte string and need to calculate the size of each rune and
// truncate manually.
//
// This is a workaround for a bug in termbox-go. Remove it when this issue is fixed:
// https://github.com/nsf/termbox-go/pull/21
if len(path) > pathMaxLength {
out := make([]byte, pathMaxLength, pathMaxLength)
length := 0
for {
r, size := utf8.DecodeRuneInString(path[length:])
if r == utf8.RuneError && size == 1 {
break
}
// utf8.EncodeRune expects there to be enough room to store the full size of the rune
if length+size <= pathMaxLength {
utf8.EncodeRune(out[length:], r)
length += size
} else {
break
}
}
path = string(out[:length])
}
return path
}

View File

@@ -1,76 +1,63 @@
/*
interactive terminal interface for local clients
*/
// interactive terminal interface for local clients
package term
import (
"fmt"
termbox "github.com/nsf/termbox-go"
"ngrok/client/ui"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
"time"
)
type TermView struct {
ctl *ui.Controller
ctl mvc.Controller
updates chan interface{}
flush chan int
subviews []ui.View
state ui.State
shutdown chan int
redraw *util.Broadcast
subviews []mvc.View
log.Logger
*area
}
func New(ctl *ui.Controller, state ui.State) *TermView {
func NewTermView(ctl mvc.Controller) *TermView {
// initialize terminal display
termbox.Init()
// make sure ngrok doesn't quit until we've cleaned up
ctl.Wait.Add(1)
w, _ := termbox.Size()
v := &TermView{
ctl: ctl,
updates: ctl.Updates.Reg(),
updates: ctl.Updates().Reg(),
redraw: util.NewBroadcast(),
flush: make(chan int),
subviews: make([]ui.View, 0),
state: state,
Logger: log.NewPrefixLogger(),
shutdown: make(chan int),
Logger: log.NewPrefixLogger("view", "term"),
area: NewArea(0, 0, w, 10),
}
v.Logger.AddLogPrefix("view")
v.Logger.AddLogPrefix("term")
switch p := state.GetProtocol().(type) {
case *proto.Http:
v.subviews = append(v.subviews, NewHttp(p, v.flush, ctl.Shutdown, 0, 10))
default:
}
v.Render()
go v.run()
go v.input()
ctl.Go(v.run)
ctl.Go(v.input)
return v
}
func colorForConn(status string) termbox.Attribute {
func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) {
switch status {
case "connecting":
return termbox.ColorCyan
case "reconnecting":
return termbox.ColorRed
case "online":
return termbox.ColorGreen
case mvc.ConnConnecting:
return "connecting", termbox.ColorCyan
case mvc.ConnReconnecting:
return "reconnecting", termbox.ColorRed
case mvc.ConnOnline:
return "online", termbox.ColorGreen
}
return termbox.ColorWhite
return "unknown", termbox.ColorWhite
}
func (v *TermView) Render() {
func (v *TermView) draw() {
state := v.ctl.State()
v.Clear()
// quit instructions
@@ -78,54 +65,98 @@ func (v *TermView) Render() {
v.Printf(v.w-len(quitMsg), 0, quitMsg)
// new version message
newVersion := v.state.GetNewVersion()
if newVersion != "" {
newVersionMsg := fmt.Sprintf("new version available at http://ngrok.com")
v.APrintf(termbox.ColorYellow, 30, 0, newVersionMsg)
updateStatus := state.GetUpdateStatus()
var updateMsg string
switch updateStatus {
case mvc.UpdateNone:
updateMsg = ""
case mvc.UpdateInstalling:
updateMsg = "ngrok is updating"
case mvc.UpdateReady:
updateMsg = "ngrok has updated: restart ngrok for the new version"
case mvc.UpdateAvailable:
updateMsg = "new version available at https://ngrok.com"
default:
pct := float64(updateStatus) / 100.0
const barLength = 25
full := int(barLength * pct)
bar := make([]byte, barLength+2)
bar[0] = '['
bar[barLength+1] = ']'
for i := 0; i < 25; i++ {
if i <= full {
bar[i+1] = '#'
} else {
bar[i+1] = ' '
}
}
updateMsg = "Downloading update: " + string(bar)
}
if updateMsg != "" {
v.APrintf(termbox.ColorYellow, 30, 0, updateMsg)
}
v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
statusStr, statusColor := connStatusRepr(state.GetConnStatus())
v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr)
status := v.state.GetStatus()
v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
v.Printf(0, 3, "%-30s%s/%s", "Version", state.GetClientVersion(), state.GetServerVersion())
var i int = 4
for _, t := range state.GetTunnels() {
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
i++
}
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
v.Printf(0, 3, "%-30s%s/%s", "Version", v.state.GetClientVersion(), v.state.GetServerVersion())
v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
connMeter, connTimer := v.state.GetConnectionMetrics()
v.Printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
connMeter, connTimer := state.GetConnectionMetrics()
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())
msec := float64(time.Millisecond)
v.Printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
termbox.Flush()
}
func (v *TermView) run() {
defer v.ctl.Wait.Done()
defer close(v.shutdown)
defer termbox.Close()
redraw := v.redraw.Reg()
defer v.redraw.UnReg(redraw)
v.draw()
for {
v.Debug("Waiting for update")
select {
case <-v.flush:
termbox.Flush()
case obj := <-v.updates:
if obj != nil {
v.state = obj.(ui.State)
}
v.Render()
case <-v.updates:
v.draw()
case <-v.ctl.Shutdown:
case <-redraw:
v.draw()
case <-v.shutdown:
return
}
}
}
func (v *TermView) Shutdown() {
v.shutdown <- 1
<-v.shutdown
}
func (v *TermView) Flush() {
v.flush <- 1
}
func (v *TermView) NewHttpView(p *proto.Http) *HttpView {
return newTermHttpView(v.ctl, v, p, 0, 12)
}
func (v *TermView) input() {
for {
ev := termbox.PollEvent()
@@ -134,21 +165,14 @@ func (v *TermView) input() {
switch ev.Key {
case termbox.KeyCtrlC:
v.Info("Got quit command")
v.ctl.Cmds <- ui.CmdQuit{}
v.ctl.Shutdown("")
}
case termbox.EventResize:
v.Info("Resize event, redrawing")
// send nil to update channel to force re-rendering
v.updates <- nil
for _, sv := range v.subviews {
sv.Render()
}
v.redraw.In() <- 1
case termbox.EventError:
if v.ctl.IsShuttingDown() {
return
}
panic(ev.Err)
}
}

View File

@@ -10,7 +10,7 @@ import (
"net/http/httputil"
"net/url"
"ngrok/client/assets"
"ngrok/client/ui"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
@@ -22,6 +22,7 @@ type SerializedTxn struct {
Id string
Duration int64
Start int64
ConnCtx mvc.ConnectionContext
*proto.HttpTxn `json:"-"`
Req SerializedRequest
Resp SerializedResponse
@@ -55,17 +56,18 @@ type SerializedResponse struct {
}
type WebHttpView struct {
log.Logger
webview *WebView
ctl *ui.Controller
ctl mvc.Controller
httpProto *proto.Http
updates chan interface{}
state chan SerializedUiState
HttpRequests *util.Ring
idToTxn map[string]*SerializedTxn
}
type SerializedUiState struct {
Url string
Tunnels []mvc.Tunnel
}
type SerializedPayload struct {
@@ -73,20 +75,18 @@ type SerializedPayload struct {
UiState SerializedUiState
}
func NewWebHttpView(wv *WebView, ctl *ui.Controller, proto *proto.Http) *WebHttpView {
w := &WebHttpView{
func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView {
whv := &WebHttpView{
Logger: log.NewPrefixLogger("view", "web", "http"),
webview: wv,
ctl: ctl,
httpProto: proto,
idToTxn: make(map[string]*SerializedTxn),
updates: ctl.Updates.Reg(),
state: make(chan SerializedUiState),
HttpRequests: util.NewRing(20),
}
go w.updateHttp()
go w.updateUiState()
w.register()
return w
ctl.Go(whv.updateHttp)
whv.register()
return whv
}
type XMLDoc struct {
@@ -112,21 +112,22 @@ func makeBody(h http.Header, body []byte) SerializedBody {
if b.RawContentType != "" {
b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0])
switch b.ContentType {
case "application/xml":
case "text/xml":
case "application/xml", "text/xml":
err = xml.Unmarshal(body, new(XMLDoc))
if err != nil {
syntaxError := err.(*xml.SyntaxError)
// xml syntax errors only give us a line number, so we
// count to find an offset
b.ErrorOffset = offsetForLine(syntaxError.Line)
if syntaxError, ok := err.(*xml.SyntaxError); ok {
// xml syntax errors only give us a line number, so we
// count to find an offset
b.ErrorOffset = offsetForLine(syntaxError.Line)
}
}
case "application/json":
err = json.Unmarshal(body, new(json.RawMessage))
if err != nil {
syntaxError := err.(*json.SyntaxError)
b.ErrorOffset = int(syntaxError.Offset)
if syntaxError, ok := err.(*json.SyntaxError); ok {
b.ErrorOffset = int(syntaxError.Offset)
}
}
case "application/x-www-form-urlencoded":
@@ -152,22 +153,16 @@ func (whv *WebHttpView) updateHttp() {
// we haven't processed this transaction yet if we haven't set the
// user data
if htxn.UserData == nil {
id, err := util.RandId(8)
if htxn.UserCtx == nil {
rawReq, err := httputil.DumpRequestOut(htxn.Req.Request, true)
if err != nil {
log.Error("Failed to generate txn identifier for web storage: %v", err)
continue
}
rawReq, err := httputil.DumpRequest(htxn.Req.Request, true)
if err != nil {
log.Error("Failed to dump request: %v", err)
whv.Error("Failed to dump request: %v", err)
continue
}
body := makeBody(htxn.Req.Header, htxn.Req.BodyBytes)
whtxn := &SerializedTxn{
Id: id,
Id: util.RandId(8),
HttpTxn: htxn,
Req: SerializedRequest{
MethodPath: htxn.Req.Method + " " + htxn.Req.URL.Path,
@@ -177,10 +172,11 @@ func (whv *WebHttpView) updateHttp() {
Body: body,
Binary: !utf8.Valid(rawReq),
},
Start: htxn.Start.Unix(),
Start: htxn.Start.Unix(),
ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext),
}
htxn.UserData = whtxn
htxn.UserCtx = whtxn
// XXX: unsafe map access from multiple go routines
whv.idToTxn[whtxn.Id] = whtxn
// XXX: use return value to delete from map so we don't leak memory
@@ -188,11 +184,11 @@ func (whv *WebHttpView) updateHttp() {
} else {
rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
if err != nil {
log.Error("Failed to dump response: %v", err)
whv.Error("Failed to dump response: %v", err)
continue
}
txn := htxn.UserData.(*SerializedTxn)
txn := htxn.UserCtx.(*SerializedTxn)
body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes)
txn.Duration = htxn.Duration.Nanoseconds()
txn.Resp = SerializedResponse{
@@ -205,43 +201,46 @@ func (whv *WebHttpView) updateHttp() {
payload, err := json.Marshal(txn)
if err != nil {
log.Error("Failed to serialized txn payload for websocket: %v", err)
whv.Error("Failed to serialized txn payload for websocket: %v", err)
}
whv.webview.wsMessages.In() <- payload
}
}
}
func (v *WebHttpView) updateUiState() {
var s SerializedUiState
for {
select {
case obj := <-v.updates:
uiState := obj.(ui.State)
s.Url = uiState.GetPublicUrl()
case v.state <- s:
}
}
}
func (h *WebHttpView) register() {
func (whv *WebHttpView) register() {
http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
whv.Error("Replay failed: %v", err)
http.Error(w, err, 500)
}
}()
r.ParseForm()
txnid := r.Form.Get("txnid")
if txn, ok := h.idToTxn[txnid]; ok {
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
if txn, ok := whv.idToTxn[txnid]; ok {
reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw)
if err != nil {
panic(err)
}
h.ctl.Cmds <- ui.CmdRequest{Payload: bodyBytes}
whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes)
w.Write([]byte(http.StatusText(200)))
} else {
// XXX: 400
http.NotFound(w, r)
http.Error(w, http.StatusText(400), 400)
}
})
http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
whv.Error("HTTP web view failed: %v", err)
http.Error(w, err, 500)
}
}()
pageTmpl, err := assets.ReadAsset("assets/client/page.html")
if err != nil {
panic(err)
@@ -250,8 +249,8 @@ func (h *WebHttpView) register() {
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
payloadData := SerializedPayload{
Txns: h.HttpRequests.Slice(),
UiState: <-h.state,
Txns: whv.HttpRequests.Slice(),
UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
}
payload, err := json.Marshal(payloadData)
@@ -265,3 +264,6 @@ func (h *WebHttpView) register() {
}
})
}
func (whv *WebHttpView) Shutdown() {
}

View File

@@ -2,11 +2,10 @@
package web
import (
"fmt"
"github.com/garyburd/go-websocket/websocket"
"net/http"
"ngrok/client/assets"
"ngrok/client/ui"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
@@ -14,34 +13,38 @@ import (
)
type WebView struct {
log.Logger
ctl mvc.Controller
// messages sent over this broadcast are sent too all websocket connections
wsMessages *util.Broadcast
}
func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
v := &WebView{
func NewWebView(ctl mvc.Controller, addr string) *WebView {
wv := &WebView{
Logger: log.NewPrefixLogger("view", "web"),
wsMessages: util.NewBroadcast(),
ctl: ctl,
}
switch p := state.GetProtocol().(type) {
case *proto.Http:
NewWebHttpView(v, ctl, p)
}
// for now, always redirect to the http view
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/http/in", 302)
})
// handle web socket connections
http.HandleFunc("/_ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r.Header, nil, 1024, 1024)
if err != nil {
http.Error(w, "Failed websocket upgrade", 400)
log.Warn("Failed websocket upgrade: %v", err)
wv.Warn("Failed websocket upgrade: %v", err)
return
}
msgs := v.wsMessages.Reg()
defer v.wsMessages.UnReg(msgs)
msgs := wv.wsMessages.Reg()
defer wv.wsMessages.UnReg(msgs)
for m := range msgs {
err := conn.WriteMessage(websocket.OpText, m.([]byte))
if err != nil {
@@ -51,17 +54,25 @@ func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
}
})
// serve static assets
http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
buf, err := assets.ReadAsset(path.Join("assets", "client", r.URL.Path[1:]))
if err != nil {
log.Warn("Error serving static file: %s", err.Error())
wv.Warn("Error serving static file: %s", err.Error())
http.NotFound(w, r)
return
}
w.Write(buf)
})
log.Info("Serving web interface on localhost:%d", port)
go http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
return v
wv.Info("Serving web interface on %s", addr)
wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
return wv
}
func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView {
return newWebHttpView(wv.ctl, wv, proto)
}
func (wv *WebView) Shutdown() {
}

View File

@@ -4,21 +4,27 @@ import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"ngrok/log"
"sync"
)
type Conn interface {
net.Conn
log.Logger
Id() string
SetType(string)
CloseRead() error
}
type tcpConn struct {
type loggedConn struct {
tcp *net.TCPConn
net.Conn
log.Logger
id int32
@@ -26,126 +32,207 @@ type tcpConn struct {
}
type Listener struct {
*net.TCPAddr
net.Addr
Conns chan Conn
}
func wrapTcpConn(conn net.Conn, typ string) *tcpConn {
c := &tcpConn{conn, log.NewPrefixLogger(), rand.Int31(), typ}
c.AddLogPrefix(c.Id())
return c
func wrapConn(conn net.Conn, typ string) *loggedConn {
switch c := conn.(type) {
case *loggedConn:
return c
case *net.TCPConn:
wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ}
wrapped.AddLogPrefix(wrapped.Id())
return wrapped
}
return nil
}
func Listen(addr *net.TCPAddr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
// listen for incoming connections
listener, err := net.ListenTCP("tcp", addr)
listener, err := net.Listen("tcp", addr)
if err != nil {
return
}
l = &Listener{
TCPAddr: listener.Addr().(*net.TCPAddr),
Conns: make(chan Conn),
Addr: listener.Addr(),
Conns: make(chan Conn),
}
go func() {
for {
tcpConn, err := listener.AcceptTCP()
rawConn, err := listener.Accept()
if err != nil {
log.Error("Failed to accept new TCP connection of type %s: %v", typ, err)
continue
}
c := wrapTcpConn(tcpConn, typ)
c := wrapConn(rawConn, typ)
if tlsCfg != nil {
c.Conn = tls.Server(c.Conn, tlsCfg)
}
c.Info("New connection from %v", tcpConn.RemoteAddr())
c.Info("New connection from %v", c.RemoteAddr())
l.Conns <- c
}
}()
return
}
func Wrap(conn net.Conn, typ string) *tcpConn {
return wrapTcpConn(conn, typ)
func Wrap(conn net.Conn, typ string) *loggedConn {
return wrapConn(conn, typ)
}
func Dial(addr, typ string, tlsCfg *tls.Config) (conn *tcpConn, err error) {
var (
tcpAddr *net.TCPAddr
tcpConn net.Conn
)
if tcpAddr, err = net.ResolveTCPAddr("tcp", addr); err != nil {
func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
var rawConn net.Conn
if rawConn, err = net.Dial("tcp", addr); err != nil {
return
}
if tcpConn, err = net.DialTCP("tcp", nil, tcpAddr); err != nil {
return
}
conn = wrapConn(rawConn, typ)
conn.Debug("New connection to: %v", rawConn.RemoteAddr())
if tlsCfg != nil {
tcpConn = tls.Client(tcpConn, tlsCfg)
conn.StartTLS(tlsCfg)
}
conn = wrapTcpConn(tcpConn, typ)
conn.Debug("New connection to: %v", tcpAddr)
return conn, nil
return
}
func (c *tcpConn) Close() error {
c.Debug("Closing")
return c.Conn.Close()
func DialHttpProxy(proxyUrl, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
// parse the proxy address
var parsedUrl *url.URL
if parsedUrl, err = url.Parse(proxyUrl); err != nil {
return
}
var proxyAuth string
if parsedUrl.User != nil {
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(parsedUrl.User.String()))
}
var proxyTlsConfig *tls.Config
switch parsedUrl.Scheme {
case "http":
proxyTlsConfig = nil
case "https":
proxyTlsConfig = new(tls.Config)
default:
err = fmt.Errorf("Proxy URL scheme must be http or https, got: %s", parsedUrl.Scheme)
return
}
// dial the proxy
if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil {
return
}
// send an HTTP proxy CONNECT message
req, err := http.NewRequest("CONNECT", "https://"+addr, nil)
if err != nil {
return
}
if proxyAuth != "" {
req.Header.Set("Proxy-Authorization", proxyAuth)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ngrok)")
req.Write(conn)
// read the proxy's response
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
return
}
resp.Body.Close()
if resp.StatusCode != 200 {
err = fmt.Errorf("Non-200 response from proxy server: %s", resp.Status)
return
}
// upgrade to TLS
conn.StartTLS(tlsCfg)
return
}
func (c *tcpConn) Id() string {
func (c *loggedConn) StartTLS(tlsCfg *tls.Config) {
c.Conn = tls.Client(c.Conn, tlsCfg)
}
func (c *loggedConn) Close() (err error) {
if err := c.Conn.Close(); err == nil {
c.Debug("Closing")
}
return
}
func (c *loggedConn) Id() string {
return fmt.Sprintf("%s:%x", c.typ, c.id)
}
func (c *loggedConn) SetType(typ string) {
oldId := c.Id()
c.typ = typ
c.ClearLogPrefixes()
c.AddLogPrefix(c.Id())
c.Info("Renamed connection %s", oldId)
}
func (c *loggedConn) CloseRead() error {
// XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner
// connection termination. Unfortunately, when I've tried that, I've observed
// failures where the connection was closed *before* flushing its write buffer,
// set with SetLinger() set properly (which it is by default).
return c.tcp.CloseRead()
}
func Join(c Conn, c2 Conn) (int64, int64) {
done := make(chan error)
var wait sync.WaitGroup
pipe := func(to Conn, from Conn, bytesCopied *int64) {
defer to.Close()
defer from.Close()
defer wait.Done()
var err error
*bytesCopied, err = io.Copy(to, from)
if err != nil {
from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err)
done <- err
} else {
from.Debug("Copied %d bytes from to %s", *bytesCopied, to.Id())
done <- nil
from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id())
}
}
wait.Add(2)
var fromBytes, toBytes int64
go pipe(c, c2, &fromBytes)
go pipe(c2, c, &toBytes)
c.Info("Joined with connection %s", c2.Id())
<-done
c.Close()
c2.Close()
<-done
wait.Wait()
return fromBytes, toBytes
}
type httpConn struct {
*tcpConn
*loggedConn
reqBuf *bytes.Buffer
}
func NewHttp(conn net.Conn, typ string) *httpConn {
return &httpConn{
wrapTcpConn(conn, typ),
wrapConn(conn, typ),
bytes.NewBuffer(make([]byte, 0, 1024)),
}
}
func (c *httpConn) ReadRequest() (*http.Request, error) {
r := io.TeeReader(c.tcpConn, c.reqBuf)
r := io.TeeReader(c.loggedConn, c.reqBuf)
return http.ReadRequest(bufio.NewReader(r))
}
func (c *tcpConn) ReadFrom(r io.Reader) (n int64, err error) {
func (c *loggedConn) ReadFrom(r io.Reader) (n int64, err error) {
// special case when we're reading from an http request where
// we had to parse the request and consume bytes from the socket
// and store them in a temporary request buffer

View File

@@ -26,6 +26,7 @@ func LogTo(target string) {
type Logger interface {
AddLogPrefix(string)
ClearLogPrefixes()
Debug(string, ...interface{})
Info(string, ...interface{})
Warn(string, ...interface{}) error
@@ -37,8 +38,14 @@ type PrefixLogger struct {
prefix string
}
func NewPrefixLogger() Logger {
return &PrefixLogger{Logger: &root}
func NewPrefixLogger(prefixes ...string) Logger {
logger := &PrefixLogger{Logger: &root}
for _, p := range prefixes {
logger.AddLogPrefix(p)
}
return logger
}
func (pl *PrefixLogger) pfx(fmtstr string) interface{} {
@@ -69,6 +76,10 @@ func (pl *PrefixLogger) AddLogPrefix(prefix string) {
pl.prefix += "[" + prefix + "]"
}
func (pl *PrefixLogger) ClearLogPrefixes() {
pl.prefix = ""
}
// we should never really use these . . . always prefer logging through a prefix logger
func Debug(arg0 string, args ...interface{}) {
root.Debug(arg0, args...)

View File

@@ -11,14 +11,15 @@ func init() {
TypeMap = make(map[string]reflect.Type)
t := func(obj interface{}) reflect.Type { return reflect.TypeOf(obj).Elem() }
TypeMap["RegMsg"] = t((*RegMsg)(nil))
TypeMap["RegAckMsg"] = t((*RegAckMsg)(nil))
TypeMap["RegProxyMsg"] = t((*RegProxyMsg)(nil))
TypeMap["ReqProxyMsg"] = t((*ReqProxyMsg)(nil))
TypeMap["PingMsg"] = t((*PingMsg)(nil))
TypeMap["PongMsg"] = t((*PongMsg)(nil))
TypeMap["VerisonMsg"] = t((*VersionMsg)(nil))
TypeMap["VersionRespMsg"] = t((*VersionRespMsg)(nil))
TypeMap["Auth"] = t((*Auth)(nil))
TypeMap["AuthResp"] = t((*AuthResp)(nil))
TypeMap["ReqTunnel"] = t((*ReqTunnel)(nil))
TypeMap["NewTunnel"] = t((*NewTunnel)(nil))
TypeMap["RegProxy"] = t((*RegProxy)(nil))
TypeMap["ReqProxy"] = t((*ReqProxy)(nil))
TypeMap["StartProxy"] = t((*StartProxy)(nil))
TypeMap["Ping"] = t((*Ping)(nil))
TypeMap["Pong"] = t((*Pong)(nil))
}
type Message interface{}
@@ -28,45 +29,92 @@ type Envelope struct {
Payload json.RawMessage
}
type RegMsg struct {
Version string
MmVersion string
Protocol string
Hostname string
Subdomain string
ClientId string
HttpAuth string
// When a client opens a new control channel to the server
// it must start by sending an Auth message.
type Auth struct {
Version string // protocol version
MmVersion string // major/minor software version (informational only)
User string
Password string
OS string
Arch string
ClientId string // empty for new sessions
}
type RegAckMsg struct {
// A server responds to an Auth message with an
// AuthResp message over the control channel.
//
// If Error is not the empty string
// the server has indicated it will not accept
// the new session and will close the connection.
//
// The server response includes a unique ClientId
// that is used to associate and authenticate future
// proxy connections via the same field in RegProxy messages.
type AuthResp struct {
Version string
MmVersion string
Url string
ProxyAddr string
ClientId string
Error string
}
type RegProxyMsg struct {
Url string
// A client sends this message to the server over the control channel
// to request a new tunnel be opened on the client's behalf.
// ReqId is a random number set by the client that it can pull
// from future NewTunnel's to correlate then to the requesting ReqTunnel.
type ReqTunnel struct {
ReqId string
Protocol string
// http only
Hostname string
Subdomain string
HttpAuth string
// tcp only
RemotePort uint16
}
type ReqProxyMsg struct {
// When the server opens a new tunnel on behalf of
// a client, it sends a NewTunnel message to notify the client.
// ReqId is the ReqId from the corresponding ReqTunnel message.
//
// A client may receive *multiple* NewTunnel messages from a single
// ReqTunnel. (ex. A client opens an https tunnel and the server
// chooses to open an http tunnel of the same name as well)
type NewTunnel struct {
ReqId string
Url string
Protocol string
Error string
}
type PingMsg struct {
// When the server wants to initiate a new tunneled connection, it sends
// this message over the control channel to the client. When a client receives
// this message, it must initiate a new proxy connection to the server.
type ReqProxy struct {
}
type PongMsg struct {
// After a client receives a ReqProxy message, it opens a new
// connection to the server and sends a RegProxy message.
type RegProxy struct {
ClientId string
}
type VersionMsg struct {
// This message is sent by the server to the client over a *proxy* connection before it
// begins to send the bytes of the proxied request.
type StartProxy struct {
Url string // URL of the tunnel this connection connection is being proxied for
ClientAddr string // Network address of the client initiating the connection to the tunnel
}
type VersionRespMsg struct {
Version string
MmVersion string
// A client or server may send this message periodically over
// the control channel to request that the remote side acknowledge
// its connection is still alive. The remote side must respond with a Pong.
type Ping struct {
}
// Sent by a client or server over the control channel to indicate
// it received a Ping.
type Pong struct {
}

View File

@@ -9,6 +9,7 @@ import (
"net/http/httputil"
"ngrok/conn"
"ngrok/util"
"sync"
"time"
)
@@ -23,11 +24,12 @@ type HttpResponse struct {
}
type HttpTxn struct {
Req *HttpRequest
Resp *HttpResponse
Start time.Time
Duration time.Duration
UserData interface{}
Req *HttpRequest
Resp *HttpResponse
Start time.Time
Duration time.Duration
UserCtx interface{}
ConnUserCtx interface{}
}
type Http struct {
@@ -54,15 +56,17 @@ func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) {
func (h *Http) GetName() string { return "http" }
func (h *Http) WrapConn(c conn.Conn) conn.Conn {
func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
tee := conn.NewTee(c)
lastTxn := make(chan *HttpTxn)
go h.readRequests(tee, lastTxn)
go h.readRequests(tee, lastTxn, ctx)
go h.readResponses(tee, lastTxn)
return tee
}
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) {
defer close(lastTxn)
for {
req, err := http.ReadRequest(tee.WriteBuffer())
if err != nil {
@@ -79,7 +83,11 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
tee.Warn("Failed to extract request body: %v", err)
}
txn := &HttpTxn{Start: time.Now()}
// golang's ReadRequest/DumpRequestOut is broken. Fix up the request so it works later
req.URL.Scheme = "http"
req.URL.Host = req.Host
txn := &HttpTxn{Start: time.Now(), ConnUserCtx: connCtx}
txn.Req = &HttpRequest{Request: req}
txn.Req.BodyBytes, txn.Req.Body, err = extractBody(req.Body)
@@ -89,9 +97,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
}
func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
for {
var err error
txn := <-lastTxn
for txn := range lastTxn {
resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request)
txn.Duration = time.Since(txn.Start)
h.reqTimer.Update(txn.Duration)
@@ -111,5 +117,29 @@ func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
}
h.Txns.In() <- txn
// XXX: remove web socket shim in favor of a real websocket protocol analyzer
if txn.Req.Header.Get("Upgrade") == "websocket" {
tee.Info("Upgrading to websocket")
var wg sync.WaitGroup
// shim for websockets
// in order for websockets to work, we need to continue reading all of the
// the bytes in the analyzer so that the joined connections will continue
// sending bytes to each other
wg.Add(2)
go func() {
ioutil.ReadAll(tee.WriteBuffer())
wg.Done()
}()
go func() {
ioutil.ReadAll(tee.ReadBuffer())
wg.Done()
}()
wg.Wait()
break
}
}
}

View File

@@ -6,5 +6,5 @@ import (
type Protocol interface {
GetName() string
WrapConn(conn.Conn) conn.Conn
WrapConn(conn.Conn, interface{}) conn.Conn
}

View File

@@ -12,6 +12,6 @@ func NewTcp() *Tcp {
func (h *Tcp) GetName() string { return "tcp" }
func (h *Tcp) WrapConn(c conn.Conn) conn.Conn {
func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
return c
}

37
src/ngrok/server/cli.go Normal file
View File

@@ -0,0 +1,37 @@
package server
import (
"flag"
)
type Options struct {
httpAddr string
httpsAddr string
tunnelAddr string
domain string
tlsCrt string
tlsKey string
logto string
}
func parseArgs() *Options {
httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable")
httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
tunnelAddr := flag.String("tunnelAddr", ":4443", "Public address listening for ngrok client")
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
tlsCrt := flag.String("tlsCrt", "", "Path to a TLS certificate file")
tlsKey := flag.String("tlsKey", "", "Path to a TLS key file")
logto := flag.String("log", "stdout", "Write log messages to this file. 'stdout' and 'none' have special meanings")
flag.Parse()
return &Options{
httpAddr: *httpAddr,
httpsAddr: *httpsAddr,
tunnelAddr: *tunnelAddr,
domain: *domain,
tlsCrt: *tlsCrt,
tlsKey: *tlsKey,
logto: *logto,
}
}

View File

@@ -1,110 +1,241 @@
package server
import (
"fmt"
"io"
"ngrok/conn"
"ngrok/msg"
"ngrok/util"
"ngrok/version"
"runtime/debug"
"strings"
"time"
)
const (
pingTimeoutInterval = 30 * time.Second
connReapInterval = 10 * time.Second
controlWriteTimeout = 10 * time.Second
proxyStaleDuration = 60 * time.Second
proxyMaxPoolSize = 10
)
type Control struct {
// auth message
auth *msg.Auth
// actual connection
conn conn.Conn
// channels for communicating messages over the connection
out chan (interface{})
in chan (msg.Message)
stop chan (msg.Message)
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// heartbeat
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
// tunnel
tun *Tunnel
// all of the tunnels this control connection handles
tunnels []*Tunnel
// proxy connections
proxies chan conn.Conn
// identifier
id string
// synchronizer for controlled shutdown of writer()
writerShutdown *util.Shutdown
// synchronizer for controlled shutdown of reader()
readerShutdown *util.Shutdown
// synchronizer for controlled shutdown of manager()
managerShutdown *util.Shutdown
// synchronizer for controller shutdown of entire Control
shutdown *util.Shutdown
}
func NewControl(conn conn.Conn) {
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
c := &Control{
conn: conn,
out: make(chan (interface{}), 1),
in: make(chan (msg.Message), 1),
stop: make(chan (msg.Message), 1),
lastPing: time.Now(),
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message),
in: make(chan msg.Message),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
writerShutdown: util.NewShutdown(),
readerShutdown: util.NewShutdown(),
managerShutdown: util.NewShutdown(),
shutdown: util.NewShutdown(),
}
go c.managerThread()
go c.readThread()
failAuth := func(e error) {
_ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()})
ctlConn.Close()
}
// register the clientid
c.id = authMsg.ClientId
if c.id == "" {
// it's a new session, assign an ID
if c.id, err = util.SecureRandId(16); err != nil {
failAuth(err)
return
}
}
// set logging prefix
ctlConn.SetType("ctl")
ctlConn.AddLogPrefix(c.id)
if authMsg.Version != version.Proto {
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
return
}
// register the control
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
replaced.shutdown.WaitComplete()
}
// start the writer first so that the following messages get sent
go c.writer()
// Respond to authentication
c.out <- &msg.AuthResp{
Version: version.Proto,
MmVersion: version.MajorMinor(),
ClientId: c.id,
}
// As a performance optimization, ask for a proxy connection up front
c.out <- &msg.ReqProxy{}
// manage the connection
go c.manager()
go c.reader()
go c.stopper()
}
func (c *Control) managerThread() {
reap := time.NewTicker(connReapInterval)
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
tunnelReq := *rawTunnelReq
tunnelReq.Protocol = proto
// all shutdown functionality in here
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
if err != nil {
c.out <- &msg.NewTunnel{Error: err.Error()}
if len(c.tunnels) == 0 {
c.shutdown.Begin()
}
// we're done
return
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
func (c *Control) manager() {
// don't crash on panics
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
}
reap.Stop()
c.conn.Close()
// shutdown the tunnel if it's open
if c.tun != nil {
c.tun.shutdown()
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the control manager stops
defer c.shutdown.Begin()
// notify that manager() has shutdown
defer c.managerShutdown.Complete()
// reaping timer for detecting heartbeat failure
reap := time.NewTicker(connReapInterval)
defer reap.Stop()
for {
select {
case m := <-c.out:
msg.WriteMsg(c.conn, m)
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
c.shutdown.Begin()
}
case mRaw, ok := <-c.in:
// c.in closes to indicate shutdown
if !ok {
return
}
case m := <-c.stop:
if m != nil {
msg.WriteMsg(c.conn, m)
}
return
case mRaw := <-c.in:
switch m := mRaw.(type) {
case *msg.RegMsg:
c.conn.Info("Registering new tunnel")
c.tun = newTunnel(m, c)
case *msg.ReqTunnel:
c.registerTunnel(m)
case *msg.PingMsg:
case *msg.Ping:
c.lastPing = time.Now()
c.out <- &msg.PongMsg{}
case *msg.VersionMsg:
c.out <- &msg.VersionRespMsg{
Version: version.Proto,
MmVersion: version.MajorMinor(),
}
c.out <- &msg.Pong{}
}
}
}
}
func (c *Control) readThread() {
func (c *Control) writer() {
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::readThread failed with error %v: %s", err, debug.Stack())
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
}
c.stop <- nil
}()
// kill everything if the writer() stops
defer c.shutdown.Begin()
// notify that we've flushed all messages
defer c.writerShutdown.Complete()
// write messages to the control channel
for m := range c.out {
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := msg.WriteMsg(c.conn, m); err != nil {
panic(err)
}
}
}
func (c *Control) reader() {
defer func() {
if err := recover(); err != nil {
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the reader stops
defer c.shutdown.Begin()
// notify that we're done
defer c.readerShutdown.Complete()
// read messages from the control channel
for {
if msg, err := msg.ReadMsg(c.conn); err != nil {
@@ -115,7 +246,111 @@ func (c *Control) readThread() {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}
func (c *Control) stopper() {
defer func() {
if r := recover(); r != nil {
c.conn.Error("Failed to shut down control: %v", r)
}
}()
// wait until we're instructed to shutdown
c.shutdown.WaitBegin()
// remove ourself from the control registry
controlRegistry.Del(c.id)
// shutdown manager() so that we have no more work to do
close(c.in)
c.managerShutdown.WaitComplete()
// shutdown writer()
close(c.out)
c.writerShutdown.WaitComplete()
// close connection fully
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// shutdown all of the proxy connections
close(c.proxies)
for p := range c.proxies {
p.Close()
}
c.shutdown.Complete()
c.conn.Info("Shutdown complete")
}
func (c *Control) RegisterProxy(conn conn.Conn) {
conn.AddLogPrefix(c.id)
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
select {
case c.proxies <- conn:
conn.Info("Registered")
default:
conn.Info("Proxies buffer is full, discarding.")
conn.Close()
}
}
// Remove a proxy connection from the pool and return it
// If not proxy connections are in the pool, request one
// and wait until it is available
// Returns an error if we couldn't get a proxy because it took too long
// or the tunnel is closing
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
var ok bool
// get a proxy connection from the pool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
default:
// no proxy available in the pool, ask for one over the control channel
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
return
}
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
return
}
// Called when this control is replaced by another control
// this can happen if the network drops out and the client reconnects
// before the old tunnel has lost its heartbeat
func (c *Control) Replaced(replacement *Control) {
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
// set the control id to empty string so that when stopper()
// calls registry.Del it won't delete the replacement
c.id = ""
// tell the old one to shutdown
c.shutdown.Begin()
}

View File

@@ -1,10 +1,13 @@
package server
import (
"crypto/tls"
"fmt"
"net"
"ngrok/conn"
"ngrok/log"
"strings"
"time"
)
const (
@@ -28,26 +31,31 @@ Bad Request
`
)
/**
* Listens for new http connections from the public internet
*/
func httpListener(addr *net.TCPAddr) {
// Listens for new http(s) connections from the public internet
func startHttpListener(addr string, tlsCfg *tls.Config) (listener *conn.Listener) {
// bind/listen for incoming connections
listener, err := conn.Listen(addr, "pub", nil)
if err != nil {
var err error
if listener, err = conn.Listen(addr, "pub", tlsCfg); err != nil {
panic(err)
}
log.Info("Listening for public http connections on %v", listener.Port)
for conn := range listener.Conns {
go httpHandler(conn)
proto := "http"
if tlsCfg != nil {
proto = "https"
}
log.Info("Listening for public %s connections on %v", proto, listener.Addr.String())
go func() {
for conn := range listener.Conns {
go httpHandler(conn, proto)
}
}()
return
}
/**
* Handles a new http connection from the public internet
*/
func httpHandler(tcpConn net.Conn) {
// Handles a new http connection from the public internet
func httpHandler(tcpConn net.Conn, proto string) {
// wrap up the connection for logging
conn := conn.NewHttp(tcpConn, "pub")
@@ -59,31 +67,41 @@ func httpHandler(tcpConn net.Conn) {
}
}()
// Make sure we detect dead connections while we decide how to multiplex
conn.SetDeadline(time.Now().Add(connReadTimeout))
// read out the http request
req, err := conn.ReadRequest()
if err != nil {
conn.Warn("Failed to read valid http request: %v", err)
conn.Warn("Failed to read valid %s request: %v", proto, err)
conn.Write([]byte(BadRequest))
return
}
// read out the Host header from the request
host := strings.ToLower(req.Host)
conn.Debug("Found hostname %s in request", host)
// multiplex to find the right backend host
conn.Debug("Found hostname %s in request", req.Host)
tunnel := tunnels.Get("http://" + req.Host)
tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host))
if tunnel == nil {
conn.Info("No tunnel found for hostname %s", req.Host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(req.Host)+18, req.Host)))
conn.Info("No tunnel found for hostname %s", host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
return
}
// satisfy auth, if necessary
conn.Debug("From client: %s", req.Header.Get("Authorization"))
conn.Debug("To match: %s", tunnel.regMsg.HttpAuth)
if req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
conn.Info("Authentication failed")
// If the client specified http auth and it doesn't match this request's auth
// then fail the request with 401 Not Authorized and request the client reissue the
// request with basic authdeny the request
if tunnel.req.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.req.HttpAuth {
conn.Info("Authentication failed: %s", req.Header.Get("Authorization"))
conn.Write([]byte(NotAuthorized))
return
}
// dead connections will now be handled by tunnel heartbeating and the client
conn.SetDeadline(time.Time{})
// let the tunnel handle the connection now
tunnel.HandlePublicConnection(conn)
}

View File

@@ -1,126 +1,141 @@
package server
import (
"flag"
"fmt"
"net"
"crypto/tls"
"math/rand"
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"ngrok/util"
"os"
"runtime/debug"
"time"
)
type Options struct {
publicPort int
proxyPort int
tunnelPort int
domain string
logto string
}
const (
registryCacheSize uint64 = 1024 * 1024 // 1 MB
connReadTimeout time.Duration = 10 * time.Second
)
/* GLOBALS */
// GLOBALS
var (
proxyAddr string
tunnels *TunnelRegistry
registryCacheSize uint64 = 1024 * 1024 // 1 MB
domain string
tunnelRegistry *TunnelRegistry
controlRegistry *ControlRegistry
// XXX: kill these global variables - they're only used in tunnel.go for constructing forwarding URLs
opts *Options
listeners map[string]*conn.Listener
)
func parseArgs() *Options {
publicPort := flag.Int("publicport", 80, "Public port")
tunnelPort := flag.Int("tunnelport", 4443, "Tunnel port")
proxyPort := flag.Int("proxyPort", 0, "Proxy port")
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
logto := flag.String(
"log",
"stdout",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
// fail gracefully if the proxy connection fails to register
defer func() {
if r := recover(); r != nil {
pxyConn.Warn("Failed with error: %v", r)
pxyConn.Close()
}
}()
flag.Parse()
// set logging prefix
pxyConn.SetType("pxy")
return &Options{
publicPort: *publicPort,
tunnelPort: *tunnelPort,
proxyPort: *proxyPort,
domain: *domain,
logto: *logto,
// look up the control connection for this proxy
pxyConn.Info("Registering new proxy for %s", regPxy.ClientId)
ctl := controlRegistry.Get(regPxy.ClientId)
if ctl == nil {
panic("No client found for identifier: " + regPxy.ClientId)
}
ctl.RegisterProxy(pxyConn)
}
/**
* Listens for new control connections from tunnel clients
*/
func controlListener(addr *net.TCPAddr, domain string) {
// Listen for incoming control and proxy connections
// We listen for incoming control and proxy connections on the same port
// for ease of deployment. The hope is that by running on port 443, using
// TLS and running all connections over the same port, we can bust through
// restrictive firewalls.
func tunnelListener(addr string, tlsConfig *tls.Config) {
// listen for incoming connections
listener, err := conn.Listen(addr, "ctl", tlsConfig)
listener, err := conn.Listen(addr, "tun", tlsConfig)
if err != nil {
panic(err)
}
log.Info("Listening for control connections on %d", listener.Port)
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
for c := range listener.Conns {
NewControl(c)
}
}
/**
* Listens for new proxy connections from tunnel clients
*/
func proxyListener(addr *net.TCPAddr, domain string) {
listener, err := conn.Listen(addr, "pxy", tlsConfig)
if err != nil {
panic(err)
}
// set global proxy addr variable
proxyAddr = fmt.Sprintf("%s:%d", domain, listener.Port)
log.Info("Listening for proxy connection on %d", listener.Port)
for proxyConn := range listener.Conns {
go func(conn conn.Conn) {
// fail gracefully if the proxy connection dies
go func(tunnelConn conn.Conn) {
// don't crash on panics
defer func() {
if r := recover(); r != nil {
conn.Warn("Failed with error: %v", r)
conn.Close()
tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack())
}
}()
// read the proxy register message
var regPxy msg.RegProxyMsg
if err = msg.ReadMsgInto(conn, &regPxy); err != nil {
panic(err)
tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout))
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
tunnelConn.Warn("Failed to read message: %v", err)
tunnelConn.Close()
return
}
// look up the tunnel for this proxy
conn.Info("Registering new proxy for %s", regPxy.Url)
tunnel := tunnels.Get(regPxy.Url)
if tunnel == nil {
panic("No tunnel found for: " + regPxy.Url)
}
// don't timeout after the initial read, tunnel heartbeating will kill
// dead connections
tunnelConn.SetReadDeadline(time.Time{})
// register the proxy connection with the tunnel
tunnel.RegisterProxy(conn)
}(proxyConn)
switch m := rawMsg.(type) {
case *msg.Auth:
NewControl(tunnelConn, m)
case *msg.RegProxy:
NewProxy(tunnelConn, m)
default:
tunnelConn.Close()
}
}(c)
}
}
func Main() {
// parse options
opts := parseArgs()
domain = opts.domain
opts = parseArgs()
// init logging
log.LogTo(opts.logto)
// init tunnel registry
// seed random number generator
seed, err := util.RandomSeed()
if err != nil {
panic(err)
}
rand.Seed(seed)
// init tunnel/control registry
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
tunnels = NewTunnelRegistry(registryCacheSize, registryCacheFile)
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
controlRegistry = NewControlRegistry()
go proxyListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.proxyPort}, opts.domain)
go controlListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.publicPort})
// start listeners
listeners = make(map[string]*conn.Listener)
// wait forever
done := make(chan int)
<-done
// load tls configuration
tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
if err != nil {
panic(err)
}
// listen for http
if opts.httpAddr != "" {
listeners["http"] = startHttpListener(opts.httpAddr, nil)
}
// listen for https
if opts.httpsAddr != "" {
listeners["https"] = startHttpListener(opts.httpsAddr, tlsConfig)
}
// ngrok clients
tunnelListener(opts.tunnelAddr, tlsConfig)
}

View File

@@ -19,12 +19,10 @@ func init() {
keenApiKey := os.Getenv("KEEN_API_KEY")
if keenApiKey != "" {
metrics = NewKeenIoMetrics()
metrics = NewKeenIoMetrics(60 * time.Second)
} else {
metrics = NewLocalMetrics(30 * time.Second)
}
metrics.AddLogPrefix("metrics")
}
type Metrics interface {
@@ -63,7 +61,7 @@ type LocalMetrics struct {
func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics {
metrics := LocalMetrics{
Logger: log.NewPrefixLogger(),
Logger: log.NewPrefixLogger("metrics"),
reportInterval: reportInterval,
windowsCounter: gometrics.NewCounter(),
linuxCounter: gometrics.NewCounter(),
@@ -96,7 +94,7 @@ func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics {
func (m *LocalMetrics) OpenTunnel(t *Tunnel) {
m.tunnelMeter.Mark(1)
switch t.regMsg.OS {
switch t.ctl.auth.OS {
case "windows":
m.windowsCounter.Inc(1)
case "linux":
@@ -107,7 +105,7 @@ func (m *LocalMetrics) OpenTunnel(t *Tunnel) {
m.otherCounter.Inc(1)
}
switch t.regMsg.Protocol {
switch t.req.Protocol {
case "tcp":
m.tcpTunnelMeter.Mark(1)
case "http":
@@ -156,9 +154,9 @@ func (m *LocalMetrics) Report() {
}
}
type KeenIoRequest struct {
Path string
Body []byte
type KeenIoMetric struct {
Collection string
Event interface{}
}
type KeenIoMetrics struct {
@@ -166,20 +164,54 @@ type KeenIoMetrics struct {
ApiKey string
ProjectToken string
HttpClient http.Client
Requests chan *KeenIoRequest
Metrics chan *KeenIoMetric
}
func NewKeenIoMetrics() *KeenIoMetrics {
func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics {
k := &KeenIoMetrics{
Logger: log.NewPrefixLogger(),
Logger: log.NewPrefixLogger("metrics"),
ApiKey: os.Getenv("KEEN_API_KEY"),
ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"),
Requests: make(chan *KeenIoRequest, 100),
Metrics: make(chan *KeenIoMetric, 1000),
}
go func() {
for req := range k.Requests {
k.AuthedRequest("POST", req.Path, bytes.NewReader(req.Body))
defer func() {
if r := recover(); r != nil {
k.Error("KeenIoMetrics failed: %v", r)
}
}()
batch := make(map[string][]interface{})
batchTimer := time.Tick(batchInterval)
for {
select {
case m := <-k.Metrics:
list, ok := batch[m.Collection]
if !ok {
list = make([]interface{}, 0)
}
batch[m.Collection] = append(list, m.Event)
case <-batchTimer:
// no metrics to report
if len(batch) == 0 {
continue
}
payload, err := json.Marshal(batch)
if err != nil {
k.Error("Failed to serialize metrics payload: %v, %v", batch, err)
} else {
for key, val := range batch {
k.Debug("Reporting %d metrics for %s", len(val), key)
}
k.AuthedRequest("POST", "/events", bytes.NewReader(payload))
}
batch = make(map[string][]interface{})
}
}
}()
@@ -200,13 +232,15 @@ func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (
req.ContentLength = int64(body.Len())
}
requestStartAt := time.Now()
resp, err = k.HttpClient.Do(req)
if err != nil {
k.Error("Failed to send metric event to keen.io %v", err)
} else {
k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds())
defer resp.Body.Close()
if resp.StatusCode != 201 {
if resp.StatusCode != 200 {
bytes, _ := ioutil.ReadAll(resp.Body)
k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes)
}
@@ -219,7 +253,7 @@ func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) {
}
func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) {
buf, err := json.Marshal(struct {
event := struct {
Keen KeenStruct `json:"keen"`
OS string
ClientId string
@@ -238,25 +272,21 @@ func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time,
Keen: KeenStruct{
Timestamp: start.UTC().Format("2006-01-02T15:04:05.000Z"),
},
OS: t.regMsg.OS,
ClientId: t.regMsg.ClientId,
Protocol: t.regMsg.Protocol,
OS: t.ctl.auth.OS,
ClientId: t.ctl.id,
Protocol: t.req.Protocol,
Url: t.url,
User: t.regMsg.User,
Version: t.regMsg.MmVersion,
HttpAuth: t.regMsg.HttpAuth != "",
Subdomain: t.regMsg.Subdomain != "",
User: t.ctl.auth.User,
Version: t.ctl.auth.MmVersion,
HttpAuth: t.req.HttpAuth != "",
Subdomain: t.req.Subdomain != "",
TunnelDuration: time.Since(t.start).Seconds(),
ConnectionDuration: time.Since(start).Seconds(),
BytesIn: in,
BytesOut: out,
})
if err != nil {
k.Error("Error serializing metric %v", err)
} else {
k.Requests <- &KeenIoRequest{Path: "/events/CloseConnection", Body: buf}
}
k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event}
}
func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) {
@@ -267,7 +297,7 @@ type KeenStruct struct {
}
func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
buf, err := json.Marshal(struct {
event := struct {
Keen KeenStruct `json:"keen"`
OS string
ClientId string
@@ -283,22 +313,17 @@ func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
Keen: KeenStruct{
Timestamp: t.start.UTC().Format("2006-01-02T15:04:05.000Z"),
},
OS: t.regMsg.OS,
ClientId: t.regMsg.ClientId,
Protocol: t.regMsg.Protocol,
OS: t.ctl.auth.OS,
ClientId: t.ctl.id,
Protocol: t.req.Protocol,
Url: t.url,
User: t.regMsg.User,
Version: t.regMsg.MmVersion,
User: t.ctl.auth.User,
Version: t.ctl.auth.MmVersion,
//Reason: reason,
Duration: time.Since(t.start).Seconds(),
HttpAuth: t.regMsg.HttpAuth != "",
Subdomain: t.regMsg.Subdomain != "",
})
if err != nil {
k.Error("Error serializing metric %v", err)
return
} else {
k.Requests <- &KeenIoRequest{Path: "/events/CloseTunnel", Body: buf}
HttpAuth: t.req.HttpAuth != "",
Subdomain: t.req.Subdomain != "",
}
k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event}
}

View File

@@ -1,9 +1,11 @@
package server
import (
"encoding/gob"
"fmt"
"net"
"ngrok/cache"
"ngrok/log"
"sync"
"time"
)
@@ -22,32 +24,53 @@ func (url cacheUrl) Size() int {
type TunnelRegistry struct {
tunnels map[string]*Tunnel
affinity *cache.LRUCache
log.Logger
sync.RWMutex
}
func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
manager := &TunnelRegistry{
registry := &TunnelRegistry{
tunnels: make(map[string]*Tunnel),
affinity: cache.NewLRUCache(cacheSize),
Logger: log.NewPrefixLogger("registry", "tun"),
}
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
// to encode or decode any non-primitive types that haven't been "registered"
// with it. Since we store cacheUrl objects, we need to register them here first
// for the encoding/decoding to work
var urlobj cacheUrl
gob.Register(urlobj)
// try to load and then periodically save the affinity cache to file, if specified
if cacheFile != "" {
// load cache entries from file
manager.affinity.LoadItemsFromFile(cacheFile)
err := registry.affinity.LoadItemsFromFile(cacheFile)
if err != nil {
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
}
// save cache periodically to file
manager.SaveCacheThread(cacheFile, cacheSaveInterval)
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
} else {
registry.Info("No affinity cache specified")
}
return manager
return registry
}
// Spawns a goroutine the periodically saves the cache to a file.
func (r *TunnelRegistry) SaveCacheThread(path string, interval time.Duration) {
go func() {
r.Info("Saving affinity cache to %s every %s", path, interval.String())
for {
time.Sleep(interval)
r.affinity.SaveItemsToFile(path)
r.Debug("Saving affinity cache")
err := r.affinity.SaveItemsToFile(path)
if err != nil {
r.Error("Failed to save affinity cache: %v", err)
} else {
r.Info("Saved affinity cache")
}
}
}()
}
@@ -67,17 +90,17 @@ func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
return nil
}
// Register a tunnel with the following process:
// Consult the affinity cache to try to assign a previously used tunnel url if possible
// Generate new urls repeatedly with the urlFn and register until one is available.
func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) string {
var url string
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
ipCacheKey := fmt.Sprintf("client-ip:%s", clientIp)
idCacheKey := fmt.Sprintf("client-id:%s", 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
}
func (r *TunnelRegistry) GetCachedRegistration(t *Tunnel) (url string) {
ipCacheKey, idCacheKey := r.cacheKeys(t)
// check cache for ID first, because we prefer that over IP which might
// not be specific to a user because of NATs
@@ -87,24 +110,42 @@ func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) string {
} else if v, ok := r.affinity.Get(ipCacheKey); ok {
url = string(v.(cacheUrl))
t.Debug("Found registry affinity %s for %s", url, ipCacheKey)
} else {
}
return
}
func (r *TunnelRegistry) RegisterAndCache(url string, t *Tunnel) (err error) {
if err = r.Register(url, t); err == nil {
// we successfully assigned a url, cache it
ipCacheKey, idCacheKey := r.cacheKeys(t)
r.affinity.Set(ipCacheKey, cacheUrl(url))
r.affinity.Set(idCacheKey, cacheUrl(url))
}
return
}
// Register a tunnel with the following process:
// Consult the affinity cache to try to assign a previously used tunnel url if possible
// Generate new urls repeatedly with the urlFn and register until one is available.
func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) (string, error) {
url := r.GetCachedRegistration(t)
if url == "" {
url = urlFn()
}
for {
if err := r.Register(url, t); err != nil {
maxAttempts := 5
for i := 0; i < maxAttempts; i++ {
if err := r.RegisterAndCache(url, t); err != nil {
// pick a new url and try again
url = urlFn()
} else {
// we successfully assigned a url, we're done
// save our choice in the cache
r.affinity.Set(ipCacheKey, cacheUrl(url))
r.affinity.Set(idCacheKey, cacheUrl(url))
return url
return url, nil
}
}
return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts)
}
func (r *TunnelRegistry) Del(url string) {
@@ -118,3 +159,49 @@ func (r *TunnelRegistry) Get(url string) *Tunnel {
defer r.RUnlock()
return r.tunnels[url]
}
// ControlRegistry maps a client ID to Control structures
type ControlRegistry struct {
controls map[string]*Control
log.Logger
sync.RWMutex
}
func NewControlRegistry() *ControlRegistry {
return &ControlRegistry{
controls: make(map[string]*Control),
Logger: log.NewPrefixLogger("registry", "ctl"),
}
}
func (r *ControlRegistry) Get(clientId string) *Control {
r.RLock()
defer r.RUnlock()
return r.controls[clientId]
}
func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) {
r.Lock()
defer r.Unlock()
oldCtl = r.controls[clientId]
if oldCtl != nil {
oldCtl.Replaced(ctl)
}
r.controls[clientId] = ctl
r.Info("Registered control with id %s", clientId)
return
}
func (r *ControlRegistry) Del(clientId string) error {
r.Lock()
defer r.Unlock()
if r.controls[clientId] == nil {
return fmt.Errorf("No control found for client id: %s", clientId)
} else {
r.Info("Removed control registry id %s", clientId)
delete(r.controls, clientId)
return nil
}
}

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,18 +8,27 @@ import (
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"ngrok/util"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
var defaultPortMap = map[string]int{
"http": 80,
"https": 443,
"smtp": 25,
}
/**
* Tunnel: A control connection, metadata and proxy connections which
* route public traffic to a firewalled endpoint.
*/
type Tunnel struct {
regMsg *msg.RegMsg
// request that opened the tunnel
req *msg.ReqTunnel
// time when the tunnel was opened
start time.Time
@@ -33,9 +42,6 @@ type Tunnel struct {
// control connection
ctl *Control
// proxy connections
proxies chan conn.Conn
// logger
log.Logger
@@ -43,65 +49,128 @@ type Tunnel struct {
closing int32
}
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
// Common functionality for registering virtually hosted protocols
func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
vhost := os.Getenv("VHOST")
if vhost == "" {
vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort)
}
// Canonicalize virtual host by removing default port (e.g. :80 on HTTP)
defaultPort, ok := defaultPortMap[protocol]
if !ok {
return fmt.Errorf("Couldn't find default port for protocol %s", protocol)
}
defaultPortSuffix := fmt.Sprintf(":%d", defaultPort)
if strings.HasSuffix(vhost, defaultPortSuffix) {
vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)]
}
// Canonicalize by always using lower-case
vhost = strings.ToLower(vhost)
// Register for specific hostname
hostname := strings.ToLower(strings.TrimSpace(t.req.Hostname))
if hostname != "" {
t.url = fmt.Sprintf("%s://%s", protocol, hostname)
return tunnelRegistry.Register(t.url, t)
}
// Register for specific subdomain
subdomain := strings.ToLower(strings.TrimSpace(t.req.Subdomain))
if subdomain != "" {
t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost)
return tunnelRegistry.Register(t.url, t)
}
// Register for random URL
t.url, err = tunnelRegistry.RegisterRepeat(func() string {
return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost)
}, t)
return
}
// Create a new tunnel from a registration message received
// on a control channel
func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
t = &Tunnel{
regMsg: m,
start: time.Now(),
ctl: ctl,
proxies: make(chan conn.Conn),
Logger: log.NewPrefixLogger(),
req: m,
start: time.Now(),
ctl: ctl,
Logger: log.NewPrefixLogger(),
}
failReg := func(err error) {
t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()}
}
switch t.regMsg.Protocol {
proto := t.req.Protocol
switch proto {
case "tcp":
var err error
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
bindTcp := func(port int) error {
if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil {
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return err
}
if err != nil {
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"}
// register it
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
return err
}
go t.listenTcp(t.listener)
return nil
}
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port)
if err = tunnels.Register(t.url, t); err != nil {
// This should never be possible because the OS will only assign
// available ports to us.
t.Error("TCP listener bound, but failed to register: %s", err.Error())
t.listener.Close()
failReg(err)
// use the custom remote port you asked for
if t.req.RemotePort != 0 {
bindTcp(int(t.req.RemotePort))
return
}
go t.listenTcp(t.listener)
case "http":
if strings.TrimSpace(t.regMsg.Hostname) != "" {
t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, domain)
}
if t.url != "" {
if err := tunnels.Register(t.url, t); err != nil {
failReg(err)
return
// try to return to you the same port you had before
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
if cachedUrl != "" {
var port int
parts := strings.Split(cachedUrl, ":")
portPart := parts[len(parts)-1]
port, err = strconv.Atoi(portPart)
if err != nil {
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
} else {
// we have a valid, cached port, let's try to bind with it
if bindTcp(port) != nil {
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
} else {
// success, we're done
return
}
}
} else {
t.url = tunnels.RegisterRepeat(func() string {
return fmt.Sprintf("http://%x.%s", rand.Int31(), domain)
}, t)
}
}
if m.Version != version.Proto {
failReg(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version))
// Bind for TCP connections
bindTcp(0)
return
case "http", "https":
l, ok := listeners[proto]
if !ok {
err = fmt.Errorf("Not listeneing for %s connections", proto)
return
}
if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil {
return
}
default:
err = fmt.Errorf("Protocol %s is not supported", proto)
return
}
@@ -110,35 +179,31 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth))
}
t.ctl.conn.AddLogPrefix(t.Id())
t.AddLogPrefix(t.Id())
t.Info("Registered new tunnel")
t.ctl.out <- &msg.RegAckMsg{
Url: t.url,
ProxyAddr: fmt.Sprintf("%s", proxyAddr),
Version: version.Proto,
MmVersion: version.MajorMinor(),
}
t.Info("Registered new tunnel on: %s", t.ctl.conn.Id())
metrics.OpenTunnel(t)
return
}
func (t *Tunnel) shutdown() {
func (t *Tunnel) Shutdown() {
t.Info("Shutting down")
// mark that we're shutting down
atomic.StoreInt32(&t.closing, 1)
// if we have a public listener (this is a raw TCP tunnel, shut it down
// if we have a public listener (this is a raw TCP tunnel), shut it down
if t.listener != nil {
t.listener.Close()
}
// remove ourselves from the tunnel registry
tunnels.Del(t.url)
tunnelRegistry.Del(t.url)
// XXX: shut down all of the proxy connections?
// let the control connection know we're shutting down
// currently, only the control connection shuts down tunnels,
// so it doesn't need to know about it
// t.ctl.stoptunnel <- t
metrics.CloseTunnel(t)
}
@@ -147,9 +212,7 @@ func (t *Tunnel) Id() string {
return t.url
}
/**
* Listens for new public tcp connections from the internet.
*/
// Listens for new public tcp connections from the internet.
func (t *Tunnel) listenTcp(listener *net.TCPListener) {
for {
defer func() {
@@ -190,20 +253,47 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
startTime := time.Now()
metrics.OpenConnection(t, publicConn)
t.Debug("Requesting new proxy connection")
t.ctl.out <- &msg.ReqProxyMsg{}
var proxyConn conn.Conn
var err error
for i := 0; i < (2 * proxyMaxPoolSize); i++ {
// get a proxy connection
if proxyConn, err = t.ctl.GetProxy(); err != nil {
t.Warn("Failed to get proxy connection: %v", err)
return
}
defer proxyConn.Close()
t.Info("Got proxy connection %s", proxyConn.Id())
proxyConn.AddLogPrefix(t.Id())
proxyConn := <-t.proxies
t.Info("Returning proxy connection %s", proxyConn.Id())
// tell the client we're going to start using this proxy connection
startPxyMsg := &msg.StartProxy{
Url: t.url,
ClientAddr: publicConn.RemoteAddr().String(),
}
defer proxyConn.Close()
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i)
proxyConn.Close()
} else {
// success
break
}
}
if err != nil {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
// To reduce latency handling tunnel connections, we employ the following curde heuristic:
// Whenever we take a proxy connection from the pool, replace it with a new one
util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} })
// no timeouts while connections are joined
proxyConn.SetDeadline(time.Time{})
// join the public and proxy connections
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)
}
func (t *Tunnel) RegisterProxy(conn conn.Conn) {
t.Info("Registered proxy connection %s", conn.Id())
conn.AddLogPrefix(t.Id())
t.proxies <- conn
}

33
src/ngrok/util/errors.go Normal file
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,11 +2,32 @@ package util
import (
"crypto/rand"
"encoding/binary"
"fmt"
mrand "math/rand"
)
// create a random identifier for this client
func RandId(idlen int) (id string, err error) {
func RandomSeed() (seed int64, err error) {
err = binary.Read(rand.Reader, binary.LittleEndian, &seed)
return
}
// creates a random identifier of the specified length
func RandId(idlen int) string {
b := make([]byte, idlen)
var randVal uint32
for i := 0; i < idlen; i++ {
byteIdx := i % 4
if byteIdx == 0 {
randVal = mrand.Uint32()
}
b[i] = byte((randVal >> (8 * uint(byteIdx))) & 0xFF)
}
return fmt.Sprintf("%x", b)
}
// like RandId, but uses a crypto/rand for secure random identifiers
func SecureRandId(idlen int) (id string, err error) {
b := make([]byte, idlen)
n, err := rand.Read(b)
@@ -23,8 +44,8 @@ func RandId(idlen int) (id string, err error) {
return
}
func RandIdOrPanic(idlen int) string {
id, err := RandId(idlen)
func SecureRandIdOrPanic(idlen int) string {
id, err := SecureRandId(idlen)
if err != nil {
panic(err)
}

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

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