73 Commits
0.12 ... 1.0

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

Fixes issue #8.
2013-07-24 01:22:59 -07:00
Caleb Spare
09953ac19a Add the angular-sanitize plugin. 2013-07-24 01:16:09 -07:00
Alan Shreve
081a1f7fde fix update to use ssl, specify the right parameters 2013-07-23 16:04:11 +01:00
Alan Shreve
26038dbf1b set correct update endpoint 2013-07-23 15:41:54 +01:00
Alan Shreve
452ba7a575 fix download completion ui handling, add some better logging 2013-07-18 18:11:18 +01:00
Alan Shreve
35d8fc20ff initial implementation of auto-updating in ngrok 2013-07-16 10:37:22 +01:00
Alan Shreve
492182497a better comments for auth 2013-07-10 14:34:09 -07:00
Alan Shreve
0d49d7223d bump version 2013-07-03 00:18:11 -07:00
Alan Shreve
b136b8f1aa fix an issue where logging during authtoken reading was never reported. fix an issue where ngrok could not find the user's home directory on linux when cross-compiled from darwin 2013-07-03 00:16:16 -07:00
Alan Shreve
86c49329c0 version 0.13 release 2013-07-02 17:15:34 -07:00
Alan Shreve
e200e75da4 Merge branch 'master' of github.com:inconshreveable/ngrok 2013-07-02 17:02:18 -07:00
Alan Shreve
235e5e1a03 show ip, duration and time since request in the web view 2013-07-02 17:01:56 -07:00
inconshreveable
b1ecf6b2b7 Merge pull request #5 from philips/patch-1
README: fix typo reploy -> replay
2013-07-01 16:26:37 -07:00
Brandon Philips
d4421470c6 README: fix typo reploy -> replay 2013-07-01 14:55:20 -07:00
Alan Shreve
33e6abd696 add hostname option to client 2013-07-01 14:38:55 -07:00
Alan Shreve
4823077f97 make sure we assign tunnels in the registry 2013-07-01 14:38:10 -07:00
Alan Shreve
885e29abde refactor tunnel registry. use an LRU size-limited cache instead a time-based cache for tunnel urls. remove nrsc script that is no longer used 2013-07-01 14:04:30 -07:00
Alan Shreve
d704c5b2c6 Merge branch 'master' of github.com:inconshreveable/ngrok 2013-06-30 12:43:02 -07:00
inconshreveable
26e9190fac Improve developer's guide documentation 2013-06-29 21:38:05 -07:00
inconshreveable
c0b7bf6429 Add readme link to developer's guide 2013-06-29 21:29:11 -07:00
49 changed files with 2411 additions and 1017 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

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

View File

@@ -1,5 +1,5 @@
# ngrok - Introspected tunnels to localhost ([homepage](https://ngrok.com))
### "I want to securely expose a web server to the internet and capture all traffic for detailed inspection and reploy"
### "I want to securely expose a web server to the internet and capture all traffic for detailed inspection and replay"
![](https://ngrok.com/static/img/overview.png)
## What is ngrok?
@@ -37,3 +37,5 @@ are only available by creating an account on ngrok.com. If you need them, [creat
cd ngrok && make
bin/ngrok [LOCAL PORT]
### Further Reading
[ngrok developer's guide](docs/DEVELOPMENT.md)

View File

@@ -6,7 +6,9 @@
<script src="/static/js/highlight.min.js"></script>
<script src="/static/js/vkbeautify.js"></script>
<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">
@@ -17,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;
@@ -46,7 +48,12 @@
<div class="span6 offset3">
<div class="well" style="padding: 20px 50px;">
<h4>No requests to display yet</h4>
<p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
<hr />
<h5>To get started, make a request to one of your tunnel URLs:</h5>
<ul>
<li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
</ul>
</p>
</div>
</div>
</div>
@@ -61,7 +68,23 @@
</tr>
</table>
</div>
<div class="span6">
<div class="span6" ng-controller="HttpTxn" ng-show="!!Txn">
<div class="row-fluid">
<div class="span4">
<span title="{{ISO8601(Txn.Start)}}" class="muted">
{{TimeFormat(Txn.Start)}}
</span>
</div>
<div class="span4">
<i class="icon-time"></i> Duration
<span style="margin-left: 8px;" class="muted">{{Txn.Duration}}</span>
</div>
<div class="span4">
<i class="icon-user"></i> IP
<span style="margin-left: 8px;" class="muted">{{Txn.ConnCtx.ClientAddr.split(":")[0]}}</span>
</div>
</div>
<hr />
<div ng-show="!!Req" ng-controller="HttpRequest">
<h3>{{ Req.MethodPath }}</h3>
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

@@ -0,0 +1,193 @@
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 1.3.0
* @requires jQuery v1.2.3+
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else if (typeof timestamp === "number") {
return inWords(new Date(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowFuture: false,
localeTitle: false,
cutoff: 0,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
wordSeparator: " ",
numbers: []
}
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator || "";
if ($l.wordSeparator === undefined) { separator = " "; }
return $.trim([prefix, words, suffix].join(separator));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
return new Date(s);
},
datetime: function(elem) {
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
// functions that can be called via $(el).timeago('action')
// init is default when no action is given
// functions are called with context of a single element
var functions = {
init: function(){
var refresh_el = $.proxy(refresh, this);
refresh_el();
var $s = $t.settings;
if ($s.refreshMillis > 0) {
setInterval(refresh_el, $s.refreshMillis);
}
},
update: function(time){
$(this).data('timeago', { datetime: $t.parse(time) });
refresh.apply(this);
},
updateFromDOM: function(){
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
refresh.apply(this);
}
};
$.fn.timeago = function(action, options) {
var fn = action ? functions[action] : functions.init;
if(!fn){
throw new Error("Unknown function name '"+ action +"' for timeago");
}
// each over objects here and call the requested function
this.each(function(){
fn.call(this, options);
});
return this;
};
function refresh() {
var data = prepareData(this);
var $s = $t.settings;
if (!isNaN(data.datetime)) {
if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) {
$(this).text(inWords(data.datetime));
}
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if ($t.settings.localeTitle) {
element.attr("title", element.data('timeago').datetime.toLocaleString());
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}));

View File

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

View File

@@ -1,6 +1,57 @@
# Changelog
## 0.23 - 09/06/2013
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay
## 0.22 - 09/04/2013
- FEATURE: ngrok now tunnels websocket requests
## 0.21 - 08/17/2013
- IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1
## 0.20 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop
## 0.19 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check
- BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up
## 0.18 - 08/15/2013
- BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons
- BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80
- BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request
- IMPROVEMENT: ngrok can now indicate manual updates again
- IMPROVEMENT: ngrok can now supports update channels
- IMPROVEMENT: ngrok can now detect some updates that will fail before downloading
## 0.17 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol
## 0.16 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error
- BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests
- BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file
- BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file
- BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified.
- BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first
- IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs
- IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles
## 0.15 - 07/27/2013
- FEATURE: ngrok can now update itself automatically
## 0.14 - 07/03/2013
- BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux
- BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens
## 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
## 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

@@ -33,18 +33,18 @@ There are Makefile targets for compiling the client and server for releases:
## Developing locally
The strategy I use for developing on ngrok is to do the following:
1. Add the following lines to /etc/hosts:
Add the following lines to /etc/hosts:
127.0.0.1 ngrok.me
127.0.0.1 tunnel.ngrok.me
1. Run ngrokd with the following options:
Run ngrokd with the following options:
./bin/ngrokd -domain ngrok.me
1. Run ngrok with the following options
Run ngrok with the following options
./bin/ngrok -server=ngrok.me:4443 -subdomain tunnel -log ngrok.log 8080
./bin/ngrok -server=ngrok.me:4443 -subdomain=tunnel -log=ngrok.log 8080
This will get you setup with an ngrok client talking to an ngrok server all locally under your control. Happy hacking!
@@ -78,10 +78,10 @@ Messages are sent over the wire as netstrings of the form:
The message length is sent as a 64-bit little endian integer.
### Code
The network protocol lives under _src/ngrok/msg_
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 exact what messages are sent between the client and server.
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.
## ngrokd - the server
### Code
@@ -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_.

46
nrsc
View File

@@ -1,46 +0,0 @@
#!/bin/bash
# Pack assets as zip payload in go executable
# Idea from Carlos Castillo (http://bit.ly/SmYXXm)
case "$1" in
-h | --help )
echo "usage: $(basename $0) EXECTABLE RESOURCE_DIR [ZIP OPTIONS]";
exit;;
--version )
echo "nrsc version 0.3.1"; exit;;
esac
if [ $# -lt 2 ]; then
$0 -h
exit 1
fi
exe=$1
shift
root=$1
shift
if [ ! -f "${exe}" ]; then
echo "error: can't find $exe"
exit 1
fi
if [ ! -d "${root}" ]; then
echo "error: ${root} is not a directory"
exit 1
fi
# Exit on 1'st error
set -e
tmp="/tmp/nrsc-$(date +%s).zip"
trap "rm -f ${tmp}" EXIT
# Create zip file
(zip -r "${tmp}" ${root} $@)
# Append zip to executable
cat "${tmp}" >> "${exe}"
# Fix zip offset in file
zip -q -A "${exe}"

245
src/ngrok/cache/lru.go vendored Normal file
View File

@@ -0,0 +1,245 @@
// Copyright 2012, Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The implementation borrows heavily from SmallLRUCache (originally by Nathan
// Schrenk). The object maintains a doubly-linked list of elements in the
// When an element is accessed it is promoted to the head of the list, and when
// space is needed the element at the tail of the list (the least recently used
// element) is evicted.
package cache
import (
"container/list"
"encoding/gob"
"fmt"
"io"
"os"
"sync"
"time"
)
type LRUCache struct {
mu sync.Mutex
// list & table of *entry objects
list *list.List
table map[string]*list.Element
// Our current size, in bytes. Obviously a gross simplification and low-grade
// approximation.
size uint64
// How many bytes we are limiting the cache to.
capacity uint64
}
// Values that go into LRUCache need to satisfy this interface.
type Value interface {
Size() int
}
type Item struct {
Key string
Value Value
}
type entry struct {
key string
value Value
size int
time_accessed time.Time
}
func NewLRUCache(capacity uint64) *LRUCache {
return &LRUCache{
list: list.New(),
table: make(map[string]*list.Element),
capacity: capacity,
}
}
func (lru *LRUCache) Get(key string) (v Value, ok bool) {
lru.mu.Lock()
defer lru.mu.Unlock()
element := lru.table[key]
if element == nil {
return nil, false
}
lru.moveToFront(element)
return element.Value.(*entry).value, true
}
func (lru *LRUCache) Set(key string, value Value) {
lru.mu.Lock()
defer lru.mu.Unlock()
if element := lru.table[key]; element != nil {
lru.updateInplace(element, value)
} else {
lru.addNew(key, value)
}
}
func (lru *LRUCache) SetIfAbsent(key string, value Value) {
lru.mu.Lock()
defer lru.mu.Unlock()
if element := lru.table[key]; element != nil {
lru.moveToFront(element)
} else {
lru.addNew(key, value)
}
}
func (lru *LRUCache) Delete(key string) bool {
lru.mu.Lock()
defer lru.mu.Unlock()
element := lru.table[key]
if element == nil {
return false
}
lru.list.Remove(element)
delete(lru.table, key)
lru.size -= uint64(element.Value.(*entry).size)
return true
}
func (lru *LRUCache) Clear() {
lru.mu.Lock()
defer lru.mu.Unlock()
lru.list.Init()
lru.table = make(map[string]*list.Element)
lru.size = 0
}
func (lru *LRUCache) SetCapacity(capacity uint64) {
lru.mu.Lock()
defer lru.mu.Unlock()
lru.capacity = capacity
lru.checkCapacity()
}
func (lru *LRUCache) Stats() (length, size, capacity uint64, oldest time.Time) {
lru.mu.Lock()
defer lru.mu.Unlock()
if lastElem := lru.list.Back(); lastElem != nil {
oldest = lastElem.Value.(*entry).time_accessed
}
return uint64(lru.list.Len()), lru.size, lru.capacity, oldest
}
func (lru *LRUCache) StatsJSON() string {
if lru == nil {
return "{}"
}
l, s, c, o := lru.Stats()
return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"OldestAccess\": \"%v\"}", l, s, c, o)
}
func (lru *LRUCache) Keys() []string {
lru.mu.Lock()
defer lru.mu.Unlock()
keys := make([]string, 0, lru.list.Len())
for e := lru.list.Front(); e != nil; e = e.Next() {
keys = append(keys, e.Value.(*entry).key)
}
return keys
}
func (lru *LRUCache) Items() []Item {
lru.mu.Lock()
defer lru.mu.Unlock()
items := make([]Item, 0, lru.list.Len())
for e := lru.list.Front(); e != nil; e = e.Next() {
v := e.Value.(*entry)
items = append(items, Item{Key: v.key, Value: v.value})
}
return items
}
func (lru *LRUCache) SaveItems(w io.Writer) error {
items := lru.Items()
encoder := gob.NewEncoder(w)
return encoder.Encode(items)
}
func (lru *LRUCache) SaveItemsToFile(path string) error {
if wr, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
return err
} else {
defer wr.Close()
return lru.SaveItems(wr)
}
}
func (lru *LRUCache) LoadItems(r io.Reader) error {
items := make([]Item, 0)
decoder := gob.NewDecoder(r)
if err := decoder.Decode(&items); err != nil {
return err
}
lru.mu.Lock()
lru.mu.Unlock()
for _, item := range items {
// XXX: copied from Set()
if element := lru.table[item.Key]; element != nil {
lru.updateInplace(element, item.Value)
} else {
lru.addNew(item.Key, item.Value)
}
}
return nil
}
func (lru *LRUCache) LoadItemsFromFile(path string) error {
if rd, err := os.Open(path); err != nil {
return err
} else {
defer rd.Close()
return lru.LoadItems(rd)
}
}
func (lru *LRUCache) updateInplace(element *list.Element, value Value) {
valueSize := value.Size()
sizeDiff := valueSize - element.Value.(*entry).size
element.Value.(*entry).value = value
element.Value.(*entry).size = valueSize
lru.size += uint64(sizeDiff)
lru.moveToFront(element)
lru.checkCapacity()
}
func (lru *LRUCache) moveToFront(element *list.Element) {
lru.list.MoveToFront(element)
element.Value.(*entry).time_accessed = time.Now()
}
func (lru *LRUCache) addNew(key string, value Value) {
newEntry := &entry{key, value, value.Size(), time.Now()}
element := lru.list.PushFront(newEntry)
lru.table[key] = element
lru.size += uint64(newEntry.size)
lru.checkCapacity()
}
func (lru *LRUCache) checkCapacity() {
// Partially duplicated from Delete
for lru.size > lru.capacity {
delElem := lru.list.Back()
delValue := delElem.Value.(*entry)
lru.list.Remove(delElem)
delete(lru.table, delValue.key)
lru.size -= uint64(delValue.size)
}
}

View File

@@ -1,39 +1,58 @@
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"
)
/*
Functions for reading and writing the auth token from the user's
home directory.
*/
var (
once sync.Once
currentAuthToken string
authTokenFile string
)
func init() {
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())
return
} else {
homeDir = user.HomeDir
}
authTokenFile = path.Join(user.HomeDir, ".ngrok")
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 == currentAuthToken || authTokenFile == "" {
if token == "" || token == LoadAuthToken() || authTokenFile == "" {
return
}
@@ -43,7 +62,3 @@ func SaveAuthToken(token string) {
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
}
}
func LoadAuthToken() string {
return currentAuthToken
}

View File

@@ -15,17 +15,16 @@ var (
)
type Options struct {
server string
httpAuth string
hostname string
localaddr string
protocol string
url string
subdomain string
historySize int
webport int
logto string
authtoken string
server string
httpAuth string
hostname string
localaddr string
protocol string
url string
subdomain string
webport int
logto string
authtoken string
}
func fail(msg string, args ...interface{}) {
@@ -92,14 +91,6 @@ func parseProtocol(proto string) string {
panic("unreachable")
}
func parseAuthToken(token string) string {
if token != "" {
return token
} else {
return LoadAuthToken()
}
}
func parseArgs() *Options {
authtoken := flag.String(
"authtoken",
@@ -121,6 +112,11 @@ func parseArgs() *Options {
"",
"Request a custom subdomain from the ngrok server. (HTTP mode only)")
hostname := flag.String(
"hostname",
"",
"Request a custom hostname from the ngrok server. (HTTP only) (requires CNAME of your DNS)")
protocol := flag.String(
"proto",
"http",
@@ -129,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",
@@ -156,6 +152,7 @@ func parseArgs() *Options {
protocol: parseProtocol(*protocol),
webport: *webport,
logto: *logto,
authtoken: parseAuthToken(*authtoken),
authtoken: *authtoken,
hostname: *hostname,
}
}

View File

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

View File

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

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

@@ -0,0 +1,352 @@
package client
import (
"fmt"
metrics "github.com/inconshreveable/go-metrics"
"io/ioutil"
"math"
"ngrok/client/mvc"
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"ngrok/version"
"runtime"
"sync/atomic"
"time"
)
const (
pingInterval = 20 * time.Second
maxPongLatency = 15 * time.Second
updateCheckInterval = 6 * time.Hour
BadGateway = `<html>
<body style="background-color: #97a8b9">
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
<h2>Tunnel %s unavailable</h2>
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
`
)
type ClientModel struct {
log.Logger
id string
tunnels map[string]mvc.Tunnel
serverVersion string
metrics *ClientMetrics
updateStatus mvc.UpdateStatus
connStatus mvc.ConnStatus
protoMap map[string]proto.Protocol
protocols []proto.Protocol
ctl mvc.Controller
serverAddr string
authToken string
}
func newClientModel(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"]}
return &ClientModel{
Logger: log.NewPrefixLogger("client"),
// unique client id
id: util.RandIdOrPanic(8),
// connection status
connStatus: mvc.ConnConnecting,
// update status
updateStatus: mvc.UpdateNone,
// metrics
metrics: NewClientMetrics(),
// protocols
protoMap: protoMap,
// protocol list
protocols: protocols,
// open tunnels
tunnels: make(map[string]mvc.Tunnel),
// controller
ctl: ctl,
}
}
// mvc.State interface
func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols }
func (c ClientModel) GetClientVersion() string { return version.MajorMinor() }
func (c ClientModel) GetServerVersion() string { return c.serverVersion }
func (c ClientModel) GetTunnels() []mvc.Tunnel {
tunnels := make([]mvc.Tunnel, 0)
for _, t := range c.tunnels {
tunnels = append(tunnels, t)
}
return tunnels
}
func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus }
func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus }
func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
return c.metrics.connMeter, c.metrics.connTimer
}
func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
return c.metrics.bytesInCount, c.metrics.bytesIn
}
func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
return c.metrics.bytesOutCount, c.metrics.bytesOut
}
// mvc.Model interface
func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
var localConn conn.Conn
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
if err != nil {
c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err)
return
}
defer localConn.Close()
localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"})
localConn.Write(payload)
ioutil.ReadAll(localConn)
}
func (c *ClientModel) Shutdown() {
}
func (c *ClientModel) update() {
c.ctl.Update(c)
}
func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, reg *msg.RegMsg, localaddr string) {
c.serverAddr = serverAddr
c.authToken = authToken
c.ctl = ctl
c.reconnectingControl(reg, localaddr)
}
func (c *ClientModel) reconnectingControl(reg *msg.RegMsg, localaddr string) {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
c.control(reg, localaddr)
if c.connStatus == mvc.ConnOnline {
wait = 1 * time.Second
}
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
time.Sleep(wait)
// exponentially increase wait time
wait = 2 * wait
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
c.connStatus = mvc.ConnReconnecting
c.update()
}
}
// Establishes and manages a tunnel control connection with the server
func (c *ClientModel) control(reg *msg.RegMsg, localaddr string) {
defer func() {
if r := recover(); r != nil {
log.Error("control recovering from failure %v", r)
}
}()
// establish control channel
conn, err := conn.Dial(c.serverAddr, "ctl", tlsConfig)
if err != nil {
panic(err)
}
defer conn.Close()
// register with the server
reg.OS = runtime.GOOS
reg.ClientId = c.id
reg.Version = version.Proto
reg.MmVersion = version.MajorMinor()
reg.User = c.authToken
if err = msg.WriteMsg(conn, reg); err != nil {
panic(err)
}
// wait for the server to ack our register
var regAck msg.RegAckMsg
if err = msg.ReadMsgInto(conn, &regAck); err != nil {
panic(err)
}
if regAck.Error != "" {
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
c.ctl.Shutdown(emsg)
return
}
tunnel := mvc.Tunnel{
PublicUrl: regAck.Url,
LocalAddr: localaddr,
Protocol: c.protoMap[reg.Protocol],
}
c.tunnels[tunnel.PublicUrl] = tunnel
// update UI state
c.id = regAck.ClientId
c.Info("Tunnel established at %v", tunnel.PublicUrl)
c.connStatus = mvc.ConnOnline
c.serverVersion = regAck.MmVersion
c.update()
SaveAuthToken(c.authToken)
// start the heartbeat
lastPong := time.Now().UnixNano()
c.ctl.Go(func() { c.heartbeat(&lastPong, conn) })
// main control loop
for {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(conn); err != nil {
panic(err)
}
switch m := rawMsg.(type) {
case *msg.ReqProxyMsg:
c.ctl.Go(c.proxy)
case *msg.PongMsg:
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
case *msg.RegAckMsg:
if m.Error != "" {
c.Error("Server failed to allocate tunnel: %s", regAck.Error)
continue
}
tunnel := mvc.Tunnel{
PublicUrl: m.Url,
LocalAddr: localaddr,
Protocol: c.protoMap[m.Protocol],
}
c.tunnels[tunnel.PublicUrl] = tunnel
c.Info("Tunnel established at %v", tunnel.PublicUrl)
c.update()
default:
conn.Warn("Ignoring unknown control message %v ", m)
}
}
}
// Establishes and manages a tunnel proxy connection with the server
func (c *ClientModel) proxy() {
remoteConn, err := conn.Dial(c.serverAddr, "pxy", tlsConfig)
if err != nil {
log.Error("Failed to establish proxy connection: %v", err)
return
}
defer remoteConn.Close()
err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{ClientId: c.id})
if err != nil {
log.Error("Failed to write RegProxyMsg: %v", err)
return
}
// wait for the server to ack our register
var startPxyMsg msg.StartProxyMsg
if err = msg.ReadMsgInto(remoteConn, &startPxyMsg); err != nil {
log.Error("Server failed to write StartProxyMsg: %v", err)
return
}
tunnel, ok := c.tunnels[startPxyMsg.Url]
if !ok {
c.Error("Couldn't find tunnel for proxy: %s", startPxyMsg.Url)
return
}
// start up the private connection
start := time.Now()
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
if err != nil {
remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err)
if tunnel.Protocol.GetName() == "http" {
// try to be helpful when you're in HTTP mode and a human might see the output
badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr)
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
Content-Type: text/html
Content-Length: %d
%s`, len(badGatewayBody), badGatewayBody)))
}
return
}
defer localConn.Close()
m := c.metrics
m.proxySetupTimer.Update(time.Since(start))
m.connMeter.Mark(1)
c.update()
m.connTimer.Time(func() {
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxyMsg.ClientAddr})
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
m.bytesIn.Update(bytesIn)
m.bytesOut.Update(bytesOut)
m.bytesInCount.Inc(bytesIn)
m.bytesOutCount.Inc(bytesOut)
})
c.update()
}
// Hearbeating to ensure our connection ngrokd is still live
func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) {
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
ping := time.NewTicker(pingInterval)
pongCheck := time.NewTicker(time.Second)
defer func() {
conn.Close()
ping.Stop()
pongCheck.Stop()
}()
for {
select {
case <-pongCheck.C:
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
needPong := lastPong.Sub(lastPing) < 0
pongLatency := time.Since(lastPing)
if needPong && pongLatency > maxPongLatency {
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
return
}
case <-ping.C:
err := msg.WriteMsg(conn, &msg.PingMsg{})
if err != nil {
conn.Debug("Got error %v when writing PingMsg", err)
return
}
lastPing = time.Now()
}
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package mvc
import (
"ngrok/msg"
)
type Model interface {
Run(serverAddr, authToken string, ctl Controller, reg *msg.RegMsg, localaddr string)
Shutdown()
PlayRequest(tunnel Tunnel, payload []byte)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
// +build release autoupdate
package client
import (
update "github.com/inconshreveable/go-update"
"net/http"
"net/url"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/version"
"runtime"
"time"
)
const (
updateEndpoint = "https://dl.ngrok.com/update"
checkEndpoint = "https://dl.ngrok.com/update/check"
)
func progressWatcher(s *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 {
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())
}
// log error to ngrok.com's servers for debugging purposes
params.Add("error", err.Error())
resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params)
if err != nil {
log.Error("Error while reporting update error: %v, %v", err, reportErr)
}
resp.Body.Close()
// tell the user to update manually
s.update = ui.UpdateAvailable
} else {
if !download.Available {
// this is the way the server tells us to update manually
log.Info("Server wants us to update manually")
s.update = ui.UpdateAvailable
} else {
// tell the user the update is ready
log.Info("Update ready!")
s.update = ui.UpdateReady
}
}
ctl.Update(s)
return
}
// try to update immediately and then at a set interval
update()
for _ = range time.Tick(updateCheckInterval) {
if !tryAgain {
break
}
update()
}
}

View File

@@ -2,6 +2,7 @@ package term
import (
termbox "github.com/nsf/termbox-go"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
@@ -12,12 +13,13 @@ const (
)
type HttpView struct {
log.Logger
*area
httpProto *proto.Http
HttpRequests *util.Ring
shutdown chan int
flush chan int
*area
log.Logger
termView *TermView
}
func colorFor(status string) termbox.Attribute {
@@ -33,19 +35,16 @@ func colorFor(status string) termbox.Attribute {
return termbox.ColorWhite
}
func NewHttp(proto *proto.Http, flush, shutdown chan int, x, y int) *HttpView {
func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView {
v := &HttpView{
httpProto: proto,
HttpRequests: util.NewRing(size),
area: NewArea(x, y, 70, size+5),
shutdown: shutdown,
flush: flush,
Logger: log.NewPrefixLogger(),
shutdown: make(chan int),
termView: termView,
Logger: log.NewPrefixLogger("view", "term", "http"),
}
v.AddLogPrefix("view")
v.AddLogPrefix("term")
v.AddLogPrefix("http")
go v.Run()
ctl.Go(v.Run)
return v
}
@@ -54,9 +53,6 @@ func (v *HttpView) Run() {
for {
select {
case <-v.shutdown:
return
case txn := <-updates:
v.Debug("Got HTTP update")
if txn.(*proto.HttpTxn).Resp == nil {
@@ -78,5 +74,9 @@ func (v *HttpView) Render() {
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
}
}
v.flush <- 1
v.termView.Flush()
}
func (v *HttpView) Shutdown() {
close(v.shutdown)
}

View File

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

View File

@@ -10,7 +10,7 @@ import (
"net/http/httputil"
"net/url"
"ngrok/client/assets"
"ngrok/client/ui"
"ngrok/client/mvc"
"ngrok/log"
"ngrok/proto"
"ngrok/util"
@@ -21,6 +21,8 @@ import (
type SerializedTxn struct {
Id string
Duration int64
Start int64
ConnCtx mvc.ConnectionContext
*proto.HttpTxn `json:"-"`
Req SerializedRequest
Resp SerializedResponse
@@ -54,17 +56,18 @@ type SerializedResponse struct {
}
type WebHttpView struct {
log.Logger
webview *WebView
ctl *ui.Controller
ctl mvc.Controller
httpProto *proto.Http
updates chan interface{}
state chan SerializedUiState
HttpRequests *util.Ring
idToTxn map[string]*SerializedTxn
}
type SerializedUiState struct {
Url string
Tunnels []mvc.Tunnel
}
type SerializedPayload struct {
@@ -72,20 +75,18 @@ type SerializedPayload struct {
UiState SerializedUiState
}
func NewWebHttpView(wv *WebView, ctl *ui.Controller, proto *proto.Http) *WebHttpView {
w := &WebHttpView{
func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView {
whv := &WebHttpView{
Logger: log.NewPrefixLogger("view", "web", "http"),
webview: wv,
ctl: ctl,
httpProto: proto,
idToTxn: make(map[string]*SerializedTxn),
updates: ctl.Updates.Reg(),
state: make(chan SerializedUiState),
HttpRequests: util.NewRing(20),
}
go w.updateHttp()
go w.updateUiState()
w.register()
return w
ctl.Go(whv.updateHttp)
whv.register()
return whv
}
type XMLDoc struct {
@@ -111,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":
@@ -151,16 +153,16 @@ func (whv *WebHttpView) updateHttp() {
// we haven't processed this transaction yet if we haven't set the
// user data
if htxn.UserData == nil {
if htxn.UserCtx == nil {
id, err := util.RandId(8)
if err != nil {
log.Error("Failed to generate txn identifier for web storage: %v", err)
whv.Error("Failed to generate txn identifier for web storage: %v", err)
continue
}
rawReq, err := httputil.DumpRequest(htxn.Req.Request, true)
rawReq, err := httputil.DumpRequestOut(htxn.Req.Request, true)
if err != nil {
log.Error("Failed to dump request: %v", err)
whv.Error("Failed to dump request: %v", err)
continue
}
@@ -176,9 +178,11 @@ func (whv *WebHttpView) updateHttp() {
Body: body,
Binary: !utf8.Valid(rawReq),
},
Start: htxn.Start.Unix(),
ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext),
}
htxn.UserData = whtxn
htxn.UserCtx = whtxn
// XXX: unsafe map access from multiple go routines
whv.idToTxn[whtxn.Id] = whtxn
// XXX: use return value to delete from map so we don't leak memory
@@ -186,11 +190,11 @@ func (whv *WebHttpView) updateHttp() {
} else {
rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
if err != nil {
log.Error("Failed to dump response: %v", err)
whv.Error("Failed to dump response: %v", err)
continue
}
txn := htxn.UserData.(*SerializedTxn)
txn := htxn.UserCtx.(*SerializedTxn)
body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes)
txn.Duration = htxn.Duration.Nanoseconds()
txn.Resp = SerializedResponse{
@@ -203,43 +207,46 @@ func (whv *WebHttpView) updateHttp() {
payload, err := json.Marshal(txn)
if err != nil {
log.Error("Failed to serialized txn payload for websocket: %v", err)
whv.Error("Failed to serialized txn payload for websocket: %v", err)
}
whv.webview.wsMessages.In() <- payload
}
}
}
func (v *WebHttpView) updateUiState() {
var s SerializedUiState
for {
select {
case obj := <-v.updates:
uiState := obj.(ui.State)
s.Url = uiState.GetPublicUrl()
case v.state <- s:
}
}
}
func (h *WebHttpView) register() {
func (whv *WebHttpView) register() {
http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
whv.Error("Replay failed: %v", err)
http.Error(w, err, 500)
}
}()
r.ParseForm()
txnid := r.Form.Get("txnid")
if txn, ok := h.idToTxn[txnid]; ok {
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
if txn, ok := whv.idToTxn[txnid]; ok {
reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw)
if err != nil {
panic(err)
}
h.ctl.Cmds <- ui.CmdRequest{Payload: bodyBytes}
whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes)
w.Write([]byte(http.StatusText(200)))
} else {
// XXX: 400
http.NotFound(w, r)
http.Error(w, http.StatusText(400), 400)
}
})
http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
err := util.MakePanicTrace(r)
whv.Error("HTTP web view failed: %v", err)
http.Error(w, err, 500)
}
}()
pageTmpl, err := assets.ReadAsset("assets/client/page.html")
if err != nil {
panic(err)
@@ -248,8 +255,8 @@ func (h *WebHttpView) register() {
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
payloadData := SerializedPayload{
Txns: h.HttpRequests.Slice(),
UiState: <-h.state,
Txns: whv.HttpRequests.Slice(),
UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
}
payload, err := json.Marshal(payloadData)
@@ -263,3 +270,6 @@ func (h *WebHttpView) register() {
}
})
}
func (whv *WebHttpView) Shutdown() {
}

View File

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

View File

@@ -16,6 +16,7 @@ type Conn interface {
net.Conn
log.Logger
Id() string
SetType(string)
}
type tcpConn struct {
@@ -103,6 +104,14 @@ func (c *tcpConn) Id() string {
return fmt.Sprintf("%s:%x", c.typ, c.id)
}
func (c *tcpConn) SetType(typ string) {
oldId := c.Id()
c.typ = typ
c.ClearLogPrefixes()
c.AddLogPrefix(c.Id())
c.Info("Renamed connection %s", oldId)
}
func Join(c Conn, c2 Conn) (int64, int64) {
done := make(chan error)
pipe := func(to Conn, from Conn, bytesCopied *int64) {

View File

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

View File

@@ -15,6 +15,7 @@ func init() {
TypeMap["RegAckMsg"] = t((*RegAckMsg)(nil))
TypeMap["RegProxyMsg"] = t((*RegProxyMsg)(nil))
TypeMap["ReqProxyMsg"] = t((*ReqProxyMsg)(nil))
TypeMap["StartProxyMsg"] = t((*StartProxyMsg)(nil))
TypeMap["PingMsg"] = t((*PingMsg)(nil))
TypeMap["PongMsg"] = t((*PongMsg)(nil))
TypeMap["VerisonMsg"] = t((*VersionMsg)(nil))
@@ -46,17 +47,23 @@ type RegAckMsg struct {
Version string
MmVersion string
Url string
ProxyAddr string
Protocol string
Error string
}
type RegProxyMsg struct {
Url string
ClientId string
}
type ReqProxyMsg struct {
}
type RegProxyMsg struct {
ClientId string
}
type StartProxyMsg struct {
Url string
ClientAddr string
}
type PingMsg struct {
}

View File

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

View File

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

View File

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

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

@@ -0,0 +1,34 @@
package server
import (
"flag"
)
type Options struct {
httpPort int
httpsPort int
tunnelPort int
domain string
logto string
}
func parseArgs() *Options {
httpPort := flag.Int("httpPort", 80, "Public HTTP port, -1 to disable")
httpsPort := flag.Int("httpsPort", 443, "Public HTTPS port, -1 to disable")
tunnelPort := flag.Int("tunnelPort", 4443, "Port to which ngrok clients connect")
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
logto := flag.String(
"log",
"stdout",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
flag.Parse()
return &Options{
httpPort: *httpPort,
httpsPort: *httpsPort,
tunnelPort: *tunnelPort,
domain: *domain,
logto: *logto,
}
}

View File

@@ -1,11 +1,14 @@
package server
import (
"fmt"
"io"
"ngrok/conn"
"ngrok/msg"
"ngrok/util"
"ngrok/version"
"runtime/debug"
"sync/atomic"
"time"
)
@@ -18,29 +21,109 @@ type Control struct {
// actual connection
conn conn.Conn
// channels for communicating messages over the connection
out chan (interface{})
in chan (msg.Message)
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// put a message in this channel to send it over
// conn to the client and then terminate this
// control connection and all of its tunnels
stop chan (msg.Message)
// heartbeat
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
// tunnel
tun *Tunnel
// all of the tunnels this control connection handles
tunnels []*Tunnel
// proxy connections
proxies chan conn.Conn
// closing indicator
closing int32
// identifier
id string
}
func NewControl(conn conn.Conn) {
func NewControl(ctlConn conn.Conn, regMsg *msg.RegMsg) {
// create the object
// channels are buffered because we read and write to them
// from the same goroutine in managerThread()
c := &Control{
conn: conn,
out: make(chan (interface{}), 1),
in: make(chan (msg.Message), 1),
stop: make(chan (msg.Message), 1),
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(),
}
// assign the random id
serverId, err := util.RandId(8)
if err != nil {
c.stop <- &msg.RegAckMsg{Error: err.Error()}
}
c.id = fmt.Sprintf("%s-%s", regMsg.ClientId, serverId)
// register the control
err = controlRegistry.Add(c.id, c)
if err != nil {
c.stop <- &msg.RegAckMsg{Error: err.Error()}
}
// set logging prefix
ctlConn.SetType("ctl")
// register the first tunnel
c.in <- regMsg
// manage the connection
go c.managerThread()
go c.readThread()
}
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(regMsg *msg.RegMsg) {
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(regMsg, c)
if err != nil {
ack := &msg.RegAckMsg{Error: err.Error()}
if len(c.tunnels) == 0 {
// you can't fail your first tunnel registration
// terminate the control connection
c.stop <- ack
} else {
// inform client of failure
c.out <- ack
}
// we're done
return
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.RegAckMsg{
Url: t.url,
Protocol: regMsg.Protocol,
Version: version.Proto,
MmVersion: version.MajorMinor(),
ClientId: c.id,
}
if regMsg.Protocol == "http" {
httpsRegMsg := *regMsg
httpsRegMsg.Protocol = "https"
c.in <- &httpsRegMsg
}
}
func (c *Control) managerThread() {
@@ -51,13 +134,33 @@ func (c *Control) managerThread() {
if err := recover(); err != nil {
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
}
// remove from the control registry
controlRegistry.Del(c.id)
// mark that we're shutting down
atomic.StoreInt32(&c.closing, 1)
// stop the reaping timer
reap.Stop()
// close the connection
c.conn.Close()
// shutdown the tunnel if it's open
if c.tun != nil {
c.tun.shutdown()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// we're safe to close(c.proxies) because c.closing
// protects us inside of RegisterProxy
close(c.proxies)
// shut down all of the proxy connections
for p := range c.proxies {
p.Close()
}
}()
for {
@@ -65,23 +168,22 @@ func (c *Control) managerThread() {
case m := <-c.out:
msg.WriteMsg(c.conn, m)
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
return
}
case m := <-c.stop:
if m != nil {
msg.WriteMsg(c.conn, m)
}
return
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
return
}
case mRaw := <-c.in:
switch m := mRaw.(type) {
case *msg.RegMsg:
c.conn.Info("Registering new tunnel")
c.tun = newTunnel(m, c)
c.registerTunnel(m)
case *msg.PingMsg:
c.lastPing = time.Now()
@@ -119,3 +221,60 @@ func (c *Control) readThread() {
}
}
}
func (c *Control) RegisterProxy(conn conn.Conn) {
if atomic.LoadInt32(&c.closing) == 1 {
c.conn.Debug("Can't register proxies for a control that is closing")
conn.Close()
return
}
select {
case c.proxies <- conn:
c.conn.Info("Registered proxy connection %s", conn.Id())
default:
// c.proxies buffer is full, discard this one
conn.Close()
}
}
// Remove a proxy connection from the pool and return it
// If not proxy connections are in the pool, request one
// and wait until it is available
// Returns an error if we couldn't get a proxy because it took too long
// or the tunnel is closing
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
// initial timeout is zero to try to get a proxy connection without asking for one
timeout := time.NewTimer(0)
// get a proxy connection. if we timeout, request one over the control channel
for proxyConn == nil {
var ok bool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
continue
case <-timeout.C:
c.conn.Debug("Requesting new proxy connection")
// request a proxy connection
c.out <- &msg.ReqProxyMsg{}
// timeout after 1 second if we don't get one
timeout.Reset(1 * time.Second)
}
}
// To try to reduce latency hanndling tunnel connections, we employ
// the following curde heuristic:
// If the proxy connection pool is empty, request a new one.
// The idea is to always have at least one proxy connection available for immediate use.
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
// It should be a good start though.
if len(c.proxies) == 0 {
c.out <- &msg.ReqProxyMsg{}
}
return
}

View File

@@ -1,10 +1,12 @@
package server
import (
"crypto/tls"
"fmt"
"net"
"ngrok/conn"
"ngrok/log"
"strings"
)
const (
@@ -28,26 +30,27 @@ Bad Request
`
)
/**
* Listens for new http connections from the public internet
*/
func httpListener(addr *net.TCPAddr) {
// Listens for new http(s) connections from the public internet
func httpListener(addr *net.TCPAddr, tlsCfg *tls.Config) {
// bind/listen for incoming connections
listener, err := conn.Listen(addr, "pub", nil)
listener, err := conn.Listen(addr, "pub", tlsCfg)
if err != nil {
panic(err)
}
log.Info("Listening for public http connections on %v", listener.Port)
proto := "http"
if tlsCfg != nil {
proto = "https"
}
log.Info("Listening for public %s connections on %v", proto, listener.Port)
for conn := range listener.Conns {
go httpHandler(conn)
go httpHandler(conn, proto)
}
}
/**
* Handles a new http connection from the public internet
*/
func httpHandler(tcpConn net.Conn) {
// Handles a new http connection from the public internet
func httpHandler(tcpConn net.Conn, proto string) {
// wrap up the connection for logging
conn := conn.NewHttp(tcpConn, "pub")
@@ -62,25 +65,28 @@ func httpHandler(tcpConn net.Conn) {
// read out the http request
req, err := conn.ReadRequest()
if err != nil {
conn.Warn("Failed to read valid http request: %v", err)
conn.Warn("Failed to read valid %s request: %v", proto, err)
conn.Write([]byte(BadRequest))
return
}
// read out the Host header from the request
host := strings.ToLower(req.Host)
conn.Debug("Found hostname %s in request", host)
// multiplex to find the right backend host
conn.Debug("Found hostname %s in request", req.Host)
tunnel := tunnels.Get("http://" + req.Host)
tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host))
if tunnel == nil {
conn.Info("No tunnel found for hostname %s", req.Host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(req.Host)+18, req.Host)))
conn.Info("No tunnel found for hostname %s", host)
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
return
}
// satisfy auth, if necessary
conn.Debug("From client: %s", req.Header.Get("Authorization"))
conn.Debug("To match: %s", tunnel.regMsg.HttpAuth)
if req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
conn.Info("Authentication failed")
// If the client specified http auth and it doesn't match this request's auth
// then fail the request with 401 Not Authorized and request the client reissue the
// request with basic authdeny the request
if tunnel.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

@@ -1,124 +1,109 @@
package server
import (
"flag"
"fmt"
"math/rand"
"net"
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"regexp"
"ngrok/util"
"os"
)
type Options struct {
publicPort int
proxyPort int
tunnelPort int
domain string
logto string
}
/* GLOBALS */
// GLOBALS
var (
hostRegex *regexp.Regexp
proxyAddr string
tunnels *TunnelManager
opts *Options
tunnelRegistry *TunnelRegistry
controlRegistry *ControlRegistry
registryCacheSize uint64 = 1024 * 1024 // 1 MB
domain string
publicPort int
)
func init() {
hostRegex = regexp.MustCompile("[H|h]ost: ([^\\(\\);:,<>]+)\n")
}
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxyMsg) {
// fail gracefully if the proxy connection fails to register
defer func() {
if r := recover(); r != nil {
pxyConn.Warn("Failed with error: %v", r)
pxyConn.Close()
}
}()
func parseArgs() *Options {
publicPort := flag.Int("publicport", 80, "Public port")
tunnelPort := flag.Int("tunnelport", 4443, "Tunnel port")
proxyPort := flag.Int("proxyPort", 0, "Proxy port")
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
logto := flag.String(
"log",
"stdout",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
// set logging prefix
pxyConn.SetType("pxy")
flag.Parse()
// look up the control connection for this proxy
pxyConn.Info("Registering new proxy for %s", regPxy.ClientId)
ctl := controlRegistry.Get(regPxy.ClientId)
return &Options{
publicPort: *publicPort,
tunnelPort: *tunnelPort,
proxyPort: *proxyPort,
domain: *domain,
logto: *logto,
if ctl == nil {
panic("No client found for identifier: " + regPxy.ClientId)
}
ctl.RegisterProxy(pxyConn)
}
/**
* Listens for new control connections from tunnel clients
*/
func controlListener(addr *net.TCPAddr, domain string) {
// Listen for incoming control and proxy connections
// We listen for incoming control and proxy connections on the same port
// for ease of deployment. The hope is that by running on port 443, using
// TLS and running all connections over the same port, we can bust through
// restrictive firewalls.
func tunnelListener(addr *net.TCPAddr, domain string) {
// listen for incoming connections
listener, err := conn.Listen(addr, "ctl", tlsConfig)
listener, err := conn.Listen(addr, "tun", tlsConfig)
if err != nil {
panic(err)
}
log.Info("Listening for control connections on %d", listener.Port)
log.Info("Listening for control and proxy connections on %d", listener.Port)
for c := range listener.Conns {
NewControl(c)
}
}
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(c); err != nil {
c.Error("Failed to read message: %v", err)
c.Close()
}
/**
* Listens for new proxy connections from tunnel clients
*/
func proxyListener(addr *net.TCPAddr, domain string) {
listener, err := conn.Listen(addr, "pxy", tlsConfig)
if err != nil {
panic(err)
}
switch m := rawMsg.(type) {
case *msg.RegMsg:
go NewControl(c, m)
// set global proxy addr variable
proxyAddr = fmt.Sprintf("%s:%d", domain, listener.Port)
log.Info("Listening for proxy connection on %d", listener.Port)
for proxyConn := range listener.Conns {
go func(conn conn.Conn) {
// fail gracefully if the proxy connection dies
defer func() {
if r := recover(); r != nil {
conn.Warn("Failed with error: %v", r)
conn.Close()
}
}()
// read the proxy register message
var regPxy msg.RegProxyMsg
if err = msg.ReadMsgInto(conn, &regPxy); err != nil {
panic(err)
}
// look up the tunnel for this proxy
conn.Info("Registering new proxy for %s", regPxy.Url)
tunnel := tunnels.Get(regPxy.Url)
if tunnel == nil {
panic("No tunnel found for: " + regPxy.Url)
}
// register the proxy connection with the tunnel
tunnel.RegisterProxy(conn)
}(proxyConn)
case *msg.RegProxyMsg:
go NewProxy(c, m)
}
}
}
func Main() {
// parse options
opts := parseArgs()
opts = parseArgs()
// init logging
log.LogTo(opts.logto)
tunnels = NewTunnelManager(opts.domain)
// seed random number generator
seed, err := util.RandomSeed()
if err != nil {
panic(err)
}
rand.Seed(seed)
go proxyListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.proxyPort}, opts.domain)
go controlListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.publicPort})
// init tunnel/control registry
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
controlRegistry = NewControlRegistry()
// ngrok clients
go tunnelListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
// listen for http
if opts.httpPort != -1 {
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpPort}, nil)
}
// listen for https
if opts.httpsPort != -1 {
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpsPort}, tlsConfig)
}
// wait forever
done := make(chan int)

View File

@@ -1,120 +0,0 @@
package server
import (
"fmt"
cache "github.com/pmylund/go-cache"
"math/rand"
"net"
"strings"
"sync"
"time"
)
const (
cacheDuration time.Duration = 24 * time.Hour
cacheCleanupInterval time.Duration = time.Minute
)
/**
* TunnelManager: Manages a set of tunnels
*/
type TunnelManager struct {
domain string
tunnels map[string]*Tunnel
idDomainAffinity *cache.Cache
ipDomainAffinity *cache.Cache
sync.RWMutex
}
func NewTunnelManager(domain string) *TunnelManager {
return &TunnelManager{
domain: domain,
tunnels: make(map[string]*Tunnel),
idDomainAffinity: cache.New(cacheDuration, cacheCleanupInterval),
ipDomainAffinity: cache.New(cacheDuration, cacheCleanupInterval),
}
}
func (m *TunnelManager) Add(t *Tunnel) error {
assignTunnel := func(url string) bool {
m.Lock()
defer m.Unlock()
if m.tunnels[url] == nil {
m.tunnels[url] = t
return true
}
return false
}
url := ""
switch t.regMsg.Protocol {
case "tcp":
addr := t.listener.Addr().(*net.TCPAddr)
url = fmt.Sprintf("tcp://%s:%d", m.domain, addr.Port)
if !assignTunnel(url) {
return t.Error("TCP at %s already registered!", url)
}
case "http":
if strings.TrimSpace(t.regMsg.Hostname) != "" {
url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, m.domain)
}
if url != "" {
if !assignTunnel(url) {
return t.Warn("The tunnel address %s is already registered!", url)
}
} else {
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
clientId := t.regMsg.ClientId
// try to give the same subdomain back if it's available
subdomain := fmt.Sprintf("%x", rand.Int31())
if lastDomain, ok := m.idDomainAffinity.Get(clientId); ok {
t.Debug("Found affinity for subdomain %s with client id %s", subdomain, clientId)
subdomain = lastDomain.(string)
} else if lastDomain, ok = m.ipDomainAffinity.Get(clientIp); ok {
t.Debug("Found affinity for subdomain %s with client ip %s", subdomain, clientIp)
subdomain = lastDomain.(string)
}
// pick one randomly
for {
url = fmt.Sprintf("http://%s.%s", subdomain, m.domain)
if assignTunnel(url) {
break
} else {
subdomain = fmt.Sprintf("%x", rand.Int31())
}
}
// save our choice so we can try to give clients back the same
// tunnel later
m.idDomainAffinity.Set(clientId, subdomain, 0)
m.ipDomainAffinity.Set(clientIp, subdomain, 0)
}
default:
return t.Error("Unrecognized protocol type %s", t.regMsg.Protocol)
}
t.url = url
return nil
}
func (m *TunnelManager) Del(url string) {
m.Lock()
defer m.Unlock()
delete(m.tunnels, url)
}
func (m *TunnelManager) Get(url string) *Tunnel {
m.RLock()
defer m.RUnlock()
return m.tunnels[url]
}

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

@@ -0,0 +1,205 @@
package server
import (
"encoding/gob"
"fmt"
"net"
"ngrok/cache"
"ngrok/log"
"sync"
"time"
)
const (
cacheSaveInterval time.Duration = 10 * time.Minute
)
type cacheUrl string
func (url cacheUrl) Size() int {
return len(url)
}
// TunnelRegistry maps a tunnel URL to Tunnel structures
type TunnelRegistry struct {
tunnels map[string]*Tunnel
affinity *cache.LRUCache
log.Logger
sync.RWMutex
}
func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
registry := &TunnelRegistry{
tunnels: make(map[string]*Tunnel),
affinity: cache.NewLRUCache(cacheSize),
Logger: log.NewPrefixLogger("registry", "tun"),
}
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
// to encode or decode any non-primitive types that haven't been "registered"
// with it. Since we store cacheUrl objects, we need to register them here first
// for the encoding/decoding to work
var urlobj cacheUrl
gob.Register(urlobj)
// try to load and then periodically save the affinity cache to file, if specified
if cacheFile != "" {
err := registry.affinity.LoadItemsFromFile(cacheFile)
if err != nil {
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
}
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
} else {
registry.Info("No affinity cache specified")
}
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.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")
}
}
}()
}
// Register a tunnel with a specific url, returns an error
// if a tunnel is already registered at that url
func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
r.Lock()
defer r.Unlock()
if r.tunnels[url] != nil {
return fmt.Errorf("The tunnel %s is already registered.", url)
}
r.tunnels[url] = t
return nil
}
func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) {
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
clientId := t.regMsg.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
if v, ok := r.affinity.Get(idCacheKey); ok {
url = string(v.(cacheUrl))
t.Debug("Found registry affinity %s for %s", url, idCacheKey)
} else if v, ok := r.affinity.Get(ipCacheKey); ok {
url = string(v.(cacheUrl))
t.Debug("Found registry affinity %s for %s", url, ipCacheKey)
}
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()
}
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
return url, nil
}
}
return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts)
}
func (r *TunnelRegistry) Del(url string) {
r.Lock()
defer r.Unlock()
delete(r.tunnels, url)
}
func (r *TunnelRegistry) Get(url string) *Tunnel {
r.RLock()
defer r.RUnlock()
return r.tunnels[url]
}
// ControlRegistry maps a client ID to Control structures
type ControlRegistry struct {
controls map[string]*Control
log.Logger
sync.RWMutex
}
func NewControlRegistry() *ControlRegistry {
return &ControlRegistry{
controls: make(map[string]*Control),
Logger: log.NewPrefixLogger("registry", "ctl"),
}
}
func (r *ControlRegistry) Get(clientId string) *Control {
r.RLock()
defer r.RUnlock()
return r.controls[clientId]
}
func (r *ControlRegistry) Add(clientId string, ctl *Control) error {
r.Lock()
defer r.Unlock()
if r.controls[clientId] == nil {
r.Info("Registered control with id %s", clientId)
r.controls[clientId] = ctl
return nil
} else {
return fmt.Errorf("Client with id %s already registered!", clientId)
}
}
func (r *ControlRegistry) Del(clientId string) error {
r.Lock()
defer r.Unlock()
if r.controls[clientId] == nil {
return fmt.Errorf("No control found for client id: %s", clientId)
} else {
r.Info("Removed control registry id %s", clientId)
delete(r.controls, clientId)
return nil
}
}

View File

@@ -3,15 +3,25 @@ package server
import (
"encoding/base64"
"fmt"
"math/rand"
"net"
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
var defaultPortMap = map[string]int{
"http": 80,
"https": 443,
"smtp": 25,
}
/**
* Tunnel: A control connection, metadata and proxy connections which
* route public traffic to a firewalled endpoint.
@@ -31,9 +41,6 @@ type Tunnel struct {
// control connection
ctl *Control
// proxy connections
proxies chan conn.Conn
// logger
log.Logger
@@ -41,38 +48,121 @@ type Tunnel struct {
closing int32
}
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
// Common functionality for registering virtually hosted protocols
func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
vhost := os.Getenv("VHOST")
if vhost == "" {
vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort)
}
// Canonicalize virtual host by removing default port (e.g. :80 on HTTP)
defaultPort, ok := defaultPortMap[protocol]
if !ok {
return fmt.Errorf("Couldn't find default port for protocol %s", protocol)
}
defaultPortSuffix := fmt.Sprintf(":%d", defaultPort)
if strings.HasSuffix(vhost, defaultPortSuffix) {
vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)]
}
// Canonicalize by always using lower-case
vhost = strings.ToLower(vhost)
t.url = strings.ToLower(t.url)
// Register for specific hostname
hostname := strings.TrimSpace(t.regMsg.Hostname)
if hostname != "" {
t.url = fmt.Sprintf("%s://%s", protocol, hostname)
return tunnelRegistry.Register(t.url, t)
}
// Register for specific subdomain
subdomain := strings.TrimSpace(t.regMsg.Subdomain)
if subdomain != "" {
t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost)
return tunnelRegistry.Register(t.url, t)
}
// Register for random URL
t.url, err = tunnelRegistry.RegisterRepeat(func() string {
return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost)
}, t)
return
}
// Create a new tunnel from a registration message received
// on a control channel
func NewTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel, err error) {
t = &Tunnel{
regMsg: m,
start: time.Now(),
ctl: ctl,
proxies: make(chan conn.Conn),
Logger: log.NewPrefixLogger(),
regMsg: m,
start: time.Now(),
ctl: ctl,
Logger: log.NewPrefixLogger(),
}
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
// try to return to you the same port you had before
cachedUrl := tunnelRegistry.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 {
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return
}
t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"}
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port)
// register it
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
return
}
go t.listenTcp(t.listener)
default:
}
case "http":
if err = registerVhost(t, "http", opts.httpPort); err != nil {
return
}
if err := tunnels.Add(t); err != nil {
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprint(err)}
return
case "https":
if err = registerVhost(t, "https", opts.httpsPort); err != nil {
return
}
}
if m.Version != version.Proto {
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprintf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)}
err = fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)
return
}
// pre-encode the http basic auth for fast comparisons later
@@ -80,35 +170,31 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth))
}
t.ctl.conn.AddLogPrefix(t.Id())
t.AddLogPrefix(t.Id())
t.Info("Registered new tunnel")
t.ctl.out <- &msg.RegAckMsg{
Url: t.url,
ProxyAddr: fmt.Sprintf("%s", proxyAddr),
Version: version.Proto,
MmVersion: version.MajorMinor(),
}
t.Info("Registered new tunnel on: %s", t.ctl.conn.Id())
metrics.OpenTunnel(t)
return
}
func (t *Tunnel) shutdown() {
func (t *Tunnel) Shutdown() {
t.Info("Shutting down")
// mark that we're shutting down
atomic.StoreInt32(&t.closing, 1)
// if we have a public listener (this is a raw TCP tunnel, shut it down
// if we have a public listener (this is a raw TCP tunnel), shut it down
if t.listener != nil {
t.listener.Close()
}
// remove ourselves from the tunnel registry
tunnels.Del(t.url)
tunnelRegistry.Del(t.url)
// XXX: shut down all of the proxy connections?
// let the control connection know we're shutting down
// currently, only the control connection shuts down tunnels,
// so it doesn't need to know about it
// t.ctl.stoptunnel <- t
metrics.CloseTunnel(t)
}
@@ -117,9 +203,7 @@ func (t *Tunnel) Id() string {
return t.url
}
/**
* Listens for new public tcp connections from the internet.
*/
// Listens for new public tcp connections from the internet.
func (t *Tunnel) listenTcp(listener *net.TCPListener) {
for {
defer func() {
@@ -160,20 +244,39 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
startTime := time.Now()
metrics.OpenConnection(t, publicConn)
t.Debug("Requesting new proxy connection")
t.ctl.out <- &msg.ReqProxyMsg{}
var proxyConn conn.Conn
var attempts int
var err error
for {
// get a proxy connection
if proxyConn, err = t.ctl.GetProxy(); err != nil {
t.Warn("Failed to get proxy connection: %v", err)
return
}
defer proxyConn.Close()
t.Info("Got proxy connection %s", proxyConn.Id())
proxyConn.AddLogPrefix(t.Id())
proxyConn := <-t.proxies
t.Info("Returning proxy connection %s", proxyConn.Id())
// tell the client we're going to start using this proxy connection
startPxyMsg := &msg.StartProxyMsg{
Url: t.url,
ClientAddr: publicConn.RemoteAddr().String(),
}
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
attempts += 1
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
if attempts > 3 {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
} else {
// success
break
}
}
defer proxyConn.Close()
// join the public and proxy connections
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)
}
func (t *Tunnel) RegisterProxy(conn conn.Conn) {
t.Info("Registered proxy connection %s", conn.Id())
conn.AddLogPrefix(t.Id())
t.proxies <- conn
}

View File

@@ -5,6 +5,26 @@ import (
"fmt"
)
func RandomSeed() (int64, error) {
b := make([]byte, 8)
n, err := rand.Read(b)
if n != 8 {
return 0, fmt.Errorf("Only generated %d random bytes, %d requested", n, 8)
}
if err != nil {
return 0, err
}
var seed int64
var i uint
for i = 0; i < 8; i++ {
seed = seed | int64(b[i]<<(i*8))
}
return seed, nil
}
// create a random identifier for this client
func RandId(idlen int) (id string, err error) {
b := make([]byte, idlen)

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

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

View File

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