Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f17bc20337 | ||
|
|
7c1977c30f | ||
|
|
e4ce075701 | ||
|
|
a5d4279cca | ||
|
|
b488fe6bd7 | ||
|
|
83af364b1d | ||
|
|
83dc742d38 | ||
|
|
937f665e51 | ||
|
|
cf5bd551be | ||
|
|
1bf705e4ee | ||
|
|
ea088b69f2 | ||
|
|
3655bd8027 | ||
|
|
2fff31ff61 | ||
|
|
a29febde5c | ||
|
|
677ea6ab95 | ||
|
|
23fd33b344 | ||
|
|
a2cf6b70c1 | ||
|
|
8d7081e747 | ||
|
|
9373eb8f4d | ||
|
|
8339a76cc1 | ||
|
|
8054e64ad7 | ||
|
|
4d08291813 | ||
|
|
ea1b1ed632 | ||
|
|
bfd7e64d9f | ||
|
|
aa99cc1cd4 | ||
|
|
01167cb136 | ||
|
|
b157045a37 | ||
|
|
bf7a36f03e | ||
|
|
c5b7e331da | ||
|
|
e6141ac6ac | ||
|
|
0754d30550 | ||
|
|
3d17d52659 | ||
|
|
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;
|
||||
|
||||
@@ -46,7 +48,12 @@
|
||||
<div class="span6 offset3">
|
||||
<div class="well" style="padding: 20px 50px;">
|
||||
<h4>No requests to display yet</h4>
|
||||
<p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
|
||||
<hr />
|
||||
<h5>To get started, make a request to one of your tunnel URLs:</h5>
|
||||
<ul>
|
||||
<li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +68,23 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<div class="span6" ng-controller="HttpTxn" ng-show="!!Txn">
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<span title="{{ISO8601(Txn.Start)}}" class="muted">
|
||||
{{TimeFormat(Txn.Start)}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<i class="icon-time"></i> Duration
|
||||
<span style="margin-left: 8px;" class="muted">{{Txn.Duration}}</span>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<i class="icon-user"></i> IP
|
||||
<span style="margin-left: 8px;" class="muted">{{Txn.ConnCtx.ClientAddr.split(":")[0]}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div ng-show="!!Req" ng-controller="HttpRequest">
|
||||
<h3>{{ Req.MethodPath }}</h3>
|
||||
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
|
||||
|
||||
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;
|
||||
|
||||
@@ -303,7 +303,7 @@ ngrok.directive({
|
||||
|
||||
ngrok.controller({
|
||||
"HttpTxns": function($scope, txnSvc) {
|
||||
$scope.publicUrl = window.data.UiState.Url;
|
||||
$scope.tunnels = window.data.UiState.Tunnels;
|
||||
$scope.txns = txnSvc.all();
|
||||
|
||||
if (!!window.WebSocket) {
|
||||
@@ -317,7 +317,7 @@ ngrok.controller({
|
||||
txnSvc.add(message.data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ws.onerror = function(err) {
|
||||
console.log("Web socket error:" + err);
|
||||
};
|
||||
@@ -347,7 +347,7 @@ ngrok.controller({
|
||||
$scope.$watch(function() { return txnSvc.active() }, setReq);
|
||||
},
|
||||
|
||||
"HttpResponse": function($scope, $element, $timeout, txnSvc) {
|
||||
"HttpResponse": function($scope, $element, txnSvc) {
|
||||
var setResp = function() {
|
||||
var txn = txnSvc.active();
|
||||
if (!!txn && txn.Resp) {
|
||||
@@ -365,4 +365,27 @@ ngrok.controller({
|
||||
txnSvc.active($scope.txn);
|
||||
};
|
||||
},
|
||||
|
||||
"HttpTxn": function($scope, txnSvc, $timeout) {
|
||||
var setTxn = function() {
|
||||
$scope.Txn = txnSvc.active();
|
||||
};
|
||||
|
||||
$scope.ISO8601 = function(ts) {
|
||||
if (!!ts) {
|
||||
return new Date(ts * 1000).toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.TimeFormat = function(ts) {
|
||||
if (!!ts) {
|
||||
return $.timeago($scope.ISO8601(ts));
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch(function() { return txnSvc.active() }, setTxn);
|
||||
|
||||
// this causes angular to update the timestamps
|
||||
setInterval(function() { $scope.$apply(function() {}); }, 30000);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
# Changelog
|
||||
## 0.23 - 09/06/2013
|
||||
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay
|
||||
|
||||
## 0.22 - 09/04/2013
|
||||
- FEATURE: ngrok now tunnels websocket requests
|
||||
|
||||
## 0.21 - 08/17/2013
|
||||
- IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1
|
||||
|
||||
## 0.20 - 08/17/2013
|
||||
- BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop
|
||||
|
||||
## 0.19 - 08/17/2013
|
||||
- BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check
|
||||
- BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up
|
||||
|
||||
## 0.18 - 08/15/2013
|
||||
- BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons
|
||||
- BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80
|
||||
- BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request
|
||||
- IMPROVEMENT: ngrok can now indicate manual updates again
|
||||
- IMPROVEMENT: ngrok can now supports update channels
|
||||
- IMPROVEMENT: ngrok can now detect some updates that will fail before downloading
|
||||
|
||||
## 0.17 - 07/30/2013
|
||||
- BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol
|
||||
|
||||
## 0.16 - 07/30/2013
|
||||
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error
|
||||
- BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error
|
||||
- BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests
|
||||
- BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file
|
||||
- BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file
|
||||
- BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified.
|
||||
- BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first
|
||||
- IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs
|
||||
- IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles
|
||||
|
||||
## 0.15 - 07/27/2013
|
||||
- FEATURE: ngrok can now update itself automatically
|
||||
|
||||
## 0.14 - 07/03/2013
|
||||
- BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux
|
||||
- BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens
|
||||
|
||||
## 0.13 - 07/02/2013
|
||||
- FEATURE: -hostname switch on client allows you to run tunnels over custom domains (requires you CNAME your DNS)
|
||||
- IMPROVEMENT: ngrok client UI now shows the client IP address for a request
|
||||
- IMPROVEMENT: ngrok client UI now shows how long ago a request was made (uservoice request 4127487)
|
||||
- IMPROVEMENT: ngrokd now uses and LRU cache for tunnel affinity data
|
||||
- IMPROVEMENT: ngrokd can now save and restore its tunnel affinity cache to a file to preserve across restarts
|
||||
|
||||
## 0.12 - 06/30/2013
|
||||
- IMPROVEMENT Improved developer documentation
|
||||
- IMPROVEMENT Simplified build process with custom version of go-bindata that compiles assets into binary releases
|
||||
- BUGFIX Github issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes.
|
||||
- IMPROVEMENT: Improved developer documentation
|
||||
- IMPROVEMENT: Simplified build process with custom version of go-bindata that compiles assets into binary releases
|
||||
- BUGFIX: Github issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
210
src/ngrok/client/controller.go
Normal file
210
src/ngrok/client/controller.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/client/views/term"
|
||||
"ngrok/client/views/web"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type command interface{}
|
||||
|
||||
type cmdQuit struct {
|
||||
// display this message after quit
|
||||
message string
|
||||
}
|
||||
|
||||
type cmdPlayRequest struct {
|
||||
// the tunnel to play this request over
|
||||
tunnel mvc.Tunnel
|
||||
|
||||
// the bytes of the request to issue
|
||||
payload []byte
|
||||
}
|
||||
|
||||
// The MVC Controller
|
||||
type Controller struct {
|
||||
// Controller logger
|
||||
log.Logger
|
||||
|
||||
// the model sends updates through this broadcast channel
|
||||
updates *util.Broadcast
|
||||
|
||||
// the model
|
||||
model mvc.Model
|
||||
|
||||
// the views
|
||||
views []mvc.View
|
||||
|
||||
// interal structure to issue commands to the controller
|
||||
cmds chan command
|
||||
|
||||
// internal structure to synchronize access to State object
|
||||
state chan mvc.State
|
||||
|
||||
// options
|
||||
opts *Options
|
||||
}
|
||||
|
||||
// public interface
|
||||
func NewController() *Controller {
|
||||
ctl := &Controller{
|
||||
Logger: log.NewPrefixLogger("controller"),
|
||||
updates: util.NewBroadcast(),
|
||||
cmds: make(chan command),
|
||||
views: make([]mvc.View, 0),
|
||||
state: make(chan mvc.State),
|
||||
}
|
||||
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (ctl *Controller) State() mvc.State {
|
||||
return <-ctl.state
|
||||
}
|
||||
|
||||
func (ctl *Controller) Update(state mvc.State) {
|
||||
ctl.updates.In() <- state
|
||||
}
|
||||
|
||||
func (ctl *Controller) Updates() *util.Broadcast {
|
||||
return ctl.updates
|
||||
}
|
||||
|
||||
func (ctl *Controller) Shutdown(message string) {
|
||||
ctl.cmds <- cmdQuit{message: message}
|
||||
}
|
||||
|
||||
func (ctl *Controller) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
|
||||
ctl.cmds <- cmdPlayRequest{tunnel: tunnel, payload: payload}
|
||||
}
|
||||
|
||||
func (ctl *Controller) Go(fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
ctl.Error(err)
|
||||
ctl.Shutdown(err)
|
||||
}
|
||||
}()
|
||||
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// private functions
|
||||
func (ctl *Controller) doShutdown() {
|
||||
ctl.Info("Shutting down")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// wait for all of the views, plus the model
|
||||
wg.Add(len(ctl.views) + 1)
|
||||
|
||||
for _, v := range ctl.views {
|
||||
vClosure := v
|
||||
ctl.Go(func() {
|
||||
vClosure.Shutdown()
|
||||
wg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
ctl.Go(func() {
|
||||
ctl.model.Shutdown()
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (ctl *Controller) addView(v mvc.View) {
|
||||
ctl.views = append(ctl.views, v)
|
||||
}
|
||||
|
||||
func (ctl *Controller) GetWebViewPort() int {
|
||||
return ctl.opts.webport
|
||||
}
|
||||
|
||||
func (ctl *Controller) Run(opts *Options) {
|
||||
// Save the options
|
||||
ctl.opts = opts
|
||||
|
||||
// init the model
|
||||
model := newClientModel(ctl)
|
||||
ctl.model = model
|
||||
var state mvc.State = model
|
||||
|
||||
// init web ui
|
||||
var webView *web.WebView
|
||||
if opts.webport != -1 {
|
||||
webView = web.NewWebView(ctl, opts.webport)
|
||||
ctl.addView(webView)
|
||||
}
|
||||
|
||||
// init term ui
|
||||
var termView *term.TermView
|
||||
if opts.logto != "stdout" {
|
||||
termView = term.NewTermView(ctl)
|
||||
ctl.addView(termView)
|
||||
}
|
||||
|
||||
for _, protocol := range model.GetProtocols() {
|
||||
switch p := protocol.(type) {
|
||||
case *proto.Http:
|
||||
if termView != nil {
|
||||
ctl.addView(termView.NewHttpView(p))
|
||||
}
|
||||
|
||||
if webView != nil {
|
||||
ctl.addView(webView.NewHttpView(p))
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
ctl.Go(func() { autoUpdate(ctl, opts.authtoken) })
|
||||
|
||||
reg := &msg.RegMsg{
|
||||
Protocol: opts.protocol,
|
||||
Hostname: opts.hostname,
|
||||
Subdomain: opts.subdomain,
|
||||
HttpAuth: opts.httpAuth,
|
||||
User: opts.authtoken,
|
||||
}
|
||||
|
||||
ctl.Go(func() { ctl.model.Run(opts.server, opts.authtoken, ctl, reg, opts.localaddr) })
|
||||
|
||||
quitMessage := ""
|
||||
defer func() {
|
||||
ctl.doShutdown()
|
||||
fmt.Println(quitMessage)
|
||||
}()
|
||||
|
||||
updates := ctl.updates.Reg()
|
||||
defer ctl.updates.UnReg(updates)
|
||||
|
||||
for {
|
||||
select {
|
||||
case obj := <-ctl.cmds:
|
||||
switch cmd := obj.(type) {
|
||||
case cmdQuit:
|
||||
quitMessage = cmd.message
|
||||
return
|
||||
|
||||
case cmdPlayRequest:
|
||||
ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) })
|
||||
}
|
||||
|
||||
case obj := <-updates:
|
||||
state = obj.(mvc.State)
|
||||
|
||||
case ctl.state <- state:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,262 +1,11 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/views/term"
|
||||
"ngrok/client/views/web"
|
||||
"ngrok/conn"
|
||||
"math/rand"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pingInterval = 20 * time.Second
|
||||
maxPongLatency = 15 * time.Second
|
||||
versionCheckInterval = 6 * time.Hour
|
||||
versionEndpoint = "http://dl.ngrok.com/versions"
|
||||
BadGateway = `<html>
|
||||
<body style="background-color: #97a8b9">
|
||||
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
|
||||
<h2>Tunnel %s unavailable</h2>
|
||||
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
|
||||
`
|
||||
)
|
||||
|
||||
/**
|
||||
* Establishes and manages a tunnel proxy connection with the server
|
||||
*/
|
||||
func proxy(proxyAddr string, s *State, ctl *ui.Controller) {
|
||||
start := time.Now()
|
||||
remoteConn, err := conn.Dial(proxyAddr, "pxy", tlsConfig)
|
||||
if err != nil {
|
||||
// XXX: What is the proper response here?
|
||||
// display something to the user?
|
||||
// retry?
|
||||
// reset control connection?
|
||||
log.Error("Failed to establish proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer remoteConn.Close()
|
||||
err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{Url: s.publicUrl})
|
||||
if err != nil {
|
||||
// XXX: What is the proper response here?
|
||||
// display something to the user?
|
||||
// retry?
|
||||
// reset control connection?
|
||||
log.Error("Failed to write RegProxyMsg: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
|
||||
if err != nil {
|
||||
remoteConn.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
|
||||
badGatewayBody := fmt.Sprintf(BadGateway, s.publicUrl, s.opts.localaddr, s.opts.localaddr)
|
||||
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
|
||||
Content-Type: text/html
|
||||
Content-Length: %d
|
||||
|
||||
%s`, len(badGatewayBody), badGatewayBody)))
|
||||
return
|
||||
}
|
||||
defer localConn.Close()
|
||||
|
||||
m := s.metrics
|
||||
m.proxySetupTimer.Update(time.Since(start))
|
||||
m.connMeter.Mark(1)
|
||||
ctl.Update(s)
|
||||
m.connTimer.Time(func() {
|
||||
localConn := s.protocol.WrapConn(localConn)
|
||||
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
|
||||
m.bytesIn.Update(bytesIn)
|
||||
m.bytesOut.Update(bytesOut)
|
||||
m.bytesInCount.Inc(bytesIn)
|
||||
m.bytesOutCount.Inc(bytesOut)
|
||||
})
|
||||
ctl.Update(s)
|
||||
}
|
||||
|
||||
func versionCheck(s *State, ctl *ui.Controller) {
|
||||
check := func() {
|
||||
resp, err := http.Get(versionEndpoint)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get version info %s: %v", versionEndpoint, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var payload struct {
|
||||
Client struct {
|
||||
Version string
|
||||
}
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
log.Warn("Failed to read version info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Client.Version != version.MajorMinor() {
|
||||
s.newVersion = payload.Client.Version
|
||||
ctl.Update(s)
|
||||
}
|
||||
}
|
||||
|
||||
// check immediately and then at a set interval
|
||||
check()
|
||||
for _ = range time.Tick(versionCheckInterval) {
|
||||
check()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Hearbeating to ensure our connection ngrokd is still live
|
||||
*/
|
||||
func heartbeat(lastPongAddr *int64, c conn.Conn) {
|
||||
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
|
||||
ping := time.NewTicker(pingInterval)
|
||||
pongCheck := time.NewTicker(time.Second)
|
||||
|
||||
defer func() {
|
||||
c.Close()
|
||||
ping.Stop()
|
||||
pongCheck.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pongCheck.C:
|
||||
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
|
||||
needPong := lastPong.Sub(lastPing) < 0
|
||||
pongLatency := time.Since(lastPing)
|
||||
|
||||
if needPong && pongLatency > maxPongLatency {
|
||||
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
|
||||
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
|
||||
return
|
||||
}
|
||||
|
||||
case <-ping.C:
|
||||
err := msg.WriteMsg(c, &msg.PingMsg{})
|
||||
if err != nil {
|
||||
c.Debug("Got error %v when writing PingMsg", err)
|
||||
return
|
||||
}
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reconnectingControl(s *State, ctl *ui.Controller) {
|
||||
// how long we should wait before we reconnect
|
||||
maxWait := 30 * time.Second
|
||||
wait := 1 * time.Second
|
||||
|
||||
for {
|
||||
control(s, ctl)
|
||||
|
||||
if s.status == "online" {
|
||||
wait = 1 * time.Second
|
||||
}
|
||||
|
||||
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
|
||||
time.Sleep(wait)
|
||||
// exponentially increase wait time
|
||||
wait = 2 * wait
|
||||
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
|
||||
s.status = "reconnecting"
|
||||
ctl.Update(s)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes and manages a tunnel control connection with the server
|
||||
*/
|
||||
func control(s *State, ctl *ui.Controller) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("control recovering from failure %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// establish control channel
|
||||
conn, err := conn.Dial(s.opts.server, "ctl", tlsConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// register with the server
|
||||
err = msg.WriteMsg(conn, &msg.RegMsg{
|
||||
Protocol: s.opts.protocol,
|
||||
OS: runtime.GOOS,
|
||||
HttpAuth: s.opts.httpAuth,
|
||||
Hostname: s.opts.hostname,
|
||||
Subdomain: s.opts.subdomain,
|
||||
ClientId: s.id,
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
User: s.opts.authtoken,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var regAck msg.RegAckMsg
|
||||
if err = msg.ReadMsgInto(conn, ®Ack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if regAck.Error != "" {
|
||||
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
|
||||
ctl.Cmds <- ui.CmdQuit{Message: emsg}
|
||||
return
|
||||
}
|
||||
|
||||
// update UI state
|
||||
conn.Info("Tunnel established at %v", regAck.Url)
|
||||
s.publicUrl = regAck.Url
|
||||
s.status = "online"
|
||||
s.serverVersion = regAck.MmVersion
|
||||
ctl.Update(s)
|
||||
|
||||
SaveAuthToken(s.opts.authtoken)
|
||||
|
||||
// start the heartbeat
|
||||
lastPong := time.Now().UnixNano()
|
||||
go heartbeat(&lastPong, conn)
|
||||
|
||||
// main control loop
|
||||
for {
|
||||
var m msg.Message
|
||||
if m, err = msg.ReadMsg(conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch m.(type) {
|
||||
case *msg.ReqProxyMsg:
|
||||
go proxy(regAck.ProxyAddr, s, ctl)
|
||||
|
||||
case *msg.PongMsg:
|
||||
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Main() {
|
||||
// parse options
|
||||
opts := parseArgs()
|
||||
@@ -264,67 +13,17 @@ func Main() {
|
||||
// set up logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
// init client state
|
||||
s := &State{
|
||||
status: "connecting",
|
||||
|
||||
// unique client id
|
||||
id: util.RandIdOrPanic(8),
|
||||
|
||||
// command-line options
|
||||
opts: opts,
|
||||
|
||||
// metrics
|
||||
metrics: NewClientMetrics(),
|
||||
// set up auth token
|
||||
if opts.authtoken == "" {
|
||||
opts.authtoken = LoadAuthToken()
|
||||
}
|
||||
|
||||
switch opts.protocol {
|
||||
case "http":
|
||||
s.protocol = proto.NewHttp()
|
||||
case "tcp":
|
||||
s.protocol = proto.NewTcp()
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
log.Error("Couldn't securely seed the random number generator!")
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
// init ui
|
||||
ctl := ui.NewController()
|
||||
web.NewWebView(ctl, s, opts.webport)
|
||||
if opts.logto != "stdout" {
|
||||
term.New(ctl, s)
|
||||
}
|
||||
|
||||
go reconnectingControl(s, ctl)
|
||||
go versionCheck(s, ctl)
|
||||
|
||||
quitMessage := ""
|
||||
ctl.Wait.Add(1)
|
||||
go func() {
|
||||
defer ctl.Wait.Done()
|
||||
for {
|
||||
select {
|
||||
case obj := <-ctl.Cmds:
|
||||
switch cmd := obj.(type) {
|
||||
case ui.CmdQuit:
|
||||
quitMessage = cmd.Message
|
||||
ctl.DoShutdown()
|
||||
return
|
||||
case ui.CmdRequest:
|
||||
go func() {
|
||||
var localConn conn.Conn
|
||||
localConn, err := conn.Dial(s.opts.localaddr, "prv", nil)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err)
|
||||
return
|
||||
}
|
||||
//defer localConn.Close()
|
||||
localConn = s.protocol.WrapConn(localConn)
|
||||
localConn.Write(cmd.Payload)
|
||||
ioutil.ReadAll(localConn)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctl.Wait.Wait()
|
||||
fmt.Println(quitMessage)
|
||||
NewController().Run(opts)
|
||||
}
|
||||
|
||||
352
src/ngrok/client/model.go
Normal file
352
src/ngrok/client/model.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pingInterval = 20 * time.Second
|
||||
maxPongLatency = 15 * time.Second
|
||||
updateCheckInterval = 6 * time.Hour
|
||||
BadGateway = `<html>
|
||||
<body style="background-color: #97a8b9">
|
||||
<div style="margin:auto; width:400px;padding: 20px 60px; background-color: #D3D3D3; border: 5px solid maroon;">
|
||||
<h2>Tunnel %s unavailable</h2>
|
||||
<p>Unable to initiate connection to <strong>%s</strong>. A web server must be running on port <strong>%s</strong> to complete the tunnel.</p>
|
||||
`
|
||||
)
|
||||
|
||||
type ClientModel struct {
|
||||
log.Logger
|
||||
|
||||
id string
|
||||
tunnels map[string]mvc.Tunnel
|
||||
serverVersion string
|
||||
metrics *ClientMetrics
|
||||
updateStatus mvc.UpdateStatus
|
||||
connStatus mvc.ConnStatus
|
||||
protoMap map[string]proto.Protocol
|
||||
protocols []proto.Protocol
|
||||
ctl mvc.Controller
|
||||
serverAddr string
|
||||
authToken string
|
||||
}
|
||||
|
||||
func newClientModel(ctl mvc.Controller) *ClientModel {
|
||||
protoMap := make(map[string]proto.Protocol)
|
||||
protoMap["http"] = proto.NewHttp()
|
||||
protoMap["https"] = protoMap["http"]
|
||||
protoMap["tcp"] = proto.NewTcp()
|
||||
protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
|
||||
|
||||
return &ClientModel{
|
||||
Logger: log.NewPrefixLogger("client"),
|
||||
|
||||
// unique client id
|
||||
id: util.RandIdOrPanic(8),
|
||||
|
||||
// connection status
|
||||
connStatus: mvc.ConnConnecting,
|
||||
|
||||
// update status
|
||||
updateStatus: mvc.UpdateNone,
|
||||
|
||||
// metrics
|
||||
metrics: NewClientMetrics(),
|
||||
|
||||
// protocols
|
||||
protoMap: protoMap,
|
||||
|
||||
// protocol list
|
||||
protocols: protocols,
|
||||
|
||||
// open tunnels
|
||||
tunnels: make(map[string]mvc.Tunnel),
|
||||
|
||||
// controller
|
||||
ctl: ctl,
|
||||
}
|
||||
}
|
||||
|
||||
// mvc.State interface
|
||||
func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols }
|
||||
func (c ClientModel) GetClientVersion() string { return version.MajorMinor() }
|
||||
func (c ClientModel) GetServerVersion() string { return c.serverVersion }
|
||||
func (c ClientModel) GetTunnels() []mvc.Tunnel {
|
||||
tunnels := make([]mvc.Tunnel, 0)
|
||||
for _, t := range c.tunnels {
|
||||
tunnels = append(tunnels, t)
|
||||
}
|
||||
return tunnels
|
||||
}
|
||||
func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus }
|
||||
func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus }
|
||||
|
||||
func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
|
||||
return c.metrics.connMeter, c.metrics.connTimer
|
||||
}
|
||||
|
||||
func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return c.metrics.bytesInCount, c.metrics.bytesIn
|
||||
}
|
||||
|
||||
func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return c.metrics.bytesOutCount, c.metrics.bytesOut
|
||||
}
|
||||
|
||||
// mvc.Model interface
|
||||
func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
|
||||
var localConn conn.Conn
|
||||
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
|
||||
if err != nil {
|
||||
c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer localConn.Close()
|
||||
localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"})
|
||||
localConn.Write(payload)
|
||||
ioutil.ReadAll(localConn)
|
||||
}
|
||||
|
||||
func (c *ClientModel) Shutdown() {
|
||||
}
|
||||
|
||||
func (c *ClientModel) update() {
|
||||
c.ctl.Update(c)
|
||||
}
|
||||
|
||||
func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, reg *msg.RegMsg, localaddr string) {
|
||||
c.serverAddr = serverAddr
|
||||
c.authToken = authToken
|
||||
c.ctl = ctl
|
||||
c.reconnectingControl(reg, localaddr)
|
||||
}
|
||||
|
||||
func (c *ClientModel) reconnectingControl(reg *msg.RegMsg, localaddr string) {
|
||||
// how long we should wait before we reconnect
|
||||
maxWait := 30 * time.Second
|
||||
wait := 1 * time.Second
|
||||
|
||||
for {
|
||||
c.control(reg, localaddr)
|
||||
|
||||
if c.connStatus == mvc.ConnOnline {
|
||||
wait = 1 * time.Second
|
||||
}
|
||||
|
||||
log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds()))
|
||||
time.Sleep(wait)
|
||||
// exponentially increase wait time
|
||||
wait = 2 * wait
|
||||
wait = time.Duration(math.Min(float64(wait), float64(maxWait)))
|
||||
c.connStatus = mvc.ConnReconnecting
|
||||
c.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Establishes and manages a tunnel control connection with the server
|
||||
func (c *ClientModel) control(reg *msg.RegMsg, localaddr string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("control recovering from failure %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// establish control channel
|
||||
conn, err := conn.Dial(c.serverAddr, "ctl", tlsConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// register with the server
|
||||
reg.OS = runtime.GOOS
|
||||
reg.ClientId = c.id
|
||||
reg.Version = version.Proto
|
||||
reg.MmVersion = version.MajorMinor()
|
||||
reg.User = c.authToken
|
||||
|
||||
if err = msg.WriteMsg(conn, reg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var regAck msg.RegAckMsg
|
||||
if err = msg.ReadMsgInto(conn, ®Ack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if regAck.Error != "" {
|
||||
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error)
|
||||
c.ctl.Shutdown(emsg)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel := mvc.Tunnel{
|
||||
PublicUrl: regAck.Url,
|
||||
LocalAddr: localaddr,
|
||||
Protocol: c.protoMap[reg.Protocol],
|
||||
}
|
||||
|
||||
c.tunnels[tunnel.PublicUrl] = tunnel
|
||||
|
||||
// update UI state
|
||||
c.id = regAck.ClientId
|
||||
c.Info("Tunnel established at %v", tunnel.PublicUrl)
|
||||
c.connStatus = mvc.ConnOnline
|
||||
c.serverVersion = regAck.MmVersion
|
||||
c.update()
|
||||
|
||||
SaveAuthToken(c.authToken)
|
||||
|
||||
// start the heartbeat
|
||||
lastPong := time.Now().UnixNano()
|
||||
c.ctl.Go(func() { c.heartbeat(&lastPong, conn) })
|
||||
|
||||
// main control loop
|
||||
for {
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.ReqProxyMsg:
|
||||
c.ctl.Go(c.proxy)
|
||||
|
||||
case *msg.PongMsg:
|
||||
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
|
||||
|
||||
case *msg.RegAckMsg:
|
||||
if m.Error != "" {
|
||||
c.Error("Server failed to allocate tunnel: %s", regAck.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
tunnel := mvc.Tunnel{
|
||||
PublicUrl: m.Url,
|
||||
LocalAddr: localaddr,
|
||||
Protocol: c.protoMap[m.Protocol],
|
||||
}
|
||||
|
||||
c.tunnels[tunnel.PublicUrl] = tunnel
|
||||
c.Info("Tunnel established at %v", tunnel.PublicUrl)
|
||||
c.update()
|
||||
|
||||
default:
|
||||
conn.Warn("Ignoring unknown control message %v ", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Establishes and manages a tunnel proxy connection with the server
|
||||
func (c *ClientModel) proxy() {
|
||||
remoteConn, err := conn.Dial(c.serverAddr, "pxy", tlsConfig)
|
||||
if err != nil {
|
||||
log.Error("Failed to establish proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer remoteConn.Close()
|
||||
err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{ClientId: c.id})
|
||||
if err != nil {
|
||||
log.Error("Failed to write RegProxyMsg: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for the server to ack our register
|
||||
var startPxyMsg msg.StartProxyMsg
|
||||
if err = msg.ReadMsgInto(remoteConn, &startPxyMsg); err != nil {
|
||||
log.Error("Server failed to write StartProxyMsg: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel, ok := c.tunnels[startPxyMsg.Url]
|
||||
if !ok {
|
||||
c.Error("Couldn't find tunnel for proxy: %s", startPxyMsg.Url)
|
||||
return
|
||||
}
|
||||
|
||||
// start up the private connection
|
||||
start := time.Now()
|
||||
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
|
||||
if err != nil {
|
||||
remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err)
|
||||
|
||||
if tunnel.Protocol.GetName() == "http" {
|
||||
// try to be helpful when you're in HTTP mode and a human might see the output
|
||||
badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr)
|
||||
remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
|
||||
Content-Type: text/html
|
||||
Content-Length: %d
|
||||
|
||||
%s`, len(badGatewayBody), badGatewayBody)))
|
||||
}
|
||||
return
|
||||
}
|
||||
defer localConn.Close()
|
||||
|
||||
m := c.metrics
|
||||
m.proxySetupTimer.Update(time.Since(start))
|
||||
m.connMeter.Mark(1)
|
||||
c.update()
|
||||
m.connTimer.Time(func() {
|
||||
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxyMsg.ClientAddr})
|
||||
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
|
||||
m.bytesIn.Update(bytesIn)
|
||||
m.bytesOut.Update(bytesOut)
|
||||
m.bytesInCount.Inc(bytesIn)
|
||||
m.bytesOutCount.Inc(bytesOut)
|
||||
})
|
||||
c.update()
|
||||
}
|
||||
|
||||
// Hearbeating to ensure our connection ngrokd is still live
|
||||
func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) {
|
||||
lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0)
|
||||
ping := time.NewTicker(pingInterval)
|
||||
pongCheck := time.NewTicker(time.Second)
|
||||
|
||||
defer func() {
|
||||
conn.Close()
|
||||
ping.Stop()
|
||||
pongCheck.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pongCheck.C:
|
||||
lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr))
|
||||
needPong := lastPong.Sub(lastPing) < 0
|
||||
pongLatency := time.Since(lastPing)
|
||||
|
||||
if needPong && pongLatency > maxPongLatency {
|
||||
c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong)
|
||||
c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds()))
|
||||
return
|
||||
}
|
||||
|
||||
case <-ping.C:
|
||||
err := msg.WriteMsg(conn, &msg.PingMsg{})
|
||||
if err != nil {
|
||||
conn.Debug("Got error %v when writing PingMsg", err)
|
||||
return
|
||||
}
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/ngrok/client/mvc/controller.go
Normal file
28
src/ngrok/client/mvc/controller.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"ngrok/util"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
// how the model communicates that it has changed state
|
||||
Update(State)
|
||||
|
||||
// instructs the controller to shut the app down
|
||||
Shutdown(message string)
|
||||
|
||||
// PlayRequest instructs the model to play requests
|
||||
PlayRequest(tunnel Tunnel, payload []byte)
|
||||
|
||||
// A channel of updates
|
||||
Updates() *util.Broadcast
|
||||
|
||||
// returns the current state
|
||||
State() State
|
||||
|
||||
// safe wrapper for running go-routines
|
||||
Go(fn func())
|
||||
|
||||
// the port where the web interface is running
|
||||
GetWebViewPort() int
|
||||
}
|
||||
13
src/ngrok/client/mvc/model.go
Normal file
13
src/ngrok/client/mvc/model.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"ngrok/msg"
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
Run(serverAddr, authToken string, ctl Controller, reg *msg.RegMsg, localaddr string)
|
||||
|
||||
Shutdown()
|
||||
|
||||
PlayRequest(tunnel Tunnel, payload []byte)
|
||||
}
|
||||
46
src/ngrok/client/mvc/state.go
Normal file
46
src/ngrok/client/mvc/state.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
)
|
||||
|
||||
type UpdateStatus int
|
||||
|
||||
const (
|
||||
UpdateNone = -1 * iota
|
||||
UpdateInstalling
|
||||
UpdateReady
|
||||
UpdateAvailable
|
||||
)
|
||||
|
||||
type ConnStatus int
|
||||
|
||||
const (
|
||||
ConnConnecting = iota
|
||||
ConnReconnecting
|
||||
ConnOnline
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
PublicUrl string
|
||||
Protocol proto.Protocol
|
||||
LocalAddr string
|
||||
}
|
||||
|
||||
type ConnectionContext struct {
|
||||
Tunnel Tunnel
|
||||
ClientAddr string
|
||||
}
|
||||
|
||||
type State interface {
|
||||
GetClientVersion() string
|
||||
GetServerVersion() string
|
||||
GetTunnels() []Tunnel
|
||||
GetProtocols() []proto.Protocol
|
||||
GetUpdateStatus() UpdateStatus
|
||||
GetConnStatus() ConnStatus
|
||||
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
|
||||
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
|
||||
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package ui
|
||||
package mvc
|
||||
|
||||
type View interface {
|
||||
Render()
|
||||
Shutdown()
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
"ngrok/version"
|
||||
)
|
||||
|
||||
// client state
|
||||
type State struct {
|
||||
id string
|
||||
publicUrl string
|
||||
serverVersion string
|
||||
newVersion string
|
||||
protocol proto.Protocol
|
||||
opts *Options
|
||||
metrics *ClientMetrics
|
||||
|
||||
// just for UI purposes
|
||||
status string
|
||||
}
|
||||
|
||||
// implement client.ui.State
|
||||
func (s State) GetClientVersion() string { return version.MajorMinor() }
|
||||
func (s State) GetServerVersion() string { return s.serverVersion }
|
||||
func (s State) GetPublicUrl() string { return s.publicUrl }
|
||||
func (s State) GetLocalAddr() string { return s.opts.localaddr }
|
||||
func (s State) GetWebPort() int { return s.opts.webport }
|
||||
func (s State) GetStatus() string { return s.status }
|
||||
func (s State) GetProtocol() proto.Protocol { return s.protocol }
|
||||
func (s State) GetNewVersion() string { return s.newVersion }
|
||||
|
||||
func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
|
||||
return s.metrics.connMeter, s.metrics.connTimer
|
||||
}
|
||||
|
||||
func (s State) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return s.metrics.bytesInCount, s.metrics.bytesIn
|
||||
}
|
||||
|
||||
func (s State) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
|
||||
return s.metrics.bytesOutCount, s.metrics.bytesOut
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package ui
|
||||
|
||||
type Command interface{}
|
||||
|
||||
type CmdQuit struct {
|
||||
// display this message after quit
|
||||
Message string
|
||||
}
|
||||
|
||||
type CmdRequest struct {
|
||||
// the bytes of the request to issue
|
||||
Payload []byte
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/* The controller in the MVC
|
||||
*/
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
// the model sends updates through this broadcast channel
|
||||
Updates *util.Broadcast
|
||||
|
||||
// all views put any commands into this channel
|
||||
Cmds chan Command
|
||||
|
||||
// all threads may add themself to this to wait for clean shutdown
|
||||
Wait *sync.WaitGroup
|
||||
|
||||
// channel to signal shutdown
|
||||
Shutdown chan int
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
ctl := &Controller{
|
||||
Updates: util.NewBroadcast(),
|
||||
Cmds: make(chan Command),
|
||||
Wait: new(sync.WaitGroup),
|
||||
Shutdown: make(chan int),
|
||||
}
|
||||
|
||||
return ctl
|
||||
}
|
||||
|
||||
func (ctl *Controller) Update(state State) {
|
||||
ctl.Updates.In() <- state
|
||||
}
|
||||
|
||||
func (ctl *Controller) DoShutdown() {
|
||||
close(ctl.Shutdown)
|
||||
}
|
||||
|
||||
func (ctl *Controller) IsShuttingDown() bool {
|
||||
select {
|
||||
case <-ctl.Shutdown:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
metrics "github.com/inconshreveable/go-metrics"
|
||||
"ngrok/proto"
|
||||
)
|
||||
|
||||
type State interface {
|
||||
GetClientVersion() string
|
||||
GetServerVersion() string
|
||||
GetNewVersion() string
|
||||
GetPublicUrl() string
|
||||
GetLocalAddr() string
|
||||
GetStatus() string
|
||||
GetProtocol() proto.Protocol
|
||||
GetWebPort() int
|
||||
GetConnectionMetrics() (metrics.Meter, metrics.Timer)
|
||||
GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
|
||||
GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
|
||||
}
|
||||
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/mvc"
|
||||
)
|
||||
|
||||
// no auto-updating in debug mode
|
||||
func autoUpdate(ctl mvc.Controller, token string) {
|
||||
}
|
||||
126
src/ngrok/client/update_release.go
Normal file
126
src/ngrok/client/update_release.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// +build release autoupdate
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
update "github.com/inconshreveable/go-update"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/version"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
updateEndpoint = "https://dl.ngrok.com/update"
|
||||
checkEndpoint = "https://dl.ngrok.com/update/check"
|
||||
)
|
||||
|
||||
func progressWatcher(s *State, ctl *ui.Controller, progress chan int, complete chan int) {
|
||||
for {
|
||||
select {
|
||||
case pct, ok := <-progress:
|
||||
if !ok {
|
||||
close(complete)
|
||||
return
|
||||
} else if pct == 100 {
|
||||
s.update = ui.UpdateInstalling
|
||||
ctl.Update(s)
|
||||
close(complete)
|
||||
return
|
||||
} else {
|
||||
if pct%25 == 0 {
|
||||
log.Info("Downloading update %d%% complete", pct)
|
||||
}
|
||||
s.update = ui.UpdateStatus(pct)
|
||||
ctl.Update(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpdate(s *State, ctl *ui.Controller, token string) {
|
||||
tryAgain := true
|
||||
|
||||
params := make(url.Values)
|
||||
params.Add("version", version.MajorMinor())
|
||||
params.Add("os", runtime.GOOS)
|
||||
params.Add("arch", runtime.GOARCH)
|
||||
params.Add("user", token)
|
||||
|
||||
updateUrl := updateEndpoint + "?" + params.Encode()
|
||||
checkUrl := checkEndpoint + "?" + params.Encode()
|
||||
|
||||
update := func() {
|
||||
log.Info("Checking for update")
|
||||
available, err := update.NewDownload(checkUrl).Check()
|
||||
if err != nil {
|
||||
log.Error("Error while checking for update: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !available {
|
||||
log.Info("No update available")
|
||||
return
|
||||
}
|
||||
|
||||
// stop trying after a single download attempt
|
||||
// XXX: improve this so the we can:
|
||||
// 1. safely update multiple times
|
||||
// 2. only retry after a network connection failure
|
||||
tryAgain = false
|
||||
|
||||
download := update.NewDownload(updateUrl)
|
||||
downloadComplete := make(chan int)
|
||||
|
||||
go progressWatcher(s, ctl, download.Progress, downloadComplete)
|
||||
|
||||
log.Info("Trying to update . . .")
|
||||
err, errRecover := download.GetAndUpdate()
|
||||
<-downloadComplete
|
||||
|
||||
if err != nil {
|
||||
// log error to console
|
||||
log.Error("Error while updating ngrok: %v", err)
|
||||
if errRecover != nil {
|
||||
log.Error("Error while recovering from failed ngrok update, your binary may be missing: %v", errRecover.Error())
|
||||
params.Add("errorRecover", errRecover.Error())
|
||||
}
|
||||
|
||||
// log error to ngrok.com's servers for debugging purposes
|
||||
params.Add("error", err.Error())
|
||||
resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params)
|
||||
if err != nil {
|
||||
log.Error("Error while reporting update error: %v, %v", err, reportErr)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// tell the user to update manually
|
||||
s.update = ui.UpdateAvailable
|
||||
} else {
|
||||
if !download.Available {
|
||||
// this is the way the server tells us to update manually
|
||||
log.Info("Server wants us to update manually")
|
||||
s.update = ui.UpdateAvailable
|
||||
} else {
|
||||
// tell the user the update is ready
|
||||
log.Info("Update ready!")
|
||||
s.update = ui.UpdateReady
|
||||
}
|
||||
}
|
||||
|
||||
ctl.Update(s)
|
||||
return
|
||||
}
|
||||
|
||||
// try to update immediately and then at a set interval
|
||||
update()
|
||||
for _ = range time.Tick(updateCheckInterval) {
|
||||
if !tryAgain {
|
||||
break
|
||||
}
|
||||
update()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package term
|
||||
|
||||
import (
|
||||
termbox "github.com/nsf/termbox-go"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -12,12 +13,13 @@ const (
|
||||
)
|
||||
|
||||
type HttpView struct {
|
||||
log.Logger
|
||||
*area
|
||||
|
||||
httpProto *proto.Http
|
||||
HttpRequests *util.Ring
|
||||
shutdown chan int
|
||||
flush chan int
|
||||
*area
|
||||
log.Logger
|
||||
termView *TermView
|
||||
}
|
||||
|
||||
func colorFor(status string) termbox.Attribute {
|
||||
@@ -33,19 +35,16 @@ func colorFor(status string) termbox.Attribute {
|
||||
return termbox.ColorWhite
|
||||
}
|
||||
|
||||
func NewHttp(proto *proto.Http, flush, shutdown chan int, x, y int) *HttpView {
|
||||
func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView {
|
||||
v := &HttpView{
|
||||
httpProto: proto,
|
||||
HttpRequests: util.NewRing(size),
|
||||
area: NewArea(x, y, 70, size+5),
|
||||
shutdown: shutdown,
|
||||
flush: flush,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
shutdown: make(chan int),
|
||||
termView: termView,
|
||||
Logger: log.NewPrefixLogger("view", "term", "http"),
|
||||
}
|
||||
v.AddLogPrefix("view")
|
||||
v.AddLogPrefix("term")
|
||||
v.AddLogPrefix("http")
|
||||
go v.Run()
|
||||
ctl.Go(v.Run)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -54,9 +53,6 @@ func (v *HttpView) Run() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-v.shutdown:
|
||||
return
|
||||
|
||||
case txn := <-updates:
|
||||
v.Debug("Got HTTP update")
|
||||
if txn.(*proto.HttpTxn).Resp == nil {
|
||||
@@ -78,5 +74,9 @@ func (v *HttpView) Render() {
|
||||
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
|
||||
}
|
||||
}
|
||||
v.flush <- 1
|
||||
v.termView.Flush()
|
||||
}
|
||||
|
||||
func (v *HttpView) Shutdown() {
|
||||
close(v.shutdown)
|
||||
}
|
||||
|
||||
@@ -1,76 +1,64 @@
|
||||
/*
|
||||
interactive terminal interface for local clients
|
||||
*/
|
||||
// interactive terminal interface for local clients
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
termbox "github.com/nsf/termbox-go"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TermView struct {
|
||||
ctl *ui.Controller
|
||||
ctl mvc.Controller
|
||||
updates chan interface{}
|
||||
flush chan int
|
||||
subviews []ui.View
|
||||
state ui.State
|
||||
shutdown chan int
|
||||
redraw *util.Broadcast
|
||||
subviews []mvc.View
|
||||
log.Logger
|
||||
*area
|
||||
}
|
||||
|
||||
func New(ctl *ui.Controller, state ui.State) *TermView {
|
||||
func NewTermView(ctl mvc.Controller) *TermView {
|
||||
// initialize terminal display
|
||||
termbox.Init()
|
||||
|
||||
// make sure ngrok doesn't quit until we've cleaned up
|
||||
ctl.Wait.Add(1)
|
||||
|
||||
w, _ := termbox.Size()
|
||||
|
||||
v := &TermView{
|
||||
ctl: ctl,
|
||||
updates: ctl.Updates.Reg(),
|
||||
updates: ctl.Updates().Reg(),
|
||||
redraw: util.NewBroadcast(),
|
||||
flush: make(chan int),
|
||||
subviews: make([]ui.View, 0),
|
||||
state: state,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
shutdown: make(chan int),
|
||||
Logger: log.NewPrefixLogger("view", "term"),
|
||||
area: NewArea(0, 0, w, 10),
|
||||
}
|
||||
|
||||
v.Logger.AddLogPrefix("view")
|
||||
v.Logger.AddLogPrefix("term")
|
||||
|
||||
switch p := state.GetProtocol().(type) {
|
||||
case *proto.Http:
|
||||
v.subviews = append(v.subviews, NewHttp(p, v.flush, ctl.Shutdown, 0, 10))
|
||||
default:
|
||||
}
|
||||
|
||||
v.Render()
|
||||
|
||||
go v.run()
|
||||
go v.input()
|
||||
ctl.Go(v.run)
|
||||
ctl.Go(v.input)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func colorForConn(status string) termbox.Attribute {
|
||||
func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) {
|
||||
switch status {
|
||||
case "connecting":
|
||||
return termbox.ColorCyan
|
||||
case "reconnecting":
|
||||
return termbox.ColorRed
|
||||
case "online":
|
||||
return termbox.ColorGreen
|
||||
case mvc.ConnConnecting:
|
||||
return "connecting", termbox.ColorCyan
|
||||
case mvc.ConnReconnecting:
|
||||
return "reconnecting", termbox.ColorRed
|
||||
case mvc.ConnOnline:
|
||||
return "online", termbox.ColorGreen
|
||||
}
|
||||
return termbox.ColorWhite
|
||||
return "unknown", termbox.ColorWhite
|
||||
}
|
||||
|
||||
func (v *TermView) Render() {
|
||||
func (v *TermView) draw() {
|
||||
state := v.ctl.State()
|
||||
|
||||
v.Clear()
|
||||
|
||||
// quit instructions
|
||||
@@ -78,54 +66,102 @@ func (v *TermView) Render() {
|
||||
v.Printf(v.w-len(quitMsg), 0, quitMsg)
|
||||
|
||||
// new version message
|
||||
newVersion := v.state.GetNewVersion()
|
||||
if newVersion != "" {
|
||||
newVersionMsg := fmt.Sprintf("new version available at http://ngrok.com")
|
||||
v.APrintf(termbox.ColorYellow, 30, 0, newVersionMsg)
|
||||
updateStatus := state.GetUpdateStatus()
|
||||
var updateMsg string
|
||||
switch updateStatus {
|
||||
case mvc.UpdateNone:
|
||||
updateMsg = ""
|
||||
case mvc.UpdateInstalling:
|
||||
updateMsg = "ngrok is updating"
|
||||
case mvc.UpdateReady:
|
||||
updateMsg = "ngrok has updated: restart ngrok for the new version"
|
||||
case mvc.UpdateAvailable:
|
||||
updateMsg = "new version available at https://ngrok.com"
|
||||
default:
|
||||
pct := float64(updateStatus) / 100.0
|
||||
const barLength = 25
|
||||
full := int(barLength * pct)
|
||||
bar := make([]byte, barLength+2)
|
||||
bar[0] = '['
|
||||
bar[barLength+1] = ']'
|
||||
for i := 0; i < 25; i++ {
|
||||
if i <= full {
|
||||
bar[i+1] = '#'
|
||||
} else {
|
||||
bar[i+1] = ' '
|
||||
}
|
||||
}
|
||||
updateMsg = "Downloading update: " + string(bar)
|
||||
}
|
||||
|
||||
if updateMsg != "" {
|
||||
v.APrintf(termbox.ColorYellow, 30, 0, updateMsg)
|
||||
}
|
||||
|
||||
v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
|
||||
statusStr, statusColor := connStatusRepr(state.GetConnStatus())
|
||||
v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr)
|
||||
|
||||
status := v.state.GetStatus()
|
||||
v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
|
||||
v.Printf(0, 3, "%-30s%s/%s", "Version", state.GetClientVersion(), state.GetServerVersion())
|
||||
var i int = 4
|
||||
for _, t := range state.GetTunnels() {
|
||||
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
|
||||
i++
|
||||
}
|
||||
webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
|
||||
if v.ctl.GetWebViewPort() == -1 {
|
||||
webAddr = "disabled"
|
||||
}
|
||||
v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
|
||||
|
||||
v.Printf(0, 3, "%-30s%s/%s", "Version", v.state.GetClientVersion(), v.state.GetServerVersion())
|
||||
v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
|
||||
v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
|
||||
webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
|
||||
v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
|
||||
|
||||
connMeter, connTimer := v.state.GetConnectionMetrics()
|
||||
v.Printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
|
||||
connMeter, connTimer := state.GetConnectionMetrics()
|
||||
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())
|
||||
|
||||
msec := float64(time.Millisecond)
|
||||
v.Printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
|
||||
v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
|
||||
|
||||
termbox.Flush()
|
||||
}
|
||||
|
||||
func (v *TermView) run() {
|
||||
defer v.ctl.Wait.Done()
|
||||
defer close(v.shutdown)
|
||||
defer termbox.Close()
|
||||
|
||||
redraw := v.redraw.Reg()
|
||||
defer v.redraw.UnReg(redraw)
|
||||
|
||||
v.draw()
|
||||
for {
|
||||
v.Debug("Waiting for update")
|
||||
select {
|
||||
case <-v.flush:
|
||||
termbox.Flush()
|
||||
|
||||
case obj := <-v.updates:
|
||||
if obj != nil {
|
||||
v.state = obj.(ui.State)
|
||||
}
|
||||
v.Render()
|
||||
case <-v.updates:
|
||||
v.draw()
|
||||
|
||||
case <-v.ctl.Shutdown:
|
||||
case <-redraw:
|
||||
v.draw()
|
||||
|
||||
case <-v.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TermView) Shutdown() {
|
||||
v.shutdown <- 1
|
||||
<-v.shutdown
|
||||
}
|
||||
|
||||
func (v *TermView) Flush() {
|
||||
v.flush <- 1
|
||||
}
|
||||
|
||||
func (v *TermView) NewHttpView(p *proto.Http) *HttpView {
|
||||
return newTermHttpView(v.ctl, v, p, 0, 12)
|
||||
}
|
||||
|
||||
func (v *TermView) input() {
|
||||
for {
|
||||
ev := termbox.PollEvent()
|
||||
@@ -134,21 +170,14 @@ func (v *TermView) input() {
|
||||
switch ev.Key {
|
||||
case termbox.KeyCtrlC:
|
||||
v.Info("Got quit command")
|
||||
v.ctl.Cmds <- ui.CmdQuit{}
|
||||
v.ctl.Shutdown("")
|
||||
}
|
||||
|
||||
case termbox.EventResize:
|
||||
v.Info("Resize event, redrawing")
|
||||
// send nil to update channel to force re-rendering
|
||||
v.updates <- nil
|
||||
for _, sv := range v.subviews {
|
||||
sv.Render()
|
||||
}
|
||||
v.redraw.In() <- 1
|
||||
|
||||
case termbox.EventError:
|
||||
if v.ctl.IsShuttingDown() {
|
||||
return
|
||||
}
|
||||
panic(ev.Err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
type SerializedTxn struct {
|
||||
Id string
|
||||
Duration int64
|
||||
Start int64
|
||||
ConnCtx mvc.ConnectionContext
|
||||
*proto.HttpTxn `json:"-"`
|
||||
Req SerializedRequest
|
||||
Resp SerializedResponse
|
||||
@@ -54,17 +56,18 @@ type SerializedResponse struct {
|
||||
}
|
||||
|
||||
type WebHttpView struct {
|
||||
log.Logger
|
||||
|
||||
webview *WebView
|
||||
ctl *ui.Controller
|
||||
ctl mvc.Controller
|
||||
httpProto *proto.Http
|
||||
updates chan interface{}
|
||||
state chan SerializedUiState
|
||||
HttpRequests *util.Ring
|
||||
idToTxn map[string]*SerializedTxn
|
||||
}
|
||||
|
||||
type SerializedUiState struct {
|
||||
Url string
|
||||
Tunnels []mvc.Tunnel
|
||||
}
|
||||
|
||||
type SerializedPayload struct {
|
||||
@@ -72,20 +75,18 @@ type SerializedPayload struct {
|
||||
UiState SerializedUiState
|
||||
}
|
||||
|
||||
func NewWebHttpView(wv *WebView, ctl *ui.Controller, proto *proto.Http) *WebHttpView {
|
||||
w := &WebHttpView{
|
||||
func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView {
|
||||
whv := &WebHttpView{
|
||||
Logger: log.NewPrefixLogger("view", "web", "http"),
|
||||
webview: wv,
|
||||
ctl: ctl,
|
||||
httpProto: proto,
|
||||
idToTxn: make(map[string]*SerializedTxn),
|
||||
updates: ctl.Updates.Reg(),
|
||||
state: make(chan SerializedUiState),
|
||||
HttpRequests: util.NewRing(20),
|
||||
}
|
||||
go w.updateHttp()
|
||||
go w.updateUiState()
|
||||
w.register()
|
||||
return w
|
||||
ctl.Go(whv.updateHttp)
|
||||
whv.register()
|
||||
return whv
|
||||
}
|
||||
|
||||
type XMLDoc struct {
|
||||
@@ -111,21 +112,22 @@ func makeBody(h http.Header, body []byte) SerializedBody {
|
||||
if b.RawContentType != "" {
|
||||
b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0])
|
||||
switch b.ContentType {
|
||||
case "application/xml":
|
||||
case "text/xml":
|
||||
case "application/xml", "text/xml":
|
||||
err = xml.Unmarshal(body, new(XMLDoc))
|
||||
if err != nil {
|
||||
syntaxError := err.(*xml.SyntaxError)
|
||||
// xml syntax errors only give us a line number, so we
|
||||
// count to find an offset
|
||||
b.ErrorOffset = offsetForLine(syntaxError.Line)
|
||||
if syntaxError, ok := err.(*xml.SyntaxError); ok {
|
||||
// xml syntax errors only give us a line number, so we
|
||||
// count to find an offset
|
||||
b.ErrorOffset = offsetForLine(syntaxError.Line)
|
||||
}
|
||||
}
|
||||
|
||||
case "application/json":
|
||||
err = json.Unmarshal(body, new(json.RawMessage))
|
||||
if err != nil {
|
||||
syntaxError := err.(*json.SyntaxError)
|
||||
b.ErrorOffset = int(syntaxError.Offset)
|
||||
if syntaxError, ok := err.(*json.SyntaxError); ok {
|
||||
b.ErrorOffset = int(syntaxError.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
@@ -151,16 +153,16 @@ func (whv *WebHttpView) updateHttp() {
|
||||
|
||||
// we haven't processed this transaction yet if we haven't set the
|
||||
// user data
|
||||
if htxn.UserData == nil {
|
||||
if htxn.UserCtx == nil {
|
||||
id, err := util.RandId(8)
|
||||
if err != nil {
|
||||
log.Error("Failed to generate txn identifier for web storage: %v", err)
|
||||
whv.Error("Failed to generate txn identifier for web storage: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
rawReq, err := httputil.DumpRequest(htxn.Req.Request, true)
|
||||
rawReq, err := httputil.DumpRequestOut(htxn.Req.Request, true)
|
||||
if err != nil {
|
||||
log.Error("Failed to dump request: %v", err)
|
||||
whv.Error("Failed to dump request: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -176,9 +178,11 @@ func (whv *WebHttpView) updateHttp() {
|
||||
Body: body,
|
||||
Binary: !utf8.Valid(rawReq),
|
||||
},
|
||||
Start: htxn.Start.Unix(),
|
||||
ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext),
|
||||
}
|
||||
|
||||
htxn.UserData = whtxn
|
||||
htxn.UserCtx = whtxn
|
||||
// XXX: unsafe map access from multiple go routines
|
||||
whv.idToTxn[whtxn.Id] = whtxn
|
||||
// XXX: use return value to delete from map so we don't leak memory
|
||||
@@ -186,11 +190,11 @@ func (whv *WebHttpView) updateHttp() {
|
||||
} else {
|
||||
rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
|
||||
if err != nil {
|
||||
log.Error("Failed to dump response: %v", err)
|
||||
whv.Error("Failed to dump response: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
txn := htxn.UserData.(*SerializedTxn)
|
||||
txn := htxn.UserCtx.(*SerializedTxn)
|
||||
body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes)
|
||||
txn.Duration = htxn.Duration.Nanoseconds()
|
||||
txn.Resp = SerializedResponse{
|
||||
@@ -203,43 +207,46 @@ func (whv *WebHttpView) updateHttp() {
|
||||
|
||||
payload, err := json.Marshal(txn)
|
||||
if err != nil {
|
||||
log.Error("Failed to serialized txn payload for websocket: %v", err)
|
||||
whv.Error("Failed to serialized txn payload for websocket: %v", err)
|
||||
}
|
||||
whv.webview.wsMessages.In() <- payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *WebHttpView) updateUiState() {
|
||||
var s SerializedUiState
|
||||
for {
|
||||
select {
|
||||
case obj := <-v.updates:
|
||||
uiState := obj.(ui.State)
|
||||
s.Url = uiState.GetPublicUrl()
|
||||
case v.state <- s:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebHttpView) register() {
|
||||
func (whv *WebHttpView) register() {
|
||||
http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
whv.Error("Replay failed: %v", err)
|
||||
http.Error(w, err, 500)
|
||||
}
|
||||
}()
|
||||
|
||||
r.ParseForm()
|
||||
txnid := r.Form.Get("txnid")
|
||||
if txn, ok := h.idToTxn[txnid]; ok {
|
||||
bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
|
||||
if txn, ok := whv.idToTxn[txnid]; ok {
|
||||
reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.ctl.Cmds <- ui.CmdRequest{Payload: bodyBytes}
|
||||
whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes)
|
||||
w.Write([]byte(http.StatusText(200)))
|
||||
} else {
|
||||
// XXX: 400
|
||||
http.NotFound(w, r)
|
||||
http.Error(w, http.StatusText(400), 400)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := util.MakePanicTrace(r)
|
||||
whv.Error("HTTP web view failed: %v", err)
|
||||
http.Error(w, err, 500)
|
||||
}
|
||||
}()
|
||||
|
||||
pageTmpl, err := assets.ReadAsset("assets/client/page.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -248,8 +255,8 @@ func (h *WebHttpView) register() {
|
||||
tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
|
||||
|
||||
payloadData := SerializedPayload{
|
||||
Txns: h.HttpRequests.Slice(),
|
||||
UiState: <-h.state,
|
||||
Txns: whv.HttpRequests.Slice(),
|
||||
UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(payloadData)
|
||||
@@ -263,3 +270,6 @@ func (h *WebHttpView) register() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (whv *WebHttpView) Shutdown() {
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/garyburd/go-websocket/websocket"
|
||||
"net/http"
|
||||
"ngrok/client/assets"
|
||||
"ngrok/client/ui"
|
||||
"ngrok/client/mvc"
|
||||
"ngrok/log"
|
||||
"ngrok/proto"
|
||||
"ngrok/util"
|
||||
@@ -14,34 +14,38 @@ import (
|
||||
)
|
||||
|
||||
type WebView struct {
|
||||
log.Logger
|
||||
|
||||
ctl mvc.Controller
|
||||
|
||||
// messages sent over this broadcast are sent too all websocket connections
|
||||
wsMessages *util.Broadcast
|
||||
}
|
||||
|
||||
func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
|
||||
v := &WebView{
|
||||
func NewWebView(ctl mvc.Controller, port int) *WebView {
|
||||
wv := &WebView{
|
||||
Logger: log.NewPrefixLogger("view", "web"),
|
||||
wsMessages: util.NewBroadcast(),
|
||||
ctl: ctl,
|
||||
}
|
||||
|
||||
switch p := state.GetProtocol().(type) {
|
||||
case *proto.Http:
|
||||
NewWebHttpView(v, ctl, p)
|
||||
}
|
||||
|
||||
// for now, always redirect to the http view
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/http/in", 302)
|
||||
})
|
||||
|
||||
// handle web socket connections
|
||||
http.HandleFunc("/_ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Upgrade(w, r.Header, nil, 1024, 1024)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed websocket upgrade", 400)
|
||||
log.Warn("Failed websocket upgrade: %v", err)
|
||||
wv.Warn("Failed websocket upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
msgs := v.wsMessages.Reg()
|
||||
defer v.wsMessages.UnReg(msgs)
|
||||
msgs := wv.wsMessages.Reg()
|
||||
defer wv.wsMessages.UnReg(msgs)
|
||||
for m := range msgs {
|
||||
err := conn.WriteMessage(websocket.OpText, m.([]byte))
|
||||
if err != nil {
|
||||
@@ -51,17 +55,25 @@ func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
|
||||
}
|
||||
})
|
||||
|
||||
// serve static assets
|
||||
http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
|
||||
buf, err := assets.ReadAsset(path.Join("assets", "client", r.URL.Path[1:]))
|
||||
if err != nil {
|
||||
log.Warn("Error serving static file: %s", err.Error())
|
||||
wv.Warn("Error serving static file: %s", err.Error())
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Write(buf)
|
||||
})
|
||||
|
||||
log.Info("Serving web interface on localhost:%d", port)
|
||||
go http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||
return v
|
||||
wv.Info("Serving web interface on localhost:%d", port)
|
||||
wv.ctl.Go(func() { http.ListenAndServe(fmt.Sprintf(":%d", port), nil) })
|
||||
return wv
|
||||
}
|
||||
|
||||
func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView {
|
||||
return newWebHttpView(wv.ctl, wv, proto)
|
||||
}
|
||||
|
||||
func (wv *WebView) Shutdown() {
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Conn interface {
|
||||
net.Conn
|
||||
log.Logger
|
||||
Id() string
|
||||
SetType(string)
|
||||
}
|
||||
|
||||
type tcpConn struct {
|
||||
@@ -103,6 +104,14 @@ func (c *tcpConn) Id() string {
|
||||
return fmt.Sprintf("%s:%x", c.typ, c.id)
|
||||
}
|
||||
|
||||
func (c *tcpConn) SetType(typ string) {
|
||||
oldId := c.Id()
|
||||
c.typ = typ
|
||||
c.ClearLogPrefixes()
|
||||
c.AddLogPrefix(c.Id())
|
||||
c.Info("Renamed connection %s", oldId)
|
||||
}
|
||||
|
||||
func Join(c Conn, c2 Conn) (int64, int64) {
|
||||
done := make(chan error)
|
||||
pipe := func(to Conn, from Conn, bytesCopied *int64) {
|
||||
|
||||
@@ -26,6 +26,7 @@ func LogTo(target string) {
|
||||
|
||||
type Logger interface {
|
||||
AddLogPrefix(string)
|
||||
ClearLogPrefixes()
|
||||
Debug(string, ...interface{})
|
||||
Info(string, ...interface{})
|
||||
Warn(string, ...interface{}) error
|
||||
@@ -37,8 +38,14 @@ type PrefixLogger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewPrefixLogger() Logger {
|
||||
return &PrefixLogger{Logger: &root}
|
||||
func NewPrefixLogger(prefixes ...string) Logger {
|
||||
logger := &PrefixLogger{Logger: &root}
|
||||
|
||||
for _, p := range prefixes {
|
||||
logger.AddLogPrefix(p)
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
func (pl *PrefixLogger) pfx(fmtstr string) interface{} {
|
||||
@@ -69,6 +76,10 @@ func (pl *PrefixLogger) AddLogPrefix(prefix string) {
|
||||
pl.prefix += "[" + prefix + "]"
|
||||
}
|
||||
|
||||
func (pl *PrefixLogger) ClearLogPrefixes() {
|
||||
pl.prefix = ""
|
||||
}
|
||||
|
||||
// we should never really use these . . . always prefer logging through a prefix logger
|
||||
func Debug(arg0 string, args ...interface{}) {
|
||||
root.Debug(arg0, args...)
|
||||
|
||||
@@ -15,6 +15,7 @@ func init() {
|
||||
TypeMap["RegAckMsg"] = t((*RegAckMsg)(nil))
|
||||
TypeMap["RegProxyMsg"] = t((*RegProxyMsg)(nil))
|
||||
TypeMap["ReqProxyMsg"] = t((*ReqProxyMsg)(nil))
|
||||
TypeMap["StartProxyMsg"] = t((*StartProxyMsg)(nil))
|
||||
TypeMap["PingMsg"] = t((*PingMsg)(nil))
|
||||
TypeMap["PongMsg"] = t((*PongMsg)(nil))
|
||||
TypeMap["VerisonMsg"] = t((*VersionMsg)(nil))
|
||||
@@ -46,17 +47,23 @@ type RegAckMsg struct {
|
||||
Version string
|
||||
MmVersion string
|
||||
Url string
|
||||
ProxyAddr string
|
||||
Protocol string
|
||||
Error string
|
||||
}
|
||||
|
||||
type RegProxyMsg struct {
|
||||
Url string
|
||||
ClientId string
|
||||
}
|
||||
|
||||
type ReqProxyMsg struct {
|
||||
}
|
||||
|
||||
type RegProxyMsg struct {
|
||||
ClientId string
|
||||
}
|
||||
|
||||
type StartProxyMsg struct {
|
||||
Url string
|
||||
ClientAddr string
|
||||
}
|
||||
|
||||
type PingMsg struct {
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"ngrok/conn"
|
||||
"ngrok/util"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,11 +24,12 @@ type HttpResponse struct {
|
||||
}
|
||||
|
||||
type HttpTxn struct {
|
||||
Req *HttpRequest
|
||||
Resp *HttpResponse
|
||||
Start time.Time
|
||||
Duration time.Duration
|
||||
UserData interface{}
|
||||
Req *HttpRequest
|
||||
Resp *HttpResponse
|
||||
Start time.Time
|
||||
Duration time.Duration
|
||||
UserCtx interface{}
|
||||
ConnUserCtx interface{}
|
||||
}
|
||||
|
||||
type Http struct {
|
||||
@@ -54,15 +56,15 @@ func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) {
|
||||
|
||||
func (h *Http) GetName() string { return "http" }
|
||||
|
||||
func (h *Http) WrapConn(c conn.Conn) conn.Conn {
|
||||
func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
|
||||
tee := conn.NewTee(c)
|
||||
lastTxn := make(chan *HttpTxn)
|
||||
go h.readRequests(tee, lastTxn)
|
||||
go h.readRequests(tee, lastTxn, ctx)
|
||||
go h.readResponses(tee, lastTxn)
|
||||
return tee
|
||||
}
|
||||
|
||||
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) {
|
||||
for {
|
||||
req, err := http.ReadRequest(tee.WriteBuffer())
|
||||
if err != nil {
|
||||
@@ -79,7 +81,11 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
tee.Warn("Failed to extract request body: %v", err)
|
||||
}
|
||||
|
||||
txn := &HttpTxn{Start: time.Now()}
|
||||
// golang's ReadRequest/DumpRequestOut is broken. Fix up the request so it works later
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = req.Host
|
||||
|
||||
txn := &HttpTxn{Start: time.Now(), ConnUserCtx: connCtx}
|
||||
txn.Req = &HttpRequest{Request: req}
|
||||
txn.Req.BodyBytes, txn.Req.Body, err = extractBody(req.Body)
|
||||
|
||||
@@ -111,5 +117,29 @@ func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
|
||||
}
|
||||
|
||||
h.Txns.In() <- txn
|
||||
|
||||
// XXX: remove web socket shim in favor of a real websocket protocol analyzer
|
||||
if txn.Req.Header.Get("Upgrade") == "websocket" {
|
||||
tee.Info("Upgrading to websocket")
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// shim for websockets
|
||||
// in order for websockets to work, we need to continue reading all of the
|
||||
// the bytes in the analyzer so that the joined connections will continue
|
||||
// sending bytes to each other
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
ioutil.ReadAll(tee.WriteBuffer())
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ioutil.ReadAll(tee.ReadBuffer())
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import (
|
||||
|
||||
type Protocol interface {
|
||||
GetName() string
|
||||
WrapConn(conn.Conn) conn.Conn
|
||||
WrapConn(conn.Conn, interface{}) conn.Conn
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ func NewTcp() *Tcp {
|
||||
|
||||
func (h *Tcp) GetName() string { return "tcp" }
|
||||
|
||||
func (h *Tcp) WrapConn(c conn.Conn) conn.Conn {
|
||||
func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
|
||||
return c
|
||||
}
|
||||
|
||||
34
src/ngrok/server/cli.go
Normal file
34
src/ngrok/server/cli.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
httpPort int
|
||||
httpsPort int
|
||||
tunnelPort int
|
||||
domain string
|
||||
logto string
|
||||
}
|
||||
|
||||
func parseArgs() *Options {
|
||||
httpPort := flag.Int("httpPort", 80, "Public HTTP port, -1 to disable")
|
||||
httpsPort := flag.Int("httpsPort", 443, "Public HTTPS port, -1 to disable")
|
||||
tunnelPort := flag.Int("tunnelPort", 4443, "Port to which ngrok clients connect")
|
||||
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"stdout",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
return &Options{
|
||||
httpPort: *httpPort,
|
||||
httpsPort: *httpsPort,
|
||||
tunnelPort: *tunnelPort,
|
||||
domain: *domain,
|
||||
logto: *logto,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"ngrok/conn"
|
||||
"ngrok/msg"
|
||||
"ngrok/util"
|
||||
"ngrok/version"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,29 +21,109 @@ type Control struct {
|
||||
// actual connection
|
||||
conn conn.Conn
|
||||
|
||||
// channels for communicating messages over the connection
|
||||
out chan (interface{})
|
||||
in chan (msg.Message)
|
||||
// put a message in this channel to send it over
|
||||
// conn to the client
|
||||
out chan (msg.Message)
|
||||
|
||||
// read from this channel to get the next message sent
|
||||
// to us over conn by the client
|
||||
in chan (msg.Message)
|
||||
|
||||
// put a message in this channel to send it over
|
||||
// conn to the client and then terminate this
|
||||
// control connection and all of its tunnels
|
||||
stop chan (msg.Message)
|
||||
|
||||
// heartbeat
|
||||
// the last time we received a ping from the client - for heartbeats
|
||||
lastPing time.Time
|
||||
|
||||
// tunnel
|
||||
tun *Tunnel
|
||||
// all of the tunnels this control connection handles
|
||||
tunnels []*Tunnel
|
||||
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// closing indicator
|
||||
closing int32
|
||||
|
||||
// identifier
|
||||
id string
|
||||
}
|
||||
|
||||
func NewControl(conn conn.Conn) {
|
||||
func NewControl(ctlConn conn.Conn, regMsg *msg.RegMsg) {
|
||||
// create the object
|
||||
// channels are buffered because we read and write to them
|
||||
// from the same goroutine in managerThread()
|
||||
c := &Control{
|
||||
conn: conn,
|
||||
out: make(chan (interface{}), 1),
|
||||
in: make(chan (msg.Message), 1),
|
||||
stop: make(chan (msg.Message), 1),
|
||||
conn: ctlConn,
|
||||
out: make(chan msg.Message, 5),
|
||||
in: make(chan msg.Message, 5),
|
||||
stop: make(chan msg.Message, 5),
|
||||
proxies: make(chan conn.Conn, 10),
|
||||
lastPing: time.Now(),
|
||||
}
|
||||
|
||||
// assign the random id
|
||||
serverId, err := util.RandId(8)
|
||||
if err != nil {
|
||||
c.stop <- &msg.RegAckMsg{Error: err.Error()}
|
||||
}
|
||||
c.id = fmt.Sprintf("%s-%s", regMsg.ClientId, serverId)
|
||||
|
||||
// register the control
|
||||
err = controlRegistry.Add(c.id, c)
|
||||
if err != nil {
|
||||
c.stop <- &msg.RegAckMsg{Error: err.Error()}
|
||||
}
|
||||
|
||||
// set logging prefix
|
||||
ctlConn.SetType("ctl")
|
||||
|
||||
// register the first tunnel
|
||||
c.in <- regMsg
|
||||
|
||||
// manage the connection
|
||||
go c.managerThread()
|
||||
go c.readThread()
|
||||
|
||||
}
|
||||
|
||||
// Register a new tunnel on this control connection
|
||||
func (c *Control) registerTunnel(regMsg *msg.RegMsg) {
|
||||
c.conn.Debug("Registering new tunnel")
|
||||
t, err := NewTunnel(regMsg, c)
|
||||
if err != nil {
|
||||
ack := &msg.RegAckMsg{Error: err.Error()}
|
||||
if len(c.tunnels) == 0 {
|
||||
// you can't fail your first tunnel registration
|
||||
// terminate the control connection
|
||||
c.stop <- ack
|
||||
} else {
|
||||
// inform client of failure
|
||||
c.out <- ack
|
||||
}
|
||||
|
||||
// we're done
|
||||
return
|
||||
}
|
||||
|
||||
// add it to the list of tunnels
|
||||
c.tunnels = append(c.tunnels, t)
|
||||
|
||||
// acknowledge success
|
||||
c.out <- &msg.RegAckMsg{
|
||||
Url: t.url,
|
||||
Protocol: regMsg.Protocol,
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
ClientId: c.id,
|
||||
}
|
||||
|
||||
if regMsg.Protocol == "http" {
|
||||
httpsRegMsg := *regMsg
|
||||
httpsRegMsg.Protocol = "https"
|
||||
c.in <- &httpsRegMsg
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) managerThread() {
|
||||
@@ -51,13 +134,33 @@ func (c *Control) managerThread() {
|
||||
if err := recover(); err != nil {
|
||||
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
|
||||
}
|
||||
|
||||
// remove from the control registry
|
||||
controlRegistry.Del(c.id)
|
||||
|
||||
// mark that we're shutting down
|
||||
atomic.StoreInt32(&c.closing, 1)
|
||||
|
||||
// stop the reaping timer
|
||||
reap.Stop()
|
||||
|
||||
// close the connection
|
||||
c.conn.Close()
|
||||
|
||||
// shutdown the tunnel if it's open
|
||||
if c.tun != nil {
|
||||
c.tun.shutdown()
|
||||
// shutdown all of the tunnels
|
||||
for _, t := range c.tunnels {
|
||||
t.Shutdown()
|
||||
}
|
||||
|
||||
// we're safe to close(c.proxies) because c.closing
|
||||
// protects us inside of RegisterProxy
|
||||
close(c.proxies)
|
||||
|
||||
// shut down all of the proxy connections
|
||||
for p := range c.proxies {
|
||||
p.Close()
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
for {
|
||||
@@ -65,23 +168,22 @@ func (c *Control) managerThread() {
|
||||
case m := <-c.out:
|
||||
msg.WriteMsg(c.conn, m)
|
||||
|
||||
case <-reap.C:
|
||||
if time.Since(c.lastPing) > pingTimeoutInterval {
|
||||
c.conn.Info("Lost heartbeat")
|
||||
return
|
||||
}
|
||||
|
||||
case m := <-c.stop:
|
||||
if m != nil {
|
||||
msg.WriteMsg(c.conn, m)
|
||||
}
|
||||
return
|
||||
|
||||
case <-reap.C:
|
||||
if time.Since(c.lastPing) > pingTimeoutInterval {
|
||||
c.conn.Info("Lost heartbeat")
|
||||
return
|
||||
}
|
||||
|
||||
case mRaw := <-c.in:
|
||||
switch m := mRaw.(type) {
|
||||
case *msg.RegMsg:
|
||||
c.conn.Info("Registering new tunnel")
|
||||
c.tun = newTunnel(m, c)
|
||||
c.registerTunnel(m)
|
||||
|
||||
case *msg.PingMsg:
|
||||
c.lastPing = time.Now()
|
||||
@@ -119,3 +221,60 @@ func (c *Control) readThread() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Control) RegisterProxy(conn conn.Conn) {
|
||||
if atomic.LoadInt32(&c.closing) == 1 {
|
||||
c.conn.Debug("Can't register proxies for a control that is closing")
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case c.proxies <- conn:
|
||||
c.conn.Info("Registered proxy connection %s", conn.Id())
|
||||
default:
|
||||
// c.proxies buffer is full, discard this one
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a proxy connection from the pool and return it
|
||||
// If not proxy connections are in the pool, request one
|
||||
// and wait until it is available
|
||||
// Returns an error if we couldn't get a proxy because it took too long
|
||||
// or the tunnel is closing
|
||||
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
|
||||
// initial timeout is zero to try to get a proxy connection without asking for one
|
||||
timeout := time.NewTimer(0)
|
||||
|
||||
// get a proxy connection. if we timeout, request one over the control channel
|
||||
for proxyConn == nil {
|
||||
var ok bool
|
||||
select {
|
||||
case proxyConn, ok = <-c.proxies:
|
||||
if !ok {
|
||||
err = fmt.Errorf("No proxy connections available, control is closing")
|
||||
return
|
||||
}
|
||||
continue
|
||||
case <-timeout.C:
|
||||
c.conn.Debug("Requesting new proxy connection")
|
||||
// request a proxy connection
|
||||
c.out <- &msg.ReqProxyMsg{}
|
||||
// timeout after 1 second if we don't get one
|
||||
timeout.Reset(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// To try to reduce latency hanndling tunnel connections, we employ
|
||||
// the following curde heuristic:
|
||||
// If the proxy connection pool is empty, request a new one.
|
||||
// The idea is to always have at least one proxy connection available for immediate use.
|
||||
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
|
||||
// It should be a good start though.
|
||||
if len(c.proxies) == 0 {
|
||||
c.out <- &msg.ReqProxyMsg{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,26 +30,27 @@ Bad Request
|
||||
`
|
||||
)
|
||||
|
||||
/**
|
||||
* Listens for new http connections from the public internet
|
||||
*/
|
||||
func httpListener(addr *net.TCPAddr) {
|
||||
// Listens for new http(s) connections from the public internet
|
||||
func httpListener(addr *net.TCPAddr, tlsCfg *tls.Config) {
|
||||
// bind/listen for incoming connections
|
||||
listener, err := conn.Listen(addr, "pub", nil)
|
||||
listener, err := conn.Listen(addr, "pub", tlsCfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Listening for public http connections on %v", listener.Port)
|
||||
proto := "http"
|
||||
if tlsCfg != nil {
|
||||
proto = "https"
|
||||
}
|
||||
|
||||
log.Info("Listening for public %s connections on %v", proto, listener.Port)
|
||||
for conn := range listener.Conns {
|
||||
go httpHandler(conn)
|
||||
go httpHandler(conn, proto)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new http connection from the public internet
|
||||
*/
|
||||
func httpHandler(tcpConn net.Conn) {
|
||||
// Handles a new http connection from the public internet
|
||||
func httpHandler(tcpConn net.Conn, proto string) {
|
||||
// wrap up the connection for logging
|
||||
conn := conn.NewHttp(tcpConn, "pub")
|
||||
|
||||
@@ -62,25 +65,28 @@ func httpHandler(tcpConn net.Conn) {
|
||||
// read out the http request
|
||||
req, err := conn.ReadRequest()
|
||||
if err != nil {
|
||||
conn.Warn("Failed to read valid http request: %v", err)
|
||||
conn.Warn("Failed to read valid %s request: %v", proto, err)
|
||||
conn.Write([]byte(BadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
// read out the Host header from the request
|
||||
host := strings.ToLower(req.Host)
|
||||
conn.Debug("Found hostname %s in request", host)
|
||||
|
||||
// multiplex to find the right backend host
|
||||
conn.Debug("Found hostname %s in request", req.Host)
|
||||
tunnel := tunnels.Get("http://" + req.Host)
|
||||
tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host))
|
||||
if tunnel == nil {
|
||||
conn.Info("No tunnel found for hostname %s", req.Host)
|
||||
conn.Write([]byte(fmt.Sprintf(NotFound, len(req.Host)+18, req.Host)))
|
||||
conn.Info("No tunnel found for hostname %s", host)
|
||||
conn.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host)))
|
||||
return
|
||||
}
|
||||
|
||||
// satisfy auth, if necessary
|
||||
conn.Debug("From client: %s", req.Header.Get("Authorization"))
|
||||
conn.Debug("To match: %s", tunnel.regMsg.HttpAuth)
|
||||
if req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
|
||||
conn.Info("Authentication failed")
|
||||
// If the client specified http auth and it doesn't match this request's auth
|
||||
// then fail the request with 401 Not Authorized and request the client reissue the
|
||||
// request with basic authdeny the request
|
||||
if tunnel.regMsg.HttpAuth != "" && req.Header.Get("Authorization") != tunnel.regMsg.HttpAuth {
|
||||
conn.Info("Authentication failed: %s", req.Header.Get("Authorization"))
|
||||
conn.Write([]byte(NotAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,124 +1,109 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
log "ngrok/log"
|
||||
"ngrok/msg"
|
||||
"regexp"
|
||||
"ngrok/util"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
publicPort int
|
||||
proxyPort int
|
||||
tunnelPort int
|
||||
domain string
|
||||
logto string
|
||||
}
|
||||
|
||||
/* GLOBALS */
|
||||
// GLOBALS
|
||||
var (
|
||||
hostRegex *regexp.Regexp
|
||||
proxyAddr string
|
||||
tunnels *TunnelManager
|
||||
opts *Options
|
||||
tunnelRegistry *TunnelRegistry
|
||||
controlRegistry *ControlRegistry
|
||||
registryCacheSize uint64 = 1024 * 1024 // 1 MB
|
||||
domain string
|
||||
publicPort int
|
||||
)
|
||||
|
||||
func init() {
|
||||
hostRegex = regexp.MustCompile("[H|h]ost: ([^\\(\\);:,<>]+)\n")
|
||||
}
|
||||
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxyMsg) {
|
||||
// fail gracefully if the proxy connection fails to register
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
pxyConn.Warn("Failed with error: %v", r)
|
||||
pxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
func parseArgs() *Options {
|
||||
publicPort := flag.Int("publicport", 80, "Public port")
|
||||
tunnelPort := flag.Int("tunnelport", 4443, "Tunnel port")
|
||||
proxyPort := flag.Int("proxyPort", 0, "Proxy port")
|
||||
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
|
||||
logto := flag.String(
|
||||
"log",
|
||||
"stdout",
|
||||
"Write log messages to this file. 'stdout' and 'none' have special meanings")
|
||||
// set logging prefix
|
||||
pxyConn.SetType("pxy")
|
||||
|
||||
flag.Parse()
|
||||
// look up the control connection for this proxy
|
||||
pxyConn.Info("Registering new proxy for %s", regPxy.ClientId)
|
||||
ctl := controlRegistry.Get(regPxy.ClientId)
|
||||
|
||||
return &Options{
|
||||
publicPort: *publicPort,
|
||||
tunnelPort: *tunnelPort,
|
||||
proxyPort: *proxyPort,
|
||||
domain: *domain,
|
||||
logto: *logto,
|
||||
if ctl == nil {
|
||||
panic("No client found for identifier: " + regPxy.ClientId)
|
||||
}
|
||||
|
||||
ctl.RegisterProxy(pxyConn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new control connections from tunnel clients
|
||||
*/
|
||||
func controlListener(addr *net.TCPAddr, domain string) {
|
||||
// Listen for incoming control and proxy connections
|
||||
// We listen for incoming control and proxy connections on the same port
|
||||
// for ease of deployment. The hope is that by running on port 443, using
|
||||
// TLS and running all connections over the same port, we can bust through
|
||||
// restrictive firewalls.
|
||||
func tunnelListener(addr *net.TCPAddr, domain string) {
|
||||
// listen for incoming connections
|
||||
listener, err := conn.Listen(addr, "ctl", tlsConfig)
|
||||
listener, err := conn.Listen(addr, "tun", tlsConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Listening for control connections on %d", listener.Port)
|
||||
log.Info("Listening for control and proxy connections on %d", listener.Port)
|
||||
for c := range listener.Conns {
|
||||
NewControl(c)
|
||||
}
|
||||
}
|
||||
var rawMsg msg.Message
|
||||
if rawMsg, err = msg.ReadMsg(c); err != nil {
|
||||
c.Error("Failed to read message: %v", err)
|
||||
c.Close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new proxy connections from tunnel clients
|
||||
*/
|
||||
func proxyListener(addr *net.TCPAddr, domain string) {
|
||||
listener, err := conn.Listen(addr, "pxy", tlsConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch m := rawMsg.(type) {
|
||||
case *msg.RegMsg:
|
||||
go NewControl(c, m)
|
||||
|
||||
// set global proxy addr variable
|
||||
proxyAddr = fmt.Sprintf("%s:%d", domain, listener.Port)
|
||||
log.Info("Listening for proxy connection on %d", listener.Port)
|
||||
for proxyConn := range listener.Conns {
|
||||
go func(conn conn.Conn) {
|
||||
// fail gracefully if the proxy connection dies
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
conn.Warn("Failed with error: %v", r)
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// read the proxy register message
|
||||
var regPxy msg.RegProxyMsg
|
||||
if err = msg.ReadMsgInto(conn, ®Pxy); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// look up the tunnel for this proxy
|
||||
conn.Info("Registering new proxy for %s", regPxy.Url)
|
||||
tunnel := tunnels.Get(regPxy.Url)
|
||||
if tunnel == nil {
|
||||
panic("No tunnel found for: " + regPxy.Url)
|
||||
}
|
||||
|
||||
// register the proxy connection with the tunnel
|
||||
tunnel.RegisterProxy(conn)
|
||||
}(proxyConn)
|
||||
case *msg.RegProxyMsg:
|
||||
go NewProxy(c, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Main() {
|
||||
// parse options
|
||||
opts := parseArgs()
|
||||
opts = parseArgs()
|
||||
|
||||
// init logging
|
||||
log.LogTo(opts.logto)
|
||||
|
||||
tunnels = NewTunnelManager(opts.domain)
|
||||
// seed random number generator
|
||||
seed, err := util.RandomSeed()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rand.Seed(seed)
|
||||
|
||||
go proxyListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.proxyPort}, opts.domain)
|
||||
go controlListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
|
||||
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.publicPort})
|
||||
// init tunnel/control registry
|
||||
registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE")
|
||||
tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile)
|
||||
controlRegistry = NewControlRegistry()
|
||||
|
||||
// ngrok clients
|
||||
go tunnelListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.tunnelPort}, opts.domain)
|
||||
|
||||
// listen for http
|
||||
if opts.httpPort != -1 {
|
||||
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpPort}, nil)
|
||||
}
|
||||
|
||||
// listen for https
|
||||
if opts.httpsPort != -1 {
|
||||
go httpListener(&net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: opts.httpsPort}, tlsConfig)
|
||||
}
|
||||
|
||||
// wait forever
|
||||
done := make(chan int)
|
||||
|
||||
@@ -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),
|
||||
|
||||
205
src/ngrok/server/registry.go
Normal file
205
src/ngrok/server/registry.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"ngrok/cache"
|
||||
"ngrok/log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheSaveInterval time.Duration = 10 * time.Minute
|
||||
)
|
||||
|
||||
type cacheUrl string
|
||||
|
||||
func (url cacheUrl) Size() int {
|
||||
return len(url)
|
||||
}
|
||||
|
||||
// TunnelRegistry maps a tunnel URL to Tunnel structures
|
||||
type TunnelRegistry struct {
|
||||
tunnels map[string]*Tunnel
|
||||
affinity *cache.LRUCache
|
||||
log.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry {
|
||||
registry := &TunnelRegistry{
|
||||
tunnels: make(map[string]*Tunnel),
|
||||
affinity: cache.NewLRUCache(cacheSize),
|
||||
Logger: log.NewPrefixLogger("registry", "tun"),
|
||||
}
|
||||
|
||||
// LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail
|
||||
// to encode or decode any non-primitive types that haven't been "registered"
|
||||
// with it. Since we store cacheUrl objects, we need to register them here first
|
||||
// for the encoding/decoding to work
|
||||
var urlobj cacheUrl
|
||||
gob.Register(urlobj)
|
||||
|
||||
// try to load and then periodically save the affinity cache to file, if specified
|
||||
if cacheFile != "" {
|
||||
err := registry.affinity.LoadItemsFromFile(cacheFile)
|
||||
if err != nil {
|
||||
registry.Error("Failed to load affinity cache %s: %v", cacheFile, err)
|
||||
}
|
||||
|
||||
registry.SaveCacheThread(cacheFile, cacheSaveInterval)
|
||||
} else {
|
||||
registry.Info("No affinity cache specified")
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// Spawns a goroutine the periodically saves the cache to a file.
|
||||
func (r *TunnelRegistry) SaveCacheThread(path string, interval time.Duration) {
|
||||
go func() {
|
||||
r.Info("Saving affinity cache to %s every %s", path, interval.String())
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
|
||||
r.Debug("Saving affinity cache")
|
||||
err := r.affinity.SaveItemsToFile(path)
|
||||
if err != nil {
|
||||
r.Error("Failed to save affinity cache: %v", err)
|
||||
} else {
|
||||
r.Info("Saved affinity cache")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Register a tunnel with a specific url, returns an error
|
||||
// if a tunnel is already registered at that url
|
||||
func (r *TunnelRegistry) Register(url string, t *Tunnel) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.tunnels[url] != nil {
|
||||
return fmt.Errorf("The tunnel %s is already registered.", url)
|
||||
}
|
||||
|
||||
r.tunnels[url] = t
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) {
|
||||
clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String()
|
||||
clientId := t.regMsg.ClientId
|
||||
|
||||
ipKey := fmt.Sprintf("client-ip-%s:%s", t.regMsg.Protocol, clientIp)
|
||||
idKey := fmt.Sprintf("client-id-%s:%s", t.regMsg.Protocol, clientId)
|
||||
return ipKey, idKey
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) GetCachedRegistration(t *Tunnel) (url string) {
|
||||
ipCacheKey, idCacheKey := r.cacheKeys(t)
|
||||
|
||||
// check cache for ID first, because we prefer that over IP which might
|
||||
// not be specific to a user because of NATs
|
||||
if v, ok := r.affinity.Get(idCacheKey); ok {
|
||||
url = string(v.(cacheUrl))
|
||||
t.Debug("Found registry affinity %s for %s", url, idCacheKey)
|
||||
} else if v, ok := r.affinity.Get(ipCacheKey); ok {
|
||||
url = string(v.(cacheUrl))
|
||||
t.Debug("Found registry affinity %s for %s", url, ipCacheKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) RegisterAndCache(url string, t *Tunnel) (err error) {
|
||||
if err = r.Register(url, t); err == nil {
|
||||
// we successfully assigned a url, cache it
|
||||
ipCacheKey, idCacheKey := r.cacheKeys(t)
|
||||
r.affinity.Set(ipCacheKey, cacheUrl(url))
|
||||
r.affinity.Set(idCacheKey, cacheUrl(url))
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Register a tunnel with the following process:
|
||||
// Consult the affinity cache to try to assign a previously used tunnel url if possible
|
||||
// Generate new urls repeatedly with the urlFn and register until one is available.
|
||||
func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) (string, error) {
|
||||
url := r.GetCachedRegistration(t)
|
||||
if url == "" {
|
||||
url = urlFn()
|
||||
}
|
||||
|
||||
maxAttempts := 5
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
if err := r.RegisterAndCache(url, t); err != nil {
|
||||
// pick a new url and try again
|
||||
url = urlFn()
|
||||
} else {
|
||||
// we successfully assigned a url, we're done
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts)
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) Del(url string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
delete(r.tunnels, url)
|
||||
}
|
||||
|
||||
func (r *TunnelRegistry) Get(url string) *Tunnel {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.tunnels[url]
|
||||
}
|
||||
|
||||
// ControlRegistry maps a client ID to Control structures
|
||||
type ControlRegistry struct {
|
||||
controls map[string]*Control
|
||||
log.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewControlRegistry() *ControlRegistry {
|
||||
return &ControlRegistry{
|
||||
controls: make(map[string]*Control),
|
||||
Logger: log.NewPrefixLogger("registry", "ctl"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Get(clientId string) *Control {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.controls[clientId]
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Add(clientId string, ctl *Control) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if r.controls[clientId] == nil {
|
||||
r.Info("Registered control with id %s", clientId)
|
||||
r.controls[clientId] = ctl
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("Client with id %s already registered!", clientId)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ControlRegistry) Del(clientId string) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if r.controls[clientId] == nil {
|
||||
return fmt.Errorf("No control found for client id: %s", clientId)
|
||||
} else {
|
||||
r.Info("Removed control registry id %s", clientId)
|
||||
delete(r.controls, clientId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,25 @@ package server
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"ngrok/conn"
|
||||
"ngrok/log"
|
||||
"ngrok/msg"
|
||||
"ngrok/version"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultPortMap = map[string]int{
|
||||
"http": 80,
|
||||
"https": 443,
|
||||
"smtp": 25,
|
||||
}
|
||||
|
||||
/**
|
||||
* Tunnel: A control connection, metadata and proxy connections which
|
||||
* route public traffic to a firewalled endpoint.
|
||||
@@ -31,9 +41,6 @@ type Tunnel struct {
|
||||
// control connection
|
||||
ctl *Control
|
||||
|
||||
// proxy connections
|
||||
proxies chan conn.Conn
|
||||
|
||||
// logger
|
||||
log.Logger
|
||||
|
||||
@@ -41,38 +48,121 @@ type Tunnel struct {
|
||||
closing int32
|
||||
}
|
||||
|
||||
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
|
||||
// Common functionality for registering virtually hosted protocols
|
||||
func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) {
|
||||
vhost := os.Getenv("VHOST")
|
||||
if vhost == "" {
|
||||
vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort)
|
||||
}
|
||||
|
||||
// Canonicalize virtual host by removing default port (e.g. :80 on HTTP)
|
||||
defaultPort, ok := defaultPortMap[protocol]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find default port for protocol %s", protocol)
|
||||
}
|
||||
|
||||
defaultPortSuffix := fmt.Sprintf(":%d", defaultPort)
|
||||
if strings.HasSuffix(vhost, defaultPortSuffix) {
|
||||
vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)]
|
||||
}
|
||||
|
||||
// Canonicalize by always using lower-case
|
||||
vhost = strings.ToLower(vhost)
|
||||
t.url = strings.ToLower(t.url)
|
||||
|
||||
// Register for specific hostname
|
||||
hostname := strings.TrimSpace(t.regMsg.Hostname)
|
||||
if hostname != "" {
|
||||
t.url = fmt.Sprintf("%s://%s", protocol, hostname)
|
||||
return tunnelRegistry.Register(t.url, t)
|
||||
}
|
||||
|
||||
// Register for specific subdomain
|
||||
subdomain := strings.TrimSpace(t.regMsg.Subdomain)
|
||||
if subdomain != "" {
|
||||
t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost)
|
||||
return tunnelRegistry.Register(t.url, t)
|
||||
}
|
||||
|
||||
// Register for random URL
|
||||
t.url, err = tunnelRegistry.RegisterRepeat(func() string {
|
||||
return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost)
|
||||
}, t)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new tunnel from a registration message received
|
||||
// on a control channel
|
||||
func NewTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel, err error) {
|
||||
t = &Tunnel{
|
||||
regMsg: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
proxies: make(chan conn.Conn),
|
||||
Logger: log.NewPrefixLogger(),
|
||||
regMsg: m,
|
||||
start: time.Now(),
|
||||
ctl: ctl,
|
||||
Logger: log.NewPrefixLogger(),
|
||||
}
|
||||
|
||||
switch t.regMsg.Protocol {
|
||||
case "tcp":
|
||||
var err error
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
|
||||
var port int = 0
|
||||
|
||||
// try to return to you the same port you had before
|
||||
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
|
||||
if cachedUrl != "" {
|
||||
parts := strings.Split(cachedUrl, ":")
|
||||
portPart := parts[len(parts)-1]
|
||||
port, err = strconv.Atoi(portPart)
|
||||
if err != nil {
|
||||
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
|
||||
// continue with zero
|
||||
port = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Bind for TCP connections
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port})
|
||||
|
||||
// If we failed with a custom port, try with a random one
|
||||
if err != nil && port != 0 {
|
||||
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
|
||||
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
|
||||
}
|
||||
|
||||
// we tried to bind with a random port and failed (no more ports available?)
|
||||
if err != nil {
|
||||
t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err)
|
||||
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"}
|
||||
// create the url
|
||||
addr := t.listener.Addr().(*net.TCPAddr)
|
||||
t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port)
|
||||
|
||||
// register it
|
||||
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
|
||||
// This should never be possible because the OS will
|
||||
// only assign available ports to us.
|
||||
t.listener.Close()
|
||||
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
|
||||
return
|
||||
}
|
||||
|
||||
go t.listenTcp(t.listener)
|
||||
|
||||
default:
|
||||
}
|
||||
case "http":
|
||||
if err = registerVhost(t, "http", opts.httpPort); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := tunnels.Add(t); err != nil {
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprint(err)}
|
||||
return
|
||||
case "https":
|
||||
if err = registerVhost(t, "https", opts.httpsPort); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if m.Version != version.Proto {
|
||||
t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprintf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)}
|
||||
err = fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)
|
||||
return
|
||||
}
|
||||
|
||||
// pre-encode the http basic auth for fast comparisons later
|
||||
@@ -80,35 +170,31 @@ func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) {
|
||||
m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth))
|
||||
}
|
||||
|
||||
t.ctl.conn.AddLogPrefix(t.Id())
|
||||
t.AddLogPrefix(t.Id())
|
||||
t.Info("Registered new tunnel")
|
||||
t.ctl.out <- &msg.RegAckMsg{
|
||||
Url: t.url,
|
||||
ProxyAddr: fmt.Sprintf("%s", proxyAddr),
|
||||
Version: version.Proto,
|
||||
MmVersion: version.MajorMinor(),
|
||||
}
|
||||
t.Info("Registered new tunnel on: %s", t.ctl.conn.Id())
|
||||
|
||||
metrics.OpenTunnel(t)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Tunnel) shutdown() {
|
||||
func (t *Tunnel) Shutdown() {
|
||||
t.Info("Shutting down")
|
||||
|
||||
// mark that we're shutting down
|
||||
atomic.StoreInt32(&t.closing, 1)
|
||||
|
||||
// if we have a public listener (this is a raw TCP tunnel, shut it down
|
||||
// if we have a public listener (this is a raw TCP tunnel), shut it down
|
||||
if t.listener != nil {
|
||||
t.listener.Close()
|
||||
}
|
||||
|
||||
// remove ourselves from the tunnel registry
|
||||
tunnels.Del(t.url)
|
||||
tunnelRegistry.Del(t.url)
|
||||
|
||||
// XXX: shut down all of the proxy connections?
|
||||
// let the control connection know we're shutting down
|
||||
// currently, only the control connection shuts down tunnels,
|
||||
// so it doesn't need to know about it
|
||||
// t.ctl.stoptunnel <- t
|
||||
|
||||
metrics.CloseTunnel(t)
|
||||
}
|
||||
@@ -117,9 +203,7 @@ func (t *Tunnel) Id() string {
|
||||
return t.url
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for new public tcp connections from the internet.
|
||||
*/
|
||||
// Listens for new public tcp connections from the internet.
|
||||
func (t *Tunnel) listenTcp(listener *net.TCPListener) {
|
||||
for {
|
||||
defer func() {
|
||||
@@ -160,20 +244,39 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
|
||||
startTime := time.Now()
|
||||
metrics.OpenConnection(t, publicConn)
|
||||
|
||||
t.Debug("Requesting new proxy connection")
|
||||
t.ctl.out <- &msg.ReqProxyMsg{}
|
||||
var proxyConn conn.Conn
|
||||
var attempts int
|
||||
var err error
|
||||
for {
|
||||
// get a proxy connection
|
||||
if proxyConn, err = t.ctl.GetProxy(); err != nil {
|
||||
t.Warn("Failed to get proxy connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer proxyConn.Close()
|
||||
t.Info("Got proxy connection %s", proxyConn.Id())
|
||||
proxyConn.AddLogPrefix(t.Id())
|
||||
|
||||
proxyConn := <-t.proxies
|
||||
t.Info("Returning proxy connection %s", proxyConn.Id())
|
||||
// tell the client we're going to start using this proxy connection
|
||||
startPxyMsg := &msg.StartProxyMsg{
|
||||
Url: t.url,
|
||||
ClientAddr: publicConn.RemoteAddr().String(),
|
||||
}
|
||||
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
|
||||
attempts += 1
|
||||
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
|
||||
if attempts > 3 {
|
||||
// give up
|
||||
publicConn.Error("Too many failures starting proxy connection")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// success
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
defer proxyConn.Close()
|
||||
// join the public and proxy connections
|
||||
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
|
||||
|
||||
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)
|
||||
}
|
||||
|
||||
func (t *Tunnel) RegisterProxy(conn conn.Conn) {
|
||||
t.Info("Registered proxy connection %s", conn.Id())
|
||||
conn.AddLogPrefix(t.Id())
|
||||
t.proxies <- conn
|
||||
}
|
||||
|
||||
@@ -5,6 +5,26 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func RandomSeed() (int64, error) {
|
||||
b := make([]byte, 8)
|
||||
n, err := rand.Read(b)
|
||||
if n != 8 {
|
||||
return 0, fmt.Errorf("Only generated %d random bytes, %d requested", n, 8)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var seed int64
|
||||
var i uint
|
||||
for i = 0; i < 8; i++ {
|
||||
seed = seed | int64(b[i]<<(i*8))
|
||||
}
|
||||
|
||||
return seed, nil
|
||||
}
|
||||
|
||||
// create a random identifier for this client
|
||||
func RandId(idlen int) (id string, err error) {
|
||||
b := make([]byte, idlen)
|
||||
|
||||
12
src/ngrok/util/trace.go
Normal file
12
src/ngrok/util/trace.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func MakePanicTrace(err interface{}) string {
|
||||
stackBuf := make([]byte, 4096)
|
||||
n := runtime.Stack(stackBuf, false)
|
||||
return fmt.Sprintf("panic: %v\n\n%s", err, stackBuf[:n])
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Proto = "1"
|
||||
Major = "0"
|
||||
Minor = "12"
|
||||
Proto = "2"
|
||||
Major = "1"
|
||||
Minor = "0"
|
||||
)
|
||||
|
||||
func MajorMinor() string {
|
||||
|
||||
Reference in New Issue
Block a user