61 Commits
0.12 ... 0.22

Author SHA1 Message Date
Alan Shreve
cf5bd551be bump version 2013-09-04 18:42:32 +03:00
Alan Shreve
1bf705e4ee add websocket tunnelling support 2013-09-04 18:41:32 +03:00
Alan Shreve
3655bd8027 make clean now cleans up all of the object files 2013-08-29 18:39:16 +03:00
Alan Shreve
a29febde5c indicate in the terminal UI when the web UI is disabled 2013-08-17 21:51:47 +02:00
Alan Shreve
677ea6ab95 update changelog 2013-08-17 21:51:22 +02:00
Alan Shreve
23fd33b344 bump version 2013-08-17 21:26:15 +02:00
Alan Shreve
a2cf6b70c1 allow the web interface to be disabled 2013-08-17 21:22:17 +02:00
Alan Shreve
8d7081e747 bump version 2013-08-17 12:19:19 +02:00
Alan Shreve
9373eb8f4d fix a variable shadowing bug 2013-08-17 12:09:49 +02:00
Alan Shreve
8339a76cc1 don't continue after a download error 2013-08-17 11:33:44 +02:00
Alan Shreve
8054e64ad7 bump version 2013-08-17 11:17:09 +02:00
Alan Shreve
4d08291813 fix a bug which causes updates to be tried without delay after the first update interval 2013-08-17 11:15:52 +02:00
Alan Shreve
bfd7e64d9f fix race condition in logging initialization. go fmt 2013-08-16 21:11:44 +02:00
Alan Shreve
aa99cc1cd4 send user id in update request 2013-08-15 13:09:23 +02:00
Alan Shreve
01167cb136 bump version 2013-08-15 12:42:18 +02:00
Alan Shreve
b157045a37 modify auto-updating to use the new API and check first so the server can signal manual updates as necessary 2013-08-15 12:14:14 +02:00
Alan Shreve
bf7a36f03e virtual host names are now case-insensitive 2013-08-13 15:11:44 +02:00
Alan Shreve
c5b7e331da Fix a bug which crashed the client when replaying requests 2013-08-13 12:46:46 +02:00
Alan Shreve
e6141ac6ac Merge branch 'master' of github.com:inconshreveable/ngrok 2013-08-04 18:33:08 +02:00
inconshreveable
0754d30550 Merge pull request #17 from jzs/jzs
Fixed copy paste error
2013-08-04 08:09:59 -07:00
jzs
51e8ca782b Fixed copy paste error 2013-08-03 20:47:17 +02:00
Alan Shreve
999f1063d0 update changelog 2013-08-03 17:57:32 +02:00
Alan Shreve
61dd957018 correctly handle virtual hosting on non default ports and report the correct public URL back to the client without modifying the protocol. add the VHOST environment variable for more flexibility around how virtual hosting is implemented 2013-08-03 17:37:28 +02:00
Alan Shreve
f88378a8da Merge branch 'master' of https://github.com/skastel/ngrok into publicport 2013-08-01 21:26:33 +02:00
Stephen Huenneke
0115a63898 Adding Stephen Huenneke to contributors. 2013-07-30 10:29:51 -07:00
Stephen Huenneke
53e28ef0c1 Fixing formatting so that we're faithfully passing on port information. 2013-07-30 10:27:29 -07:00
Alan Shreve
77e3f140ca separate cache keys for each protocol 2013-07-30 16:06:35 +02:00
Alan Shreve
fb0c343161 bump version 2013-07-30 15:25:45 +02:00
Alan Shreve
7032606103 Fixed an issues where the affinity cache could not be serialized/deserialized or saved/loaded from disk. Added better logging around affinity cache load/save. TCP tunnel urls are now handled in the affinity cache and will attempt to return to you the same port you had previously. 2013-07-30 15:24:08 +02:00
Stephen Huenneke
aea0524bb5 Fixing over-rides of public port so that they don't result in mis-matched host names due to Request.Host including the port in the returned hostname. 2013-07-29 16:00:24 -07:00
Alan Shreve
078bbb1fdc fix makefile to get bindata dependency properly 2013-07-28 01:01:18 +02:00
Alan Shreve
fbcde8774b add basic travis.yml 2013-07-28 00:51:50 +02:00
Alan Shreve
bd3356071e Merge branch 'autoupdate' 2013-07-28 00:14:04 +02:00
Alan Shreve
b63f9bc718 Merge branch 'master' of github.com:inconshreveable/ngrok 2013-07-28 00:13:51 +02:00
Alan Shreve
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
31 changed files with 1143 additions and 326 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;
@@ -61,7 +63,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.Req.Header['X-Real-Ip'][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;
@@ -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,51 @@
# Changelog
## 0.21 - 08/17/2013
- IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1
## 0.20 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop
## 0.19 - 08/17/2013
- BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check
- BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up
## 0.18 - 08/15/2013
- BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons
- BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80
- BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request
- IMPROVEMENT: ngrok can now indicate manual updates again
- IMPROVEMENT: ngrok can now supports update channels
- IMPROVEMENT: ngrok can now detect some updates that will fail before downloading
## 0.17 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol
## 0.16 - 07/30/2013
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error
- BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests
- BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file
- BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file
- BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified.
- BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first
- IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs
- IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles
## 0.15 - 07/27/2013
- FEATURE: ngrok can now update itself automatically
## 0.14 - 07/03/2013
- BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux
- BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens
## 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

@@ -1,11 +1,9 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"ngrok/client/ui"
"ngrok/client/views/term"
"ngrok/client/views/web"
@@ -21,11 +19,10 @@ import (
)
const (
pingInterval = 20 * time.Second
maxPongLatency = 15 * time.Second
versionCheckInterval = 6 * time.Hour
versionEndpoint = "http://dl.ngrok.com/versions"
BadGateway = `<html>
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>
@@ -87,40 +84,6 @@ Content-Length: %d
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
*/
@@ -228,8 +191,8 @@ func control(s *State, ctl *ui.Controller) {
}
// update UI state
conn.Info("Tunnel established at %v", regAck.Url)
s.publicUrl = regAck.Url
conn.Info("Tunnel established at %v", s.GetPublicUrl())
s.status = "online"
s.serverVersion = regAck.MmVersion
ctl.Update(s)
@@ -264,6 +227,11 @@ func Main() {
// set up logging
log.LogTo(opts.logto)
// set up auth token
if opts.authtoken == "" {
opts.authtoken = LoadAuthToken()
}
// init client state
s := &State{
status: "connecting",
@@ -287,13 +255,16 @@ func Main() {
// init ui
ctl := ui.NewController()
web.NewWebView(ctl, s, opts.webport)
if opts.webport != -1 {
web.NewWebView(ctl, s, opts.webport)
}
if opts.logto != "stdout" {
term.New(ctl, s)
}
go reconnectingControl(s, ctl)
go versionCheck(s, ctl)
go autoUpdate(s, ctl, opts.authtoken)
quitMessage := ""
ctl.Wait.Add(1)

View File

@@ -2,6 +2,7 @@ package client
import (
metrics "github.com/inconshreveable/go-metrics"
"ngrok/client/ui"
"ngrok/proto"
"ngrok/version"
)
@@ -11,7 +12,7 @@ type State struct {
id string
publicUrl string
serverVersion string
newVersion string
update ui.UpdateStatus
protocol proto.Protocol
opts *Options
metrics *ClientMetrics
@@ -23,12 +24,12 @@ type State struct {
// implement client.ui.State
func (s State) GetClientVersion() string { return version.MajorMinor() }
func (s State) GetServerVersion() string { return s.serverVersion }
func (s State) GetPublicUrl() string { return s.publicUrl }
func (s State) GetLocalAddr() string { return s.opts.localaddr }
func (s State) GetWebPort() int { return s.opts.webport }
func (s State) GetStatus() string { return s.status }
func (s State) GetProtocol() proto.Protocol { return s.protocol }
func (s State) GetNewVersion() string { return s.newVersion }
func (s State) GetUpdate() ui.UpdateStatus { return s.update }
func (s State) GetPublicUrl() string { return s.publicUrl }
func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
return s.metrics.connMeter, s.metrics.connTimer

View File

@@ -5,10 +5,19 @@ import (
"ngrok/proto"
)
type UpdateStatus int
const (
UpdateNone = -1 * iota
UpdateInstalling
UpdateReady
UpdateAvailable
)
type State interface {
GetClientVersion() string
GetServerVersion() string
GetNewVersion() string
GetUpdate() UpdateStatus
GetPublicUrl() string
GetLocalAddr() string
GetStatus() string

View File

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

View File

@@ -0,0 +1,127 @@
// +build release autoupdate
package client
import (
update "github.com/inconshreveable/go-update"
"net/http"
"net/url"
"ngrok/client/ui"
"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

@@ -78,10 +78,36 @@ 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 := v.state.GetUpdate()
var updateMsg string
switch updateStatus {
case ui.UpdateNone:
updateMsg = ""
case ui.UpdateInstalling:
updateMsg = "ngrok is updating"
case ui.UpdateReady:
updateMsg = "ngrok has updated: restart ngrok for the new version"
case ui.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")
@@ -93,6 +119,9 @@ func (v *TermView) Render() {
v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
if v.state.GetWebPort() == -1 {
webAddr = "disabled"
}
v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
connMeter, connTimer := v.state.GetConnectionMetrics()

View File

@@ -21,6 +21,7 @@ import (
type SerializedTxn struct {
Id string
Duration int64
Start int64
*proto.HttpTxn `json:"-"`
Req SerializedRequest
Resp SerializedResponse
@@ -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":
@@ -176,6 +178,7 @@ func (whv *WebHttpView) updateHttp() {
Body: body,
Binary: !utf8.Valid(rawReq),
},
Start: htxn.Start.Unix(),
}
htxn.UserData = whtxn
@@ -227,7 +230,7 @@ func (h *WebHttpView) register() {
r.ParseForm()
txnid := r.Form.Get("txnid")
if txn, ok := h.idToTxn[txnid]; ok {
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
bodyBytes, err := httputil.DumpRequest(txn.HttpTxn.Req.Request, true)
if err != nil {
panic(err)
}

View File

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

View File

@@ -9,6 +9,7 @@ import (
"net/http/httputil"
"ngrok/conn"
"ngrok/util"
"sync"
"time"
)
@@ -111,5 +112,29 @@ func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
}
h.Txns.In() <- txn
// XXX: remove web socket shim in favor of a real websocket protocol analyzer
if txn.Req.Header.Get("Upgrade") == "websocket" {
tee.Info("Upgrading to websocket")
var wg sync.WaitGroup
// shim for websockets
// in order for websockets to work, we need to continue reading all of the
// the bytes in the analyzer so that the joined connections will continue
// sending bytes to each other
wg.Add(2)
go func() {
ioutil.ReadAll(tee.WriteBuffer())
wg.Done()
}()
go func() {
ioutil.ReadAll(tee.ReadBuffer())
wg.Done()
}()
wg.Wait()
break
}
}
}

View File

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

View File

@@ -7,7 +7,7 @@ import (
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"regexp"
"os"
)
type Options struct {
@@ -20,15 +20,13 @@ type Options struct {
/* GLOBALS */
var (
hostRegex *regexp.Regexp
proxyAddr string
tunnels *TunnelManager
proxyAddr string
tunnels *TunnelRegistry
registryCacheSize uint64 = 1024 * 1024 // 1 MB
domain string
publicPort int
)
func init() {
hostRegex = regexp.MustCompile("[H|h]ost: ([^\\(\\);:,<>]+)\n")
}
func parseArgs() *Options {
publicPort := flag.Int("publicport", 80, "Public port")
tunnelPort := flag.Int("tunnelport", 4443, "Tunnel port")
@@ -110,11 +108,15 @@ func proxyListener(addr *net.TCPAddr, domain string) {
func Main() {
// parse options
opts := parseArgs()
domain = opts.domain
publicPort = opts.publicPort
// init logging
log.LogTo(opts.logto)
tunnels = NewTunnelManager(opts.domain)
// init tunnel registry
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
tunnels = NewTunnelRegistry(registryCacheSize, registryCacheFile)
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)

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,162 @@
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"),
}
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
// to encode or decode any non-primitive types that haven't been "registered"
// with it. Since we store cacheUrl objects, we need to register them here first
// for the encoding/decoding to work
var urlobj cacheUrl
gob.Register(urlobj)
if cacheFile != "" {
// load cache entries from file
err := registry.affinity.LoadItemsFromFile(cacheFile)
if err != nil {
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
}
// save cache periodically to file
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
} else {
registry.Info("No affinity cache specified")
}
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]
}

View File

@@ -3,11 +3,15 @@ package server
import (
"encoding/base64"
"fmt"
"math/rand"
"net"
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/version"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
@@ -50,29 +54,99 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
Logger: log.NewPrefixLogger(),
}
failReg := func(err error) {
t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()}
}
var err error
switch t.regMsg.Protocol {
case "tcp":
var err error
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
var port int = 0
// try to return to you the same port you had before
cachedUrl := tunnels.GetCachedRegistration(t)
if cachedUrl != "" {
parts := strings.Split(cachedUrl, ":")
portPart := parts[len(parts)-1]
port, err = strconv.Atoi(portPart)
if err != nil {
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
// continue with zero
port = 0
}
}
// Bind for TCP connections
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port})
// If we failed with a custom port, try with a random one
if err != nil && port != 0 {
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
}
// we tried to bind with a random port and failed (no more ports available?)
if err != nil {
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
failReg(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 = tunnels.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
failReg(fmt.Errorf("TCP listener bound, but failed to register %s", t.url))
return
}
go t.listenTcp(t.listener)
default:
}
case "http":
vhost := os.Getenv("VHOST")
if vhost == "" {
vhost = fmt.Sprintf("%s:%d", domain, publicPort)
}
if err := tunnels.Add(t); err != nil {
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprint(err)}
return
// Canonicalize virtual host on default port 80
if strings.HasSuffix(vhost, ":80") {
vhost = vhost[0 : len(vhost)-3]
}
if strings.TrimSpace(t.regMsg.Hostname) != "" {
t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname)
} else if strings.TrimSpace(t.regMsg.Subdomain) != "" {
t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, vhost)
}
vhost = strings.ToLower(vhost)
t.url = strings.ToLower(t.url)
if t.url != "" {
if err := tunnels.Register(t.url, t); err != nil {
failReg(err)
return
}
} else {
t.url, err = tunnels.RegisterRepeat(func() string {
return fmt.Sprintf("http://%x.%s", rand.Int31(), vhost)
}, t)
if err != nil {
failReg(err)
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)}
failReg(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

View File

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