Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c00a3fd8b | ||
|
|
7a9f972030 | ||
|
|
264fbe83fe | ||
|
|
3f8e4b8e0b | ||
|
|
5ddfa6232c | ||
|
|
cda0e2c050 | ||
|
|
69733b1e3c | ||
|
|
92569be139 | ||
|
|
c72868cbf5 | ||
|
|
e58d35972d | ||
|
|
98902cff73 | ||
|
|
a269effc05 | ||
|
|
ed2abcaa95 | ||
|
|
ea93073174 | ||
|
|
f2009d7f52 | ||
|
|
f956a32d64 | ||
|
|
85723df89b | ||
|
|
1860b0a1e1 | ||
|
|
f20d426690 | ||
|
|
d0c3f9cd03 | ||
|
|
148702929b | ||
|
|
3af3e39b20 | ||
|
|
ef9aa6251d | ||
|
|
4ce53025e1 | ||
|
|
46dd79a949 | ||
|
|
d08b89020f | ||
|
|
39bebc5e25 | ||
|
|
5335f85202 | ||
|
|
0134667055 | ||
|
|
9de09f3f1d | ||
|
|
0ca93d33aa | ||
|
|
0468760901 | ||
|
|
c47241cf3a | ||
|
|
54b2056c12 | ||
|
|
1d93622aec | ||
|
|
c7c9a2d07a | ||
|
|
f136b8486e | ||
|
|
7fc854b620 | ||
|
|
3d8f09ba08 | ||
|
|
ed5729bd63 | ||
|
|
ce2b352574 | ||
|
|
179a76b8cf | ||
|
|
8d044e722c | ||
|
|
5608df7ec5 | ||
|
|
7bf4547911 | ||
|
|
7b25fe80e0 | ||
|
|
97fcb417ac | ||
|
|
22537c0136 | ||
|
|
11dfbc7045 | ||
|
|
824a4b10ab | ||
|
|
13be54d4e7 | ||
|
|
15e017d285 | ||
|
|
00ae5d77db | ||
|
|
a911f7fb65 | ||
|
|
a9f659216e | ||
|
|
264ac352f7 | ||
|
|
d0b9161993 | ||
|
|
fd394cb336 | ||
|
|
8f6637c784 | ||
|
|
eb60b72aa4 | ||
|
|
63c70c4996 | ||
|
|
20364f6de0 | ||
|
|
d914e26055 | ||
|
|
f5a24b30e8 | ||
|
|
bfbfdd9ee9 | ||
|
|
b4082e7f89 | ||
|
|
151433e2fd | ||
|
|
eef7b6b755 | ||
|
|
816f5e8882 | ||
|
|
8bfebdc277 | ||
|
|
4e20bd9db8 | ||
|
|
d2ec1ec080 | ||
|
|
8017c0653b | ||
|
|
167cf25dae | ||
|
|
9f8bd8adc3 | ||
|
|
4db6e7143d | ||
|
|
65411b54bd | ||
|
|
213eb595a6 | ||
|
|
160bf79cb5 | ||
|
|
95ffd273f2 | ||
|
|
063389d36b | ||
|
|
514c16c91f | ||
|
|
f6a3742a74 | ||
|
|
d09037b0ff | ||
|
|
0c970983e2 | ||
|
|
a72e0fd7c7 | ||
|
|
41865cb20d | ||
|
|
66a919e63b | ||
|
|
569013e691 | ||
|
|
bf6ce69203 | ||
|
|
030b436d62 | ||
|
|
5b27861135 | ||
|
|
aa3fa9a08b | ||
|
|
53a940ec18 | ||
|
|
e5840c1e5d | ||
|
|
f1766e5884 | ||
|
|
404e70c1eb | ||
|
|
421b365483 | ||
|
|
c072be702c | ||
|
|
0f09e5feae |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,5 +4,6 @@ pkg/
|
||||
src/code.google.com
|
||||
src/github.com
|
||||
src/bitbucket.org
|
||||
src/launchpad.net
|
||||
src/ngrok/client/assets/assets_release.go
|
||||
src/ngrok/server/assets/assets_release.go
|
||||
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,4 +1,22 @@
|
||||
script: make release-all
|
||||
install: true
|
||||
after_success:
|
||||
- sudo pip install boto
|
||||
- ls -lr bin/*
|
||||
- NGROK="bin/${GOOS}_${GOARCH}/ngrok" NGROKD="bin/${GOOS}_${GOARCH}/ngrokd" VERSION=$(GOOS="" GOARCH="" GOPATH=$(pwd) go run build/version.go) python build/travis.py
|
||||
before_script:
|
||||
- pushd /home/travis/.gvm/gos/go1.1.1/src
|
||||
- ./make.bash
|
||||
- popd
|
||||
env:
|
||||
global:
|
||||
- secure: Fd5zHi58jx8lsPDv4tkRFzXSY0KnPJZuZ+LvnRcpX4+3xJsuZU6moOfrOcGqDOm7/SqZRVZRKZapE772+8sXNKPmwSXHRsZEsUgqxdehFzlVP4PQN5efOdI/quO2ibwVpZ6Idze5pelZburALd7/VbfHCTB/0P0WDMNvfHuFPQg=
|
||||
- secure: Yg+y996B4S7zHXO8j6JrRbgMf6yilHGWv6I+7oZf02d8IHYtAb6A9DveX/q+v24O8Q9WzXRU4ZIaG5nJksVwb19qcy4vpaUbvx00COi8tg0l9hdKIotrzAs+i8q5h0xUifE1iuqYJw39vYU/9vC/727GTvOfRpBCr+edcpc9uy0=
|
||||
- secure: CMF8ZfcoZK8AhFrmo6ljwP4ulYOSI9Pftp02rfNcM7LYPzU5HxFmcR7M4+3Qt0HAWxYrReozNQZYrV0s5jqIO0H4LZeHxgi7EHTam1BelhmHxL6VBMoR1mTi53bErWNhpoc0qyCnWMpnzNMObwtBVm9vjacm4NJMWqQjte/0Kdw=
|
||||
matrix:
|
||||
- GOOS=linux GOARCH=arm
|
||||
- GOOS=linux GOARCH=386
|
||||
- GOOS=windows GOARCH=386
|
||||
- GOOS=darwin GOARCH=amd64
|
||||
go:
|
||||
- 1.1
|
||||
|
||||
@@ -3,3 +3,4 @@ Alan Shreve (inconshreveable)
|
||||
Kyle Conroy (kyleconroy)
|
||||
Caleb Spare (cespare)
|
||||
Stephen Huenneke (skastel)
|
||||
Nick Presta (nickpresta)
|
||||
|
||||
@@ -31,11 +31,4 @@ are only available by creating an account on ngrok.com. If you need them, [creat
|
||||
|
||||
|
||||
## Developing on ngrok
|
||||
### Compiling ngrok
|
||||
|
||||
git clone git@github.com:inconshreveable/ngrok.git
|
||||
cd ngrok && make
|
||||
bin/ngrok [LOCAL PORT]
|
||||
|
||||
### Further Reading
|
||||
[ngrok developer's guide](docs/DEVELOPMENT.md)
|
||||
|
||||
@@ -23,7 +23,14 @@
|
||||
background-color: #ff9999;
|
||||
background-color: #000000;
|
||||
color:white;
|
||||
|
||||
}
|
||||
.path {
|
||||
width: 300px;
|
||||
}
|
||||
.wrapped {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -62,7 +69,7 @@
|
||||
<h4>All Requests</h4>
|
||||
<table class="table txn-selector">
|
||||
<tr ng-controller="TxnNavItem" ng-class="{'selected':isActive()}" ng-repeat="txn in txns" ng-click="makeActive()">
|
||||
<td>{{ txn.Req.MethodPath }}</td>
|
||||
<td class="wrapped"><div class="path">{{ txn.Req.MethodPath }}</div></td>
|
||||
<td>{{ txn.Resp.Status }}</td>
|
||||
<td><span class="pull-right">{{ txn.Duration }}</span></td>
|
||||
</tr>
|
||||
@@ -86,7 +93,7 @@
|
||||
</div>
|
||||
<hr />
|
||||
<div ng-show="!!Req" ng-controller="HttpRequest">
|
||||
<h3>{{ Req.MethodPath }}</h3>
|
||||
<h3 class="wrapped">{{ Req.MethodPath }}</h3>
|
||||
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
|
||||
</div>
|
||||
|
||||
|
||||
22
build/travis.py
Executable file
22
build/travis.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
import os, os.path, boto.s3.connection
|
||||
|
||||
access_key = os.getenv("AWS_ACCESS_KEY")
|
||||
secret_key = os.getenv("AWS_SECRET_KEY")
|
||||
bucket = os.getenv("BUCKET")
|
||||
version = os.getenv("VERSION")
|
||||
goos = os.getenv("GOOS")
|
||||
|
||||
s3 = boto.s3.connection.S3Connection(access_key, secret_key)
|
||||
bucket = s3.get_bucket(bucket)
|
||||
|
||||
for envpath in ["NGROK", "NGROKD"]:
|
||||
file_path = os.getenv(envpath)
|
||||
if goos == "windows":
|
||||
file_path += ".exe"
|
||||
dir_path, name = os.path.split(file_path)
|
||||
_, platform = os.path.split(dir_path)
|
||||
key_name = "%s/%s/%s" % (platform, version, name)
|
||||
key = bucket.new_key(key_name)
|
||||
key.set_contents_from_filename(file_path)
|
||||
10
build/version.go
Normal file
10
build/version.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ngrok/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(version.MajorMinor())
|
||||
}
|
||||
@@ -1,4 +1,24 @@
|
||||
# Changelog
|
||||
## 1.6 - 10/25/2013
|
||||
- BUGFIX: Fixed a goroutine/memory leak in ngrok/proto's parsing of http traffic
|
||||
- IMPROVEMENT: The web inspection API can now be disabled again by setting inspect_addr: disabled in the config file
|
||||
|
||||
## 1.5 - 10/20/2013
|
||||
- FEATURE: Added support a "remote_port" configuration parameter that lets you request a specific remote port for TCP tunnels
|
||||
- IMPROVEMENT: Upload instructions on crash reports are displayed after the dump where it is more likely to be seen
|
||||
- IMPROVEMENT: Improvements to ngrok's logging for easier debugging
|
||||
- IMPROVEMENT: Batch metric reporting to Keen to not be limited by the speed of their API at high request loads
|
||||
- IMPROVEMENT: Added additional safety to ensure the server doesn't crash on panics()
|
||||
- BUGFIX: Fixed an issue with prefetching tunnel connections that could hang tunnel connections when behind an aggresive NAT
|
||||
- BUGFIX: Fixed a race condition where ngrokd could send back a different message instead of AuthResp first
|
||||
- BUGFIX: Fixed an issue where under some circumstances, reconnecting would fail and tell the client the tunnels were still in use
|
||||
- BUGFIX: Fixed an issue where a race-condition with handling pings could cause a tunnel to hang forever and stop handling requests
|
||||
|
||||
## 1.4 - 09/27/2013
|
||||
- BUGFIX: Fixed an issue where long URL paths were not truncated in the terminal UI
|
||||
- BUGFIX: Fixed an issue where long URL paths ruined the web UI's formatting
|
||||
- BUGFIX: Fixed an issue where authtokens would not be remembered if an existing configuration file didn't exist
|
||||
|
||||
## 0.23 - 09/06/2013
|
||||
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ There are Makefile targets for compiling just the client or server.
|
||||
make client
|
||||
make server
|
||||
|
||||
**NB: You must compile with Go 1.1+!**
|
||||
|
||||
### Compiling release versions
|
||||
Both the client and the server contain static asset files.
|
||||
These include TLS/SSL certificates and the html/css/js for the client's web interface.
|
||||
@@ -36,15 +38,25 @@ The strategy I use for developing on ngrok is to do the following:
|
||||
Add the following lines to /etc/hosts:
|
||||
|
||||
127.0.0.1 ngrok.me
|
||||
127.0.0.1 tunnel.ngrok.me
|
||||
127.0.0.1 test.ngrok.me
|
||||
|
||||
Run ngrokd with the following options:
|
||||
|
||||
./bin/ngrokd -domain ngrok.me
|
||||
|
||||
Run ngrok with the following options
|
||||
Create an ngrok configuration file, "debug.yml" with the following contents:
|
||||
|
||||
./bin/ngrok -server=ngrok.me:4443 -subdomain=tunnel -log=ngrok.log 8080
|
||||
server_addr: ngrok.me:4443
|
||||
tunnels:
|
||||
test:
|
||||
proto:
|
||||
http: 8080
|
||||
|
||||
|
||||
Then run ngrok with either of these commands:
|
||||
|
||||
./bin/ngrok -config=debug.yml -log=ngrok.log start test
|
||||
./bin/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080
|
||||
|
||||
This will get you setup with an ngrok client talking to an ngrok server all locally under your control. Happy hacking!
|
||||
|
||||
@@ -52,18 +64,23 @@ This will get you setup with an ngrok client talking to an ngrok server all loca
|
||||
## Network protocol and tunneling
|
||||
At a high level, ngrok's tunneling works as follows:
|
||||
|
||||
### Setup
|
||||
### Connection Setup and Authentication
|
||||
1. The client initiates a long-lived TCP connection to the server over which they will pass JSON instruction messages. This connection is called the *Control Connection*.
|
||||
1. After the connection is established, the client sends a registration message.
|
||||
1. The server registers the tunnel and then sends a registration ACK message with information about the created tunnel.
|
||||
1. After the connection is established, the client sends an *Auth* message with authentication and version information.
|
||||
1. The server validates the client's *Auth* message and sends an *AuthResp* message indicating either success or failure.
|
||||
|
||||
### Tunnel creation
|
||||
1. The client may then ask the server to create tunnels for it by sending *ReqTunnel* messages.
|
||||
1. When the server receives a *ReqTunnel* message, it will send 1 or more *NewTunnel* messages that indicate successful tunnel creation or indicate failure.
|
||||
|
||||
### Tunneling connections
|
||||
1. When the server receives a new public connection, it locates the approriate tunnel by examing the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*.
|
||||
1. The server sends a proxy request message to the client over the control connection.
|
||||
1. When the server receives a new public connection, it locates the approriate tunnel by examining the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*.
|
||||
1. The server sends a *ReqProxy* message to the client over the control connection.
|
||||
1. The client initiates a new TCP connection to the server called a *Proxy Connection*.
|
||||
1. The client sends a proxy registration message over the proxy connection so the server can associate it to the public connection that caused the request for the proxy connection to be initiated.
|
||||
1. The client sends a *RegProxy* message over the proxy connection so the server can associate it to a control connection (and thus the tunnels it's responsible for).
|
||||
1. The server sends a *StartProxy* message over the proxy connection with metadata information about the connection (the client IP and name of the tunnel).
|
||||
1. The server begins copying the traffic byte-for-byte from the public connection to the proxy connection and vice-versa.
|
||||
1. The client opens a connection to the local port that was specified at startup. This is called the *Private Connection*.
|
||||
1. The client opens a connection to the local address configured for that tunnel. This is called the *Private Connection*.
|
||||
1. The client begins copying the traffic byte-for-byte from the proxied connection to the private connection and vice-versa.
|
||||
|
||||
### Detecting dead tunnels
|
||||
@@ -81,7 +98,7 @@ The message length is sent as a 64-bit little endian integer.
|
||||
The definitions and shared protocol routines lives under _src/ngrok/msg_
|
||||
|
||||
#### src/ngrok/msg/msg.go
|
||||
All of the different message types (Reg, PxyReq, Ping, etc) are defined here. This is a good place to go to understand exactly what messages are sent between the client and server.
|
||||
All of the different message types (Auth, AuthResp, ReqTunnel, RegProxy, StartProxy, etc) are defined here and their fields documented. This is a good place to go to understand exactly what messages are sent between the client and server.
|
||||
|
||||
## ngrokd - the server
|
||||
### Code
|
||||
|
||||
63
docs/SELFHOSTING.md
Normal file
63
docs/SELFHOSTING.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# How to run your own ngrokd server
|
||||
|
||||
Running your own ngrok server is really easy! The instructions below will guide you along your way!
|
||||
|
||||
## 1. Get an SSL certificate
|
||||
ngrok provides secure tunnels via TLS, so you'll need an SSL certificate. Assuming you want to create
|
||||
tunnels on *.example.com, buy a wildcard SSL certificate for *.example.com.
|
||||
|
||||
## 2. Modify your DNS
|
||||
You need to use the DNS management tools given to you by your provider to create an A
|
||||
record which points *.example.com to the IP address of the server where you will run ngrokd.
|
||||
|
||||
## 3. Compile it
|
||||
You can compile an ngrokd server with the following command:
|
||||
|
||||
make release-server
|
||||
|
||||
Make sure you compile it with the GOOS/GOARCH environment variables set to the platform of
|
||||
your target server. Then copy the binary over to your server.
|
||||
|
||||
## 4. Run the server
|
||||
You'll run the server with the following command.
|
||||
|
||||
|
||||
./ngrokd -tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt" -domain="example.com"
|
||||
|
||||
### Specifying your TLS certificate and key
|
||||
ngrok only makes TLS-encrypted connections. When you run ngrokd, you'll need to instruct it
|
||||
where to find your TLS certificate and private key. Specify the paths with the following switches:
|
||||
|
||||
-tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt"
|
||||
|
||||
### Setting the server's domain
|
||||
When you run your own ngrokd server, you need to tell ngrokd the domain it's running on so that it
|
||||
knows what URLs to issue to clients.
|
||||
|
||||
-domain="example.com"
|
||||
|
||||
## 5. Configure the client
|
||||
In order to connect with a client, you'll need to set two options in ngrok's configuration file.
|
||||
The ngrok configuration file is a simple YAML file that is read from ~/.ngrok by default. You may specify
|
||||
a custom configuration file path with the -config switch. Your config file must contain the following two
|
||||
options.
|
||||
|
||||
server_addr: example.com:4443
|
||||
trust_host_root_certs: true
|
||||
|
||||
Subsitute the address of your ngrokd server for "example.com:4443". The "trust_host_root_certs" parameter instructs
|
||||
ngrok to trust the root certificates on your computer when establishing TLS connections to the server. By default, ngrok
|
||||
only trusts the root certificate for ngrok.com.
|
||||
|
||||
## 6. Connect with a client
|
||||
Then, just run ngrok as usual to connect securely to your own ngrokd server!
|
||||
|
||||
ngrok 80
|
||||
|
||||
## FAQ
|
||||
#### Do I really need a wildcard certificate?
|
||||
If you don't need to run https tunnels, then you don't need a wildcard cert.
|
||||
|
||||
#### I don't want to pay for an SSL certificate, can I use a self-signed one?
|
||||
Yes, it's possible to use a self-signed certificate, but you'll need to recompile ngrok with your signing CA.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package client
|
||||
|
||||
/*
|
||||
Functions for reading and writing the auth token from the user's
|
||||
home directory.
|
||||
*/
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"ngrok/log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
currentAuthToken string
|
||||
authTokenFile string
|
||||
)
|
||||
|
||||
func initAuth() {
|
||||
user, err := user.Current()
|
||||
|
||||
// user.Current() does not work on linux when cross compilling because
|
||||
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
|
||||
homeDir := os.Getenv("HOME")
|
||||
if err != nil {
|
||||
log.Warn("Failed to get user's home directory: %s", err.Error())
|
||||
} else {
|
||||
homeDir = user.HomeDir
|
||||
}
|
||||
|
||||
authTokenFile = path.Join(homeDir, ".ngrok")
|
||||
|
||||
log.Debug("Reading auth token from file %s", authTokenFile)
|
||||
tokenBytes, err := ioutil.ReadFile(authTokenFile)
|
||||
|
||||
if err == nil {
|
||||
currentAuthToken = string(tokenBytes)
|
||||
} else {
|
||||
log.Warn("Failed to read ~/.ngrok for auth token: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Load the auth token from file
|
||||
func LoadAuthToken() string {
|
||||
once.Do(initAuth)
|
||||
return currentAuthToken
|
||||
}
|
||||
|
||||
// Save the auth token to file
|
||||
func SaveAuthToken(token string) {
|
||||
if token == "" || token == LoadAuthToken() || authTokenFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
perms := os.FileMode(0644)
|
||||
err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
|
||||
if err != nil {
|
||||
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,112 +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 {
|
||||
serverAddr string
|
||||
proxyAddr string
|
||||
httpAuth string
|
||||
hostname string
|
||||
localaddr string
|
||||
protocol string
|
||||
url string
|
||||
subdomain string
|
||||
webport int
|
||||
logto string
|
||||
authtoken string
|
||||
config string
|
||||
logto string
|
||||
authtoken string
|
||||
httpauth string
|
||||
hostname string
|
||||
protocol string
|
||||
subdomain string
|
||||
command string
|
||||
args []string
|
||||
}
|
||||
|
||||
func fail(msg string, args ...interface{}) {
|
||||
fmt.Printf(msg+"\n", args...)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func parsePort(portString string) (err error) {
|
||||
var port int
|
||||
if port, err = strconv.Atoi(portString); err != nil {
|
||||
return err
|
||||
func parseArgs() (opts *Options, err error) {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, usage1, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, usage2)
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
return PORT_OUT_OF_RANGE
|
||||
}
|
||||
config := flag.String(
|
||||
"config",
|
||||
"",
|
||||
"Path to ngrok configuration file. (default: $HOME/.ngrok)")
|
||||
|
||||
return
|
||||
}
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"none",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
// Local address could be a port of a host:port string
|
||||
// we always return a host:port string from this function or fail
|
||||
func parseLocalAddr() string {
|
||||
if flag.NArg() == 0 {
|
||||
fail("LOCAL not specified, specify a port number or host:port connection string")
|
||||
}
|
||||
|
||||
if flag.NArg() > 1 {
|
||||
fail("Only one LOCAL may be specified, not %d", flag.NArg())
|
||||
}
|
||||
|
||||
addr := flag.Arg(0)
|
||||
|
||||
// try to parse as a port number
|
||||
if err := parsePort(addr); err == nil {
|
||||
return fmt.Sprintf("127.0.0.1:%s", addr)
|
||||
} else if err == PORT_OUT_OF_RANGE {
|
||||
fail("%s is not in the valid port range 1-65535")
|
||||
}
|
||||
|
||||
// try to parse as a connection string
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
fail("%v", err)
|
||||
}
|
||||
|
||||
if parsePort(port) != nil {
|
||||
fail("'%s' is not a valid port number (1-65535)", port)
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
func parseProtocol(proto string) string {
|
||||
switch proto {
|
||||
case "http", "https", "http+https", "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")
|
||||
|
||||
serverAddr := flag.String(
|
||||
"serverAddr",
|
||||
"ngrokd.ngrok.com:443",
|
||||
"Address of the remote ngrokd server")
|
||||
|
||||
proxyAddr := flag.String(
|
||||
"proxyAddr",
|
||||
"",
|
||||
"The address of an http proxy to connect through (ex: proxy.example.org:3128)")
|
||||
|
||||
httpAuth := flag.String(
|
||||
httpauth := flag.String(
|
||||
"httpauth",
|
||||
"",
|
||||
"username:password HTTP basic auth creds protecting the public tunnel endpoint")
|
||||
@@ -114,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",
|
||||
@@ -126,38 +86,43 @@ func parseArgs() *Options {
|
||||
"http+https",
|
||||
"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
|
||||
|
||||
webport := flag.Int(
|
||||
"webport",
|
||||
4040,
|
||||
"The port on which the web interface is served, -1 to disable")
|
||||
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"none",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
v := flag.Bool(
|
||||
"version",
|
||||
false,
|
||||
"Print ngrok version and exit")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *v {
|
||||
fmt.Println(version.MajorMinor())
|
||||
os.Exit(0)
|
||||
opts = &Options{
|
||||
config: *config,
|
||||
logto: *logto,
|
||||
httpauth: *httpauth,
|
||||
subdomain: *subdomain,
|
||||
protocol: *protocol,
|
||||
authtoken: *authtoken,
|
||||
hostname: *hostname,
|
||||
command: flag.Arg(0),
|
||||
}
|
||||
|
||||
return &Options{
|
||||
serverAddr: *serverAddr,
|
||||
proxyAddr: *proxyAddr,
|
||||
httpAuth: *httpAuth,
|
||||
subdomain: *subdomain,
|
||||
localaddr: parseLocalAddr(),
|
||||
protocol: parseProtocol(*protocol),
|
||||
webport: *webport,
|
||||
logto: *logto,
|
||||
authtoken: *authtoken,
|
||||
hostname: *hostname,
|
||||
switch opts.command {
|
||||
case "start":
|
||||
opts.args = flag.Args()[1:]
|
||||
case "version":
|
||||
fmt.Println(version.MajorMinor())
|
||||
os.Exit(0)
|
||||
case "help":
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
case "":
|
||||
err = fmt.Errorf("You must specify a local port to tunnel or an ngrok command.")
|
||||
return
|
||||
|
||||
default:
|
||||
if len(flag.Args()) > 1 {
|
||||
err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v",
|
||||
len(flag.Args()),
|
||||
flag.Args())
|
||||
return
|
||||
}
|
||||
|
||||
opts.command = "default"
|
||||
opts.args = flag.Args()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
266
src/ngrok/client/config.go
Normal file
266
src/ngrok/client/config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"launchpad.net/goyaml"
|
||||
"net"
|
||||
"net/url"
|
||||
"ngrok/log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
HttpProxy string `yaml:"http_proxy,omitempty"`
|
||||
ServerAddr string `yaml:"server_addr,omitempty"`
|
||||
InspectAddr string `yaml:"inspect_addr,omitempty"`
|
||||
TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"`
|
||||
AuthToken string `yaml:"auth_token,omitempty"`
|
||||
Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
|
||||
LogTo string `yaml:"-"`
|
||||
Path string `yaml:"-"`
|
||||
}
|
||||
|
||||
type TunnelConfiguration struct {
|
||||
Subdomain string `yaml:"subdomain,omitempty"`
|
||||
Hostname string `yaml:"hostname,omitempty"`
|
||||
Protocols map[string]string `yaml:"proto,omitempty"`
|
||||
HttpAuth string `yaml:"auth,omitempty"`
|
||||
RemotePort uint16 `yaml:"remote_port,omitempty"`
|
||||
}
|
||||
|
||||
func LoadConfiguration(opts *Options) (config *Configuration, err error) {
|
||||
configPath := opts.config
|
||||
if configPath == "" {
|
||||
configPath = defaultPath()
|
||||
}
|
||||
|
||||
log.Info("Reading configuration file %s", configPath)
|
||||
configBuf, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
// failure to read a configuration file is only a fatal error if
|
||||
// the user specified one explicitly
|
||||
if opts.config != "" {
|
||||
err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// deserialize/parse the config
|
||||
config = new(Configuration)
|
||||
if err = goyaml.Unmarshal(configBuf, &config); err != nil {
|
||||
err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
// try to parse the old .ngrok format for backwards compatibility
|
||||
matched := false
|
||||
content := strings.TrimSpace(string(configBuf))
|
||||
if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
|
||||
return
|
||||
} else if matched {
|
||||
config = &Configuration{AuthToken: content}
|
||||
}
|
||||
|
||||
// set configuration defaults
|
||||
if config.ServerAddr == "" {
|
||||
config.ServerAddr = defaultServerAddr
|
||||
}
|
||||
|
||||
if config.InspectAddr == "" {
|
||||
config.InspectAddr = "127.0.0.1:4040"
|
||||
}
|
||||
|
||||
if config.HttpProxy == "" {
|
||||
config.HttpProxy = os.Getenv("http_proxy")
|
||||
}
|
||||
|
||||
// validate and normalize configuration
|
||||
if config.InspectAddr != "disabled" {
|
||||
if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if config.HttpProxy != "" {
|
||||
var proxyUrl *url.URL
|
||||
if proxyUrl, err = url.Parse(config.HttpProxy); err != nil {
|
||||
return
|
||||
} else {
|
||||
if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
|
||||
err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for name, t := range config.Tunnels {
|
||||
if t == nil || t.Protocols == nil || len(t.Protocols) == 0 {
|
||||
err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name)
|
||||
return
|
||||
}
|
||||
|
||||
for k, addr := range t.Protocols {
|
||||
tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
|
||||
if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateProtocol(k, tunnelName); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// use the name of the tunnel as the subdomain if none is specified
|
||||
if t.Hostname == "" && t.Subdomain == "" {
|
||||
// XXX: a crude heuristic, really we should be checking if the last part
|
||||
// is a TLD
|
||||
if len(strings.Split(name, ".")) > 1 {
|
||||
t.Hostname = name
|
||||
} else {
|
||||
t.Subdomain = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// override configuration with command-line options
|
||||
config.LogTo = opts.logto
|
||||
config.Path = configPath
|
||||
if opts.authtoken != "" {
|
||||
config.AuthToken = opts.authtoken
|
||||
}
|
||||
|
||||
switch opts.command {
|
||||
// start a single tunnel, the default, simple ngrok behavior
|
||||
case "default":
|
||||
config.Tunnels = make(map[string]*TunnelConfiguration)
|
||||
config.Tunnels["default"] = &TunnelConfiguration{
|
||||
Subdomain: opts.subdomain,
|
||||
Hostname: opts.hostname,
|
||||
HttpAuth: opts.httpauth,
|
||||
Protocols: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, proto := range strings.Split(opts.protocol, "+") {
|
||||
if err = validateProtocol(proto, "default"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start tunnels
|
||||
case "start":
|
||||
if len(opts.args) == 0 {
|
||||
err = fmt.Errorf("You must specify at least one tunnel to start")
|
||||
return
|
||||
}
|
||||
|
||||
requestedTunnels := make(map[string]bool)
|
||||
for _, arg := range opts.args {
|
||||
requestedTunnels[arg] = true
|
||||
|
||||
if _, ok := config.Tunnels[arg]; !ok {
|
||||
err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for name, _ := range config.Tunnels {
|
||||
if !requestedTunnels[name] {
|
||||
delete(config.Tunnels, name)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("Unknown command: %s", opts.command)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func defaultPath() string {
|
||||
user, err := user.Current()
|
||||
|
||||
// user.Current() does not work on linux when cross compilling because
|
||||
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
|
||||
homeDir := os.Getenv("HOME")
|
||||
if err != nil {
|
||||
log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
|
||||
} else {
|
||||
homeDir = user.HomeDir
|
||||
}
|
||||
|
||||
return path.Join(homeDir, ".ngrok")
|
||||
}
|
||||
|
||||
func normalizeAddress(addr string, propName string) (string, error) {
|
||||
// normalize port to address
|
||||
if _, err := strconv.Atoi(addr); err == nil {
|
||||
addr = ":" + addr
|
||||
}
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
|
||||
}
|
||||
|
||||
if tcpAddr.IP == nil {
|
||||
tcpAddr.IP = net.ParseIP("127.0.0.1")
|
||||
}
|
||||
|
||||
return tcpAddr.String(), nil
|
||||
}
|
||||
|
||||
func validateProtocol(proto, propName string) (err error) {
|
||||
switch proto {
|
||||
case "http", "https", "http+https", "tcp":
|
||||
default:
|
||||
err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func SaveAuthToken(configPath, authtoken string) (err error) {
|
||||
// empty configuration by default for the case that we can't read it
|
||||
c := new(Configuration)
|
||||
|
||||
// read the configuration
|
||||
oldConfigBytes, err := ioutil.ReadFile(configPath)
|
||||
if err == nil {
|
||||
// unmarshal if we successfully read the configuration file
|
||||
if err = goyaml.Unmarshal(oldConfigBytes, c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no need to save, the authtoken is already the correct value
|
||||
if c.AuthToken == authtoken {
|
||||
return
|
||||
}
|
||||
|
||||
// update auth token
|
||||
c.AuthToken = authtoken
|
||||
|
||||
// rewrite configuration
|
||||
newConfigBytes, err := goyaml.Marshal(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
|
||||
return
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"ngrok/client/views/term"
|
||||
"ngrok/client/views/web"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
@@ -48,7 +47,7 @@ type Controller struct {
|
||||
state chan mvc.State
|
||||
|
||||
// options
|
||||
opts *Options
|
||||
config *Configuration
|
||||
}
|
||||
|
||||
// public interface
|
||||
@@ -127,29 +126,29 @@ func (ctl *Controller) addView(v mvc.View) {
|
||||
ctl.views = append(ctl.views, v)
|
||||
}
|
||||
|
||||
func (ctl *Controller) GetWebViewPort() int {
|
||||
return ctl.opts.webport
|
||||
func (ctl *Controller) GetWebInspectAddr() string {
|
||||
return ctl.config.InspectAddr
|
||||
}
|
||||
|
||||
func (ctl *Controller) Run(opts *Options) {
|
||||
// Save the options
|
||||
ctl.opts = opts
|
||||
func (ctl *Controller) Run(config *Configuration) {
|
||||
// Save the configuration
|
||||
ctl.config = config
|
||||
|
||||
// init the model
|
||||
model := newClientModel(ctl)
|
||||
model := newClientModel(config, ctl)
|
||||
ctl.model = model
|
||||
var state mvc.State = model
|
||||
|
||||
// init web ui
|
||||
var webView *web.WebView
|
||||
if opts.webport != -1 {
|
||||
webView = web.NewWebView(ctl, opts.webport)
|
||||
if config.InspectAddr != "disabled" {
|
||||
webView = web.NewWebView(ctl, config.InspectAddr)
|
||||
ctl.addView(webView)
|
||||
}
|
||||
|
||||
// init term ui
|
||||
var termView *term.TermView
|
||||
if opts.logto != "stdout" {
|
||||
if config.LogTo != "stdout" {
|
||||
termView = term.NewTermView(ctl)
|
||||
ctl.addView(termView)
|
||||
}
|
||||
@@ -168,16 +167,8 @@ func (ctl *Controller) Run(opts *Options) {
|
||||
}
|
||||
}
|
||||
|
||||
ctl.Go(func() { autoUpdate(state, opts.authtoken) })
|
||||
|
||||
reqTunnel := &msg.ReqTunnel{
|
||||
Protocol: opts.protocol,
|
||||
Hostname: opts.hostname,
|
||||
Subdomain: opts.subdomain,
|
||||
HttpAuth: opts.httpAuth,
|
||||
}
|
||||
|
||||
ctl.Go(func() { ctl.model.Run(opts.serverAddr, opts.proxyAddr, opts.authtoken, ctl, reqTunnel, opts.localaddr) })
|
||||
ctl.Go(func() { autoUpdate(state, config.AuthToken) })
|
||||
ctl.Go(ctl.model.Run)
|
||||
|
||||
updates := ctl.updates.Reg()
|
||||
defer ctl.updates.UnReg(updates)
|
||||
|
||||
7
src/ngrok/client/debug.go
Normal file
7
src/ngrok/client/debug.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !release
|
||||
|
||||
package client
|
||||
|
||||
var (
|
||||
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
|
||||
)
|
||||
@@ -1,29 +1,38 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"ngrok/log"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
// parse options
|
||||
opts := parseArgs()
|
||||
opts, err := parseArgs()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// set up logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
// set up auth token
|
||||
if opts.authtoken == "" {
|
||||
opts.authtoken = LoadAuthToken()
|
||||
// read configuration file
|
||||
config, err := LoadConfiguration(opts)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
log.Error("Couldn't securely seed the random number generator!")
|
||||
fmt.Printf("Couldn't securely seed the random number generator!")
|
||||
os.Exit(1)
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
NewController().Run(opts)
|
||||
NewController().Run(config)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"io/ioutil"
|
||||
@@ -10,13 +11,16 @@ import (
|
||||
"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
|
||||
@@ -41,20 +45,43 @@ type ClientModel struct {
|
||||
protocols []proto.Protocol
|
||||
ctl mvc.Controller
|
||||
serverAddr string
|
||||
proxyAddr string
|
||||
proxyUrl string
|
||||
authToken string
|
||||
tlsConfig *tls.Config
|
||||
tunnelConfig map[string]*TunnelConfiguration
|
||||
configPath string
|
||||
}
|
||||
|
||||
func newClientModel(ctl mvc.Controller) *ClientModel {
|
||||
func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel {
|
||||
protoMap := make(map[string]proto.Protocol)
|
||||
protoMap["http"] = proto.NewHttp()
|
||||
protoMap["https"] = protoMap["http"]
|
||||
protoMap["tcp"] = proto.NewTcp()
|
||||
protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
|
||||
|
||||
// configure TLS
|
||||
var tlsConfig *tls.Config
|
||||
if config.TrustHostRootCerts {
|
||||
tlsConfig = &tls.Config{}
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ClientModel{
|
||||
Logger: log.NewPrefixLogger("client"),
|
||||
|
||||
// server address
|
||||
serverAddr: config.ServerAddr,
|
||||
|
||||
// proxy address
|
||||
proxyUrl: config.HttpProxy,
|
||||
|
||||
// auth token
|
||||
authToken: config.AuthToken,
|
||||
|
||||
// connection status
|
||||
connStatus: mvc.ConnConnecting,
|
||||
|
||||
@@ -75,6 +102,15 @@ func newClientModel(ctl mvc.Controller) *ClientModel {
|
||||
|
||||
// controller
|
||||
ctl: ctl,
|
||||
|
||||
// tls configuration
|
||||
tlsConfig: tlsConfig,
|
||||
|
||||
// tunnel configuration
|
||||
tunnelConfig: config.Tunnels,
|
||||
|
||||
// config path
|
||||
configPath: config.Path,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,22 +166,16 @@ func (c *ClientModel) update() {
|
||||
c.ctl.Update(c)
|
||||
}
|
||||
|
||||
func (c *ClientModel) Run(serverAddr, proxyAddr, authToken string, ctl mvc.Controller, reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
c.serverAddr = serverAddr
|
||||
c.proxyAddr = proxyAddr
|
||||
c.authToken = authToken
|
||||
c.ctl = ctl
|
||||
c.reconnectingControl(reqTunnel, localaddr)
|
||||
}
|
||||
|
||||
func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
func (c *ClientModel) Run() {
|
||||
// how long we should wait before we reconnect
|
||||
maxWait := 30 * time.Second
|
||||
wait := 1 * time.Second
|
||||
|
||||
for {
|
||||
c.control(reqTunnel, localaddr)
|
||||
// run the control channel
|
||||
c.control()
|
||||
|
||||
// control oonly returns when a failure has occurred, so we're going to try to reconnect
|
||||
if c.connStatus == mvc.ConnOnline {
|
||||
wait = 1 * time.Second
|
||||
}
|
||||
@@ -161,7 +191,7 @@ func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr st
|
||||
}
|
||||
|
||||
// Establishes and manages a tunnel control connection with the server
|
||||
func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
func (c *ClientModel) control() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("control recovering from failure %v", r)
|
||||
@@ -173,11 +203,11 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
ctlConn conn.Conn
|
||||
err error
|
||||
)
|
||||
if c.proxyAddr == "" {
|
||||
if c.proxyUrl == "" {
|
||||
// simple non-proxied case, just connect to the server
|
||||
ctlConn, err = conn.Dial(c.serverAddr, "ctl", tlsConfig)
|
||||
ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
|
||||
} else {
|
||||
ctlConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "ctl", tlsConfig)
|
||||
ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -214,11 +244,36 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
c.serverVersion = authResp.MmVersion
|
||||
c.Info("Authenticated with server, client id: %v", c.id)
|
||||
c.update()
|
||||
SaveAuthToken(c.authToken)
|
||||
if err = SaveAuthToken(c.configPath, c.authToken); err != nil {
|
||||
c.Error("Failed to save auth token: %v", err)
|
||||
}
|
||||
|
||||
// register the tunnel
|
||||
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
|
||||
panic(err)
|
||||
// request tunnels
|
||||
reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)
|
||||
for _, config := range c.tunnelConfig {
|
||||
// create the protocol list to ask for
|
||||
var protocols []string
|
||||
for proto, _ := range config.Protocols {
|
||||
protocols = append(protocols, proto)
|
||||
}
|
||||
|
||||
reqTunnel := &msg.ReqTunnel{
|
||||
ReqId: util.RandId(8),
|
||||
Protocol: strings.Join(protocols, "+"),
|
||||
Hostname: config.Hostname,
|
||||
Subdomain: config.Subdomain,
|
||||
HttpAuth: config.HttpAuth,
|
||||
RemotePort: config.RemotePort,
|
||||
}
|
||||
|
||||
// send the tunnel request
|
||||
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// save request id association so we know which local address
|
||||
// to proxy to later
|
||||
reqIdToTunnelConfig[reqTunnel.ReqId] = config
|
||||
}
|
||||
|
||||
// start the heartbeat
|
||||
@@ -249,7 +304,7 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
|
||||
|
||||
tunnel := mvc.Tunnel{
|
||||
PublicUrl: m.Url,
|
||||
LocalAddr: localaddr,
|
||||
LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
|
||||
Protocol: c.protoMap[m.Protocol],
|
||||
}
|
||||
|
||||
@@ -271,10 +326,10 @@ func (c *ClientModel) proxy() {
|
||||
err error
|
||||
)
|
||||
|
||||
if c.proxyAddr == "" {
|
||||
remoteConn, err = conn.Dial(c.serverAddr, "pxy", tlsConfig)
|
||||
if c.proxyUrl == "" {
|
||||
remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
|
||||
} else {
|
||||
remoteConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "pxy", tlsConfig)
|
||||
remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -285,20 +340,20 @@ func (c *ClientModel) proxy() {
|
||||
|
||||
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
|
||||
if err != nil {
|
||||
log.Error("Failed to write RegProxy: %v", err)
|
||||
remoteConn.Error("Failed to write RegProxy: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var startPxy msg.StartProxy
|
||||
if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil {
|
||||
log.Error("Server failed to write StartProxy: %v", err)
|
||||
remoteConn.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)
|
||||
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ type Controller interface {
|
||||
// safe wrapper for running go-routines
|
||||
Go(fn func())
|
||||
|
||||
// the port where the web interface is running
|
||||
GetWebViewPort() int
|
||||
// the address where the web inspection interface is running
|
||||
GetWebInspectAddr() string
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"ngrok/msg"
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
Run(serverAddr, proxyAddr, authToken string, ctl Controller, reqTunnel *msg.ReqTunnel, localaddr string)
|
||||
Run()
|
||||
|
||||
Shutdown()
|
||||
|
||||
|
||||
7
src/ngrok/client/release.go
Normal file
7
src/ngrok/client/release.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build release
|
||||
|
||||
package client
|
||||
|
||||
var (
|
||||
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
size = 10
|
||||
size = 10
|
||||
pathMaxLength = 25
|
||||
)
|
||||
|
||||
type HttpView struct {
|
||||
@@ -69,7 +71,8 @@ func (v *HttpView) Render() {
|
||||
v.Printf(0, 1, "-------------")
|
||||
for i, obj := range v.HttpRequests.Slice() {
|
||||
txn := obj.(*proto.HttpTxn)
|
||||
v.Printf(0, 3+i, "%s %v", txn.Req.Method, txn.Req.URL.Path)
|
||||
path := truncatePath(txn.Req.URL.Path)
|
||||
v.Printf(0, 3+i, "%s %v", txn.Req.Method, path)
|
||||
if txn.Resp != nil {
|
||||
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
|
||||
}
|
||||
@@ -80,3 +83,37 @@ func (v *HttpView) Render() {
|
||||
func (v *HttpView) Shutdown() {
|
||||
close(v.shutdown)
|
||||
}
|
||||
|
||||
func truncatePath(path string) string {
|
||||
// Truncate all long strings based on rune count
|
||||
if utf8.RuneCountInString(path) > pathMaxLength {
|
||||
path = string([]rune(path)[:pathMaxLength])
|
||||
}
|
||||
|
||||
// By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes.
|
||||
// Otherwise, we have a multi-byte string and need to calculate the size of each rune and
|
||||
// truncate manually.
|
||||
//
|
||||
// This is a workaround for a bug in termbox-go. Remove it when this issue is fixed:
|
||||
// https://github.com/nsf/termbox-go/pull/21
|
||||
if len(path) > pathMaxLength {
|
||||
out := make([]byte, pathMaxLength, pathMaxLength)
|
||||
length := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(path[length:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
// utf8.EncodeRune expects there to be enough room to store the full size of the rune
|
||||
if length+size <= pathMaxLength {
|
||||
utf8.EncodeRune(out[length:], r)
|
||||
length += size
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
path = string(out[:length])
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
termbox "github.com/nsf/termbox-go"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
@@ -108,11 +107,7 @@ func (v *TermView) draw() {
|
||||
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
|
||||
i++
|
||||
}
|
||||
webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
|
||||
if v.ctl.GetWebViewPort() == -1 {
|
||||
webAddr = "disabled"
|
||||
}
|
||||
v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
|
||||
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
|
||||
|
||||
connMeter, connTimer := state.GetConnectionMetrics()
|
||||
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/garyburd/go-websocket/websocket"
|
||||
"net/http"
|
||||
"ngrok/client/assets"
|
||||
@@ -22,7 +21,7 @@ type WebView struct {
|
||||
wsMessages *util.Broadcast
|
||||
}
|
||||
|
||||
func NewWebView(ctl mvc.Controller, port int) *WebView {
|
||||
func NewWebView(ctl mvc.Controller, addr string) *WebView {
|
||||
wv := &WebView{
|
||||
Logger: log.NewPrefixLogger("view", "web"),
|
||||
wsMessages: util.NewBroadcast(),
|
||||
@@ -66,8 +65,8 @@ func NewWebView(ctl mvc.Controller, port int) *WebView {
|
||||
w.Write(buf)
|
||||
})
|
||||
|
||||
wv.Info("Serving web interface on localhost:%d", port)
|
||||
wv.ctl.Go(func() { http.ListenAndServe(fmt.Sprintf(":%d", port), nil) })
|
||||
wv.Info("Serving web interface on %s", addr)
|
||||
wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
|
||||
return wv
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,15 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"ngrok/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Conn interface {
|
||||
@@ -17,9 +20,11 @@ type Conn interface {
|
||||
log.Logger
|
||||
Id() string
|
||||
SetType(string)
|
||||
CloseRead() error
|
||||
}
|
||||
|
||||
type loggedConn struct {
|
||||
tcp *net.TCPConn
|
||||
net.Conn
|
||||
log.Logger
|
||||
id int32
|
||||
@@ -32,9 +37,16 @@ type Listener struct {
|
||||
}
|
||||
|
||||
func wrapConn(conn net.Conn, typ string) *loggedConn {
|
||||
c := &loggedConn{conn, log.NewPrefixLogger(), rand.Int31(), typ}
|
||||
c.AddLogPrefix(c.Id())
|
||||
return c
|
||||
switch c := conn.(type) {
|
||||
case *loggedConn:
|
||||
return c
|
||||
case *net.TCPConn:
|
||||
wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ}
|
||||
wrapped.AddLogPrefix(wrapped.Id())
|
||||
return wrapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
|
||||
@@ -88,17 +100,43 @@ func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func DialHttpProxy(proxyAddr, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err 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(proxyAddr, typ, nil); err != nil {
|
||||
if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send an HTTP proxy CONNECT message
|
||||
req, err := http.NewRequest("CONNECT", "http://"+addr, nil)
|
||||
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)
|
||||
|
||||
@@ -124,9 +162,11 @@ 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 *loggedConn) Close() (err error) {
|
||||
if err := c.Conn.Close(); err == nil {
|
||||
c.Debug("Closing")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *loggedConn) Id() string {
|
||||
@@ -141,28 +181,37 @@ func (c *loggedConn) SetType(typ string) {
|
||||
c.Info("Renamed connection %s", oldId)
|
||||
}
|
||||
|
||||
func (c *loggedConn) CloseRead() error {
|
||||
// XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner
|
||||
// connection termination. Unfortunately, when I've tried that, I've observed
|
||||
// failures where the connection was closed *before* flushing its write buffer,
|
||||
// set with SetLinger() set properly (which it is by default).
|
||||
return c.tcp.CloseRead()
|
||||
}
|
||||
|
||||
func Join(c Conn, c2 Conn) (int64, int64) {
|
||||
done := make(chan error)
|
||||
var wait sync.WaitGroup
|
||||
|
||||
pipe := func(to Conn, from Conn, bytesCopied *int64) {
|
||||
defer to.Close()
|
||||
defer from.Close()
|
||||
defer wait.Done()
|
||||
|
||||
var err error
|
||||
*bytesCopied, err = io.Copy(to, from)
|
||||
if err != nil {
|
||||
from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err)
|
||||
done <- err
|
||||
} else {
|
||||
from.Debug("Copied %d bytes from to %s", *bytesCopied, to.Id())
|
||||
done <- nil
|
||||
from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id())
|
||||
}
|
||||
}
|
||||
|
||||
wait.Add(2)
|
||||
var fromBytes, toBytes int64
|
||||
go pipe(c, c2, &fromBytes)
|
||||
go pipe(c2, c, &toBytes)
|
||||
c.Info("Joined with connection %s", c2.Id())
|
||||
<-done
|
||||
c.Close()
|
||||
c2.Close()
|
||||
<-done
|
||||
wait.Wait()
|
||||
return fromBytes, toBytes
|
||||
}
|
||||
|
||||
|
||||
@@ -63,11 +63,16 @@ type AuthResp struct {
|
||||
// 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
|
||||
ReqId string
|
||||
Protocol string
|
||||
|
||||
// http only
|
||||
Hostname string
|
||||
Subdomain string
|
||||
HttpAuth string
|
||||
|
||||
// tcp only
|
||||
RemotePort uint16
|
||||
}
|
||||
|
||||
// When the server opens a new tunnel on behalf of
|
||||
|
||||
@@ -65,6 +65,8 @@ func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
|
||||
}
|
||||
|
||||
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) {
|
||||
defer close(lastTxn)
|
||||
|
||||
for {
|
||||
req, err := http.ReadRequest(tee.WriteBuffer())
|
||||
if err != nil {
|
||||
@@ -95,9 +97,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interf
|
||||
}
|
||||
|
||||
func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
for {
|
||||
var err error
|
||||
txn := <-lastTxn
|
||||
for txn := range lastTxn {
|
||||
resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request)
|
||||
txn.Duration = time.Since(txn.Start)
|
||||
h.reqTimer.Update(txn.Duration)
|
||||
|
||||
@@ -9,6 +9,8 @@ type Options struct {
|
||||
httpsAddr string
|
||||
tunnelAddr string
|
||||
domain string
|
||||
tlsCrt string
|
||||
tlsKey string
|
||||
logto string
|
||||
}
|
||||
|
||||
@@ -17,6 +19,8 @@ func parseArgs() *Options {
|
||||
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()
|
||||
@@ -26,6 +30,8 @@ func parseArgs() *Options {
|
||||
httpsAddr: *httpsAddr,
|
||||
tunnelAddr: *tunnelAddr,
|
||||
domain: *domain,
|
||||
tlsCrt: *tlsCrt,
|
||||
tlsKey: *tlsKey,
|
||||
logto: *logto,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@ import (
|
||||
"ngrok/version"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pingTimeoutInterval = 30 * time.Second
|
||||
connReapInterval = 10 * time.Second
|
||||
controlWriteTimeout = 10 * time.Second
|
||||
proxyStaleDuration = 60 * time.Second
|
||||
proxyMaxPoolSize = 10
|
||||
)
|
||||
|
||||
type Control struct {
|
||||
@@ -33,11 +35,6 @@ type Control struct {
|
||||
// to us over conn by the client
|
||||
in chan (msg.Message)
|
||||
|
||||
// put a message in this channel to send it over
|
||||
// conn to the client and then terminate this
|
||||
// control connection and all of its tunnels
|
||||
stop chan (msg.Message)
|
||||
|
||||
// the last time we received a ping from the client - for heartbeats
|
||||
lastPing time.Time
|
||||
|
||||
@@ -47,27 +44,37 @@ type Control struct {
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// closing indicator
|
||||
closing int32
|
||||
|
||||
// identifier
|
||||
id string
|
||||
|
||||
// synchronizer for controlled shutdown of writer()
|
||||
writerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controlled shutdown of reader()
|
||||
readerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controlled shutdown of manager()
|
||||
managerShutdown *util.Shutdown
|
||||
|
||||
// synchronizer for controller shutdown of entire Control
|
||||
shutdown *util.Shutdown
|
||||
}
|
||||
|
||||
func NewControl(ctlConn conn.Conn, 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{
|
||||
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(),
|
||||
auth: authMsg,
|
||||
conn: ctlConn,
|
||||
out: make(chan msg.Message),
|
||||
in: make(chan msg.Message),
|
||||
proxies: make(chan conn.Conn, 10),
|
||||
lastPing: time.Now(),
|
||||
writerShutdown: util.NewShutdown(),
|
||||
readerShutdown: util.NewShutdown(),
|
||||
managerShutdown: util.NewShutdown(),
|
||||
shutdown: util.NewShutdown(),
|
||||
}
|
||||
|
||||
failAuth := func(e error) {
|
||||
@@ -85,14 +92,24 @@ func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
|
||||
}
|
||||
}
|
||||
|
||||
// set logging prefix
|
||||
ctlConn.SetType("ctl")
|
||||
ctlConn.AddLogPrefix(c.id)
|
||||
|
||||
if authMsg.Version != version.Proto {
|
||||
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
|
||||
return
|
||||
}
|
||||
|
||||
// register the control
|
||||
controlRegistry.Add(c.id, c)
|
||||
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
|
||||
replaced.shutdown.WaitComplete()
|
||||
}
|
||||
|
||||
// start the writer first so that the following messages get sent
|
||||
go c.writer()
|
||||
|
||||
// Respond to authentication
|
||||
c.out <- &msg.AuthResp{
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
@@ -102,12 +119,10 @@ func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
|
||||
// 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()
|
||||
go c.manager()
|
||||
go c.reader()
|
||||
go c.stopper()
|
||||
}
|
||||
|
||||
// Register a new tunnel on this control connection
|
||||
@@ -119,14 +134,9 @@ func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
|
||||
c.conn.Debug("Registering new tunnel")
|
||||
t, err := NewTunnel(&tunnelReq, c)
|
||||
if err != nil {
|
||||
ack := &msg.NewTunnel{Error: err.Error()}
|
||||
c.out <- &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
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
// we're done
|
||||
@@ -140,67 +150,45 @@ func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
|
||||
c.out <- &msg.NewTunnel{
|
||||
Url: t.url,
|
||||
Protocol: proto,
|
||||
ReqId: rawTunnelReq.ReqId,
|
||||
}
|
||||
|
||||
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) managerThread() {
|
||||
reap := time.NewTicker(connReapInterval)
|
||||
|
||||
// all shutdown functionality in here
|
||||
func (c *Control) manager() {
|
||||
// don't crash on panics
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
|
||||
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
|
||||
// remove from the control registry
|
||||
controlRegistry.Del(c.id)
|
||||
|
||||
// mark that we're shutting down
|
||||
atomic.StoreInt32(&c.closing, 1)
|
||||
|
||||
// stop the reaping timer
|
||||
reap.Stop()
|
||||
|
||||
// close the connection
|
||||
c.conn.Close()
|
||||
|
||||
// shutdown all of the tunnels
|
||||
for _, t := range c.tunnels {
|
||||
t.Shutdown()
|
||||
}
|
||||
|
||||
// we're safe to close(c.proxies) because c.closing
|
||||
// protects us inside of RegisterProxy
|
||||
close(c.proxies)
|
||||
|
||||
// shut down all of the proxy connections
|
||||
for p := range c.proxies {
|
||||
p.Close()
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// kill everything if the control manager stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that manager() has shutdown
|
||||
defer c.managerShutdown.Complete()
|
||||
|
||||
// reaping timer for detecting heartbeat failure
|
||||
reap := time.NewTicker(connReapInterval)
|
||||
defer reap.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case m := <-c.out:
|
||||
msg.WriteMsg(c.conn, m)
|
||||
|
||||
case m := <-c.stop:
|
||||
if m != nil {
|
||||
msg.WriteMsg(c.conn, m)
|
||||
}
|
||||
return
|
||||
|
||||
case <-reap.C:
|
||||
if time.Since(c.lastPing) > pingTimeoutInterval {
|
||||
c.conn.Info("Lost heartbeat")
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
case mRaw, ok := <-c.in:
|
||||
// c.in closes to indicate shutdown
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
case mRaw := <-c.in:
|
||||
switch m := mRaw.(type) {
|
||||
case *msg.ReqTunnel:
|
||||
c.registerTunnel(m)
|
||||
@@ -213,14 +201,41 @@ func (c *Control) managerThread() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) readThread() {
|
||||
func (c *Control) writer() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Info("Control::readThread failed with error %v: %s", err, debug.Stack())
|
||||
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
c.stop <- nil
|
||||
}()
|
||||
|
||||
// kill everything if the writer() stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that we've flushed all messages
|
||||
defer c.writerShutdown.Complete()
|
||||
|
||||
// write messages to the control channel
|
||||
for m := range c.out {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
|
||||
if err := msg.WriteMsg(c.conn, m); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) reader() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
// kill everything if the reader stops
|
||||
defer c.shutdown.Begin()
|
||||
|
||||
// notify that we're done
|
||||
defer c.readerShutdown.Complete()
|
||||
|
||||
// read messages from the control channel
|
||||
for {
|
||||
if msg, err := msg.ReadMsg(c.conn); err != nil {
|
||||
@@ -231,23 +246,60 @@ func (c *Control) readThread() {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
// this can also panic during shutdown
|
||||
c.in <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) RegisterProxy(conn conn.Conn) {
|
||||
if atomic.LoadInt32(&c.closing) == 1 {
|
||||
c.conn.Debug("Can't register proxies for a control that is closing")
|
||||
conn.Close()
|
||||
return
|
||||
func (c *Control) stopper() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.conn.Error("Failed to shut down control: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait until we're instructed to shutdown
|
||||
c.shutdown.WaitBegin()
|
||||
|
||||
// remove ourself from the control registry
|
||||
controlRegistry.Del(c.id)
|
||||
|
||||
// shutdown manager() so that we have no more work to do
|
||||
close(c.in)
|
||||
c.managerShutdown.WaitComplete()
|
||||
|
||||
// shutdown writer()
|
||||
close(c.out)
|
||||
c.writerShutdown.WaitComplete()
|
||||
|
||||
// close connection fully
|
||||
c.conn.Close()
|
||||
|
||||
// shutdown all of the tunnels
|
||||
for _, t := range c.tunnels {
|
||||
t.Shutdown()
|
||||
}
|
||||
|
||||
// shutdown all of the proxy connections
|
||||
close(c.proxies)
|
||||
for p := range c.proxies {
|
||||
p.Close()
|
||||
}
|
||||
|
||||
c.shutdown.Complete()
|
||||
c.conn.Info("Shutdown complete")
|
||||
}
|
||||
|
||||
func (c *Control) RegisterProxy(conn conn.Conn) {
|
||||
conn.AddLogPrefix(c.id)
|
||||
|
||||
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
|
||||
select {
|
||||
case c.proxies <- conn:
|
||||
c.conn.Info("Registered proxy connection %s", conn.Id())
|
||||
conn.Info("Registered")
|
||||
default:
|
||||
// c.proxies buffer is full, discard this one
|
||||
conn.Info("Proxies buffer is full, discarding.")
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
@@ -258,37 +310,47 @@ func (c *Control) RegisterProxy(conn conn.Conn) {
|
||||
// Returns an error if we couldn't get a proxy because it took too long
|
||||
// or the tunnel is closing
|
||||
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
|
||||
// initial timeout is zero to try to get a proxy connection without asking for one
|
||||
timeout := time.NewTimer(0)
|
||||
var ok bool
|
||||
|
||||
// get a proxy connection from the pool
|
||||
select {
|
||||
case proxyConn, ok = <-c.proxies:
|
||||
if !ok {
|
||||
err = fmt.Errorf("No proxy connections available, control is closing")
|
||||
return
|
||||
}
|
||||
default:
|
||||
// no proxy available in the pool, ask for one over the control channel
|
||||
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
|
||||
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get a proxy connection. if we timeout, request one over the control channel
|
||||
for proxyConn == nil {
|
||||
var ok bool
|
||||
select {
|
||||
case proxyConn, ok = <-c.proxies:
|
||||
if !ok {
|
||||
err = fmt.Errorf("No proxy connections available, control is closing")
|
||||
return
|
||||
}
|
||||
continue
|
||||
case <-timeout.C:
|
||||
c.conn.Debug("Requesting new proxy connection")
|
||||
// request a proxy connection
|
||||
c.out <- &msg.ReqProxy{}
|
||||
// timeout after 1 second if we don't get one
|
||||
timeout.Reset(1 * time.Second)
|
||||
|
||||
case <-time.After(pingTimeoutInterval):
|
||||
err = fmt.Errorf("Timeout trying to get proxy connection")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// To try to reduce latency hanndling tunnel connections, we employ
|
||||
// the following curde heuristic:
|
||||
// If the proxy connection pool is empty, request a new one.
|
||||
// The idea is to always have at least one proxy connection available for immediate use.
|
||||
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
|
||||
// It should be a good start though.
|
||||
if len(c.proxies) == 0 {
|
||||
c.out <- &msg.ReqProxy{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Called when this control is replaced by another control
|
||||
// this can happen if the network drops out and the client reconnects
|
||||
// before the old tunnel has lost its heartbeat
|
||||
func (c *Control) Replaced(replacement *Control) {
|
||||
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
|
||||
|
||||
// set the control id to empty string so that when stopper()
|
||||
// calls registry.Del it won't delete the replacement
|
||||
c.id = ""
|
||||
|
||||
// tell the old one to shutdown
|
||||
c.shutdown.Begin()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -66,6 +67,9 @@ func httpHandler(tcpConn net.Conn, proto string) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Make sure we detect dead connections while we decide how to multiplex
|
||||
conn.SetDeadline(time.Now().Add(connReadTimeout))
|
||||
|
||||
// read out the http request
|
||||
req, err := conn.ReadRequest()
|
||||
if err != nil {
|
||||
@@ -95,5 +99,9 @@ func httpHandler(tcpConn net.Conn, proto string) {
|
||||
return
|
||||
}
|
||||
|
||||
// dead connections will now be handled by tunnel heartbeating and the client
|
||||
conn.SetDeadline(time.Time{})
|
||||
|
||||
// let the tunnel handle the connection now
|
||||
tunnel.HandlePublicConnection(conn)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math/rand"
|
||||
"ngrok/conn"
|
||||
log "ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
registryCacheSize uint64 = 1024 * 1024 // 1 MB
|
||||
registryCacheSize uint64 = 1024 * 1024 // 1 MB
|
||||
connReadTimeout time.Duration = 10 * time.Second
|
||||
)
|
||||
|
||||
// GLOBALS
|
||||
@@ -51,7 +55,7 @@ func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
|
||||
// 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) {
|
||||
func tunnelListener(addr string, tlsConfig *tls.Config) {
|
||||
// listen for incoming connections
|
||||
listener, err := conn.Listen(addr, "tun", tlsConfig)
|
||||
if err != nil {
|
||||
@@ -60,19 +64,37 @@ func tunnelListener(addr string) {
|
||||
|
||||
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
|
||||
for c := range listener.Conns {
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(c); err != nil {
|
||||
c.Error("Failed to read message: %v", err)
|
||||
c.Close()
|
||||
}
|
||||
go func(tunnelConn conn.Conn) {
|
||||
// don't crash on panics
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.Auth:
|
||||
go NewControl(c, m)
|
||||
tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout))
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
|
||||
tunnelConn.Warn("Failed to read message: %v", err)
|
||||
tunnelConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
case *msg.RegProxy:
|
||||
go NewProxy(c, m)
|
||||
}
|
||||
// don't timeout after the initial read, tunnel heartbeating will kill
|
||||
// dead connections
|
||||
tunnelConn.SetReadDeadline(time.Time{})
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.Auth:
|
||||
NewControl(tunnelConn, m)
|
||||
|
||||
case *msg.RegProxy:
|
||||
NewProxy(tunnelConn, m)
|
||||
|
||||
default:
|
||||
tunnelConn.Close()
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +120,12 @@ func Main() {
|
||||
// start listeners
|
||||
listeners = make(map[string]*conn.Listener)
|
||||
|
||||
// load tls configuration
|
||||
tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// listen for http
|
||||
if opts.httpAddr != "" {
|
||||
listeners["http"] = startHttpListener(opts.httpAddr, nil)
|
||||
@@ -109,5 +137,5 @@ func Main() {
|
||||
}
|
||||
|
||||
// ngrok clients
|
||||
tunnelListener(opts.tunnelAddr)
|
||||
tunnelListener(opts.tunnelAddr, tlsConfig)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func init() {
|
||||
keenApiKey := os.Getenv("KEEN_API_KEY")
|
||||
|
||||
if keenApiKey != "" {
|
||||
metrics = NewKeenIoMetrics()
|
||||
metrics = NewKeenIoMetrics(60 * time.Second)
|
||||
} else {
|
||||
metrics = NewLocalMetrics(30 * time.Second)
|
||||
}
|
||||
@@ -154,9 +154,9 @@ func (m *LocalMetrics) Report() {
|
||||
}
|
||||
}
|
||||
|
||||
type KeenIoRequest struct {
|
||||
Path string
|
||||
Body []byte
|
||||
type KeenIoMetric struct {
|
||||
Collection string
|
||||
Event interface{}
|
||||
}
|
||||
|
||||
type KeenIoMetrics struct {
|
||||
@@ -164,20 +164,54 @@ type KeenIoMetrics struct {
|
||||
ApiKey string
|
||||
ProjectToken string
|
||||
HttpClient http.Client
|
||||
Requests chan *KeenIoRequest
|
||||
Metrics chan *KeenIoMetric
|
||||
}
|
||||
|
||||
func NewKeenIoMetrics() *KeenIoMetrics {
|
||||
func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics {
|
||||
k := &KeenIoMetrics{
|
||||
Logger: log.NewPrefixLogger("metrics"),
|
||||
ApiKey: os.Getenv("KEEN_API_KEY"),
|
||||
ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"),
|
||||
Requests: make(chan *KeenIoRequest, 100),
|
||||
Metrics: make(chan *KeenIoMetric, 1000),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for req := range k.Requests {
|
||||
k.AuthedRequest("POST", req.Path, bytes.NewReader(req.Body))
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
k.Error("KeenIoMetrics failed: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
batch := make(map[string][]interface{})
|
||||
batchTimer := time.Tick(batchInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case m := <-k.Metrics:
|
||||
list, ok := batch[m.Collection]
|
||||
if !ok {
|
||||
list = make([]interface{}, 0)
|
||||
}
|
||||
batch[m.Collection] = append(list, m.Event)
|
||||
|
||||
case <-batchTimer:
|
||||
// no metrics to report
|
||||
if len(batch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(batch)
|
||||
if err != nil {
|
||||
k.Error("Failed to serialize metrics payload: %v, %v", batch, err)
|
||||
} else {
|
||||
for key, val := range batch {
|
||||
k.Debug("Reporting %d metrics for %s", len(val), key)
|
||||
}
|
||||
|
||||
k.AuthedRequest("POST", "/events", bytes.NewReader(payload))
|
||||
}
|
||||
batch = make(map[string][]interface{})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -198,13 +232,15 @@ func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (
|
||||
req.ContentLength = int64(body.Len())
|
||||
}
|
||||
|
||||
requestStartAt := time.Now()
|
||||
resp, err = k.HttpClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
k.Error("Failed to send metric event to keen.io %v", err)
|
||||
} else {
|
||||
k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds())
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 201 {
|
||||
if resp.StatusCode != 200 {
|
||||
bytes, _ := ioutil.ReadAll(resp.Body)
|
||||
k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes)
|
||||
}
|
||||
@@ -217,7 +253,7 @@ func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) {
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) {
|
||||
buf, err := json.Marshal(struct {
|
||||
event := struct {
|
||||
Keen KeenStruct `json:"keen"`
|
||||
OS string
|
||||
ClientId string
|
||||
@@ -248,13 +284,9 @@ func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time,
|
||||
ConnectionDuration: time.Since(start).Seconds(),
|
||||
BytesIn: in,
|
||||
BytesOut: out,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
k.Error("Error serializing metric %v", err)
|
||||
} else {
|
||||
k.Requests <- &KeenIoRequest{Path: "/events/CloseConnection", Body: buf}
|
||||
}
|
||||
|
||||
k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event}
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) {
|
||||
@@ -265,7 +297,7 @@ type KeenStruct struct {
|
||||
}
|
||||
|
||||
func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
|
||||
buf, err := json.Marshal(struct {
|
||||
event := struct {
|
||||
Keen KeenStruct `json:"keen"`
|
||||
OS string
|
||||
ClientId string
|
||||
@@ -291,12 +323,7 @@ func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
|
||||
Duration: time.Since(t.start).Seconds(),
|
||||
HttpAuth: t.req.HttpAuth != "",
|
||||
Subdomain: t.req.Subdomain != "",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
k.Error("Error serializing metric %v", err)
|
||||
return
|
||||
} else {
|
||||
k.Requests <- &KeenIoRequest{Path: "/events/CloseTunnel", Body: buf}
|
||||
}
|
||||
|
||||
k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event}
|
||||
}
|
||||
|
||||
@@ -180,11 +180,18 @@ func (r *ControlRegistry) Get(clientId string) *Control {
|
||||
return r.controls[clientId]
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Add(clientId string, ctl *Control) {
|
||||
func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
oldCtl = r.controls[clientId]
|
||||
if oldCtl != nil {
|
||||
oldCtl.Replaced(ctl)
|
||||
}
|
||||
|
||||
r.controls[clientId] = ctl
|
||||
r.Info("Registered control with id %s", clientId)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Del(clientId string) error {
|
||||
|
||||
@@ -4,40 +4,40 @@ import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"ngrok/server/assets"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
readOrBytes := func(envVar string, default_path string) []byte {
|
||||
f := os.Getenv(envVar)
|
||||
if f == "" {
|
||||
b, err := assets.ReadAsset(default_path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
} else {
|
||||
if b, err := ioutil.ReadFile(f); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
func LoadTLSConfig(crtPath string, keyPath string) (tlsConfig *tls.Config, err error) {
|
||||
fileOrAsset := func(path string, default_path string) ([]byte, error) {
|
||||
loadFn := ioutil.ReadFile
|
||||
if path == "" {
|
||||
loadFn = assets.ReadAsset
|
||||
path = default_path
|
||||
}
|
||||
|
||||
return loadFn(path)
|
||||
}
|
||||
|
||||
crt := readOrBytes("TLS_CRT_FILE", "assets/server/tls/snakeoil.crt")
|
||||
key := readOrBytes("TLS_KEY_FILE", "assets/server/tls/snakeoil.key")
|
||||
cert, err := tls.X509KeyPair(crt, key)
|
||||
var (
|
||||
crt []byte
|
||||
key []byte
|
||||
cert tls.Certificate
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if crt, err = fileOrAsset(crtPath, "assets/server/tls/snakeoil.crt"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cert, err = tls.X509KeyPair(crt, key); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -104,50 +105,58 @@ func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
|
||||
proto := t.req.Protocol
|
||||
switch proto {
|
||||
case "tcp":
|
||||
var port int = 0
|
||||
bindTcp := func(port int) error {
|
||||
if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil {
|
||||
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
|
||||
|
||||
// register it
|
||||
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
|
||||
// This should never be possible because the OS will
|
||||
// only assign available ports to us.
|
||||
t.listener.Close()
|
||||
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
|
||||
return err
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
// use the custom remote port you asked for
|
||||
if t.req.RemotePort != 0 {
|
||||
bindTcp(int(t.req.RemotePort))
|
||||
return
|
||||
}
|
||||
|
||||
// try to return to you the same port you had before
|
||||
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
|
||||
if cachedUrl != "" {
|
||||
var port int
|
||||
parts := strings.Split(cachedUrl, ":")
|
||||
portPart := parts[len(parts)-1]
|
||||
port, err = strconv.Atoi(portPart)
|
||||
if err != nil {
|
||||
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
|
||||
// continue with zero
|
||||
port = 0
|
||||
} else {
|
||||
// we have a valid, cached port, let's try to bind with it
|
||||
if bindTcp(port) != nil {
|
||||
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
|
||||
} else {
|
||||
// success, we're done
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bind for TCP connections
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port})
|
||||
|
||||
// If we failed with a custom port, try with a random one
|
||||
if err != nil && port != 0 {
|
||||
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
|
||||
}
|
||||
|
||||
// we tried to bind with a random port and failed (no more ports available?)
|
||||
if err != nil {
|
||||
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
|
||||
|
||||
// register it
|
||||
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
|
||||
// This should never be possible because the OS will
|
||||
// only assign available ports to us.
|
||||
t.listener.Close()
|
||||
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
|
||||
return
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
bindTcp(0)
|
||||
return
|
||||
|
||||
case "http", "https":
|
||||
l, ok := listeners[proto]
|
||||
@@ -245,9 +254,8 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
|
||||
metrics.OpenConnection(t, publicConn)
|
||||
|
||||
var proxyConn conn.Conn
|
||||
var attempts int
|
||||
var err error
|
||||
for {
|
||||
for i := 0; i < (2 * proxyMaxPoolSize); i++ {
|
||||
// get a proxy connection
|
||||
if proxyConn, err = t.ctl.GetProxy(); err != nil {
|
||||
t.Warn("Failed to get proxy connection: %v", err)
|
||||
@@ -262,20 +270,29 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
|
||||
Url: t.url,
|
||||
ClientAddr: publicConn.RemoteAddr().String(),
|
||||
}
|
||||
|
||||
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
|
||||
attempts += 1
|
||||
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
|
||||
if attempts > 3 {
|
||||
// give up
|
||||
publicConn.Error("Too many failures starting proxy connection")
|
||||
return
|
||||
}
|
||||
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i)
|
||||
proxyConn.Close()
|
||||
} else {
|
||||
// success
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// give up
|
||||
publicConn.Error("Too many failures starting proxy connection")
|
||||
return
|
||||
}
|
||||
|
||||
// To reduce latency handling tunnel connections, we employ the following curde heuristic:
|
||||
// Whenever we take a proxy connection from the pool, replace it with a new one
|
||||
util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} })
|
||||
|
||||
// no timeouts while connections are joined
|
||||
proxyConn.SetDeadline(time.Time{})
|
||||
|
||||
// join the public and proxy connections
|
||||
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
|
||||
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)
|
||||
|
||||
33
src/ngrok/util/errors.go
Normal file
33
src/ngrok/util/errors.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const crashMessage = `panic: %v
|
||||
|
||||
%s
|
||||
|
||||
Oh noes! ngrok crashed!
|
||||
|
||||
Please submit the stack trace and any relevant information to:
|
||||
github.com/inconshreveable/ngrok/issues`
|
||||
|
||||
func MakePanicTrace(err interface{}) string {
|
||||
stackBuf := make([]byte, 4096)
|
||||
n := runtime.Stack(stackBuf, false)
|
||||
return fmt.Sprintf(crashMessage, err, stackBuf[:n])
|
||||
}
|
||||
|
||||
// Runs the given function and converts any panic encountered while doing so
|
||||
// into an error. Useful for sending to channels that will close
|
||||
func PanicToError(fn func()) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Panic: %v", r)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return
|
||||
}
|
||||
43
src/ngrok/util/shutdown.go
Normal file
43
src/ngrok/util/shutdown.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A small utility class for managing controlled shutdowns
|
||||
type Shutdown struct {
|
||||
sync.Mutex
|
||||
inProgress bool
|
||||
begin chan int // closed when the shutdown begins
|
||||
complete chan int // closed when the shutdown completes
|
||||
}
|
||||
|
||||
func NewShutdown() *Shutdown {
|
||||
return &Shutdown{
|
||||
begin: make(chan int),
|
||||
complete: make(chan int),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shutdown) Begin() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.inProgress == true {
|
||||
return
|
||||
} else {
|
||||
s.inProgress = true
|
||||
close(s.begin)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shutdown) WaitBegin() {
|
||||
<-s.begin
|
||||
}
|
||||
|
||||
func (s *Shutdown) Complete() {
|
||||
close(s.complete)
|
||||
}
|
||||
|
||||
func (s *Shutdown) WaitComplete() {
|
||||
<-s.complete
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func MakePanicTrace(err interface{}) string {
|
||||
stackBuf := make([]byte, 4096)
|
||||
n := runtime.Stack(stackBuf, false)
|
||||
return fmt.Sprintf("panic: %v\n\n%s", err, stackBuf[:n])
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
const (
|
||||
Proto = "2"
|
||||
Major = "1"
|
||||
Minor = "1"
|
||||
Minor = "6"
|
||||
)
|
||||
|
||||
func MajorMinor() string {
|
||||
|
||||
Reference in New Issue
Block a user