Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5bd551be | ||
|
|
1bf705e4ee | ||
|
|
3655bd8027 | ||
|
|
a29febde5c | ||
|
|
677ea6ab95 | ||
|
|
23fd33b344 | ||
|
|
a2cf6b70c1 | ||
|
|
8d7081e747 | ||
|
|
9373eb8f4d | ||
|
|
8339a76cc1 | ||
|
|
8054e64ad7 | ||
|
|
4d08291813 | ||
|
|
bfd7e64d9f | ||
|
|
aa99cc1cd4 | ||
|
|
01167cb136 | ||
|
|
b157045a37 | ||
|
|
bf7a36f03e | ||
|
|
c5b7e331da | ||
|
|
e6141ac6ac | ||
|
|
0754d30550 | ||
|
|
51e8ca782b | ||
|
|
999f1063d0 | ||
|
|
61dd957018 | ||
|
|
f88378a8da | ||
|
|
0115a63898 | ||
|
|
53e28ef0c1 | ||
|
|
77e3f140ca | ||
|
|
fb0c343161 | ||
|
|
7032606103 | ||
|
|
aea0524bb5 | ||
|
|
078bbb1fdc | ||
|
|
fbcde8774b | ||
|
|
bd3356071e | ||
|
|
b63f9bc718 | ||
|
|
aa4543b824 | ||
|
|
896afc0141 | ||
|
|
4f4bd7bd06 | ||
|
|
7fb02bb768 | ||
|
|
6a2b9c31b1 | ||
|
|
07c2fdc693 | ||
|
|
b275aa7123 | ||
|
|
10a80333da | ||
|
|
09953ac19a | ||
|
|
081a1f7fde | ||
|
|
26038dbf1b | ||
|
|
452ba7a575 | ||
|
|
35d8fc20ff | ||
|
|
492182497a | ||
|
|
0d49d7223d | ||
|
|
b136b8f1aa | ||
|
|
86c49329c0 | ||
|
|
e200e75da4 | ||
|
|
235e5e1a03 | ||
|
|
b1ecf6b2b7 | ||
|
|
d4421470c6 | ||
|
|
33e6abd696 | ||
|
|
4823077f97 | ||
|
|
885e29abde | ||
|
|
d704c5b2c6 | ||
|
|
26e9190fac | ||
|
|
c0b7bf6429 |
4
.travis.yml
Normal file
4
.travis.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
script: make release-all
|
||||
install: true
|
||||
go:
|
||||
- 1.1
|
||||
5
CONTRIBUTORS
Normal file
5
CONTRIBUTORS
Normal file
@@ -0,0 +1,5 @@
|
||||
Contributors to ngrok, both large and small:
|
||||
Alan Shreve (inconshreveable)
|
||||
Kyle Conroy (kyleconroy)
|
||||
Caleb Spare (cespare)
|
||||
Stephen Huenneke (skastel)
|
||||
5
Makefile
5
Makefile
@@ -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/...
|
||||
|
||||
@@ -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"
|
||||

|
||||
|
||||
## 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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
BIN
assets/client/static/img/glyphicons-halflings.png
Normal file
BIN
assets/client/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
13
assets/client/static/js/angular-sanitize.min.js
vendored
Normal file
13
assets/client/static/js/angular-sanitize.min.js
vendored
Normal 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,"<");return l.innerText||l.textContent||""}function t(a){return a.replace(/&/g,"&").replace(F,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}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);
|
||||
193
assets/client/static/js/jquery.timeago.js
Normal file
193
assets/client/static/js/jquery.timeago.js
Normal 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");
|
||||
}));
|
||||
@@ -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, '&').replace(/</gm, '<').replace(/>/gm, '>');
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
46
nrsc
@@ -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
245
src/ngrok/cache/lru.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
src/ngrok/client/update_debug.go
Normal file
11
src/ngrok/client/update_debug.go
Normal 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) {
|
||||
}
|
||||
127
src/ngrok/client/update_release.go
Normal file
127
src/ngrok/client/update_release.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{} {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
162
src/ngrok/server/registry.go
Normal file
162
src/ngrok/server/registry.go
Normal 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]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
const (
|
||||
Proto = "1"
|
||||
Major = "0"
|
||||
Minor = "12"
|
||||
Minor = "22"
|
||||
)
|
||||
|
||||
func MajorMinor() string {
|
||||
|
||||
Reference in New Issue
Block a user