72 Commits
0.16 ... 1.4

Author SHA1 Message Date
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
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
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
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
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
a2cf6b70c1 allow the web interface to be disabled 2013-08-17 21:22:17 +02:00
Alan Shreve
8d7081e747 bump version 2013-08-17 12:19:19 +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
8054e64ad7 bump version 2013-08-17 11:17:09 +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
aa99cc1cd4 send user id in update request 2013-08-15 13:09:23 +02:00
Alan Shreve
01167cb136 bump version 2013-08-15 12:42:18 +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
77e3f140ca separate cache keys for each protocol 2013-07-30 16:06:35 +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
48 changed files with 2367 additions and 1182 deletions

1
.gitignore vendored
View File

@@ -4,5 +4,6 @@ pkg/
src/code.google.com
src/github.com
src/bitbucket.org
src/launchpad.net
src/ngrok/client/assets/assets_release.go
src/ngrok/server/assets/assets_release.go

View File

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

View File

@@ -36,4 +36,4 @@ release-all: release-client release-server
all: fmt client server
clean:
go clean ngrok/...
go clean -i -r ngrok/...

View File

@@ -23,7 +23,14 @@
background-color: #ff9999;
background-color: #000000;
color:white;
}
.path {
width: 300px;
}
.wrapped {
word-wrap: break-word;
word-break: break-word;
overflow: hidden;
}
</style>
</head>
@@ -48,7 +55,12 @@
<div class="span6 offset3">
<div class="well" style="padding: 20px 50px;">
<h4>No requests to display yet</h4>
<p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
<hr />
<h5>To get started, make a request to one of your tunnel URLs:</h5>
<ul>
<li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
</ul>
</p>
</div>
</div>
</div>
@@ -57,7 +69,7 @@
<h4>All Requests</h4>
<table class="table txn-selector">
<tr ng-controller="TxnNavItem" ng-class="{'selected':isActive()}" ng-repeat="txn in txns" ng-click="makeActive()">
<td>{{ txn.Req.MethodPath }}</td>
<td class="wrapped"><div class="path">{{ txn.Req.MethodPath }}</div></td>
<td>{{ txn.Resp.Status }}</td>
<td><span class="pull-right">{{ txn.Duration }}</span></td>
</tr>
@@ -76,12 +88,12 @@
</div>
<div class="span4">
<i class="icon-user"></i> IP
<span style="margin-left: 8px;" class="muted">{{Txn.Req.Header['X-Real-Ip'][0]}}</span>
<span style="margin-left: 8px;" class="muted">{{Txn.ConnCtx.ClientAddr.split(":")[0]}}</span>
</div>
</div>
<hr />
<div ng-show="!!Req" ng-controller="HttpRequest">
<h3>{{ Req.MethodPath }}</h3>
<h3 class="wrapped">{{ Req.MethodPath }}</h3>
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
</div>

View File

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

View File

@@ -1,17 +1,57 @@
# Changelog
## 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
- 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

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

@@ -1,64 +0,0 @@
package client
/*
Functions for reading and writing the auth token from the user's
home directory.
*/
import (
"io/ioutil"
"ngrok/log"
"os"
"os/user"
"path"
"sync"
)
var (
once sync.Once
currentAuthToken string
authTokenFile string
)
func initAuth() {
user, err := user.Current()
// user.Current() does not work on linux when cross compilling because
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
homeDir := os.Getenv("HOME")
if err != nil {
log.Warn("Failed to get user's home directory: %s", err.Error())
} else {
homeDir = user.HomeDir
}
authTokenFile = path.Join(homeDir, ".ngrok")
log.Debug("Reading auth token from file %s", authTokenFile)
tokenBytes, err := ioutil.ReadFile(authTokenFile)
if err == nil {
currentAuthToken = string(tokenBytes)
} else {
log.Warn("Failed to read ~/.ngrok for auth token: %s", err.Error())
}
}
// Load the auth token from file
func LoadAuthToken() string {
once.Do(initAuth)
return currentAuthToken
}
// Save the auth token to file
func SaveAuthToken(token string) {
if token == "" || token == LoadAuthToken() || authTokenFile == "" {
return
}
perms := os.FileMode(0644)
err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
if err != nil {
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
}
}

View File

@@ -1,108 +1,72 @@
package client
import (
"errors"
"flag"
"fmt"
"net"
"ngrok/version"
"os"
"strconv"
)
var (
PORT_OUT_OF_RANGE error = errors.New("Port number must be between 1 and 65535")
)
const usage1 string = `Usage: %s [OPTIONS] <local port or address>
Options:
`
const usage2 string = `
Examples:
ngrok 80
ngrok -subdomain=example 8080
ngrok -proto=tcp 22
ngrok -hostname="example.com" -httpauth="user:password" 10.0.0.1
Advanced usage: ngrok [OPTIONS] <command> [command args] [...]
Commands:
ngrok start [tunnel] [...] Start tunnels by name from config file
ngrok help Print help
ngrok version Print ngrok version
Examples:
ngrok start www api blog pubsub
ngrok -log=stdout -config=ngrok.yml start ssh
ngrok version
`
type Options struct {
server string
httpAuth string
hostname string
localaddr string
protocol string
url string
subdomain string
webport int
config string
logto string
authtoken string
httpauth string
hostname string
protocol string
subdomain string
command string
args []string
}
func fail(msg string, args ...interface{}) {
fmt.Printf(msg+"\n", args...)
flag.PrintDefaults()
os.Exit(1)
}
func parsePort(portString string) (err error) {
var port int
if port, err = strconv.Atoi(portString); err != nil {
return err
func parseArgs() (opts *Options, err error) {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage1, os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, usage2)
}
if port < 1 || port > 65535 {
return PORT_OUT_OF_RANGE
}
config := flag.String(
"config",
"",
"Path to ngrok configuration file. (default: $HOME/.ngrok)")
return
}
logto := flag.String(
"log",
"none",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
// Local address could be a port of a host:port string
// we always return a host:port string from this function or fail
func parseLocalAddr() string {
if flag.NArg() == 0 {
fail("LOCAL not specified, specify a port number or host:port connection string")
}
if flag.NArg() > 1 {
fail("Only one LOCAL may be specified, not %d", flag.NArg())
}
addr := flag.Arg(0)
// try to parse as a port number
if err := parsePort(addr); err == nil {
return fmt.Sprintf("127.0.0.1:%s", addr)
} else if err == PORT_OUT_OF_RANGE {
fail("%s is not in the valid port range 1-65535")
}
// try to parse as a connection string
_, port, err := net.SplitHostPort(addr)
if err != nil {
fail("%v", err)
}
if parsePort(port) != nil {
fail("'%s' is not a valid port number (1-65535)", port)
}
return addr
}
func parseProtocol(proto string) string {
switch proto {
case "http":
fallthrough
case "tcp":
return proto
default:
fail("%s is not a valid protocol", proto)
}
panic("unreachable")
}
func parseArgs() *Options {
authtoken := flag.String(
"authtoken",
"",
"Authentication token for identifying a premium ngrok.com account")
"Authentication token for identifying an ngrok.com account")
server := flag.String(
"server",
"ngrok.com:4443",
"The remote ngrok server")
httpAuth := flag.String(
httpauth := flag.String(
"httpauth",
"",
"username:password HTTP basic auth creds protecting the public tunnel endpoint")
@@ -110,7 +74,7 @@ func parseArgs() *Options {
subdomain := flag.String(
"subdomain",
"",
"Request a custom subdomain from the ngrok server. (HTTP mode only)")
"Request a custom subdomain from the ngrok server. (HTTP only)")
hostname := flag.String(
"hostname",
@@ -119,40 +83,46 @@ func parseArgs() *Options {
protocol := flag.String(
"proto",
"http",
"The protocol of the traffic over the tunnel {'http', 'tcp'} (default: 'http')")
webport := flag.Int(
"webport",
4040,
"The port on which the web interface is served")
logto := flag.String(
"log",
"none",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
v := flag.Bool(
"version",
false,
"Print ngrok version and exit")
"http+https",
"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
flag.Parse()
if *v {
fmt.Println(version.MajorMinor())
os.Exit(0)
}
return &Options{
server: *server,
httpAuth: *httpAuth,
subdomain: *subdomain,
localaddr: parseLocalAddr(),
protocol: parseProtocol(*protocol),
webport: *webport,
opts = &Options{
config: *config,
logto: *logto,
httpauth: *httpauth,
subdomain: *subdomain,
protocol: *protocol,
authtoken: *authtoken,
hostname: *hostname,
command: flag.Arg(0),
}
switch opts.command {
case "start":
opts.args = flag.Args()[1:]
case "version":
fmt.Println(version.MajorMinor())
os.Exit(0)
case "help":
flag.Usage()
os.Exit(0)
case "":
err = fmt.Errorf("You must specify a local port to tunnel or an ngrok command.")
return
default:
if len(flag.Args()) > 1 {
err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v",
len(flag.Args()),
flag.Args())
return
}
opts.command = "default"
opts.args = flag.Args()
}
return
}

263
src/ngrok/client/config.go Normal file
View File

@@ -0,0 +1,263 @@
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"`
}
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, 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

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

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

@@ -0,0 +1,427 @@
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,
}
// 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 {
log.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 {
log.Error("Server failed to write StartProxy: %v", err)
return
}
tunnel, ok := c.tunnels[startPxy.Url]
if !ok {
c.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,44 +0,0 @@
package client
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/client/ui"
"ngrok/proto"
"ngrok/version"
)
// client state
type State struct {
id string
publicUrl string
serverVersion string
update ui.UpdateStatus
protocol proto.Protocol
opts *Options
metrics *ClientMetrics
// just for UI purposes
status string
}
// implement client.ui.State
func (s State) GetClientVersion() string { return version.MajorMinor() }
func (s State) GetServerVersion() string { return s.serverVersion }
func (s State) 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) GetUpdate() ui.UpdateStatus { return s.update }
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,29 +0,0 @@
package ui
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/proto"
)
type UpdateStatus int
const (
UpdateNone = -1 * iota
UpdateInstalling
UpdateReady
UpdateError
)
type State interface {
GetClientVersion() string
GetServerVersion() string
GetUpdate() UpdateStatus
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

@@ -3,9 +3,9 @@
package client
import (
"ngrok/client/ui"
"ngrok/client/mvc"
)
// no auto-updating in debug mode
func autoUpdate(s *State, ctl *ui.Controller, token string) {
func autoUpdate(state mvc.State, token string) {
}

View File

@@ -6,7 +6,7 @@ import (
update "github.com/inconshreveable/go-update"
"net/http"
"net/url"
"ngrok/client/ui"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/version"
"runtime"
@@ -15,82 +15,109 @@ import (
const (
updateEndpoint = "https://dl.ngrok.com/update"
checkEndpoint = "https://dl.ngrok.com/update/check"
)
func autoUpdate(s *State, ctl *ui.Controller, token string) {
update := func() (updateSuccessful bool) {
params := make(url.Values)
params.Add("version", version.MajorMinor())
params.Add("os", runtime.GOOS)
params.Add("arch", runtime.GOARCH)
download := update.NewDownload()
downloadComplete := make(chan int)
go func() {
for {
select {
case progress, ok := <-download.Progress:
if !ok {
close(downloadComplete)
return
} else if progress == 100 {
s.update = ui.UpdateInstalling
ctl.Update(s)
close(downloadComplete)
return
} else {
if progress%25 == 0 {
log.Info("Downloading update %d%% complete", progress)
}
s.update = ui.UpdateStatus(progress)
ctl.Update(s)
}
}
}
}()
log.Info("Checking for update")
err := download.UpdateFromUrl(updateEndpoint + "?" + params.Encode())
<-downloadComplete
if err != nil {
log.Error("Error while updating ngrok: %v", err)
if download.Available {
s.update = ui.UpdateError
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 {
s.update = ui.UpdateNone
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())
}
// record the error to ngrok.com's servers for debugging purposes
// log error to ngrok.com's servers for debugging purposes
params.Add("error", err.Error())
params.Add("user", token)
resp, err := http.PostForm("https://dl.ngrok.com/update/error", params)
resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params)
if err != nil {
log.Error("Error while reporting update error")
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 {
log.Info("Marked update ready")
s.update = ui.UpdateReady
updateSuccessful = true
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 {
log.Info("No update available at this time")
s.update = ui.UpdateNone
// tell the user the update is ready
log.Info("Update ready!")
s.SetUpdateStatus(mvc.UpdateReady)
}
}
ctl.Update(s)
return
}
// try to update immediately and then at a set interval
update()
for _ = range time.Tick(updateCheckInterval) {
if update() {
// stop trying to update if the update function is successful
// XXX: improve this by trying to download versions newer than
// the last one we downloaded
return
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,16 +65,16 @@ func (v *TermView) Render() {
v.Printf(v.w-len(quitMsg), 0, quitMsg)
// new version message
updateStatus := v.state.GetUpdate()
updateStatus := state.GetUpdateStatus()
var updateMsg string
switch updateStatus {
case ui.UpdateNone:
case mvc.UpdateNone:
updateMsg = ""
case ui.UpdateInstalling:
case mvc.UpdateInstalling:
updateMsg = "ngrok is updating"
case ui.UpdateReady:
case mvc.UpdateReady:
updateMsg = "ngrok has updated: restart ngrok for the new version"
case ui.UpdateError:
case mvc.UpdateAvailable:
updateMsg = "new version available at https://ngrok.com"
default:
pct := float64(updateStatus) / 100.0
@@ -111,47 +98,65 @@ func (v *TermView) Render() {
}
v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
statusStr, statusColor := connStatusRepr(state.GetConnStatus())
v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr)
status := v.state.GetStatus()
v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
v.Printf(0, 3, "%-30s%s/%s", "Version", 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()
@@ -160,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 {
@@ -153,22 +153,16 @@ func (whv *WebHttpView) updateHttp() {
// we haven't processed this transaction yet if we haven't set the
// user data
if htxn.UserData == nil {
id, err := util.RandId(8)
if 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,
@@ -178,10 +172,11 @@ func (whv *WebHttpView) updateHttp() {
Body: body,
Binary: !utf8.Valid(rawReq),
},
Start: htxn.Start.Unix(),
Start: htxn.Start.Unix(),
ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext),
}
htxn.UserData = whtxn
htxn.UserCtx = whtxn
// XXX: unsafe map access from multiple go routines
whv.idToTxn[whtxn.Id] = whtxn
// XXX: use return value to delete from map so we don't leak memory
@@ -189,11 +184,11 @@ func (whv *WebHttpView) updateHttp() {
} else {
rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
if err != nil {
log.Error("Failed to dump response: %v", err)
whv.Error("Failed to dump response: %v", err)
continue
}
txn := htxn.UserData.(*SerializedTxn)
txn := htxn.UserCtx.(*SerializedTxn)
body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes)
txn.Duration = htxn.Duration.Nanoseconds()
txn.Resp = SerializedResponse{
@@ -206,43 +201,46 @@ func (whv *WebHttpView) updateHttp() {
payload, err := json.Marshal(txn)
if err != nil {
log.Error("Failed to serialized txn payload for websocket: %v", err)
whv.Error("Failed to serialized txn payload for websocket: %v", err)
}
whv.webview.wsMessages.In() <- payload
}
}
}
func (v *WebHttpView) updateUiState() {
var s SerializedUiState
for {
select {
case obj := <-v.updates:
uiState := obj.(ui.State)
s.Url = uiState.GetPublicUrl()
case v.state <- s:
}
}
}
func (h *WebHttpView) register() {
func (whv *WebHttpView) register() {
http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
whv.Error("Replay failed: %v", err)
http.Error(w, err, 500)
}
}()
r.ParseForm()
txnid := r.Form.Get("txnid")
if txn, ok := h.idToTxn[txnid]; ok {
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)
@@ -251,8 +249,8 @@ func (h *WebHttpView) register() {
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
payloadData := SerializedPayload{
Txns: h.HttpRequests.Slice(),
UiState: <-h.state,
Txns: whv.HttpRequests.Slice(),
UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
}
payload, err := json.Marshal(payloadData)
@@ -266,3 +264,6 @@ func (h *WebHttpView) register() {
}
})
}
func (whv *WebHttpView) Shutdown() {
}

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,11 +4,13 @@ import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"ngrok/log"
)
@@ -16,9 +18,10 @@ type Conn interface {
net.Conn
log.Logger
Id() string
SetType(string)
}
type tcpConn struct {
type loggedConn struct {
net.Conn
log.Logger
id int32
@@ -26,83 +29,146 @@ 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}
func wrapConn(conn net.Conn, typ string) *loggedConn {
c := &loggedConn{conn, log.NewPrefixLogger(), rand.Int31(), typ}
c.AddLogPrefix(c.Id())
return c
}
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 {
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 *loggedConn) StartTLS(tlsCfg *tls.Config) {
c.Conn = tls.Client(c.Conn, tlsCfg)
}
func (c *loggedConn) Close() error {
c.Debug("Closing")
return c.Conn.Close()
}
func (c *tcpConn) Id() string {
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 Join(c Conn, c2 Conn) (int64, int64) {
done := make(chan error)
pipe := func(to Conn, from Conn, bytesCopied *int64) {
@@ -129,23 +195,23 @@ func Join(c Conn, c2 Conn) (int64, int64) {
}
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
@@ -75,6 +76,10 @@ func (pl *PrefixLogger) AddLogPrefix(prefix string) {
pl.prefix += "[" + prefix + "]"
}
func (pl *PrefixLogger) ClearLogPrefixes() {
pl.prefix = ""
}
// we should never really use these . . . always prefer logging through a prefix logger
func Debug(arg0 string, args ...interface{}) {
root.Debug(arg0, args...)

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,87 @@ 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
Hostname string
Subdomain string
HttpAuth string
}
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,15 @@ 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{}) {
for {
req, err := http.ReadRequest(tee.WriteBuffer())
if err != nil {
@@ -79,7 +81,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)
@@ -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,11 +1,15 @@
package server
import (
"fmt"
"io"
"ngrok/conn"
"ngrok/msg"
"ngrok/util"
"ngrok/version"
"runtime/debug"
"strings"
"sync/atomic"
"time"
)
@@ -15,34 +19,134 @@ const (
)
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)
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// put a message in this channel to send it over
// conn to the client and then terminate this
// control connection and all of its tunnels
stop chan (msg.Message)
// heartbeat
// 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
// closing indicator
closing int32
// identifier
id string
}
func NewControl(conn conn.Conn) {
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
// channels are buffered because we read and write to them
// from the same goroutine in managerThread()
c := &Control{
conn: conn,
out: make(chan (interface{}), 1),
in: make(chan (msg.Message), 1),
stop: make(chan (msg.Message), 1),
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message, 5),
in: make(chan msg.Message, 5),
stop: make(chan msg.Message, 5),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
}
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
}
}
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
controlRegistry.Add(c.id, c)
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{}
// set logging prefix
ctlConn.SetType("ctl")
// manage the connection
go c.managerThread()
go c.readThread()
}
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
tunnelReq := *rawTunnelReq
tunnelReq.Protocol = proto
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
if err != nil {
ack := &msg.NewTunnel{Error: err.Error()}
if len(c.tunnels) == 0 {
// you can't fail your first tunnel registration
// terminate the control connection
c.stop <- ack
} else {
// inform client of failure
c.out <- ack
}
// we're done
return
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
func (c *Control) managerThread() {
reap := time.NewTicker(connReapInterval)
@@ -51,13 +155,33 @@ func (c *Control) managerThread() {
if err := recover(); err != nil {
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
}
// remove from the control registry
controlRegistry.Del(c.id)
// mark that we're shutting down
atomic.StoreInt32(&c.closing, 1)
// stop the reaping timer
reap.Stop()
// close the connection
c.conn.Close()
// shutdown the tunnel if it's open
if c.tun != nil {
c.tun.shutdown()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// we're safe to close(c.proxies) because c.closing
// protects us inside of RegisterProxy
close(c.proxies)
// shut down all of the proxy connections
for p := range c.proxies {
p.Close()
}
}()
for {
@@ -65,33 +189,26 @@ func (c *Control) managerThread() {
case m := <-c.out:
msg.WriteMsg(c.conn, m)
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
return
}
case m := <-c.stop:
if m != nil {
msg.WriteMsg(c.conn, m)
}
return
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
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{}
}
}
}
@@ -119,3 +236,60 @@ func (c *Control) readThread() {
}
}
}
func (c *Control) RegisterProxy(conn conn.Conn) {
if atomic.LoadInt32(&c.closing) == 1 {
c.conn.Debug("Can't register proxies for a control that is closing")
conn.Close()
return
}
select {
case c.proxies <- conn:
c.conn.Info("Registered proxy connection %s", conn.Id())
default:
// c.proxies buffer is full, discard this one
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) {
// initial timeout is zero to try to get a proxy connection without asking for one
timeout := time.NewTimer(0)
// get a proxy connection. if we timeout, request one over the control channel
for proxyConn == nil {
var ok bool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
continue
case <-timeout.C:
c.conn.Debug("Requesting new proxy connection")
// request a proxy connection
c.out <- &msg.ReqProxy{}
// timeout after 1 second if we don't get one
timeout.Reset(1 * time.Second)
}
}
// To try to reduce latency hanndling tunnel connections, we employ
// the following curde heuristic:
// If the proxy connection pool is empty, request a new one.
// The idea is to always have at least one proxy connection available for immediate use.
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
// It should be a good start though.
if len(c.proxies) == 0 {
c.out <- &msg.ReqProxy{}
}
return
}

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
}
// If the client specified http auth and it doesn't match this request's auth
// then fail the request with 401 Not Authorized and request the client reissue the
// request with basic authdeny the request
if tunnel.regMsg.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
if tunnel.req.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.req.HttpAuth {
conn.Info("Authentication failed: %s", req.Header.Get("Authorization"))
conn.Write([]byte(NotAuthorized))
return
}
// dead connections will now be handled by tunnel heartbeating and the client
conn.SetDeadline(time.Time{})
// let the tunnel handle the connection now
tunnel.HandlePublicConnection(conn)
}

View File

@@ -1,126 +1,133 @@
package server
import (
"flag"
"fmt"
"net"
"crypto/tls"
"math/rand"
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"ngrok/util"
"os"
"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
defer func() {
if r := recover(); r != nil {
conn.Warn("Failed with error: %v", r)
conn.Close()
}
}()
// read the proxy register message
var regPxy msg.RegProxyMsg
if err = msg.ReadMsgInto(conn, &regPxy); err != nil {
panic(err)
go func(tunnelConn conn.Conn) {
tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout))
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
tunnelConn.Error("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

@@ -23,8 +23,6 @@ func init() {
} 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":
@@ -171,7 +169,7 @@ type KeenIoMetrics struct {
func NewKeenIoMetrics() *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),
@@ -238,14 +236,14 @@ 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,
@@ -283,16 +281,16 @@ 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 != "",
HttpAuth: t.req.HttpAuth != "",
Subdomain: t.req.Subdomain != "",
})
if err != nil {

View File

@@ -32,7 +32,7 @@ func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
registry := &TunnelRegistry{
tunnels: make(map[string]*Tunnel),
affinity: cache.NewLRUCache(cacheSize),
Logger: log.NewPrefixLogger("registry"),
Logger: log.NewPrefixLogger("registry", "tun"),
}
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
@@ -42,14 +42,13 @@ func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
var urlobj cacheUrl
gob.Register(urlobj)
// try to load and then periodically save the affinity cache to file, if specified
if cacheFile != "" {
// load cache entries from file
err := registry.affinity.LoadItemsFromFile(cacheFile)
if err != nil {
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
}
// save cache periodically to file
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
} else {
registry.Info("No affinity cache specified")
@@ -93,9 +92,11 @@ func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) {
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
clientId := t.regMsg.ClientId
clientId := t.ctl.id
return fmt.Sprintf("client-ip:%s", clientIp), 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) {
@@ -158,3 +159,42 @@ 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) {
r.Lock()
defer r.Unlock()
r.controls[clientId] = ctl
r.Info("Registered control with id %s", clientId)
}
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,19 +8,26 @@ import (
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"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
@@ -34,9 +41,6 @@ type Tunnel struct {
// control connection
ctl *Control
// proxy connections
proxies chan conn.Conn
// logger
log.Logger
@@ -44,27 +48,66 @@ type Tunnel struct {
closing int32
}
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
// Common functionality for registering virtually hosted protocols
func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
vhost := os.Getenv("VHOST")
if vhost == "" {
vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort)
}
// Canonicalize virtual host by removing default port (e.g. :80 on HTTP)
defaultPort, ok := defaultPortMap[protocol]
if !ok {
return fmt.Errorf("Couldn't find default port for protocol %s", protocol)
}
defaultPortSuffix := fmt.Sprintf(":%d", defaultPort)
if strings.HasSuffix(vhost, defaultPortSuffix) {
vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)]
}
// Canonicalize by always using lower-case
vhost = strings.ToLower(vhost)
// Register for specific hostname
hostname := strings.ToLower(strings.TrimSpace(t.req.Hostname))
if hostname != "" {
t.url = fmt.Sprintf("%s://%s", protocol, hostname)
return tunnelRegistry.Register(t.url, t)
}
// Register for specific subdomain
subdomain := strings.ToLower(strings.TrimSpace(t.req.Subdomain))
if subdomain != "" {
t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost)
return tunnelRegistry.Register(t.url, t)
}
// Register for random URL
t.url, err = tunnelRegistry.RegisterRepeat(func() string {
return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost)
}, t)
return
}
// Create a new tunnel from a registration message received
// on a control channel
func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
t = &Tunnel{
regMsg: m,
start: time.Now(),
ctl: ctl,
proxies: make(chan conn.Conn),
Logger: log.NewPrefixLogger(),
req: m,
start: time.Now(),
ctl: ctl,
Logger: log.NewPrefixLogger(),
}
failReg := func(err error) {
t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()}
}
var err error
switch t.regMsg.Protocol {
proto := t.req.Protocol
switch proto {
case "tcp":
var port int = 0
// try to return to you the same port you had before
cachedUrl := tunnels.GetCachedRegistration(t)
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
if cachedUrl != "" {
parts := strings.Split(cachedUrl, ":")
portPart := parts[len(parts)-1]
@@ -87,51 +130,38 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
// we tried to bind with a random port and failed (no more ports available?)
if err != nil {
failReg(t.ctl.conn.Error("Error binding TCP listener: %v", err))
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return
}
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port)
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
// register it
if err = tunnels.RegisterAndCache(t.url, t); err != nil {
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()
failReg(fmt.Errorf("TCP listener bound, but failed to register %s", t.url))
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
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)
case "http", "https":
l, ok := listeners[proto]
if !ok {
err = fmt.Errorf("Not listeneing for %s connections", proto)
return
}
if t.url != "" {
if err := tunnels.Register(t.url, t); err != nil {
failReg(err)
return
}
} else {
t.url, err = tunnels.RegisterRepeat(func() string {
return fmt.Sprintf("http://%x.%s", rand.Int31(), domain)
}, t)
if err != nil {
failReg(err)
return
}
if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil {
return
}
}
if m.Version != version.Proto {
failReg(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version))
default:
err = fmt.Errorf("Protocol %s is not supported", proto)
return
}
@@ -140,35 +170,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)
}
@@ -177,9 +203,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() {
@@ -220,20 +244,39 @@ 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 attempts int
var err error
for {
// 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(),
}
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
attempts += 1
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
if attempts > 3 {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
} else {
// success
break
}
}
defer proxyConn.Close()
// 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
}

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

21
src/ngrok/util/trace.go Normal file
View File

@@ -0,0 +1,21 @@
package util
import (
"fmt"
"runtime"
)
const crashMessage = `Oh noes! ngrok crashed!
Please submit the stack trace and any relevant information to:
github.com/inconshreveable/ngrok/issues
panic: %v
%s`
func MakePanicTrace(err interface{}) string {
stackBuf := make([]byte, 4096)
n := runtime.Stack(stackBuf, false)
return fmt.Sprintf(crashMessage, err, stackBuf[:n])
}

View File

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