42 Commits
0.15 ... 0.22

Author SHA1 Message Date
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
3655bd8027 make clean now cleans up all of the object files 2013-08-29 18:39:16 +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
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
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
Alan Shreve
fb0c343161 bump version 2013-07-30 15:25:45 +02:00
Alan Shreve
7032606103 Fixed an issues where the affinity cache could not be serialized/deserialized or saved/loaded from disk. Added better logging around affinity cache load/save. TCP tunnel urls are now handled in the affinity cache and will attempt to return to you the same port you had previously. 2013-07-30 15:24:08 +02:00
Stephen Huenneke
aea0524bb5 Fixing over-rides of public port so that they don't result in mis-matched host names due to Request.Host including the port in the returned hostname. 2013-07-29 16:00:24 -07:00
Alan Shreve
078bbb1fdc fix makefile to get bindata dependency properly 2013-07-28 01:01:18 +02:00
Alan Shreve
fbcde8774b add basic travis.yml 2013-07-28 00:51:50 +02:00
Alan Shreve
bd3356071e Merge branch 'autoupdate' 2013-07-28 00:14:04 +02:00
Alan Shreve
b63f9bc718 Merge branch 'master' of github.com:inconshreveable/ngrok 2013-07-28 00:13:51 +02:00
Alan Shreve
896afc0141 add contributors file 2013-07-26 14:00:31 +02:00
inconshreveable
4f4bd7bd06 Merge pull request #10 from cespare/7-xml-syntax-crash
XML and JSON crash
2013-07-26 04:58:17 -07:00
inconshreveable
7fb02bb768 Merge pull request #9 from cespare/8-response-summary-fix
Response summary fix
2013-07-26 04:57:05 -07:00
Alan Shreve
6a2b9c31b1 fix a bug where ngrokd would reject http requests with an authorization header if the client had not specified any authentication necessary 2013-07-26 13:32:47 +02:00
Caleb Spare
07c2fdc693 Handle 'application/xml' the same as 'text/xml'
(Go does not fallthrough switch statements.)
2013-07-24 01:54:31 -07:00
Caleb Spare
b275aa7123 Fix some unsafe type assertions.
Fixes issue #7.
2013-07-24 01:53:35 -07:00
Caleb Spare
10a80333da Do the code highlighting where the bodies are processed.
This fixes an issue where highlight.js was removing a bound element
out from underneath angular, which breaks the updates of the body
elements for requests and responses.

Fixes issue #8.
2013-07-24 01:22:59 -07:00
Caleb Spare
09953ac19a Add the angular-sanitize plugin. 2013-07-24 01:16:09 -07:00
24 changed files with 374 additions and 161 deletions

4
.travis.yml Normal file
View File

@@ -0,0 +1,4 @@
script: make release-all
install: true
go:
- 1.1

5
CONTRIBUTORS Normal file
View File

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

View File

@@ -16,11 +16,13 @@ client: deps
go install -tags '$(BUILDTAGS)' ngrok/main/ngrok
client-assets:
go install github.com/inconshreveable/go-bindata
go get github.com/inconshreveable/go-bindata
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
bin/go-bindata -o src/ngrok/client/assets assets/client
server-assets:
go install github.com/inconshreveable/go-bindata
go get github.com/inconshreveable/go-bindata
GOOS="" GOARCH="" go install github.com/inconshreveable/go-bindata
bin/go-bindata -o src/ngrok/server/assets assets/server
release-client: BUILDTAGS=release
@@ -34,4 +36,4 @@ release-all: release-client release-server
all: fmt client server
clean:
go clean ngrok/...
go clean -i -r ngrok/...

View File

@@ -8,6 +8,7 @@
<script src="/static/js/jquery-1.9.1.min.js"></script>
<script src="/static/js/jquery.timeago.js"></script>
<script src="/static/js/angular.js"></script>
<script src="/static/js/angular-sanitize.min.js"></script>
<script src="/static/js/base64.js"></script>
<script src="/static/js/ngrok.js"></script>
<script type="text/javascript">
@@ -18,8 +19,8 @@
table.params { font-size: 12px; font-family: Courier, monospace; }
.txn-selector tr { cursor: pointer; }
.txn-selector tr:hover { background-color: #ddd; }
tr.selected, tr.selected:hover {
background-color: #ff9999;
tr.selected, tr.selected:hover {
background-color: #ff9999;
background-color: #000000;
color:white;
@@ -70,11 +71,11 @@
</span>
</div>
<div class="span4">
<i class="icon-time"></i> Duration
<i class="icon-time"></i> Duration
<span style="margin-left: 8px;" class="muted">{{Txn.Duration}}</span>
</div>
<div class="span4">
<i class="icon-user"></i> IP
<i class="icon-user"></i> IP
<span style="margin-left: 8px;" class="muted">{{Txn.Req.Header['X-Real-Ip'][0]}}</span>
</div>
</div>

View File

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

View File

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

View File

@@ -1,17 +1,51 @@
# Changelog
## 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_.

View File

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

View File

@@ -125,7 +125,7 @@ func parseArgs() *Options {
webport := flag.Int(
"webport",
4040,
"The port on which the web interface is served")
"The port on which the web interface is served, -1 to disable")
logto := flag.String(
"log",

View File

@@ -191,8 +191,8 @@ func control(s *State, ctl *ui.Controller) {
}
// update UI state
conn.Info("Tunnel established at %v", regAck.Url)
s.publicUrl = regAck.Url
conn.Info("Tunnel established at %v", s.GetPublicUrl())
s.status = "online"
s.serverVersion = regAck.MmVersion
ctl.Update(s)
@@ -255,7 +255,10 @@ func Main() {
// init ui
ctl := ui.NewController()
web.NewWebView(ctl, s, opts.webport)
if opts.webport != -1 {
web.NewWebView(ctl, s, opts.webport)
}
if opts.logto != "stdout" {
term.New(ctl, s)
}

View File

@@ -24,12 +24,12 @@ type State struct {
// 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) GetPublicUrl() string { return s.publicUrl }
func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
return s.metrics.connMeter, s.metrics.connTimer

View File

@@ -11,7 +11,7 @@ const (
UpdateNone = -1 * iota
UpdateInstalling
UpdateReady
UpdateError
UpdateAvailable
)
type State interface {

View File

@@ -15,67 +15,100 @@ 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 *State, ctl *ui.Controller, progress chan int, complete chan int) {
for {
select {
case pct, ok := <-progress:
if !ok {
close(complete)
return
} else if pct == 100 {
s.update = ui.UpdateInstalling
ctl.Update(s)
close(complete)
return
} else {
s.update = ui.UpdateNone
if pct%25 == 0 {
log.Info("Downloading update %d%% complete", pct)
}
s.update = ui.UpdateStatus(pct)
ctl.Update(s)
}
}
}
}
func autoUpdate(s *State, ctl *ui.Controller, 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, ctl, 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.update = ui.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.update = ui.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.update = ui.UpdateReady
}
}
@@ -86,11 +119,9 @@ func autoUpdate(s *State, ctl *ui.Controller, token string) {
// 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

@@ -87,7 +87,7 @@ func (v *TermView) Render() {
updateMsg = "ngrok is updating"
case ui.UpdateReady:
updateMsg = "ngrok has updated: restart ngrok for the new version"
case ui.UpdateError:
case ui.UpdateAvailable:
updateMsg = "new version available at https://ngrok.com"
default:
pct := float64(updateStatus) / 100.0
@@ -119,6 +119,9 @@ func (v *TermView) Render() {
v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
if v.state.GetWebPort() == -1 {
webAddr = "disabled"
}
v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
connMeter, connTimer := v.state.GetConnectionMetrics()

View File

@@ -112,21 +112,22 @@ func makeBody(h http.Header, body []byte) SerializedBody {
if b.RawContentType != "" {
b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0])
switch b.ContentType {
case "application/xml":
case "text/xml":
case "application/xml", "text/xml":
err = xml.Unmarshal(body, new(XMLDoc))
if err != nil {
syntaxError := err.(*xml.SyntaxError)
// xml syntax errors only give us a line number, so we
// count to find an offset
b.ErrorOffset = offsetForLine(syntaxError.Line)
if syntaxError, ok := err.(*xml.SyntaxError); ok {
// xml syntax errors only give us a line number, so we
// count to find an offset
b.ErrorOffset = offsetForLine(syntaxError.Line)
}
}
case "application/json":
err = json.Unmarshal(body, new(json.RawMessage))
if err != nil {
syntaxError := err.(*json.SyntaxError)
b.ErrorOffset = int(syntaxError.Offset)
if syntaxError, ok := err.(*json.SyntaxError); ok {
b.ErrorOffset = int(syntaxError.Offset)
}
}
case "application/x-www-form-urlencoded":
@@ -229,7 +230,7 @@ func (h *WebHttpView) register() {
r.ParseForm()
txnid := r.Form.Get("txnid")
if txn, ok := h.idToTxn[txnid]; ok {
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
bodyBytes, err := httputil.DumpRequest(txn.HttpTxn.Req.Request, true)
if err != nil {
panic(err)
}

View File

@@ -37,8 +37,14 @@ type PrefixLogger struct {
prefix string
}
func NewPrefixLogger() Logger {
return &PrefixLogger{Logger: &root}
func NewPrefixLogger(prefixes ...string) Logger {
logger := &PrefixLogger{Logger: &root}
for _, p := range prefixes {
logger.AddLogPrefix(p)
}
return logger
}
func (pl *PrefixLogger) pfx(fmtstr string) interface{} {

View File

@@ -9,6 +9,7 @@ import (
"net/http/httputil"
"ngrok/conn"
"ngrok/util"
"sync"
"time"
)
@@ -111,5 +112,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

@@ -5,6 +5,7 @@ import (
"net"
"ngrok/conn"
"ngrok/log"
"strings"
)
const (
@@ -67,20 +68,23 @@ func httpHandler(tcpConn net.Conn) {
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 := tunnels.Get("http://" + host)
if tunnel == nil {
conn.Info("No tunnel found for hostname %s", req.Host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(req.Host)+18, req.Host)))
conn.Info("No tunnel found for hostname %s", host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
return
}
// satisfy auth, if necessary
conn.Debug("From client: %s", req.Header.Get("Authorization"))
conn.Debug("To match: %s", tunnel.regMsg.HttpAuth)
if req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
conn.Info("Authentication failed")
// If the client specified http auth and it doesn't match this request's auth
// then fail the request with 401 Not Authorized and request the client reissue the
// request with basic authdeny the request
if tunnel.regMsg.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
conn.Info("Authentication failed: %s", req.Header.Get("Authorization"))
conn.Write([]byte(NotAuthorized))
return
}

View File

@@ -24,6 +24,7 @@ var (
tunnels *TunnelRegistry
registryCacheSize uint64 = 1024 * 1024 // 1 MB
domain string
publicPort int
)
func parseArgs() *Options {
@@ -108,6 +109,7 @@ func Main() {
// parse options
opts := parseArgs()
domain = opts.domain
publicPort = opts.publicPort
// init logging
log.LogTo(opts.logto)

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

View File

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

View File

@@ -9,6 +9,8 @@ import (
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
@@ -56,47 +58,89 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()}
}
var err error
switch t.regMsg.Protocol {
case "tcp":
var err error
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
var port int = 0
if err != nil {
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"}
// try to return to you the same port you had before
cachedUrl := tunnels.GetCachedRegistration(t)
if cachedUrl != "" {
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
}
}
// 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 {
failReg(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)
if err = tunnels.Register(t.url, t); err != nil {
// This should never be possible because the OS will only assign
// available ports to us.
t.Error("TCP listener bound, but failed to register: %s", err.Error())
// register it
if err = tunnels.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
failReg(err)
failReg(fmt.Errorf("TCP listener bound, but failed to register %s", t.url))
return
}
go t.listenTcp(t.listener)
case "http":
vhost := os.Getenv("VHOST")
if vhost == "" {
vhost = fmt.Sprintf("%s:%d", domain, publicPort)
}
// Canonicalize virtual host on default port 80
if strings.HasSuffix(vhost, ":80") {
vhost = vhost[0 : len(vhost)-3]
}
if strings.TrimSpace(t.regMsg.Hostname) != "" {
t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, domain)
t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, vhost)
}
vhost = strings.ToLower(vhost)
t.url = strings.ToLower(t.url)
if t.url != "" {
if err := tunnels.Register(t.url, t); err != nil {
failReg(err)
return
}
} else {
t.url = tunnels.RegisterRepeat(func() string {
return fmt.Sprintf("http://%x.%s", rand.Int31(), domain)
t.url, err = tunnels.RegisterRepeat(func() string {
return fmt.Sprintf("http://%x.%s", rand.Int31(), vhost)
}, t)
if err != nil {
failReg(err)
return
}
}
}

View File

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