├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── Makefile ├── README.md ├── assets ├── client │ ├── page.html │ ├── static │ │ ├── css │ │ │ ├── bootstrap.min.css │ │ │ └── highlight.min.css │ │ ├── img │ │ │ └── glyphicons-halflings.png │ │ └── js │ │ │ ├── angular-sanitize.min.js │ │ │ ├── angular.js │ │ │ ├── base64.js │ │ │ ├── highlight.min.js │ │ │ ├── jquery-1.9.1.min.js │ │ │ ├── jquery.timeago.js │ │ │ ├── ngrok.js │ │ │ └── vkbeautify.js │ └── tls │ │ ├── ngrokroot.crt │ │ └── snakeoilca.crt └── server │ └── tls │ ├── snakeoil.crt │ └── snakeoil.key ├── contrib └── com.ngrok.client.plist ├── docs ├── CHANGELOG.md ├── DEVELOPMENT.md └── SELFHOSTING.md └── src └── ngrok ├── cache └── lru.go ├── client ├── cli.go ├── config.go ├── controller.go ├── debug.go ├── main.go ├── metrics.go ├── model.go ├── mvc │ ├── controller.go │ ├── model.go │ ├── state.go │ └── view.go ├── release.go ├── tls.go ├── update_debug.go ├── update_release.go └── views │ ├── term │ ├── area.go │ ├── http.go │ └── view.go │ └── web │ ├── http.go │ └── view.go ├── conn ├── conn.go └── tee.go ├── log └── logger.go ├── main ├── ngrok │ └── ngrok.go └── ngrokd │ └── ngrokd.go ├── msg ├── conn.go ├── msg.go └── pack.go ├── proto ├── http.go ├── interface.go └── tcp.go ├── server ├── cli.go ├── control.go ├── http.go ├── main.go ├── metrics.go ├── registry.go ├── tls.go └── tunnel.go ├── util ├── broadcast.go ├── errors.go ├── id.go ├── ring.go └── shutdown.go └── version └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | bin/ 3 | pkg/ 4 | src/code.google.com 5 | src/github.com 6 | src/bitbucket.org 7 | src/launchpad.net 8 | src/gopkg.in 9 | src/ngrok/client/assets/ 10 | src/ngrok/server/assets/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | script: make release-all 4 | install: true 5 | go: 6 | - 1.3 7 | - 1.4 8 | - 1.5 9 | - tip 10 | 11 | matrix: 12 | allow_failures: 13 | - go: tip 14 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Contributors to ngrok, both large and small: 2 | 3 | - Alan Shreve 4 | - Brandon Philips 5 | - Caleb Spare 6 | - Jay Hayes 7 | - Kevin Burke 8 | - Kyle Conroy 9 | - Nick Presta 10 | - Stephen Huenneke 11 | - inconshreveable 12 | - jzs 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Alan Shreve 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default server client deps fmt clean all release-all assets client-assets server-assets contributors 2 | export GOPATH:=$(shell pwd) 3 | 4 | BUILDTAGS=debug 5 | default: all 6 | 7 | deps: assets 8 | go get -tags '$(BUILDTAGS)' -d -v ngrok/... 9 | 10 | server: deps 11 | go install -tags '$(BUILDTAGS)' ngrok/main/ngrokd 12 | 13 | fmt: 14 | go fmt ngrok/... 15 | 16 | client: deps 17 | go install -tags '$(BUILDTAGS)' ngrok/main/ngrok 18 | 19 | assets: client-assets server-assets 20 | 21 | bin/go-bindata: 22 | GOOS="" GOARCH="" go get github.com/jteeuwen/go-bindata/go-bindata 23 | 24 | client-assets: bin/go-bindata 25 | bin/go-bindata -nomemcopy -pkg=assets -tags=$(BUILDTAGS) \ 26 | -debug=$(if $(findstring debug,$(BUILDTAGS)),true,false) \ 27 | -o=src/ngrok/client/assets/assets_$(BUILDTAGS).go \ 28 | assets/client/... 29 | 30 | server-assets: bin/go-bindata 31 | bin/go-bindata -nomemcopy -pkg=assets -tags=$(BUILDTAGS) \ 32 | -debug=$(if $(findstring debug,$(BUILDTAGS)),true,false) \ 33 | -o=src/ngrok/server/assets/assets_$(BUILDTAGS).go \ 34 | assets/server/... 35 | 36 | release-client: BUILDTAGS=release 37 | release-client: client 38 | 39 | release-server: BUILDTAGS=release 40 | release-server: server 41 | 42 | release-all: fmt release-client release-server 43 | 44 | all: fmt client server 45 | 46 | clean: 47 | go clean -i -r ngrok/... 48 | rm -rf src/ngrok/client/assets/ src/ngrok/server/assets/ 49 | 50 | contributors: 51 | echo "Contributors to ngrok, both large and small:\n" > CONTRIBUTORS 52 | git log --raw | grep "^Author: " | sort | uniq | cut -d ' ' -f2- | sed 's/^/- /' | cut -d '<' -f1 >> CONTRIBUTORS 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build 2 | status](https://travis-ci.org/inconshreveable/ngrok.svg)](https://travis-ci.org/inconshreveable/ngrok) 3 | 4 | # ngrok - Introspected tunnels to localhost ([homepage](https://ngrok.com)) 5 | ### "I want to securely expose a web server to the internet and capture all traffic for detailed inspection and replay" 6 | ![](https://ngrok.com/static/img/overview.png) 7 | 8 | ## What is ngrok? 9 | ngrok is a reverse proxy that creates a secure tunnel from a public endpoint to a locally running web service. 10 | ngrok captures and analyzes all traffic over the tunnel for later inspection and replay. 11 | 12 | ## ngrok 2.0 13 | **NOTE** This repository contains the code for ngrok 1.0. The code for ngrok 2.0 is not open source. 14 | 15 | ## What can I do with ngrok? 16 | - Expose any http service behind a NAT or firewall to the internet on a subdomain of ngrok.com 17 | - Expose any tcp service behind a NAT or firewall to the internet on a random port of ngrok.com 18 | - Inspect all http requests/responses that are transmitted over the tunnel 19 | - Replay any request that was transmitted over the tunnel 20 | 21 | 22 | ## What is ngrok useful for? 23 | - Temporarily sharing a website that is only running on your development machine 24 | - Demoing an app at a hackathon without deploying 25 | - Developing any services which consume webhooks (HTTP callbacks) by allowing you to replay those requests 26 | - Debugging and understanding any web service by inspecting the HTTP traffic 27 | - Running networked services on machines that are firewalled off from the internet 28 | 29 | 30 | ## Downloading and installing ngrok 31 | ngrok has _no_ runtime dependencies. Just download a single binary for your platform and run it. Some premium features 32 | are only available by creating an account on ngrok.com. If you need them, [create an account on ngrok.com](https://ngrok.com/signup). 33 | 34 | #### [Download ngrok for your platform](https://ngrok.com/download) 35 | 36 | ## Developing on ngrok 37 | [ngrok developer's guide](docs/DEVELOPMENT.md) 38 | -------------------------------------------------------------------------------- /assets/client/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ngrok 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 36 | 37 | 38 | 39 |
40 | 54 |
55 |
56 |
57 |

No requests to display yet

58 |
59 |
To get started, make a request to one of your tunnel URLs:
60 | 63 |

64 |
65 |
66 |
67 |
68 |
69 |

All Requests

70 | 71 | 72 | 73 | 74 | 75 | 76 |
{{ txn.Req.MethodPath }}
{{ txn.Resp.Status }}{{ txn.Duration }}
77 |
78 |
79 |
80 |
81 | 82 | {{TimeFormat(Txn.Start)}} 83 | 84 |
85 |
86 | Duration 87 | {{Txn.Duration}} 88 |
89 |
90 | IP 91 | {{Txn.ConnCtx.ClientAddr.split(":")[0]}} 92 |
93 |
94 |
95 |
96 |

{{ Req.MethodPath }}

97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 | 105 |
106 | 107 |
108 | 109 |
110 |
{{ Req.RawText }}
111 |
112 | 113 |
114 |
{{ Req.RawBytes }}
115 |
116 | 117 |
118 | 119 |
120 | 121 |
122 |

{{ Resp.Status }}

123 | 124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 |
132 | 133 |
134 |
{{ Resp.RawText }}
135 |
136 | 137 |
138 |
{{ Resp.RawBytes }}
139 |
140 |
141 |
142 |
143 |
144 | 145 | 146 | 147 | 148 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /assets/client/static/css/highlight.min.css: -------------------------------------------------------------------------------- 1 | pre code{display:block;padding:.5em;background:#f0f0f0}pre code,pre .subst,pre .tag .title,pre .lisp .title,pre .clojure .built_in,pre .nginx .title{color:black}pre .string,pre .title,pre .constant,pre .parent,pre .tag .value,pre .rules .value,pre .rules .value .number,pre .preprocessor,pre .ruby .symbol,pre .ruby .symbol .string,pre .aggregate,pre .template_tag,pre .django .variable,pre .smalltalk .class,pre .addition,pre .flow,pre .stream,pre .bash .variable,pre .apache .tag,pre .apache .cbracket,pre .tex .command,pre .tex .special,pre .erlang_repl .function_or_atom,pre .markdown .header{color:#800}pre .comment,pre .annotation,pre .template_comment,pre .diff .header,pre .chunk,pre .markdown .blockquote{color:#888}pre .number,pre .date,pre .regexp,pre .literal,pre .smalltalk .symbol,pre .smalltalk .char,pre .go .constant,pre .change,pre .markdown .bullet,pre .markdown .link_url{color:#080}pre .label,pre .javadoc,pre .ruby .string,pre .decorator,pre .filter .argument,pre .localvars,pre .array,pre .attr_selector,pre .important,pre .pseudo,pre .pi,pre .doctype,pre .deletion,pre .envvar,pre .shebang,pre .apache .sqbracket,pre .nginx .built_in,pre .tex .formula,pre .erlang_repl .reserved,pre .prompt,pre .markdown .link_label,pre .vhdl .attribute,pre .clojure .attribute,pre .coffeescript .property{color:#88F}pre .keyword,pre .id,pre .phpdoc,pre .title,pre .built_in,pre .aggregate,pre .css .tag,pre .javadoctag,pre .phpdoc,pre .yardoctag,pre .smalltalk .class,pre .winutils,pre .bash .variable,pre .apache .tag,pre .go .typename,pre .tex .command,pre .markdown .strong,pre .request,pre .status{font-weight:bold}pre .markdown .emphasis{font-style:italic}pre .nginx .built_in{font-weight:normal}pre .coffeescript .javascript,pre .javascript .xml,pre .tex .formula,pre .xml .javascript,pre .xml .vbscript,pre .xml .css,pre .xml .cdata{opacity:.5} -------------------------------------------------------------------------------- /assets/client/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docker-archive/tutum-ngrok/787e3763bfc504a071be0f3ee3443b508a0c4e20/assets/client/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /assets/client/static/js/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.1.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,h){'use strict';function i(a){var d={},a=a.split(","),c;for(c=0;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= 7 | 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*"+ 8 | 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,">")}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]== 9 | !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(""));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=/^/g, 10 | E=//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")), 11 | 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=[]; 12 | 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= 13 | 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); 14 | -------------------------------------------------------------------------------- /assets/client/static/js/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008 Fred Palmer fred.palmer_at_gmail.com 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | function StringBuffer() 26 | { 27 | this.buffer = []; 28 | } 29 | 30 | StringBuffer.prototype.append = function append(string) 31 | { 32 | this.buffer.push(string); 33 | return this; 34 | }; 35 | 36 | StringBuffer.prototype.toString = function toString() 37 | { 38 | return this.buffer.join(""); 39 | }; 40 | 41 | window.Base64 = 42 | { 43 | codex : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 44 | 45 | encode : function (input) 46 | { 47 | var output = new StringBuffer(); 48 | 49 | var enumerator = new Utf8EncodeEnumerator(input); 50 | while (enumerator.moveNext()) 51 | { 52 | var chr1 = enumerator.current; 53 | 54 | enumerator.moveNext(); 55 | var chr2 = enumerator.current; 56 | 57 | enumerator.moveNext(); 58 | var chr3 = enumerator.current; 59 | 60 | var enc1 = chr1 >> 2; 61 | var enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 62 | var enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 63 | var enc4 = chr3 & 63; 64 | 65 | if (isNaN(chr2)) 66 | { 67 | enc3 = enc4 = 64; 68 | } 69 | else if (isNaN(chr3)) 70 | { 71 | enc4 = 64; 72 | } 73 | 74 | output.append(this.codex.charAt(enc1) + this.codex.charAt(enc2) + this.codex.charAt(enc3) + this.codex.charAt(enc4)); 75 | } 76 | 77 | return output.toString(); 78 | }, 79 | 80 | decode : function (input) 81 | { 82 | var output = new StringBuffer(); 83 | var outputBytes = []; 84 | 85 | var enumerator = new Base64DecodeEnumerator(input); 86 | while (enumerator.moveNext()) 87 | { 88 | var charCode = enumerator.current; 89 | outputBytes.push(charCode); 90 | 91 | if (charCode < 128) 92 | output.append(String.fromCharCode(charCode)); 93 | else if ((charCode > 191) && (charCode < 224)) 94 | { 95 | enumerator.moveNext(); 96 | var charCode2 = enumerator.current; 97 | outputBytes.push(charCode2); 98 | 99 | output.append(String.fromCharCode(((charCode & 31) << 6) | (charCode2 & 63))); 100 | } 101 | else 102 | { 103 | enumerator.moveNext(); 104 | var charCode2 = enumerator.current; 105 | outputBytes.push(charCode2); 106 | 107 | enumerator.moveNext(); 108 | var charCode3 = enumerator.current; 109 | outputBytes.push(charCode3); 110 | 111 | output.append(String.fromCharCode(((charCode & 15) << 12) | ((charCode2 & 63) << 6) | (charCode3 & 63))); 112 | } 113 | } 114 | 115 | return { 116 | "bytes": outputBytes, 117 | "text": output.toString() 118 | }; 119 | } 120 | }; 121 | 122 | function Utf8EncodeEnumerator(input) 123 | { 124 | this._input = input; 125 | this._index = -1; 126 | this._buffer = []; 127 | } 128 | 129 | Utf8EncodeEnumerator.prototype = 130 | { 131 | current: Number.NaN, 132 | 133 | moveNext: function() 134 | { 135 | if (this._buffer.length > 0) 136 | { 137 | this.current = this._buffer.shift(); 138 | return true; 139 | } 140 | else if (this._index >= (this._input.length - 1)) 141 | { 142 | this.current = Number.NaN; 143 | return false; 144 | } 145 | else 146 | { 147 | var charCode = this._input.charCodeAt(++this._index); 148 | 149 | // "\r\n" -> "\n" 150 | // 151 | if ((charCode == 13) && (this._input.charCodeAt(this._index + 1) == 10)) 152 | { 153 | charCode = 10; 154 | this._index += 2; 155 | } 156 | 157 | if (charCode < 128) 158 | { 159 | this.current = charCode; 160 | } 161 | else if ((charCode > 127) && (charCode < 2048)) 162 | { 163 | this.current = (charCode >> 6) | 192; 164 | this._buffer.push((charCode & 63) | 128); 165 | } 166 | else 167 | { 168 | this.current = (charCode >> 12) | 224; 169 | this._buffer.push(((charCode >> 6) & 63) | 128); 170 | this._buffer.push((charCode & 63) | 128); 171 | } 172 | 173 | return true; 174 | } 175 | } 176 | } 177 | 178 | function Base64DecodeEnumerator(input) 179 | { 180 | this._input = input; 181 | this._index = -1; 182 | this._buffer = []; 183 | } 184 | 185 | Base64DecodeEnumerator.prototype = 186 | { 187 | current: 64, 188 | 189 | moveNext: function() 190 | { 191 | if (this._buffer.length > 0) 192 | { 193 | this.current = this._buffer.shift(); 194 | return true; 195 | } 196 | else if (this._index >= (this._input.length - 1)) 197 | { 198 | this.current = 64; 199 | return false; 200 | } 201 | else 202 | { 203 | var enc1 = Base64.codex.indexOf(this._input.charAt(++this._index)); 204 | var enc2 = Base64.codex.indexOf(this._input.charAt(++this._index)); 205 | var enc3 = Base64.codex.indexOf(this._input.charAt(++this._index)); 206 | var enc4 = Base64.codex.indexOf(this._input.charAt(++this._index)); 207 | 208 | var chr1 = (enc1 << 2) | (enc2 >> 4); 209 | var chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 210 | var chr3 = ((enc3 & 3) << 6) | enc4; 211 | 212 | this.current = chr1; 213 | 214 | if (enc3 != 64) 215 | this._buffer.push(chr2); 216 | 217 | if (enc4 != 64) 218 | this._buffer.push(chr3); 219 | 220 | return true; 221 | } 222 | } 223 | }; 224 | -------------------------------------------------------------------------------- /assets/client/static/js/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timeago is a jQuery plugin that makes it easy to support automatically 3 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 4 | * 5 | * @name timeago 6 | * @version 1.3.0 7 | * @requires jQuery v1.2.3+ 8 | * @author Ryan McGeary 9 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * For usage and examples, visit: 12 | * http://timeago.yarp.com/ 13 | * 14 | * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) 15 | */ 16 | 17 | (function (factory) { 18 | if (typeof define === 'function' && define.amd) { 19 | // AMD. Register as an anonymous module. 20 | define(['jquery'], factory); 21 | } else { 22 | // Browser globals 23 | factory(jQuery); 24 | } 25 | }(function ($) { 26 | $.timeago = function(timestamp) { 27 | if (timestamp instanceof Date) { 28 | return inWords(timestamp); 29 | } else if (typeof timestamp === "string") { 30 | return inWords($.timeago.parse(timestamp)); 31 | } else if (typeof timestamp === "number") { 32 | return inWords(new Date(timestamp)); 33 | } else { 34 | return inWords($.timeago.datetime(timestamp)); 35 | } 36 | }; 37 | var $t = $.timeago; 38 | 39 | $.extend($.timeago, { 40 | settings: { 41 | refreshMillis: 60000, 42 | allowFuture: false, 43 | localeTitle: false, 44 | cutoff: 0, 45 | strings: { 46 | prefixAgo: null, 47 | prefixFromNow: null, 48 | suffixAgo: "ago", 49 | suffixFromNow: "from now", 50 | seconds: "less than a minute", 51 | minute: "about a minute", 52 | minutes: "%d minutes", 53 | hour: "about an hour", 54 | hours: "about %d hours", 55 | day: "a day", 56 | days: "%d days", 57 | month: "about a month", 58 | months: "%d months", 59 | year: "about a year", 60 | years: "%d years", 61 | wordSeparator: " ", 62 | numbers: [] 63 | } 64 | }, 65 | inWords: function(distanceMillis) { 66 | var $l = this.settings.strings; 67 | var prefix = $l.prefixAgo; 68 | var suffix = $l.suffixAgo; 69 | if (this.settings.allowFuture) { 70 | if (distanceMillis < 0) { 71 | prefix = $l.prefixFromNow; 72 | suffix = $l.suffixFromNow; 73 | } 74 | } 75 | 76 | var seconds = Math.abs(distanceMillis) / 1000; 77 | var minutes = seconds / 60; 78 | var hours = minutes / 60; 79 | var days = hours / 24; 80 | var years = days / 365; 81 | 82 | function substitute(stringOrFunction, number) { 83 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 84 | var value = ($l.numbers && $l.numbers[number]) || number; 85 | return string.replace(/%d/i, value); 86 | } 87 | 88 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 89 | seconds < 90 && substitute($l.minute, 1) || 90 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 91 | minutes < 90 && substitute($l.hour, 1) || 92 | hours < 24 && substitute($l.hours, Math.round(hours)) || 93 | hours < 42 && substitute($l.day, 1) || 94 | days < 30 && substitute($l.days, Math.round(days)) || 95 | days < 45 && substitute($l.month, 1) || 96 | days < 365 && substitute($l.months, Math.round(days / 30)) || 97 | years < 1.5 && substitute($l.year, 1) || 98 | substitute($l.years, Math.round(years)); 99 | 100 | var separator = $l.wordSeparator || ""; 101 | if ($l.wordSeparator === undefined) { separator = " "; } 102 | return $.trim([prefix, words, suffix].join(separator)); 103 | }, 104 | parse: function(iso8601) { 105 | var s = $.trim(iso8601); 106 | s = s.replace(/\.\d+/,""); // remove milliseconds 107 | s = s.replace(/-/,"/").replace(/-/,"/"); 108 | s = s.replace(/T/," ").replace(/Z/," UTC"); 109 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 110 | return new Date(s); 111 | }, 112 | datetime: function(elem) { 113 | var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); 114 | return $t.parse(iso8601); 115 | }, 116 | isTime: function(elem) { 117 | // jQuery's `is()` doesn't play well with HTML5 in IE 118 | return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 119 | } 120 | }); 121 | 122 | // functions that can be called via $(el).timeago('action') 123 | // init is default when no action is given 124 | // functions are called with context of a single element 125 | var functions = { 126 | init: function(){ 127 | var refresh_el = $.proxy(refresh, this); 128 | refresh_el(); 129 | var $s = $t.settings; 130 | if ($s.refreshMillis > 0) { 131 | setInterval(refresh_el, $s.refreshMillis); 132 | } 133 | }, 134 | update: function(time){ 135 | $(this).data('timeago', { datetime: $t.parse(time) }); 136 | refresh.apply(this); 137 | }, 138 | updateFromDOM: function(){ 139 | $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); 140 | refresh.apply(this); 141 | } 142 | }; 143 | 144 | $.fn.timeago = function(action, options) { 145 | var fn = action ? functions[action] : functions.init; 146 | if(!fn){ 147 | throw new Error("Unknown function name '"+ action +"' for timeago"); 148 | } 149 | // each over objects here and call the requested function 150 | this.each(function(){ 151 | fn.call(this, options); 152 | }); 153 | return this; 154 | }; 155 | 156 | function refresh() { 157 | var data = prepareData(this); 158 | var $s = $t.settings; 159 | 160 | if (!isNaN(data.datetime)) { 161 | if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) { 162 | $(this).text(inWords(data.datetime)); 163 | } 164 | } 165 | return this; 166 | } 167 | 168 | function prepareData(element) { 169 | element = $(element); 170 | if (!element.data("timeago")) { 171 | element.data("timeago", { datetime: $t.datetime(element) }); 172 | var text = $.trim(element.text()); 173 | if ($t.settings.localeTitle) { 174 | element.attr("title", element.data('timeago').datetime.toLocaleString()); 175 | } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { 176 | element.attr("title", text); 177 | } 178 | } 179 | return element.data("timeago"); 180 | } 181 | 182 | function inWords(date) { 183 | return $t.inWords(distance(date)); 184 | } 185 | 186 | function distance(date) { 187 | return (new Date().getTime() - date.getTime()); 188 | } 189 | 190 | // fix for IE6 suckage 191 | document.createElement("abbr"); 192 | document.createElement("time"); 193 | })); 194 | -------------------------------------------------------------------------------- /assets/client/static/js/ngrok.js: -------------------------------------------------------------------------------- 1 | var ngrok = angular.module("ngrok", ["ngSanitize"]); 2 | 3 | var hexRepr = function(bytes) { 4 | var buf = []; 5 | var ascii = []; 6 | for (var i=0; i 0x7e) { 21 | ascii.push('.'); 22 | } else { 23 | ascii.push(String.fromCharCode(b)); 24 | } 25 | 26 | buf.push(b.toString(16)); 27 | buf.push(" "); 28 | ascii.push(" "); 29 | } 30 | 31 | if (ascii.length > 0) { 32 | var charsLeft = 8 - (ascii.length / 2); 33 | for (i=0; i 0; 48 | body.hasError = !!body.Error; 49 | 50 | var syntaxClass = { 51 | "text/xml": "xml", 52 | "application/xml": "xml", 53 | "text/html": "xml", 54 | "text/css": "css", 55 | "application/json": "json", 56 | "text/javascript": "javascript", 57 | "application/javascript": "javascript", 58 | }[body.ContentType]; 59 | 60 | // decode body 61 | if (binary) { 62 | body.Text = ""; 63 | } else { 64 | body.Text = Base64.decode(body.Text).text; 65 | } 66 | 67 | // prettify 68 | var transform = { 69 | "xml": "xml", 70 | "json": "json" 71 | }[syntaxClass]; 72 | 73 | if (!body.hasError && !!transform) { 74 | try { 75 | // vkbeautify does poorly at formatting html 76 | if (body.ContentType != "text/html") { 77 | body.Text = vkbeautify[transform](body.Text); 78 | } 79 | } catch (e) { 80 | } 81 | } 82 | 83 | if (!!syntaxClass) { 84 | body.Text = hljs.highlight(syntaxClass, body.Text).value; 85 | } else { 86 | // highlight.js doesn't have a 'plaintext' syntax, so we'll just copy its escaping function. 87 | body.Text = body.Text.replace(/&/gm, '&').replace(//gm, '>'); 88 | } 89 | }; 90 | 91 | var processReq = function(req) { 92 | if (!req.RawBytes) { 93 | var decoded = Base64.decode(req.Raw); 94 | req.RawBytes = hexRepr(decoded.bytes); 95 | 96 | if (!req.Binary) { 97 | req.RawText = decoded.text; 98 | } 99 | } 100 | 101 | processBody(req.Body, req.Binary); 102 | }; 103 | 104 | var processResp = function(resp) { 105 | resp.statusClass = { 106 | '2': "text-info", 107 | '3': "muted", 108 | '4': "text-warning", 109 | '5': "text-error" 110 | }[resp.Status[0]]; 111 | 112 | if (!resp.RawBytes) { 113 | var decoded = Base64.decode(resp.Raw); 114 | resp.RawBytes = hexRepr(decoded.bytes); 115 | 116 | if (!resp.Binary) { 117 | resp.RawText = decoded.text; 118 | } 119 | } 120 | 121 | processBody(resp.Body, resp.Binary); 122 | }; 123 | 124 | var processTxn = function(txn) { 125 | processReq(txn.Req); 126 | processResp(txn.Resp); 127 | }; 128 | 129 | var preprocessTxn = function(txn) { 130 | var toFixed = function(value, precision) { 131 | var power = Math.pow(10, precision || 0); 132 | return String(Math.round(value * power) / power); 133 | } 134 | // parse nanosecond count 135 | var ns = txn.Duration; 136 | var ms = ns / (1000 * 1000); 137 | txn.Duration = ms; 138 | if (ms > 1000) { 139 | txn.Duration = toFixed(ms / 1000, 2) + "s"; 140 | } else { 141 | txn.Duration = toFixed(ms, 2) + "ms"; 142 | } 143 | }; 144 | 145 | 146 | var active; 147 | var txns = window.data.Txns; 148 | txns.forEach(function(t) { 149 | preprocessTxn(t); 150 | }); 151 | 152 | var activate = function(txn) { 153 | if (!txn.processed) { 154 | processTxn(txn); 155 | txn.processed = true; 156 | } 157 | active = txn; 158 | } 159 | 160 | if (txns.length > 0) { 161 | activate(txns[0]); 162 | } 163 | 164 | return { 165 | add: function(txnData) { 166 | txns.unshift(JSON.parse(txnData)); 167 | preprocessTxn(txns[0]); 168 | if (!active) { 169 | activate(txns[0]); 170 | } 171 | }, 172 | all: function() { 173 | return txns; 174 | }, 175 | active: function(txn) { 176 | if (!txn) { 177 | return active; 178 | } else { 179 | activate(txn); 180 | } 181 | }, 182 | isActive: function(txn) { 183 | return !!active && txn.Id == active.Id; 184 | } 185 | }; 186 | }); 187 | 188 | ngrok.directive({ 189 | "keyval": function() { 190 | return { 191 | scope: { 192 | title: "@", 193 | tuples: "=", 194 | }, 195 | replace: true, 196 | restrict: "E", 197 | template: "" + 198 | '
' + 199 | '
{{title}}
' + 200 | '' + 201 | '' + 202 | '' + 203 | '' + 204 | '' + 205 | '
{{ key }}{{ value }}
' + 206 | '
', 207 | link: function($scope) { 208 | $scope.hasKeys = function() { 209 | for (key in $scope.tuples) { return true; } 210 | return false; 211 | }; 212 | } 213 | }; 214 | }, 215 | 216 | "tabs": function() { 217 | return { 218 | scope: { 219 | "tabs": "@", 220 | "btn": "@", 221 | "onbtnclick": "&" 222 | }, 223 | replace: true, 224 | template: '' + 225 | '', 231 | link: function postLink(scope, element, attrs) { 232 | scope.tabNames = attrs.tabs.split(","); 233 | scope.activeTab = scope.tabNames[0]; 234 | scope.setTab = function(t) { 235 | scope.activeTab = t; 236 | }; 237 | scope.$parent.isTab = scope.isTab = function(t) { 238 | return t == scope.activeTab; 239 | }; 240 | }, 241 | }; 242 | }, 243 | 244 | "body": function() { 245 | return { 246 | scope: { 247 | "body": "=", 248 | "binary": "=" 249 | }, 250 | template: '' + 251 | '
' + 252 | '{{ body.Length }} bytes ' + 253 | '{{ body.RawContentType }}' + 254 | '
' + 255 | '' + 256 | '
' + 257 | '
' + 258 | '
' + 259 | '' + 260 | '
' + 261 | '' + 262 | '
' + 263 | '
' + 264 | '{{ body.Error }}' + 265 | '
', 266 | 267 | link: function($scope, $elem) { 268 | $scope.$watch(function() { return $scope.body; }, function() { 269 | if ($scope.body && $scope.body.ErrorOffset > -1) { 270 | var offset = $scope.body.ErrorOffset; 271 | 272 | function textNodes(node) { 273 | var textNodes = []; 274 | 275 | function getTextNodes(node) { 276 | if (node.nodeType == 3) { 277 | textNodes.push(node); 278 | } else { 279 | for (var i = 0, len = node.childNodes.length; i < len; ++i) { 280 | getTextNodes(node.childNodes[i]); 281 | } 282 | } 283 | } 284 | 285 | getTextNodes(node); 286 | return textNodes; 287 | } 288 | 289 | var tNodes = textNodes($elem.find("code").get(0)); 290 | for (var i=0; i\s{0,}<") 88 | .replace(/ or -1) { 102 | str += shift[deep]+ar[ix]; 103 | inComment = true; 104 | // end comment or // 105 | if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1 ) { 106 | inComment = false; 107 | } 108 | } else 109 | // end comment or // 110 | if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1) { 111 | str += ar[ix]; 112 | inComment = false; 113 | } else 114 | // // 115 | if( /^<\w/.exec(ar[ix-1]) && /^<\/\w/.exec(ar[ix]) && 116 | /^<[\w:\-\.\,]+/.exec(ar[ix-1]) == /^<\/[\w:\-\.\,]+/.exec(ar[ix])[0].replace('/','')) { 117 | str += ar[ix]; 118 | if(!inComment) deep--; 119 | } else 120 | // // 121 | if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) == -1 && ar[ix].search(/\/>/) == -1 ) { 122 | str = !inComment ? str += shift[deep++]+ar[ix] : str += ar[ix]; 123 | } else 124 | // ... // 125 | if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) > -1) { 126 | str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix]; 127 | } else 128 | // // 129 | if(ar[ix].search(/<\//) > -1) { 130 | str = !inComment ? str += shift[--deep]+ar[ix] : str += ar[ix]; 131 | } else 132 | // // 133 | if(ar[ix].search(/\/>/) > -1 ) { 134 | str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix]; 135 | } else 136 | // // 137 | if(ar[ix].search(/<\?/) > -1) { 138 | str += shift[deep]+ar[ix]; 139 | } else 140 | // xmlns // 141 | if( ar[ix].search(/xmlns\:/) > -1 || ar[ix].search(/xmlns\=/) > -1) { 142 | str += shift[deep]+ar[ix]; 143 | } 144 | 145 | else { 146 | str += ar[ix]; 147 | } 148 | } 149 | 150 | return (str[0] == '\n') ? str.slice(1) : str; 151 | } 152 | 153 | vkbeautify.prototype.json = function(text,step) { 154 | 155 | var step = step ? step : this.step; 156 | 157 | if (typeof JSON === 'undefined' ) return text; 158 | 159 | if ( typeof text === "string" ) return JSON.stringify(JSON.parse(text), null, step); 160 | if ( typeof text === "object" ) return JSON.stringify(text, null, step); 161 | 162 | return text; // text is not string nor object 163 | } 164 | 165 | vkbeautify.prototype.css = function(text, step) { 166 | 167 | var ar = text.replace(/\s{1,}/g,' ') 168 | .replace(/\{/g,"{~::~") 169 | .replace(/\}/g,"~::~}~::~") 170 | .replace(/\;/g,";~::~") 171 | .replace(/\/\*/g,"~::~/*") 172 | .replace(/\*\//g,"*/~::~") 173 | .replace(/~::~\s{0,}~::~/g,"~::~") 174 | .split('~::~'), 175 | len = ar.length, 176 | deep = 0, 177 | str = '', 178 | ix = 0, 179 | shift = step ? createShiftArr(step) : this.shift; 180 | 181 | for(ix=0;ix/g,"") 326 | .replace(/[ \r\n\t]{1,}xmlns/g, ' xmlns'); 327 | return str.replace(/>\s{0,}<"); 328 | } 329 | 330 | vkbeautify.prototype.jsonmin = function(text) { 331 | 332 | if (typeof JSON === 'undefined' ) return text; 333 | 334 | return JSON.stringify(JSON.parse(text), null, 0); 335 | 336 | } 337 | 338 | vkbeautify.prototype.cssmin = function(text, preserveComments) { 339 | 340 | var str = preserveComments ? text 341 | : text.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g,"") ; 342 | 343 | return str.replace(/\s{1,}/g,' ') 344 | .replace(/\{\s{1,}/g,"{") 345 | .replace(/\}\s{1,}/g,"}") 346 | .replace(/\;\s{1,}/g,";") 347 | .replace(/\/\*\s{1,}/g,"/*") 348 | .replace(/\*\/\s{1,}/g,"*/"); 349 | } 350 | 351 | vkbeautify.prototype.sqlmin = function(text) { 352 | return text.replace(/\s{1,}/g," ").replace(/\s{1,}\(/,"(").replace(/\s{1,}\)/,")"); 353 | } 354 | 355 | window.vkbeautify = new vkbeautify(); 356 | 357 | })(); 358 | 359 | -------------------------------------------------------------------------------- /assets/client/tls/ngrokroot.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU 3 | MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs 4 | IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 5 | MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux 6 | FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h 7 | bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v 8 | dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt 9 | H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 10 | uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX 11 | mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX 12 | a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN 13 | E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 14 | WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD 15 | VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 16 | Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU 17 | cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx 18 | IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN 19 | AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH 20 | YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 21 | 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC 22 | Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX 23 | c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a 24 | mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /assets/client/tls/snakeoilca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFFDCCAvwCCQCkbN0RG/o15DANBgkqhkiG9w0BAQUFADBMMQswCQYDVQQGEwJV 3 | UzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEChMJbmdyb2suY29tMRQwEgYD 4 | VQQDFAsqLm5ncm9rLmNvbTAeFw0xMzA2MDMwMzUxNTZaFw0yMzA2MDEwMzUxNTZa 5 | MEwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRIwEAYDVQQKEwlu 6 | Z3Jvay5jb20xFDASBgNVBAMUCyoubmdyb2suY29tMIICIjANBgkqhkiG9w0BAQEF 7 | AAOCAg8AMIICCgKCAgEA6QryXeKl8AWWa9uG2UbSOpooH74zLkXs3FZfk9gKvqki 8 | zXXQCRmtU6Dyn0+OuS3sE/rRmZAsSjkQG/YDtdE/SgL4dV6S62qiQngPokjR0USh 9 | PC4Hwb8TjM9W5Cd+owVzMQ0vl0AYhQk8Yc/0vX+zDOwmRWGjNKPq422usF9CJFc/ 10 | 8QY+ODJDHun8VVAkq3XfcPXgytHIqxvSJnYgDouFCA+GTsKp/65S5cigSlIrQZbH 11 | 775cTWhCjvYnq6gzyrk3RiGdb1IGuIJftMJxuJyJVbfTFtqgMGTmjHZxiLvM7dz7 12 | j/bmrz4PvnhbQSZZLhsvP1o8mxnoNMpo/To5tHp/Ts6b5FQNL7FHpmOVLAoQ3FdX 13 | VryTjoSjiE3JLDGZINQ9MFEPgPzR8mrzqFo/6e7uB4AYlKoQo01Kzx+YmVwRKEtr 14 | VCTRZRcl66+gMkcX0ryoVnggjIWWu4d8uAh3jf++Kd0Djb/l7DCPpEgSJwZYTjCL 15 | Z6SxiBwQ4o9dEQadMGgk3tlDFCBsrHoq7NyzvXHP0BF2HKb8KREBEKCIuQj9RChW 16 | g07zmOpjngWs0CXaYly+TDP+5DZCMGD9kmXkQY9q/zqqvMt+T+/TBK9lwUsoi2Uc 17 | v93wS+TNu06aRopqPo9YZr38ka3xKPiO964pk2BoFN57g767G8k9TbhkBxitvFUC 18 | AwEAATANBgkqhkiG9w0BAQUFAAOCAgEARf/hVYntzUwFUgQrWD0l/UaCBgrlvxVC 19 | yUa8Isj3vezAhFSyZntEL+ELFv8vvQbtBGHH/tCn76WuqjwOjVL23yxkaJsrnR9+ 20 | TRNFnVeB8157+IF6HKzLCL/HIAiQ0kw/2OSLD0lZnAkg24A0/9SHcpI5GA0WlThE 21 | 4GqgcUiN9m+mL8jWG3gj+SXC7IcVS3vAvS1J7Kz0NzTh1dYkQNWJlauO2Qn95T99 22 | plkPPh87yZO9a9bxpX9PUJkTJzOwUkZISRZEbTA0CfspUpq/phzuTViy7o2fr+To 23 | xVVa2aKT912vlQadWw5oqEK7xyxTqsYwV7CUljtCnhpS7wOZhwzI3qUk9HKH0Rt9 24 | /HQsANuSikZosNvdM3/hv3c5DRUOwKiKdbgZCyqQf8XSeJRM1iQvcHo8U8kEJEEt 25 | dmftn+0gH3RPsV028+7XD6wraqfc7dzNzLnq2rDLSAfG3T7pp0n19JyUieWsTR45 26 | pGcwNpXDk+weqQKknwbNoha3o51Xq/1nhRtqrUPIEgOnBWBCV8vkhXMh9Vbe+oAm 27 | LmweN1Mr6MjrHWddVn+JGcB5p+AMWr8zE+bhpPCAnupFR98z3fbOleMCWq6Q+hMP 28 | MUThqJROHamHRIJ3Iz8whIrj7EKkDBKLEfE1ExS3B9VQ+YdHy6sjexdmCIQgElq4 29 | 4SMuY/JkZlo= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /assets/server/tls/snakeoil.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFDDCCAvQCAQEwDQYJKoZIhvcNAQEFBQAwTDELMAkGA1UEBhMCVVMxEzARBgNV 3 | BAgTCkNhbGlmb3JuaWExEjAQBgNVBAoTCW5ncm9rLmNvbTEUMBIGA1UEAxQLKi5u 4 | Z3Jvay5jb20wHhcNMTMwNjAzMDQyMzAwWhcNMjMwNjAxMDQyMzAwWjBMMQswCQYD 5 | VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEChMJbmdyb2suY29t 6 | MRQwEgYDVQQDFAsqLm5ncm9rLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC 7 | AgoCggIBANouXC6wJacPIFYEiKIGGhliShxsy181LJ064ZTD3ARMTi9ONOsRkHXT 8 | oM0CV1ZNDniPp3JILiGL52FPp0hdJegQ+pzz/gDjX0h9MCWexM4sqLpY0TfEqk9l 9 | HWKPJg+ODK8wa1/wuQOK++m/XY3wcbZgLj23EHcGybl/xGJkaeaqGNvrJo/4RgSL 10 | /jTpp0dDWKUmKBpTH4PuD6bkl8qKQAP1eul6ZS+Gr/llFYJWqwgd3SD3Wh/61Y4n 11 | 6sDusem2NQltnlFwe5VS1LfopVtO3e2N5fveEUdbg2YDs7evNYVK+1i0kjo0dD3D 12 | tbmhe13OQ/baufxcHc1xjtGsJ57ws8nePL4eo+vFuD5Qjc9ESfzunSem0hC5AJgF 13 | rsWzIYsr7UsiuKG4gQdD17L+DogVCqRcdYXugrPEJONi8YG3eRRP/Q9klX4PWy7O 14 | Zj7KX/xBrvS5GeDNqrDVsPL0W24FJ//Sus7BSF0fc5DSYqtD4Zgf1GRi0+xSa3m9 15 | AgJwWOHm6iC0mHwddozPLd+CP/M7PYzPRDiWY/ehoCFiqRAglYO0mhKyJvoTOjq6 16 | TBYYp/8LcbqfqUkI6DSnZ2fNdXKDyirMSOT4K1ug1MHaq8obucLqx+5Hie5d6CKR 17 | gnN15sJwGKIy27fg6LAzi1GWuCzZ1kHqjuQh3oVfuBKnFfsdLUnFAgMBAAEwDQYJ 18 | KoZIhvcNAQEFBQADggIBADael2+SR8LC5xNCBcu4eqmil30Whb7qqr16EHU/MSQb 19 | nmFMxBqTa2B8RDZIIkb7LukH8rsAdU8Bzkc2yRdjoAfEMMcJA/fMpwuaXI5cuaKV 20 | idZFpNUyR+K5UG/CntcCvwzZp4//g+LVK9qPDZ2BJmA/PMR6OphRwRwG+ruSLUCi 21 | ywgFFhNlPMpPZ49vsFm/Q0A4JmLpZXaARt53zNbNiHT1FgTP/9L1HIpkbfoFQT6R 22 | pB/VYe8O+GmrwaL3l+L8aO07JRM+u0OKNPxOgxWgE7UngiWulXpnIHAXYxTVMQ8V 23 | 1IYutv9bYJ/+TAy9Kxzc4Q33K+5qvS58GCAjOF10NnHkiTQPbhUWISoj21eZJ1hb 24 | Z4BbC5z/Y8zNbMao0ACF0QmlnXlt0YrkjkcZQuqerzpC6YMw32vsemREc55X9UQM 25 | mVbnDah4xyPM+yNIg/uKHVTYJ6+4TttWEp1JGRaP2EOYr0yGe4XFFrKIPH2DlApM 26 | nNfqR+Mfx9WZS3n6FuWPGWUDBye2fdxOjW9Jwc/+JDR3BsS65LlzPrs6C8TcNnnD 27 | EdWJ4klYzWkuSdUiV1EXbsB1sSIKmUud2f4vJuOqlBsgS8/mTxjk123uXaN9zaN6 28 | A9cMWQI2MbobK0HErkk0QyNTtVTKumEEko/c2ktsn8lrdJcKqCeQ9EKwp6r0Lmtp 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /assets/server/tls/snakeoil.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEA2i5cLrAlpw8gVgSIogYaGWJKHGzLXzUsnTrhlMPcBExOL040 3 | 6xGQddOgzQJXVk0OeI+nckguIYvnYU+nSF0l6BD6nPP+AONfSH0wJZ7EziyouljR 4 | N8SqT2UdYo8mD44MrzBrX/C5A4r76b9djfBxtmAuPbcQdwbJuX/EYmRp5qoY2+sm 5 | j/hGBIv+NOmnR0NYpSYoGlMfg+4PpuSXyopAA/V66XplL4av+WUVglarCB3dIPda 6 | H/rVjifqwO6x6bY1CW2eUXB7lVLUt+ilW07d7Y3l+94RR1uDZgOzt681hUr7WLSS 7 | OjR0PcO1uaF7Xc5D9tq5/FwdzXGO0awnnvCzyd48vh6j68W4PlCNz0RJ/O6dJ6bS 8 | ELkAmAWuxbMhiyvtSyK4obiBB0PXsv4OiBUKpFx1he6Cs8Qk42Lxgbd5FE/9D2SV 9 | fg9bLs5mPspf/EGu9LkZ4M2qsNWw8vRbbgUn/9K6zsFIXR9zkNJiq0PhmB/UZGLT 10 | 7FJreb0CAnBY4ebqILSYfB12jM8t34I/8zs9jM9EOJZj96GgIWKpECCVg7SaErIm 11 | +hM6OrpMFhin/wtxup+pSQjoNKdnZ811coPKKsxI5PgrW6DUwdqryhu5wurH7keJ 12 | 7l3oIpGCc3XmwnAYojLbt+DosDOLUZa4LNnWQeqO5CHehV+4EqcV+x0tScUCAwEA 13 | AQKCAgBb6Ru8L0gtUBn3IoHMf3WPK/C8eLhTqzrYIW3WFYwh42MsWm3AeO26NSSQ 14 | OGRCXsOx1hJb+jw0tZMLU1rNCTBmyoBIjiB6j04cY2Bc+L0/fWC236ODMr3sJFR0 15 | qIkIFHcTdfpFuEq4S1xD4/GtUZUVlv7j0LKG8b0Y/9HjARn7qbw/KJheHeChGbhE 16 | 4gkt5Bj7uU87h7jHAwpk6/dlw0ekY00b/guSMdL/5K1i8s+p46q7sHeu8SP1dqtW 17 | Cze3lKJTDnKbLB9jkDk8IC1IgbjL0fMIX0w4Gz0HRJf40T5ioGuxup+/FUnCmyd6 18 | w6QMqE/JNesTfFqxqRzZBwTJ1+xkXkSFmFhZ9McfxmEtozau0LH6TtkGmHvfmn6/ 19 | I8ImlIRyHGOUVHsEAR8c7aD9dag0LbXmjyolfin8Dow130/G3hTXYnDETx66k1RT 20 | Bh6kDEYvuxvFqkXYKRAu5u11vQA9udYqN4i+htQ2OOYOEKNX8Kxy562rXgd85MyT 21 | eTL4Dmh/6J7VtWG4nN2G8pnj5Lg99Q8fzEFQmFLFcK0kJwHu04pJaqrHL+uHHd5T 22 | KOfPAr0pUbeu2sVd9chgDhADUE1/8YPhSCsHudM4No4tDCZVEqKJ1pGLCpUw3H1v 23 | h0aA/B582p2CGFpGoInKfFKvUbYt2neDZZQygmYCUHywqNxQAQKCAQEA/H0MCJYV 24 | VRRLufZkz3fpC+wT+coqbguwrT2bpVBuNdvyDKYIkbcSDtIVNsO/1p1/CGormDll 25 | k1jNFhR+lqBB8QQfb1ZetPohxzPNncPiJypb9Uivu7GG1MNUHJUecFt8uITcY93s 26 | UgRBVsCpLgUP7SF6m1Mpu2BaXQ/sEM8dYCSmt4Fax7LW00cIpLleZmCXoc/jsTUM 27 | ixYTM5r5UWXBhl196Lfu+WjTmoxGgFeTwF8x1wuo7mQvPfripvUX/bjGR4o5JQOw 28 | 1I0XK13yrhFkEAIV61nHBGiyhmJShPaSI2uDudx0ZkfuUIM1UFvqeqWsjATaLmOW 29 | X7lE0qNg75lsRQKCAQEA3Tcqgviss9a4Owr0BagleObUnTMFzk8bgpemNdzi47eG 30 | 7C572Wx8Gs6XgNbO28lpE0PD7KU/Rvt36wP9mzGiP0M3Zr4h81AfLvftoMMs6ytk 31 | wHJOdtPUCZqvnblvKOI409nGnoUD+3nfH/qqHk4ZL3fn4vHbxwModZfOXRrH2hJr 32 | EVqAnnexi9S0JbJJxdcSlBMI5vQMKGLCj52C2mfcF+XRMuF88bwfJ7Xcb3yhyv8i 33 | pXHHZZ9QKE7eG76UO+EGKz8gn3wWKuh5HXp3HoyKidgzXtPIbu8fd4UzXl/BTMFc 34 | jWAG0Ycsm5dTobQ0Yo0Xeju5Hs5QCtItY7OAFsH/gQKCAQBBCTC9UXNjO9wZpYbo 35 | DdoAkSnAELwHJom2xgS+e044H1Rkv6u7ZO2I1cJTHe7fKChdkYNzLW2lm50QD+1f 36 | fR4fJ9G1CwlQEpH6zrQq7Bbnwbh4IOXrMdoqGbojtqFljZs9qDNgofxKUABIiU3K 37 | pdEpYpNDSROZyULdb8l9tuu5JRewcuhgQgel2kk2rOzM8Bp+up7KuYBmnyQJCeUo 38 | e05y/sf81sv+gGrpBzLtwiEzzxF2c/FqnnGwxFv3Z3BrkVm5ebgoeZ/l0AXkzMlC 39 | 3wXoPbFJsxFZaGJ7zP22dBDGgN4oVMnCwsp3AKUN8u8d8mjUlDdi9ZH5TC6XFzBT 40 | 5zAFAoIBAFCjlXmc0MfV096iBYYyX0aNTp/nQ4yLRcn7IfmshYDhG+vonfkKFMto 41 | 1819gHaaGxWMtFUFf+WOMY6YK9Bw7WYGSKHJWXLqmBN1CUh7HVq0vMtyX6vtV/QQ 42 | UUg7movau0BuuHp8npEDQhTUOUNG0ON+4CbYZ3dKbWtAZVeHNacG48S1qwEZPL1u 43 | UiUTstTNq9YSgkI+YFgweCAGGPcouRB1FCdqDzPHkcvV/X8efZQUITsSGM+wnXW0 44 | Gj8e38ZcJvWI04mPoD0P9WaLh/S44p+REljU9tGJlXzqL2mNmlcyfVyDzrh+gAJP 45 | zYq6uAXczNwf/UF/j6oCJ82aV2z0VwECggEBAJga2Kb1ovNLMQ8Vum1tE8BOku9D 46 | hTVpz+sFugZuY5Dzl9sPGT3UXGjZZ8pGEGpLPQK2YP5yc+ehCng3ePtXV0sPUR5W 47 | 17jibM2JYFkR9RJCCA8c++2OwxZTVJcSDUkHG7cpl5QLbzOJAWG78EziiNqZEyC0 48 | RsYKPad6e0enYtPvYlnCYr77Y4KN6mU5Ot3OzqGeP4++dUMznL21vb2wmO6xHLR0 49 | Mwa6w7fIXYNAOuCk0GMeYzSHZT7ZYytUUhiiTVbHZuazJR6JyR7yf3dnjkPfRVHe 50 | qLsKIVNbyWayop61fNZWfN4FgLMmUDzCNNDz9sFX09Jm1QaEo+Dun7Dw8Ss= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /contrib/com.ngrok.client.plist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | KeepAlive 11 | 12 | Label 13 | com.ngrok.client 14 | ProgramArguments 15 | 16 | /usr/local/bin/ngrok 17 | -log 18 | stdout 19 | -subdomain 20 | mySubDomain 21 | 80 22 | 23 | StandardOutPath 24 | /tmp/ngrok.log 25 | StandardErrorPath 26 | /tmp/ngrok.log 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 1.7 - 6/6/2014 3 | - IMPROVEMENT: Print a better help message when run without any arguments 4 | - IMPROVEMENT: Display useful help message and instructions when double-clicked from explorer on Windows 5 | - IMPROVEMENT: ngrok now uses the specified server_addr to set the SNI header instead of forcing ngrokd.ngrok.com 6 | - IMPROVEMENT: ngrok now uses equinox.io for automatic updates with greater speed and safety 7 | - IMPROVEMENT: Many documentation improvements 8 | - IMPROVEMENT: Added example plist file for autostart on OS X 9 | - BUGFIX: Fixed an issue where ngrok could crash when parsing some websocket requests 10 | - BUGFIX: Fixed an issue where the web UI would truncate the raw request to 8192 bytes 11 | - BUGFIX: Fixed an issue where ngrok could not replay requests where the request was larger than 8192 bytes 12 | - BUGFIX: Fixed an issue where the web UI would not update in realtime when not accessed over localhost 13 | - BUGFIX: Fixed an unlikely race condition in ngrokd when loading the tunnel URL cache 14 | - BUGFIX: Check for a valid server address without trying to resolve for less confusing errors 15 | 16 | ## 1.6 - 10/25/2013 17 | - BUGFIX: Fixed a goroutine/memory leak in ngrok/proto's parsing of http traffic 18 | - IMPROVEMENT: The web inspection API can now be disabled again by setting inspect_addr: disabled in the config file 19 | 20 | ## 1.5 - 10/20/2013 21 | - FEATURE: Added support a "remote_port" configuration parameter that lets you request a specific remote port for TCP tunnels 22 | - IMPROVEMENT: Upload instructions on crash reports are displayed after the dump where it is more likely to be seen 23 | - IMPROVEMENT: Improvements to ngrok's logging for easier debugging 24 | - IMPROVEMENT: Batch metric reporting to Keen to not be limited by the speed of their API at high request loads 25 | - IMPROVEMENT: Added additional safety to ensure the server doesn't crash on panics() 26 | - BUGFIX: Fixed an issue with prefetching tunnel connections that could hang tunnel connections when behind an aggressive NAT 27 | - BUGFIX: Fixed a race condition where ngrokd could send back a different message instead of AuthResp first 28 | - BUGFIX: Fixed an issue where under some circumstances, reconnecting would fail and tell the client the tunnels were still in use 29 | - BUGFIX: Fixed an issue where a race-condition with handling pings could cause a tunnel to hang forever and stop handling requests 30 | 31 | ## 1.4 - 09/27/2013 32 | - BUGFIX: Fixed an issue where long URL paths were not truncated in the terminal UI 33 | - BUGFIX: Fixed an issue where long URL paths ruined the web UI's formatting 34 | - BUGFIX: Fixed an issue where authtokens would not be remembered if an existing configuration file didn't exist 35 | 36 | ## 0.23 - 09/06/2013 37 | - BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay 38 | 39 | ## 0.22 - 09/04/2013 40 | - FEATURE: ngrok now tunnels websocket requests 41 | 42 | ## 0.21 - 08/17/2013 43 | - IMPROVEMENT: The ngrok web ui can now be disabled with -webport=-1 44 | 45 | ## 0.20 - 08/17/2013 46 | - BUGFIX: Fixed a bug where ngrok would not stop its autoupdate loop even after it should stop 47 | 48 | ## 0.19 - 08/17/2013 49 | - BUGFIX: Fixed a bug where ngrok's would loop infinitely trying to checking for updates after the second update check 50 | - BUGFIX: Fixed a race condition in ngrokd's metrics logging immediately after start up 51 | 52 | ## 0.18 - 08/15/2013 53 | - BUGFIX: Fixed a bug where ngrok would compare the Host header for virtual hosting using case-sensitive comparisons 54 | - BUGFIX: Fixed a bug where ngrok would not include the port number in the virtual host when not serving on port 80 55 | - BUGFIX: Fixed a bug where ngrok would crash when trying to replay a request 56 | - IMPROVEMENT: ngrok can now indicate manual updates again 57 | - IMPROVEMENT: ngrok can now supports update channels 58 | - IMPROVEMENT: ngrok can now detect some updates that will fail before downloading 59 | 60 | ## 0.17 - 07/30/2013 61 | - BUGFIX: Fixed an issue where ngrok's registry cache would return a URL from a different protocol 62 | 63 | ## 0.16 - 07/30/2013 64 | - BUGFIX: Fixed an issue where ngrok would crash when parsing bad XML that wasn't a syntax error 65 | - BUGFIX: Fixed an issue where ngrok would crash when parsing bad JSON that wasn't a syntax error 66 | - BUGFIX: Fixed an issue where the web ui would sometimes not update the request body when changing requests 67 | - BUGFIX: Fixed an issue where ngrokd's registry cache would not load from file 68 | - BUGFIX: Fixed an issue where ngrokd's registry cache would not save to file 69 | - BUGFIX: Fixed an issue where ngrok would refuse requests with an Authorization header if no HTTP auth was specified. 70 | - BUGFIX: Fixed a bug where ngrok would fail to cross-compile in you hadn't compiled natively first 71 | - IMPROVEMENT: ngrok's registry cache now handles and attempts to restore TCP URLs 72 | - IMPROVEMENT: Added simple Travis CI integration to make sure ngrok compiles 73 | 74 | ## 0.15 - 07/27/2013 75 | - FEATURE: ngrok can now update itself automatically 76 | 77 | ## 0.14 - 07/03/2013 78 | - BUGFIX: Fix an issue where ngrok could never save/load the authtoken file on linux 79 | - BUGFIX: Fix an issue where ngrok wouldn't emit log messages while loading authtokens 80 | 81 | ## 0.13 - 07/02/2013 82 | - FEATURE: -hostname switch on client allows you to run tunnels over custom domains (requires you CNAME your DNS) 83 | - IMPROVEMENT: ngrok client UI now shows the client IP address for a request 84 | - IMPROVEMENT: ngrok client UI now shows how long ago a request was made (uservoice request 4127487) 85 | - IMPROVEMENT: ngrokd now uses and LRU cache for tunnel affinity data 86 | - IMPROVEMENT: ngrokd can now save and restore its tunnel affinity cache to a file to preserve across restarts 87 | 88 | ## 0.12 - 06/30/2013 89 | - IMPROVEMENT: Improved developer documentation 90 | - IMPROVEMENT: Simplified build process with custom version of go-bindata that compiles assets into binary releases 91 | - BUGFIX: GitHub issue #4: Raw/Binary requests bodies are no longer truncated at 8192 bytes. 92 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Developer's guide to ngrok 2 | 3 | 4 | ## Components 5 | The ngrok project is composed of two components, the ngrok client (ngrok) and the ngrok server (ngrokd). 6 | The ngrok client is the more complicated piece because it has UIs for displaying saved requests and responses. 7 | 8 | ## Compiling 9 | 10 | git clone git@github.com:inconshreveable/ngrok.git 11 | cd ngrok && make 12 | bin/ngrok [LOCAL PORT] 13 | 14 | There are Makefile targets for compiling just the client or server. 15 | 16 | make client 17 | make server 18 | 19 | **NB: You must compile with Go 1.1+! You must have Mercurial SCM Installed.** 20 | 21 | ### Compiling release versions 22 | Both the client and the server contain static asset files. 23 | These include TLS/SSL certificates and the html/css/js for the client's web interface. 24 | The release versions embed all of this data into the binaries themselves, whereas the debug versions read these files from the filesystem. 25 | 26 | *You should always develop on debug versions so that you don't have to recompile when testing changes in the static assets.* 27 | 28 | There are Makefile targets for compiling the client and server for releases: 29 | 30 | make release-client 31 | make release-server 32 | make release-all 33 | 34 | 35 | ## Developing locally 36 | The strategy I use for developing on ngrok is to do the following: 37 | 38 | Add the following lines to /etc/hosts: 39 | 40 | 127.0.0.1 ngrok.me 41 | 127.0.0.1 test.ngrok.me 42 | 43 | Run ngrokd with the following options: 44 | 45 | ./bin/ngrokd -domain ngrok.me 46 | 47 | Create an ngrok configuration file, "debug.yml" with the following contents: 48 | 49 | server_addr: ngrok.me:4443 50 | tunnels: 51 | test: 52 | proto: 53 | http: 8080 54 | 55 | 56 | Then run ngrok with either of these commands: 57 | 58 | ./bin/ngrok -config=debug.yml -log=ngrok.log start test 59 | ./bin/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080 60 | 61 | This will get you setup with an ngrok client talking to an ngrok server all locally under your control. Happy hacking! 62 | 63 | 64 | ## Network protocol and tunneling 65 | At a high level, ngrok's tunneling works as follows: 66 | 67 | ### Connection Setup and Authentication 68 | 1. The client initiates a long-lived TCP connection to the server over which they will pass JSON instruction messages. This connection is called the *Control Connection*. 69 | 1. After the connection is established, the client sends an *Auth* message with authentication and version information. 70 | 1. The server validates the client's *Auth* message and sends an *AuthResp* message indicating either success or failure. 71 | 72 | ### Tunnel creation 73 | 1. The client may then ask the server to create tunnels for it by sending *ReqTunnel* messages. 74 | 1. When the server receives a *ReqTunnel* message, it will send 1 or more *NewTunnel* messages that indicate successful tunnel creation or indicate failure. 75 | 76 | ### Tunneling connections 77 | 1. When the server receives a new public connection, it locates the appropriate tunnel by examining the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*. 78 | 1. The server sends a *ReqProxy* message to the client over the control connection. 79 | 1. The client initiates a new TCP connection to the server called a *Proxy Connection*. 80 | 1. The client sends a *RegProxy* message over the proxy connection so the server can associate it to a control connection (and thus the tunnels it's responsible for). 81 | 1. The server sends a *StartProxy* message over the proxy connection with metadata information about the connection (the client IP and name of the tunnel). 82 | 1. The server begins copying the traffic byte-for-byte from the public connection to the proxy connection and vice-versa. 83 | 1. The client opens a connection to the local address configured for that tunnel. This is called the *Private Connection*. 84 | 1. The client begins copying the traffic byte-for-byte from the proxied connection to the private connection and vice-versa. 85 | 86 | ### Detecting dead tunnels 87 | 1. In order to determine whether a tunnel is still alive, the client periodically sends Ping messages over the control connection to the server, which replies with Pong messages. 88 | 1. When a tunnel is detected to be dead, the server will clean up all of that tunnel's state and the client will attempt to reconnect and establish a new tunnel. 89 | 90 | ### Wire format 91 | Messages are sent over the wire as netstrings of the form: 92 | 93 | 94 | 95 | The message length is sent as a 64-bit little endian integer. 96 | 97 | ### Code 98 | The definitions and shared protocol routines lives under _src/ngrok/msg_ 99 | 100 | #### src/ngrok/msg/msg.go 101 | All of the different message types (Auth, AuthResp, ReqTunnel, RegProxy, StartProxy, etc) are defined here and their fields documented. This is a good place to go to understand exactly what messages are sent between the client and server. 102 | 103 | ## ngrokd - the server 104 | ### Code 105 | Code for the server lives under src/ngrok/server 106 | 107 | ### Entry point 108 | The ngrokd entry point is in _src/ngrok/server/main.go_. 109 | There is a stub at _src/ngrok/main/ngrokd/ngrokd.go_ for the purposes of creating a properly named binary and being in its own "main" package to comply with go's build system. 110 | 111 | ## ngrok - the client 112 | ### Code 113 | Code for the client lives under src/ngrok/client 114 | 115 | ### Entry point 116 | The ngrok entry point is in _src/ngrok/client/main.go_. 117 | There is a stub at _src/ngrok/main/ngrok/ngrok.go_ for the purposes of creating a properly named binary and being in its own "main" package to comply with go's build system. 118 | 119 | ## Static assets 120 | The html and javascript code for the ngrok web interface as well as other static assets like TLS/SSL certificates live under the top-level _assets_ directory. 121 | 122 | ## Beyond 123 | More documentation can be found in the comments of the code itself. 124 | -------------------------------------------------------------------------------- /docs/SELFHOSTING.md: -------------------------------------------------------------------------------- 1 | # How to run your own ngrokd server 2 | 3 | Running your own ngrok server is really easy! The instructions below will guide you along your way! 4 | 5 | ## 1. Get an SSL certificate 6 | ngrok provides secure tunnels via TLS, so you'll need an SSL certificate. Assuming you want to create 7 | tunnels on *.example.com, buy a wildcard SSL certificate for *.example.com. Note that if you 8 | don't need to run https tunnels that you don't need a wildcard certificate. (In fact, you can 9 | just use a self-signed cert at that point, see the section on that later in the document). 10 | 11 | ## 2. Modify your DNS 12 | You need to use the DNS management tools given to you by your provider to create an A 13 | record which points *.example.com to the IP address of the server where you will run ngrokd. 14 | 15 | ## 3. Compile it 16 | You can compile an ngrokd server with the following command: 17 | 18 | make release-server 19 | 20 | Make sure you compile it with the GOOS/GOARCH environment variables set to the platform of 21 | your target server. Then copy the binary over to your server. 22 | 23 | ## 4. Run the server 24 | You'll run the server with the following command. 25 | 26 | 27 | ./ngrokd -tlsKey="/path/to/tls.key" -tlsCrt="/path/to/tls.crt" -domain="example.com" 28 | 29 | ### Specifying your TLS certificate and key 30 | ngrok only makes TLS-encrypted connections. When you run ngrokd, you'll need to instruct it 31 | where to find your TLS certificate and private key. Specify the paths with the following switches: 32 | 33 | -tlsKey="/path/to/tls.key" -tlsCrt="/path/to/tls.crt" 34 | 35 | ### Setting the server's domain 36 | When you run your own ngrokd server, you need to tell ngrokd the domain it's running on so that it 37 | knows what URLs to issue to clients. 38 | 39 | -domain="example.com" 40 | 41 | ## 5. Configure the client 42 | In order to connect with a client, you'll need to set two options in ngrok's configuration file. 43 | The ngrok configuration file is a simple YAML file that is read from ~/.ngrok by default. You may specify 44 | a custom configuration file path with the -config switch. Your config file must contain the following two 45 | options. 46 | 47 | server_addr: example.com:4443 48 | trust_host_root_certs: true 49 | 50 | Substitute the address of your ngrokd server for "example.com:4443". The "trust_host_root_certs" parameter instructs 51 | ngrok to trust the root certificates on your computer when establishing TLS connections to the server. By default, ngrok 52 | only trusts the root certificate for ngrok.com. 53 | 54 | ## 6. Connect with a client 55 | Then, just run ngrok as usual to connect securely to your own ngrokd server! 56 | 57 | ngrok 80 58 | 59 | # ngrokd with a self-signed SSL certificate 60 | It's possible to run ngrokd with a a self-signed certificate, but you'll need to recompile ngrok with your signing CA. 61 | If you do choose to use a self-signed cert, please note that you must either remove the configuration value for 62 | trust_host_root_certs or set it to false: 63 | 64 | trust_host_root_certs: false 65 | 66 | Special thanks @kk86bioinfo, @lyoshenka and everyone in the thread https://github.com/inconshreveable/ngrok/issues/84 for help in writing up instructions on how to do it: 67 | 68 | https://gist.github.com/lyoshenka/002b7fbd801d0fd21f2f 69 | https://github.com/inconshreveable/ngrok/issues/84 70 | 71 | -------------------------------------------------------------------------------- /src/ngrok/cache/lru.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012, Google Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // The implementation borrows heavily from SmallLRUCache (originally by Nathan 6 | // Schrenk). The object maintains a doubly-linked list of elements in the 7 | // When an element is accessed it is promoted to the head of the list, and when 8 | // space is needed the element at the tail of the list (the least recently used 9 | // element) is evicted. 10 | package cache 11 | 12 | import ( 13 | "container/list" 14 | "encoding/gob" 15 | "fmt" 16 | "io" 17 | "os" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | type LRUCache struct { 23 | mu sync.Mutex 24 | 25 | // list & table of *entry objects 26 | list *list.List 27 | table map[string]*list.Element 28 | 29 | // Our current size, in bytes. Obviously a gross simplification and low-grade 30 | // approximation. 31 | size uint64 32 | 33 | // How many bytes we are limiting the cache to. 34 | capacity uint64 35 | } 36 | 37 | // Values that go into LRUCache need to satisfy this interface. 38 | type Value interface { 39 | Size() int 40 | } 41 | 42 | type Item struct { 43 | Key string 44 | Value Value 45 | } 46 | 47 | type entry struct { 48 | key string 49 | value Value 50 | size int 51 | time_accessed time.Time 52 | } 53 | 54 | func NewLRUCache(capacity uint64) *LRUCache { 55 | return &LRUCache{ 56 | list: list.New(), 57 | table: make(map[string]*list.Element), 58 | capacity: capacity, 59 | } 60 | } 61 | 62 | func (lru *LRUCache) Get(key string) (v Value, ok bool) { 63 | lru.mu.Lock() 64 | defer lru.mu.Unlock() 65 | 66 | element := lru.table[key] 67 | if element == nil { 68 | return nil, false 69 | } 70 | lru.moveToFront(element) 71 | return element.Value.(*entry).value, true 72 | } 73 | 74 | func (lru *LRUCache) Set(key string, value Value) { 75 | lru.mu.Lock() 76 | defer lru.mu.Unlock() 77 | 78 | if element := lru.table[key]; element != nil { 79 | lru.updateInplace(element, value) 80 | } else { 81 | lru.addNew(key, value) 82 | } 83 | } 84 | 85 | func (lru *LRUCache) SetIfAbsent(key string, value Value) { 86 | lru.mu.Lock() 87 | defer lru.mu.Unlock() 88 | 89 | if element := lru.table[key]; element != nil { 90 | lru.moveToFront(element) 91 | } else { 92 | lru.addNew(key, value) 93 | } 94 | } 95 | 96 | func (lru *LRUCache) Delete(key string) bool { 97 | lru.mu.Lock() 98 | defer lru.mu.Unlock() 99 | 100 | element := lru.table[key] 101 | if element == nil { 102 | return false 103 | } 104 | 105 | lru.list.Remove(element) 106 | delete(lru.table, key) 107 | lru.size -= uint64(element.Value.(*entry).size) 108 | return true 109 | } 110 | 111 | func (lru *LRUCache) Clear() { 112 | lru.mu.Lock() 113 | defer lru.mu.Unlock() 114 | 115 | lru.list.Init() 116 | lru.table = make(map[string]*list.Element) 117 | lru.size = 0 118 | } 119 | 120 | func (lru *LRUCache) SetCapacity(capacity uint64) { 121 | lru.mu.Lock() 122 | defer lru.mu.Unlock() 123 | 124 | lru.capacity = capacity 125 | lru.checkCapacity() 126 | } 127 | 128 | func (lru *LRUCache) Stats() (length, size, capacity uint64, oldest time.Time) { 129 | lru.mu.Lock() 130 | defer lru.mu.Unlock() 131 | if lastElem := lru.list.Back(); lastElem != nil { 132 | oldest = lastElem.Value.(*entry).time_accessed 133 | } 134 | return uint64(lru.list.Len()), lru.size, lru.capacity, oldest 135 | } 136 | 137 | func (lru *LRUCache) StatsJSON() string { 138 | if lru == nil { 139 | return "{}" 140 | } 141 | l, s, c, o := lru.Stats() 142 | return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"OldestAccess\": \"%v\"}", l, s, c, o) 143 | } 144 | 145 | func (lru *LRUCache) Keys() []string { 146 | lru.mu.Lock() 147 | defer lru.mu.Unlock() 148 | 149 | keys := make([]string, 0, lru.list.Len()) 150 | for e := lru.list.Front(); e != nil; e = e.Next() { 151 | keys = append(keys, e.Value.(*entry).key) 152 | } 153 | return keys 154 | } 155 | 156 | func (lru *LRUCache) Items() []Item { 157 | lru.mu.Lock() 158 | defer lru.mu.Unlock() 159 | 160 | items := make([]Item, 0, lru.list.Len()) 161 | for e := lru.list.Front(); e != nil; e = e.Next() { 162 | v := e.Value.(*entry) 163 | items = append(items, Item{Key: v.key, Value: v.value}) 164 | } 165 | return items 166 | } 167 | 168 | func (lru *LRUCache) SaveItems(w io.Writer) error { 169 | items := lru.Items() 170 | encoder := gob.NewEncoder(w) 171 | return encoder.Encode(items) 172 | } 173 | 174 | func (lru *LRUCache) SaveItemsToFile(path string) error { 175 | if wr, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { 176 | return err 177 | } else { 178 | defer wr.Close() 179 | return lru.SaveItems(wr) 180 | } 181 | } 182 | 183 | func (lru *LRUCache) LoadItems(r io.Reader) error { 184 | items := make([]Item, 0) 185 | decoder := gob.NewDecoder(r) 186 | if err := decoder.Decode(&items); err != nil { 187 | return err 188 | } 189 | 190 | lru.mu.Lock() 191 | defer lru.mu.Unlock() 192 | for _, item := range items { 193 | // XXX: copied from Set() 194 | if element := lru.table[item.Key]; element != nil { 195 | lru.updateInplace(element, item.Value) 196 | } else { 197 | lru.addNew(item.Key, item.Value) 198 | } 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func (lru *LRUCache) LoadItemsFromFile(path string) error { 205 | if rd, err := os.Open(path); err != nil { 206 | return err 207 | } else { 208 | defer rd.Close() 209 | return lru.LoadItems(rd) 210 | } 211 | } 212 | 213 | func (lru *LRUCache) updateInplace(element *list.Element, value Value) { 214 | valueSize := value.Size() 215 | sizeDiff := valueSize - element.Value.(*entry).size 216 | element.Value.(*entry).value = value 217 | element.Value.(*entry).size = valueSize 218 | lru.size += uint64(sizeDiff) 219 | lru.moveToFront(element) 220 | lru.checkCapacity() 221 | } 222 | 223 | func (lru *LRUCache) moveToFront(element *list.Element) { 224 | lru.list.MoveToFront(element) 225 | element.Value.(*entry).time_accessed = time.Now() 226 | } 227 | 228 | func (lru *LRUCache) addNew(key string, value Value) { 229 | newEntry := &entry{key, value, value.Size(), time.Now()} 230 | element := lru.list.PushFront(newEntry) 231 | lru.table[key] = element 232 | lru.size += uint64(newEntry.size) 233 | lru.checkCapacity() 234 | } 235 | 236 | func (lru *LRUCache) checkCapacity() { 237 | // Partially duplicated from Delete 238 | for lru.size > lru.capacity { 239 | delElem := lru.list.Back() 240 | delValue := delElem.Value.(*entry) 241 | lru.list.Remove(delElem) 242 | delete(lru.table, delValue.key) 243 | lru.size -= uint64(delValue.size) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/ngrok/client/cli.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "ngrok/version" 7 | "os" 8 | ) 9 | 10 | const usage1 string = `Usage: %s [OPTIONS] 11 | Options: 12 | ` 13 | 14 | const usage2 string = ` 15 | Examples: 16 | ngrok 80 17 | ngrok -subdomain=example 8080 18 | ngrok -proto=tcp 22 19 | ngrok -hostname="example.com" -httpauth="user:password" 10.0.0.1 20 | 21 | 22 | Advanced usage: ngrok [OPTIONS] [command args] [...] 23 | Commands: 24 | ngrok start [tunnel] [...] Start tunnels by name from config file 25 | ngrok list List tunnel names from config file 26 | ngrok help Print help 27 | ngrok version Print ngrok version 28 | 29 | Examples: 30 | ngrok start www api blog pubsub 31 | ngrok -log=stdout -config=ngrok.yml start ssh 32 | ngrok version 33 | 34 | ` 35 | 36 | type Options struct { 37 | config string 38 | logto string 39 | loglevel string 40 | authtoken string 41 | httpauth string 42 | hostname string 43 | protocol string 44 | subdomain string 45 | command string 46 | args []string 47 | } 48 | 49 | func ParseArgs() (opts *Options, err error) { 50 | flag.Usage = func() { 51 | fmt.Fprintf(os.Stderr, usage1, os.Args[0]) 52 | flag.PrintDefaults() 53 | fmt.Fprintf(os.Stderr, usage2) 54 | } 55 | 56 | config := flag.String( 57 | "config", 58 | "", 59 | "Path to ngrok configuration file. (default: $HOME/.ngrok)") 60 | 61 | logto := flag.String( 62 | "log", 63 | "none", 64 | "Write log messages to this file. 'stdout' and 'none' have special meanings") 65 | 66 | loglevel := flag.String( 67 | "log-level", 68 | "DEBUG", 69 | "The level of messages to log. One of: DEBUG, INFO, WARNING, ERROR") 70 | 71 | authtoken := flag.String( 72 | "authtoken", 73 | "", 74 | "Authentication token for identifying an ngrok.com account") 75 | 76 | httpauth := flag.String( 77 | "httpauth", 78 | "", 79 | "username:password HTTP basic auth creds protecting the public tunnel endpoint") 80 | 81 | subdomain := flag.String( 82 | "subdomain", 83 | "", 84 | "Request a custom subdomain from the ngrok server. (HTTP only)") 85 | 86 | hostname := flag.String( 87 | "hostname", 88 | "", 89 | "Request a custom hostname from the ngrok server. (HTTP only) (requires CNAME of your DNS)") 90 | 91 | protocol := flag.String( 92 | "proto", 93 | "http+https", 94 | "The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')") 95 | 96 | flag.Parse() 97 | 98 | opts = &Options{ 99 | config: *config, 100 | logto: *logto, 101 | loglevel: *loglevel, 102 | httpauth: *httpauth, 103 | subdomain: *subdomain, 104 | protocol: *protocol, 105 | authtoken: *authtoken, 106 | hostname: *hostname, 107 | command: flag.Arg(0), 108 | } 109 | 110 | switch opts.command { 111 | case "list": 112 | opts.args = flag.Args()[1:] 113 | case "start": 114 | opts.args = flag.Args()[1:] 115 | case "version": 116 | fmt.Println(version.MajorMinor()) 117 | os.Exit(0) 118 | case "help": 119 | flag.Usage() 120 | os.Exit(0) 121 | case "": 122 | err = fmt.Errorf("Error: Specify a local port to tunnel to, or " + 123 | "an ngrok command.\n\nExample: To expose port 80, run " + 124 | "'ngrok 80'") 125 | return 126 | 127 | default: 128 | if len(flag.Args()) > 1 { 129 | err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v", 130 | len(flag.Args()), 131 | flag.Args()) 132 | return 133 | } 134 | 135 | opts.command = "default" 136 | opts.args = flag.Args() 137 | } 138 | 139 | return 140 | } 141 | -------------------------------------------------------------------------------- /src/ngrok/client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v1" 6 | "io/ioutil" 7 | "net" 8 | "net/url" 9 | "ngrok/log" 10 | "os" 11 | "os/user" 12 | "path" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | type Configuration struct { 19 | HttpProxy string `yaml:"http_proxy,omitempty"` 20 | ServerAddr string `yaml:"server_addr,omitempty"` 21 | InspectAddr string `yaml:"inspect_addr,omitempty"` 22 | TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"` 23 | AuthToken string `yaml:"auth_token,omitempty"` 24 | Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"` 25 | LogTo string `yaml:"-"` 26 | Path string `yaml:"-"` 27 | } 28 | 29 | type TunnelConfiguration struct { 30 | Subdomain string `yaml:"subdomain,omitempty"` 31 | Hostname string `yaml:"hostname,omitempty"` 32 | Protocols map[string]string `yaml:"proto,omitempty"` 33 | HttpAuth string `yaml:"auth,omitempty"` 34 | RemotePort uint16 `yaml:"remote_port,omitempty"` 35 | } 36 | 37 | func LoadConfiguration(opts *Options) (config *Configuration, err error) { 38 | configPath := opts.config 39 | if configPath == "" { 40 | configPath = defaultPath() 41 | } 42 | 43 | log.Info("Reading configuration file %s", configPath) 44 | configBuf, err := ioutil.ReadFile(configPath) 45 | if err != nil { 46 | // failure to read a configuration file is only a fatal error if 47 | // the user specified one explicitly 48 | if opts.config != "" { 49 | err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err) 50 | return 51 | } 52 | } 53 | 54 | // deserialize/parse the config 55 | config = new(Configuration) 56 | if err = yaml.Unmarshal(configBuf, &config); err != nil { 57 | err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err) 58 | return 59 | } 60 | 61 | // try to parse the old .ngrok format for backwards compatibility 62 | matched := false 63 | content := strings.TrimSpace(string(configBuf)) 64 | if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil { 65 | return 66 | } else if matched { 67 | config = &Configuration{AuthToken: content} 68 | } 69 | 70 | // set configuration defaults 71 | if config.ServerAddr == "" { 72 | config.ServerAddr = defaultServerAddr 73 | } 74 | 75 | if config.InspectAddr == "" { 76 | config.InspectAddr = "127.0.0.1:4040" 77 | } 78 | 79 | if config.HttpProxy == "" { 80 | config.HttpProxy = os.Getenv("http_proxy") 81 | } 82 | 83 | // validate and normalize configuration 84 | if config.InspectAddr != "disabled" { 85 | if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil { 86 | return 87 | } 88 | } 89 | 90 | if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil { 91 | return 92 | } 93 | 94 | if config.HttpProxy != "" { 95 | var proxyUrl *url.URL 96 | if proxyUrl, err = url.Parse(config.HttpProxy); err != nil { 97 | return 98 | } else { 99 | if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" { 100 | err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme) 101 | return 102 | } 103 | } 104 | } 105 | 106 | for name, t := range config.Tunnels { 107 | if t == nil || t.Protocols == nil || len(t.Protocols) == 0 { 108 | err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name) 109 | return 110 | } 111 | 112 | for k, addr := range t.Protocols { 113 | tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k) 114 | if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil { 115 | return 116 | } 117 | 118 | if err = validateProtocol(k, tunnelName); err != nil { 119 | return 120 | } 121 | } 122 | 123 | // use the name of the tunnel as the subdomain if none is specified 124 | if t.Hostname == "" && t.Subdomain == "" { 125 | // XXX: a crude heuristic, really we should be checking if the last part 126 | // is a TLD 127 | if len(strings.Split(name, ".")) > 1 { 128 | t.Hostname = name 129 | } else { 130 | t.Subdomain = name 131 | } 132 | } 133 | } 134 | 135 | // override configuration with command-line options 136 | config.LogTo = opts.logto 137 | config.Path = configPath 138 | if opts.authtoken != "" { 139 | config.AuthToken = opts.authtoken 140 | } 141 | 142 | switch opts.command { 143 | // start a single tunnel, the default, simple ngrok behavior 144 | case "default": 145 | config.Tunnels = make(map[string]*TunnelConfiguration) 146 | config.Tunnels["default"] = &TunnelConfiguration{ 147 | Subdomain: opts.subdomain, 148 | Hostname: opts.hostname, 149 | HttpAuth: opts.httpauth, 150 | Protocols: make(map[string]string), 151 | } 152 | 153 | for _, proto := range strings.Split(opts.protocol, "+") { 154 | if err = validateProtocol(proto, "default"); err != nil { 155 | return 156 | } 157 | 158 | if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil { 159 | return 160 | } 161 | } 162 | 163 | // list tunnels 164 | case "list": 165 | for name, _ := range config.Tunnels { 166 | fmt.Println(name) 167 | } 168 | os.Exit(0) 169 | 170 | // start tunnels 171 | case "start": 172 | if len(opts.args) == 0 { 173 | err = fmt.Errorf("You must specify at least one tunnel to start") 174 | return 175 | } 176 | 177 | requestedTunnels := make(map[string]bool) 178 | for _, arg := range opts.args { 179 | requestedTunnels[arg] = true 180 | 181 | if _, ok := config.Tunnels[arg]; !ok { 182 | err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg) 183 | return 184 | } 185 | } 186 | 187 | for name, _ := range config.Tunnels { 188 | if !requestedTunnels[name] { 189 | delete(config.Tunnels, name) 190 | } 191 | } 192 | 193 | default: 194 | err = fmt.Errorf("Unknown command: %s", opts.command) 195 | return 196 | } 197 | 198 | return 199 | } 200 | 201 | func defaultPath() string { 202 | user, err := user.Current() 203 | 204 | // user.Current() does not work on linux when cross compiling because 205 | // it requires CGO; use os.Getenv("HOME") hack until we compile natively 206 | homeDir := os.Getenv("HOME") 207 | if err != nil { 208 | log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir) 209 | } else { 210 | homeDir = user.HomeDir 211 | } 212 | 213 | return path.Join(homeDir, ".ngrok") 214 | } 215 | 216 | func normalizeAddress(addr string, propName string) (string, error) { 217 | // normalize port to address 218 | if _, err := strconv.Atoi(addr); err == nil { 219 | addr = ":" + addr 220 | } 221 | 222 | host, port, err := net.SplitHostPort(addr) 223 | if err != nil { 224 | return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error()) 225 | } 226 | 227 | if host == "" { 228 | host = "127.0.0.1" 229 | } 230 | 231 | return fmt.Sprintf("%s:%s", host, port), nil 232 | } 233 | 234 | func validateProtocol(proto, propName string) (err error) { 235 | switch proto { 236 | case "http", "https", "http+https", "tcp": 237 | default: 238 | err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto) 239 | } 240 | 241 | return 242 | } 243 | 244 | func SaveAuthToken(configPath, authtoken string) (err error) { 245 | // empty configuration by default for the case that we can't read it 246 | c := new(Configuration) 247 | 248 | // read the configuration 249 | oldConfigBytes, err := ioutil.ReadFile(configPath) 250 | if err == nil { 251 | // unmarshal if we successfully read the configuration file 252 | if err = yaml.Unmarshal(oldConfigBytes, c); err != nil { 253 | return 254 | } 255 | } 256 | 257 | // no need to save, the authtoken is already the correct value 258 | if c.AuthToken == authtoken { 259 | return 260 | } 261 | 262 | // update auth token 263 | c.AuthToken = authtoken 264 | 265 | // rewrite configuration 266 | newConfigBytes, err := yaml.Marshal(c) 267 | if err != nil { 268 | return 269 | } 270 | 271 | err = ioutil.WriteFile(configPath, newConfigBytes, 0600) 272 | return 273 | } 274 | -------------------------------------------------------------------------------- /src/ngrok/client/controller.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "ngrok/client/mvc" 6 | "ngrok/client/views/term" 7 | "ngrok/client/views/web" 8 | "ngrok/log" 9 | "ngrok/proto" 10 | "ngrok/util" 11 | "sync" 12 | ) 13 | 14 | type command interface{} 15 | 16 | type cmdQuit struct { 17 | // display this message after quit 18 | message string 19 | } 20 | 21 | type cmdPlayRequest struct { 22 | // the tunnel to play this request over 23 | tunnel mvc.Tunnel 24 | 25 | // the bytes of the request to issue 26 | payload []byte 27 | } 28 | 29 | // The MVC Controller 30 | type Controller struct { 31 | // Controller logger 32 | log.Logger 33 | 34 | // the model sends updates through this broadcast channel 35 | updates *util.Broadcast 36 | 37 | // the model 38 | model mvc.Model 39 | 40 | // the views 41 | views []mvc.View 42 | 43 | // internal structure to issue commands to the controller 44 | cmds chan command 45 | 46 | // internal structure to synchronize access to State object 47 | state chan mvc.State 48 | 49 | // options 50 | config *Configuration 51 | } 52 | 53 | // public interface 54 | func NewController() *Controller { 55 | ctl := &Controller{ 56 | Logger: log.NewPrefixLogger("controller"), 57 | updates: util.NewBroadcast(), 58 | cmds: make(chan command), 59 | views: make([]mvc.View, 0), 60 | state: make(chan mvc.State), 61 | } 62 | 63 | return ctl 64 | } 65 | 66 | func (ctl *Controller) State() mvc.State { 67 | return <-ctl.state 68 | } 69 | 70 | func (ctl *Controller) Update(state mvc.State) { 71 | ctl.updates.In() <- state 72 | } 73 | 74 | func (ctl *Controller) Updates() *util.Broadcast { 75 | return ctl.updates 76 | } 77 | 78 | func (ctl *Controller) Shutdown(message string) { 79 | ctl.cmds <- cmdQuit{message: message} 80 | } 81 | 82 | func (ctl *Controller) PlayRequest(tunnel mvc.Tunnel, payload []byte) { 83 | ctl.cmds <- cmdPlayRequest{tunnel: tunnel, payload: payload} 84 | } 85 | 86 | func (ctl *Controller) Go(fn func()) { 87 | go func() { 88 | defer func() { 89 | if r := recover(); r != nil { 90 | err := util.MakePanicTrace(r) 91 | ctl.Error(err) 92 | ctl.Shutdown(err) 93 | } 94 | }() 95 | 96 | fn() 97 | }() 98 | } 99 | 100 | // private functions 101 | func (ctl *Controller) doShutdown() { 102 | ctl.Info("Shutting down") 103 | 104 | var wg sync.WaitGroup 105 | 106 | // wait for all of the views, plus the model 107 | wg.Add(len(ctl.views) + 1) 108 | 109 | for _, v := range ctl.views { 110 | vClosure := v 111 | ctl.Go(func() { 112 | vClosure.Shutdown() 113 | wg.Done() 114 | }) 115 | } 116 | 117 | ctl.Go(func() { 118 | ctl.model.Shutdown() 119 | wg.Done() 120 | }) 121 | 122 | wg.Wait() 123 | } 124 | 125 | func (ctl *Controller) AddView(v mvc.View) { 126 | ctl.views = append(ctl.views, v) 127 | } 128 | 129 | func (ctl *Controller) GetWebInspectAddr() string { 130 | return ctl.config.InspectAddr 131 | } 132 | 133 | func (ctl *Controller) SetupModel(config *Configuration) *ClientModel { 134 | model := newClientModel(config, ctl) 135 | ctl.model = model 136 | return model 137 | } 138 | 139 | func (ctl *Controller) GetModel() *ClientModel { 140 | return ctl.model.(*ClientModel) 141 | } 142 | 143 | func (ctl *Controller) Run(config *Configuration) { 144 | // Save the configuration 145 | ctl.config = config 146 | 147 | var model *ClientModel 148 | 149 | if ctl.model == nil { 150 | model = ctl.SetupModel(config) 151 | } else { 152 | model = ctl.model.(*ClientModel) 153 | } 154 | 155 | // init the model 156 | var state mvc.State = model 157 | 158 | // init web ui 159 | var webView *web.WebView 160 | if config.InspectAddr != "disabled" { 161 | webView = web.NewWebView(ctl, config.InspectAddr) 162 | ctl.AddView(webView) 163 | } 164 | 165 | // init term ui 166 | var termView *term.TermView 167 | if config.LogTo != "stdout" { 168 | termView = term.NewTermView(ctl) 169 | ctl.AddView(termView) 170 | } 171 | 172 | for _, protocol := range model.GetProtocols() { 173 | switch p := protocol.(type) { 174 | case *proto.Http: 175 | if termView != nil { 176 | ctl.AddView(termView.NewHttpView(p)) 177 | } 178 | 179 | if webView != nil { 180 | ctl.AddView(webView.NewHttpView(p)) 181 | } 182 | default: 183 | } 184 | } 185 | 186 | ctl.Go(func() { autoUpdate(state, config.AuthToken) }) 187 | ctl.Go(ctl.model.Run) 188 | 189 | updates := ctl.updates.Reg() 190 | defer ctl.updates.UnReg(updates) 191 | 192 | done := make(chan int) 193 | for { 194 | select { 195 | case obj := <-ctl.cmds: 196 | switch cmd := obj.(type) { 197 | case cmdQuit: 198 | msg := cmd.message 199 | go func() { 200 | ctl.doShutdown() 201 | fmt.Println(msg) 202 | done <- 1 203 | }() 204 | 205 | case cmdPlayRequest: 206 | ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) }) 207 | } 208 | 209 | case obj := <-updates: 210 | state = obj.(mvc.State) 211 | 212 | case ctl.state <- state: 213 | case <-done: 214 | return 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/ngrok/client/debug.go: -------------------------------------------------------------------------------- 1 | // +build !release 2 | 3 | package client 4 | 5 | var ( 6 | rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"} 7 | ) 8 | 9 | // no server name in debug builds so that when you connect it will always work 10 | func serverName(addr string) string { 11 | return "" 12 | } 13 | -------------------------------------------------------------------------------- /src/ngrok/client/main.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/inconshreveable/mousetrap" 6 | "math/rand" 7 | "ngrok/log" 8 | "ngrok/util" 9 | "os" 10 | "runtime" 11 | "time" 12 | ) 13 | 14 | func init() { 15 | if runtime.GOOS == "windows" { 16 | if mousetrap.StartedByExplorer() { 17 | fmt.Println("Don't double-click ngrok!") 18 | fmt.Println("You need to open cmd.exe and run it from the command line!") 19 | time.Sleep(5 * time.Second) 20 | os.Exit(1) 21 | } 22 | } 23 | } 24 | 25 | func Main() { 26 | // parse options 27 | opts, err := ParseArgs() 28 | if err != nil { 29 | fmt.Println(err) 30 | os.Exit(1) 31 | } 32 | 33 | // set up logging 34 | log.LogTo(opts.logto, opts.loglevel) 35 | 36 | // read configuration file 37 | config, err := LoadConfiguration(opts) 38 | if err != nil { 39 | fmt.Println(err) 40 | os.Exit(1) 41 | } 42 | 43 | // seed random number generator 44 | seed, err := util.RandomSeed() 45 | if err != nil { 46 | fmt.Printf("Couldn't securely seed the random number generator!") 47 | os.Exit(1) 48 | } 49 | rand.Seed(seed) 50 | 51 | NewController().Run(config) 52 | } 53 | -------------------------------------------------------------------------------- /src/ngrok/client/metrics.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | metrics "github.com/rcrowley/go-metrics" 5 | ) 6 | 7 | const ( 8 | sampleSize int = 1028 9 | sampleAlpha float64 = 0.015 10 | ) 11 | 12 | type ClientMetrics struct { 13 | // metrics 14 | connGauge metrics.Gauge 15 | connMeter metrics.Meter 16 | connTimer metrics.Timer 17 | proxySetupTimer metrics.Timer 18 | bytesIn metrics.Histogram 19 | bytesOut metrics.Histogram 20 | bytesInCount metrics.Counter 21 | bytesOutCount metrics.Counter 22 | } 23 | 24 | func NewClientMetrics() *ClientMetrics { 25 | return &ClientMetrics{ 26 | connGauge: metrics.NewGauge(), 27 | connMeter: metrics.NewMeter(), 28 | connTimer: metrics.NewTimer(), 29 | proxySetupTimer: metrics.NewTimer(), 30 | bytesIn: metrics.NewHistogram(metrics.NewExpDecaySample(sampleSize, sampleAlpha)), 31 | bytesOut: metrics.NewHistogram(metrics.NewExpDecaySample(sampleSize, sampleAlpha)), 32 | bytesInCount: metrics.NewCounter(), 33 | bytesOutCount: metrics.NewCounter(), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ngrok/client/model.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | metrics "github.com/rcrowley/go-metrics" 7 | "io/ioutil" 8 | "math" 9 | "ngrok/client/mvc" 10 | "ngrok/conn" 11 | "ngrok/log" 12 | "ngrok/msg" 13 | "ngrok/proto" 14 | "ngrok/util" 15 | "ngrok/version" 16 | "runtime" 17 | "strings" 18 | "sync/atomic" 19 | "time" 20 | ) 21 | 22 | const ( 23 | defaultServerAddr = "ngrokd.ngrok.com:443" 24 | pingInterval = 20 * time.Second 25 | maxPongLatency = 15 * time.Second 26 | updateCheckInterval = 6 * time.Hour 27 | BadGateway = ` 28 | 29 |
30 |

Tunnel %s unavailable

31 |

Unable to initiate connection to %s. A web server must be running on port %s to complete the tunnel.

32 | ` 33 | ) 34 | 35 | type ClientModel struct { 36 | log.Logger 37 | 38 | id string 39 | tunnels map[string]mvc.Tunnel 40 | serverVersion string 41 | metrics *ClientMetrics 42 | updateStatus mvc.UpdateStatus 43 | connStatus mvc.ConnStatus 44 | protoMap map[string]proto.Protocol 45 | protocols []proto.Protocol 46 | ctl mvc.Controller 47 | serverAddr string 48 | proxyUrl string 49 | authToken string 50 | tlsConfig *tls.Config 51 | tunnelConfig map[string]*TunnelConfiguration 52 | configPath string 53 | } 54 | 55 | func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel { 56 | protoMap := make(map[string]proto.Protocol) 57 | protoMap["http"] = proto.NewHttp() 58 | protoMap["https"] = protoMap["http"] 59 | protoMap["tcp"] = proto.NewTcp() 60 | protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]} 61 | 62 | m := &ClientModel{ 63 | Logger: log.NewPrefixLogger("client"), 64 | 65 | // server address 66 | serverAddr: config.ServerAddr, 67 | 68 | // proxy address 69 | proxyUrl: config.HttpProxy, 70 | 71 | // auth token 72 | authToken: config.AuthToken, 73 | 74 | // connection status 75 | connStatus: mvc.ConnConnecting, 76 | 77 | // update status 78 | updateStatus: mvc.UpdateNone, 79 | 80 | // metrics 81 | metrics: NewClientMetrics(), 82 | 83 | // protocols 84 | protoMap: protoMap, 85 | 86 | // protocol list 87 | protocols: protocols, 88 | 89 | // open tunnels 90 | tunnels: make(map[string]mvc.Tunnel), 91 | 92 | // controller 93 | ctl: ctl, 94 | 95 | // tunnel configuration 96 | tunnelConfig: config.Tunnels, 97 | 98 | // config path 99 | configPath: config.Path, 100 | } 101 | 102 | // configure TLS 103 | if config.TrustHostRootCerts { 104 | m.Info("Trusting host's root certificates") 105 | m.tlsConfig = &tls.Config{} 106 | } else { 107 | m.Info("Trusting root CAs: %v", rootCrtPaths) 108 | var err error 109 | if m.tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil { 110 | panic(err) 111 | } 112 | } 113 | 114 | // configure TLS SNI 115 | m.tlsConfig.ServerName = serverName(m.serverAddr) 116 | 117 | return m 118 | } 119 | 120 | // mvc.State interface 121 | func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols } 122 | func (c ClientModel) GetClientVersion() string { return version.MajorMinor() } 123 | func (c ClientModel) GetServerVersion() string { return c.serverVersion } 124 | func (c ClientModel) GetTunnels() []mvc.Tunnel { 125 | tunnels := make([]mvc.Tunnel, 0) 126 | for _, t := range c.tunnels { 127 | tunnels = append(tunnels, t) 128 | } 129 | return tunnels 130 | } 131 | func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus } 132 | func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus } 133 | 134 | func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) { 135 | return c.metrics.connMeter, c.metrics.connTimer 136 | } 137 | 138 | func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) { 139 | return c.metrics.bytesInCount, c.metrics.bytesIn 140 | } 141 | 142 | func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) { 143 | return c.metrics.bytesOutCount, c.metrics.bytesOut 144 | } 145 | func (c ClientModel) SetUpdateStatus(updateStatus mvc.UpdateStatus) { 146 | c.updateStatus = updateStatus 147 | c.update() 148 | } 149 | 150 | // mvc.Model interface 151 | func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) { 152 | var localConn conn.Conn 153 | localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) 154 | if err != nil { 155 | c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err) 156 | return 157 | } 158 | 159 | defer localConn.Close() 160 | localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"}) 161 | localConn.Write(payload) 162 | ioutil.ReadAll(localConn) 163 | } 164 | 165 | func (c *ClientModel) Shutdown() { 166 | } 167 | 168 | func (c *ClientModel) update() { 169 | c.ctl.Update(c) 170 | } 171 | 172 | func (c *ClientModel) Run() { 173 | // how long we should wait before we reconnect 174 | maxWait := 30 * time.Second 175 | wait := 1 * time.Second 176 | 177 | for { 178 | // run the control channel 179 | c.control() 180 | 181 | // control only returns when a failure has occurred, so we're going to try to reconnect 182 | if c.connStatus == mvc.ConnOnline { 183 | wait = 1 * time.Second 184 | } 185 | 186 | log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds())) 187 | time.Sleep(wait) 188 | // exponentially increase wait time 189 | wait = 2 * wait 190 | wait = time.Duration(math.Min(float64(wait), float64(maxWait))) 191 | c.connStatus = mvc.ConnReconnecting 192 | c.update() 193 | } 194 | } 195 | 196 | // Establishes and manages a tunnel control connection with the server 197 | func (c *ClientModel) control() { 198 | defer func() { 199 | if r := recover(); r != nil { 200 | log.Error("control recovering from failure %v", r) 201 | } 202 | }() 203 | 204 | // establish control channel 205 | var ( 206 | ctlConn conn.Conn 207 | err error 208 | ) 209 | if c.proxyUrl == "" { 210 | // simple non-proxied case, just connect to the server 211 | ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig) 212 | } else { 213 | ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig) 214 | } 215 | if err != nil { 216 | panic(err) 217 | } 218 | defer ctlConn.Close() 219 | 220 | // authenticate with the server 221 | auth := &msg.Auth{ 222 | ClientId: c.id, 223 | OS: runtime.GOOS, 224 | Arch: runtime.GOARCH, 225 | Version: version.Proto, 226 | MmVersion: version.MajorMinor(), 227 | User: c.authToken, 228 | } 229 | 230 | if err = msg.WriteMsg(ctlConn, auth); err != nil { 231 | panic(err) 232 | } 233 | 234 | // wait for the server to authenticate us 235 | var authResp msg.AuthResp 236 | if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil { 237 | panic(err) 238 | } 239 | 240 | if authResp.Error != "" { 241 | emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error) 242 | c.ctl.Shutdown(emsg) 243 | return 244 | } 245 | 246 | c.id = authResp.ClientId 247 | c.serverVersion = authResp.MmVersion 248 | c.Info("Authenticated with server, client id: %v", c.id) 249 | c.update() 250 | if err = SaveAuthToken(c.configPath, c.authToken); err != nil { 251 | c.Error("Failed to save auth token: %v", err) 252 | } 253 | 254 | // request tunnels 255 | reqIdToTunnelConfig := make(map[string]*TunnelConfiguration) 256 | for _, config := range c.tunnelConfig { 257 | // create the protocol list to ask for 258 | var protocols []string 259 | for proto, _ := range config.Protocols { 260 | protocols = append(protocols, proto) 261 | } 262 | 263 | reqTunnel := &msg.ReqTunnel{ 264 | ReqId: util.RandId(8), 265 | Protocol: strings.Join(protocols, "+"), 266 | Hostname: config.Hostname, 267 | Subdomain: config.Subdomain, 268 | HttpAuth: config.HttpAuth, 269 | RemotePort: config.RemotePort, 270 | } 271 | 272 | // send the tunnel request 273 | if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil { 274 | panic(err) 275 | } 276 | 277 | // save request id association so we know which local address 278 | // to proxy to later 279 | reqIdToTunnelConfig[reqTunnel.ReqId] = config 280 | } 281 | 282 | // start the heartbeat 283 | lastPong := time.Now().UnixNano() 284 | c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) }) 285 | 286 | // main control loop 287 | for { 288 | var rawMsg msg.Message 289 | if rawMsg, err = msg.ReadMsg(ctlConn); err != nil { 290 | panic(err) 291 | } 292 | 293 | switch m := rawMsg.(type) { 294 | case *msg.ReqProxy: 295 | c.ctl.Go(c.proxy) 296 | 297 | case *msg.Pong: 298 | atomic.StoreInt64(&lastPong, time.Now().UnixNano()) 299 | 300 | case *msg.NewTunnel: 301 | if m.Error != "" { 302 | emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error) 303 | c.Error(emsg) 304 | c.ctl.Shutdown(emsg) 305 | continue 306 | } 307 | 308 | tunnel := mvc.Tunnel{ 309 | PublicUrl: m.Url, 310 | LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol], 311 | Protocol: c.protoMap[m.Protocol], 312 | } 313 | 314 | c.tunnels[tunnel.PublicUrl] = tunnel 315 | c.connStatus = mvc.ConnOnline 316 | c.Info("Tunnel established at %v", tunnel.PublicUrl) 317 | c.update() 318 | 319 | default: 320 | ctlConn.Warn("Ignoring unknown control message %v ", m) 321 | } 322 | } 323 | } 324 | 325 | // Establishes and manages a tunnel proxy connection with the server 326 | func (c *ClientModel) proxy() { 327 | var ( 328 | remoteConn conn.Conn 329 | err error 330 | ) 331 | 332 | if c.proxyUrl == "" { 333 | remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig) 334 | } else { 335 | remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig) 336 | } 337 | 338 | if err != nil { 339 | log.Error("Failed to establish proxy connection: %v", err) 340 | return 341 | } 342 | defer remoteConn.Close() 343 | 344 | err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id}) 345 | if err != nil { 346 | remoteConn.Error("Failed to write RegProxy: %v", err) 347 | return 348 | } 349 | 350 | // wait for the server to ack our register 351 | var startPxy msg.StartProxy 352 | if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil { 353 | remoteConn.Error("Server failed to write StartProxy: %v", err) 354 | return 355 | } 356 | 357 | tunnel, ok := c.tunnels[startPxy.Url] 358 | if !ok { 359 | remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url) 360 | return 361 | } 362 | 363 | // start up the private connection 364 | start := time.Now() 365 | localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) 366 | if err != nil { 367 | remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err) 368 | 369 | if tunnel.Protocol.GetName() == "http" { 370 | // try to be helpful when you're in HTTP mode and a human might see the output 371 | badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr) 372 | remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway 373 | Content-Type: text/html 374 | Content-Length: %d 375 | 376 | %s`, len(badGatewayBody), badGatewayBody))) 377 | } 378 | return 379 | } 380 | defer localConn.Close() 381 | 382 | m := c.metrics 383 | m.proxySetupTimer.Update(time.Since(start)) 384 | m.connMeter.Mark(1) 385 | c.update() 386 | m.connTimer.Time(func() { 387 | localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr}) 388 | bytesIn, bytesOut := conn.Join(localConn, remoteConn) 389 | m.bytesIn.Update(bytesIn) 390 | m.bytesOut.Update(bytesOut) 391 | m.bytesInCount.Inc(bytesIn) 392 | m.bytesOutCount.Inc(bytesOut) 393 | }) 394 | c.update() 395 | } 396 | 397 | // Hearbeating to ensure our connection ngrokd is still live 398 | func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) { 399 | lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0) 400 | ping := time.NewTicker(pingInterval) 401 | pongCheck := time.NewTicker(time.Second) 402 | 403 | defer func() { 404 | conn.Close() 405 | ping.Stop() 406 | pongCheck.Stop() 407 | }() 408 | 409 | for { 410 | select { 411 | case <-pongCheck.C: 412 | lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr)) 413 | needPong := lastPong.Sub(lastPing) < 0 414 | pongLatency := time.Since(lastPing) 415 | 416 | if needPong && pongLatency > maxPongLatency { 417 | c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong) 418 | c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds())) 419 | return 420 | } 421 | 422 | case <-ping.C: 423 | err := msg.WriteMsg(conn, &msg.Ping{}) 424 | if err != nil { 425 | conn.Debug("Got error %v when writing PingMsg", err) 426 | return 427 | } 428 | lastPing = time.Now() 429 | } 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/ngrok/client/mvc/controller.go: -------------------------------------------------------------------------------- 1 | package mvc 2 | 3 | import ( 4 | "ngrok/util" 5 | ) 6 | 7 | type Controller interface { 8 | // how the model communicates that it has changed state 9 | Update(State) 10 | 11 | // instructs the controller to shut the app down 12 | Shutdown(message string) 13 | 14 | // PlayRequest instructs the model to play requests 15 | PlayRequest(tunnel Tunnel, payload []byte) 16 | 17 | // A channel of updates 18 | Updates() *util.Broadcast 19 | 20 | // returns the current state 21 | State() State 22 | 23 | // safe wrapper for running go-routines 24 | Go(fn func()) 25 | 26 | // the address where the web inspection interface is running 27 | GetWebInspectAddr() string 28 | } 29 | -------------------------------------------------------------------------------- /src/ngrok/client/mvc/model.go: -------------------------------------------------------------------------------- 1 | package mvc 2 | 3 | type Model interface { 4 | Run() 5 | 6 | Shutdown() 7 | 8 | PlayRequest(tunnel Tunnel, payload []byte) 9 | } 10 | -------------------------------------------------------------------------------- /src/ngrok/client/mvc/state.go: -------------------------------------------------------------------------------- 1 | package mvc 2 | 3 | import ( 4 | metrics "github.com/rcrowley/go-metrics" 5 | "ngrok/proto" 6 | ) 7 | 8 | type UpdateStatus int 9 | 10 | const ( 11 | UpdateNone = -1 * iota 12 | UpdateInstalling 13 | UpdateReady 14 | UpdateAvailable 15 | ) 16 | 17 | type ConnStatus int 18 | 19 | const ( 20 | ConnConnecting = iota 21 | ConnReconnecting 22 | ConnOnline 23 | ) 24 | 25 | type Tunnel struct { 26 | PublicUrl string 27 | Protocol proto.Protocol 28 | LocalAddr string 29 | } 30 | 31 | type ConnectionContext struct { 32 | Tunnel Tunnel 33 | ClientAddr string 34 | } 35 | 36 | type State interface { 37 | GetClientVersion() string 38 | GetServerVersion() string 39 | GetTunnels() []Tunnel 40 | GetProtocols() []proto.Protocol 41 | GetUpdateStatus() UpdateStatus 42 | GetConnStatus() ConnStatus 43 | GetConnectionMetrics() (metrics.Meter, metrics.Timer) 44 | GetBytesInMetrics() (metrics.Counter, metrics.Histogram) 45 | GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) 46 | SetUpdateStatus(UpdateStatus) 47 | } 48 | -------------------------------------------------------------------------------- /src/ngrok/client/mvc/view.go: -------------------------------------------------------------------------------- 1 | package mvc 2 | 3 | type View interface { 4 | Shutdown() 5 | } 6 | -------------------------------------------------------------------------------- /src/ngrok/client/release.go: -------------------------------------------------------------------------------- 1 | // +build release 2 | 3 | package client 4 | 5 | import "net" 6 | 7 | var ( 8 | rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"} 9 | ) 10 | 11 | // server name in release builds is the host part of the server address 12 | func serverName(addr string) string { 13 | host, _, err := net.SplitHostPort(addr) 14 | 15 | // should never panic because the config parser calls SplitHostPort first 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | return host 21 | } 22 | -------------------------------------------------------------------------------- /src/ngrok/client/tls.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | _ "crypto/sha512" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "ngrok/client/assets" 10 | ) 11 | 12 | func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) { 13 | pool := x509.NewCertPool() 14 | 15 | for _, certPath := range rootCertPaths { 16 | rootCrt, err := assets.Asset(certPath) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | pemBlock, _ := pem.Decode(rootCrt) 22 | if pemBlock == nil { 23 | return nil, fmt.Errorf("Bad PEM data") 24 | } 25 | 26 | certs, err := x509.ParseCertificates(pemBlock.Bytes) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | pool.AddCert(certs[0]) 32 | } 33 | 34 | return &tls.Config{RootCAs: pool}, nil 35 | } 36 | -------------------------------------------------------------------------------- /src/ngrok/client/update_debug.go: -------------------------------------------------------------------------------- 1 | // +build !release,!autoupdate 2 | 3 | package client 4 | 5 | import ( 6 | "ngrok/client/mvc" 7 | ) 8 | 9 | // no auto-updating in debug mode 10 | func autoUpdate(state mvc.State, token string) { 11 | } 12 | -------------------------------------------------------------------------------- /src/ngrok/client/update_release.go: -------------------------------------------------------------------------------- 1 | // +build release autoupdate 2 | 3 | package client 4 | 5 | import ( 6 | "ngrok/client/mvc" 7 | "ngrok/log" 8 | "ngrok/version" 9 | "time" 10 | 11 | "gopkg.in/inconshreveable/go-update.v0" 12 | "gopkg.in/inconshreveable/go-update.v0/check" 13 | ) 14 | 15 | const ( 16 | appId = "ap_pJSFC5wQYkAyI0FIVwKYs9h1hW" 17 | updateEndpoint = "https://api.equinox.io/1/Updates" 18 | ) 19 | 20 | const publicKey = `-----BEGIN PUBLIC KEY----- 21 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Gx8r9no1QBtCruJW2tu 22 | 082MJJ5ZA7k803GisR2c6WglPOD1b/+kUg+dx5Y0TKXz+uNlR3GrCxLh8WkoA95M 23 | T38CQldIjoVN/bWP6jzFxL+6BRoKy5L1TcaIf3xb9B8OhwEq60cvFy7BBrLKEHJN 24 | ua/D1S5axgNOAJ8tQ2w8gISICd84ng+U9tNMqIcEjUN89h3Z4zablfNIfVkbqbSR 25 | fnkR9boUaMr6S1w8OeInjWdiab9sUr87GmEo/3tVxrHVCzHB8pzzoZceCkjgI551 26 | d/hHfAl567YhlkQMNz8dawxBjQwCHHekgC8gAvTO7kmXkAm6YAbpa9kjwgnorPEP 27 | ywIDAQAB 28 | -----END PUBLIC KEY-----` 29 | 30 | func autoUpdate(s mvc.State, token string) { 31 | up, err := update.New().VerifySignatureWithPEM([]byte(publicKey)) 32 | if err != nil { 33 | log.Error("Failed to create update with signature: %v", err) 34 | return 35 | } 36 | 37 | update := func() (tryAgain bool) { 38 | log.Info("Checking for update") 39 | params := check.Params{ 40 | AppId: appId, 41 | AppVersion: version.MajorMinor(), 42 | UserId: token, 43 | } 44 | 45 | result, err := params.CheckForUpdate(updateEndpoint, up) 46 | if err == check.NoUpdateAvailable { 47 | log.Info("No update available") 48 | return true 49 | } else if err != nil { 50 | log.Error("Error while checking for update: %v", err) 51 | return true 52 | } 53 | 54 | if result.Initiative == check.INITIATIVE_AUTO { 55 | if err := up.CanUpdate(); err != nil { 56 | log.Error("Can't update: insufficient permissions: %v", err) 57 | // tell the user to update manually 58 | s.SetUpdateStatus(mvc.UpdateAvailable) 59 | } else { 60 | applyUpdate(s, result) 61 | } 62 | } else if result.Initiative == check.INITIATIVE_MANUAL { 63 | // this is the way the server tells us to update manually 64 | log.Info("Server wants us to update manually") 65 | s.SetUpdateStatus(mvc.UpdateAvailable) 66 | } else { 67 | log.Info("Update available, but ignoring") 68 | } 69 | 70 | // stop trying after a single download attempt 71 | // XXX: improve this so the we can: 72 | // 1. safely update multiple times 73 | // 2. only retry after temporary errors 74 | return false 75 | } 76 | 77 | // try to update immediately and then at a set interval 78 | for { 79 | if tryAgain := update(); !tryAgain { 80 | break 81 | } 82 | 83 | time.Sleep(updateCheckInterval) 84 | } 85 | } 86 | 87 | func applyUpdate(s mvc.State, result *check.Result) { 88 | err, errRecover := result.Update() 89 | if err == nil { 90 | log.Info("Update ready!") 91 | s.SetUpdateStatus(mvc.UpdateReady) 92 | return 93 | } 94 | 95 | log.Error("Error while updating ngrok: %v", err) 96 | if errRecover != nil { 97 | log.Error("Error while recovering from failed ngrok update, your binary may be missing: %v", errRecover.Error()) 98 | } 99 | 100 | // tell the user to update manually 101 | s.SetUpdateStatus(mvc.UpdateAvailable) 102 | } 103 | -------------------------------------------------------------------------------- /src/ngrok/client/views/term/area.go: -------------------------------------------------------------------------------- 1 | // shared internal functions for handling output to the terminal 2 | package term 3 | 4 | import ( 5 | "fmt" 6 | termbox "github.com/nsf/termbox-go" 7 | ) 8 | 9 | const ( 10 | fgColor = termbox.ColorWhite 11 | bgColor = termbox.ColorDefault 12 | ) 13 | 14 | type area struct { 15 | // top-left corner 16 | x, y int 17 | 18 | // size of the area 19 | w, h int 20 | 21 | // default colors 22 | fgColor, bgColor termbox.Attribute 23 | } 24 | 25 | func NewArea(x, y, w, h int) *area { 26 | return &area{x, y, w, h, fgColor, bgColor} 27 | } 28 | 29 | func (a *area) Clear() { 30 | for i := 0; i < a.w; i++ { 31 | for j := 0; j < a.h; j++ { 32 | termbox.SetCell(a.x+i, a.y+j, ' ', a.fgColor, a.bgColor) 33 | } 34 | } 35 | } 36 | 37 | func (a *area) APrintf(fg termbox.Attribute, x, y int, arg0 string, args ...interface{}) { 38 | s := fmt.Sprintf(arg0, args...) 39 | for i, ch := range s { 40 | termbox.SetCell(a.x+x+i, a.y+y, ch, fg, bgColor) 41 | } 42 | } 43 | 44 | func (a *area) Printf(x, y int, arg0 string, args ...interface{}) { 45 | a.APrintf(a.fgColor, x, y, arg0, args...) 46 | } 47 | -------------------------------------------------------------------------------- /src/ngrok/client/views/term/http.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | termbox "github.com/nsf/termbox-go" 5 | "ngrok/client/mvc" 6 | "ngrok/log" 7 | "ngrok/proto" 8 | "ngrok/util" 9 | "unicode/utf8" 10 | ) 11 | 12 | const ( 13 | size = 10 14 | pathMaxLength = 25 15 | ) 16 | 17 | type HttpView struct { 18 | log.Logger 19 | *area 20 | 21 | httpProto *proto.Http 22 | HttpRequests *util.Ring 23 | shutdown chan int 24 | termView *TermView 25 | } 26 | 27 | func colorFor(status string) termbox.Attribute { 28 | switch status[0] { 29 | case '3': 30 | return termbox.ColorCyan 31 | case '4': 32 | return termbox.ColorYellow 33 | case '5': 34 | return termbox.ColorRed 35 | default: 36 | } 37 | return termbox.ColorWhite 38 | } 39 | 40 | func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView { 41 | v := &HttpView{ 42 | httpProto: proto, 43 | HttpRequests: util.NewRing(size), 44 | area: NewArea(x, y, 70, size+5), 45 | shutdown: make(chan int), 46 | termView: termView, 47 | Logger: log.NewPrefixLogger("view", "term", "http"), 48 | } 49 | ctl.Go(v.Run) 50 | return v 51 | } 52 | 53 | func (v *HttpView) Run() { 54 | updates := v.httpProto.Txns.Reg() 55 | 56 | for { 57 | select { 58 | case txn := <-updates: 59 | v.Debug("Got HTTP update") 60 | if txn.(*proto.HttpTxn).Resp == nil { 61 | v.HttpRequests.Add(txn) 62 | } 63 | v.Render() 64 | } 65 | } 66 | } 67 | 68 | func (v *HttpView) Render() { 69 | v.Clear() 70 | v.Printf(0, 0, "HTTP Requests") 71 | v.Printf(0, 1, "-------------") 72 | for i, obj := range v.HttpRequests.Slice() { 73 | txn := obj.(*proto.HttpTxn) 74 | path := truncatePath(txn.Req.URL.Path) 75 | v.Printf(0, 3+i, "%s %v", txn.Req.Method, path) 76 | if txn.Resp != nil { 77 | v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status) 78 | } 79 | } 80 | v.termView.Flush() 81 | } 82 | 83 | func (v *HttpView) Shutdown() { 84 | close(v.shutdown) 85 | } 86 | 87 | func truncatePath(path string) string { 88 | // Truncate all long strings based on rune count 89 | if utf8.RuneCountInString(path) > pathMaxLength { 90 | path = string([]rune(path)[:pathMaxLength]) 91 | } 92 | 93 | // By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes. 94 | // Otherwise, we have a multi-byte string and need to calculate the size of each rune and 95 | // truncate manually. 96 | // 97 | // This is a workaround for a bug in termbox-go. Remove it when this issue is fixed: 98 | // https://github.com/nsf/termbox-go/pull/21 99 | if len(path) > pathMaxLength { 100 | out := make([]byte, pathMaxLength, pathMaxLength) 101 | length := 0 102 | for { 103 | r, size := utf8.DecodeRuneInString(path[length:]) 104 | if r == utf8.RuneError && size == 1 { 105 | break 106 | } 107 | 108 | // utf8.EncodeRune expects there to be enough room to store the full size of the rune 109 | if length+size <= pathMaxLength { 110 | utf8.EncodeRune(out[length:], r) 111 | length += size 112 | } else { 113 | break 114 | } 115 | } 116 | path = string(out[:length]) 117 | } 118 | return path 119 | } 120 | -------------------------------------------------------------------------------- /src/ngrok/client/views/term/view.go: -------------------------------------------------------------------------------- 1 | // interactive terminal interface for local clients 2 | package term 3 | 4 | import ( 5 | termbox "github.com/nsf/termbox-go" 6 | "ngrok/client/mvc" 7 | "ngrok/log" 8 | "ngrok/proto" 9 | "ngrok/util" 10 | "time" 11 | ) 12 | 13 | type TermView struct { 14 | ctl mvc.Controller 15 | updates chan interface{} 16 | flush chan int 17 | shutdown chan int 18 | redraw *util.Broadcast 19 | subviews []mvc.View 20 | log.Logger 21 | *area 22 | } 23 | 24 | func NewTermView(ctl mvc.Controller) *TermView { 25 | // initialize terminal display 26 | termbox.Init() 27 | 28 | w, _ := termbox.Size() 29 | 30 | v := &TermView{ 31 | ctl: ctl, 32 | updates: ctl.Updates().Reg(), 33 | redraw: util.NewBroadcast(), 34 | flush: make(chan int), 35 | shutdown: make(chan int), 36 | Logger: log.NewPrefixLogger("view", "term"), 37 | area: NewArea(0, 0, w, 10), 38 | } 39 | 40 | ctl.Go(v.run) 41 | ctl.Go(v.input) 42 | 43 | return v 44 | } 45 | 46 | func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) { 47 | switch status { 48 | case mvc.ConnConnecting: 49 | return "connecting", termbox.ColorCyan 50 | case mvc.ConnReconnecting: 51 | return "reconnecting", termbox.ColorRed 52 | case mvc.ConnOnline: 53 | return "online", termbox.ColorGreen 54 | } 55 | return "unknown", termbox.ColorWhite 56 | } 57 | 58 | func (v *TermView) draw() { 59 | state := v.ctl.State() 60 | 61 | v.Clear() 62 | 63 | // quit instructions 64 | quitMsg := "(Ctrl+C to quit)" 65 | v.Printf(v.w-len(quitMsg), 0, quitMsg) 66 | 67 | // new version message 68 | updateStatus := state.GetUpdateStatus() 69 | var updateMsg string 70 | switch updateStatus { 71 | case mvc.UpdateNone: 72 | updateMsg = "" 73 | case mvc.UpdateInstalling: 74 | updateMsg = "ngrok is updating" 75 | case mvc.UpdateReady: 76 | updateMsg = "ngrok has updated: restart ngrok for the new version" 77 | case mvc.UpdateAvailable: 78 | updateMsg = "new version available at https://ngrok.com" 79 | default: 80 | pct := float64(updateStatus) / 100.0 81 | const barLength = 25 82 | full := int(barLength * pct) 83 | bar := make([]byte, barLength+2) 84 | bar[0] = '[' 85 | bar[barLength+1] = ']' 86 | for i := 0; i < 25; i++ { 87 | if i <= full { 88 | bar[i+1] = '#' 89 | } else { 90 | bar[i+1] = ' ' 91 | } 92 | } 93 | updateMsg = "Downloading update: " + string(bar) 94 | } 95 | 96 | if updateMsg != "" { 97 | v.APrintf(termbox.ColorYellow, 30, 0, updateMsg) 98 | } 99 | 100 | v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok") 101 | statusStr, statusColor := connStatusRepr(state.GetConnStatus()) 102 | v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr) 103 | 104 | v.Printf(0, 3, "%-30s%s/%s", "Version", state.GetClientVersion(), state.GetServerVersion()) 105 | var i int = 4 106 | for _, t := range state.GetTunnels() { 107 | v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr) 108 | i++ 109 | } 110 | v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr()) 111 | 112 | connMeter, connTimer := state.GetConnectionMetrics() 113 | v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count()) 114 | 115 | msec := float64(time.Millisecond) 116 | v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec) 117 | 118 | termbox.Flush() 119 | } 120 | 121 | func (v *TermView) run() { 122 | defer close(v.shutdown) 123 | defer termbox.Close() 124 | 125 | redraw := v.redraw.Reg() 126 | defer v.redraw.UnReg(redraw) 127 | 128 | v.draw() 129 | for { 130 | v.Debug("Waiting for update") 131 | select { 132 | case <-v.flush: 133 | termbox.Flush() 134 | 135 | case <-v.updates: 136 | v.draw() 137 | 138 | case <-redraw: 139 | v.draw() 140 | 141 | case <-v.shutdown: 142 | return 143 | } 144 | } 145 | } 146 | 147 | func (v *TermView) Shutdown() { 148 | v.shutdown <- 1 149 | <-v.shutdown 150 | } 151 | 152 | func (v *TermView) Flush() { 153 | v.flush <- 1 154 | } 155 | 156 | func (v *TermView) NewHttpView(p *proto.Http) *HttpView { 157 | return newTermHttpView(v.ctl, v, p, 0, 12) 158 | } 159 | 160 | func (v *TermView) input() { 161 | for { 162 | ev := termbox.PollEvent() 163 | switch ev.Type { 164 | case termbox.EventKey: 165 | switch ev.Key { 166 | case termbox.KeyCtrlC: 167 | v.Info("Got quit command") 168 | v.ctl.Shutdown("") 169 | } 170 | 171 | case termbox.EventResize: 172 | v.Info("Resize event, redrawing") 173 | v.redraw.In() <- 1 174 | 175 | case termbox.EventError: 176 | panic(ev.Err) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/ngrok/client/views/web/http.go: -------------------------------------------------------------------------------- 1 | // interactive web user interface 2 | package web 3 | 4 | import ( 5 | "encoding/base64" 6 | "encoding/json" 7 | "encoding/xml" 8 | "html/template" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "ngrok/client/assets" 13 | "ngrok/client/mvc" 14 | "ngrok/log" 15 | "ngrok/proto" 16 | "ngrok/util" 17 | "strings" 18 | "unicode/utf8" 19 | ) 20 | 21 | type SerializedTxn struct { 22 | Id string 23 | Duration int64 24 | Start int64 25 | ConnCtx mvc.ConnectionContext 26 | *proto.HttpTxn `json:"-"` 27 | Req SerializedRequest 28 | Resp SerializedResponse 29 | } 30 | 31 | type SerializedBody struct { 32 | RawContentType string 33 | ContentType string 34 | Text string 35 | Length int 36 | Error string 37 | ErrorOffset int 38 | Form url.Values 39 | } 40 | 41 | type SerializedRequest struct { 42 | Raw string 43 | MethodPath string 44 | Params url.Values 45 | Header http.Header 46 | Body SerializedBody 47 | Binary bool 48 | } 49 | 50 | type SerializedResponse struct { 51 | Raw string 52 | Status string 53 | Header http.Header 54 | Body SerializedBody 55 | Binary bool 56 | } 57 | 58 | type WebHttpView struct { 59 | log.Logger 60 | 61 | webview *WebView 62 | ctl mvc.Controller 63 | httpProto *proto.Http 64 | state chan SerializedUiState 65 | HttpRequests *util.Ring 66 | idToTxn map[string]*SerializedTxn 67 | } 68 | 69 | type SerializedUiState struct { 70 | Tunnels []mvc.Tunnel 71 | } 72 | 73 | type SerializedPayload struct { 74 | Txns []interface{} 75 | UiState SerializedUiState 76 | } 77 | 78 | func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView { 79 | whv := &WebHttpView{ 80 | Logger: log.NewPrefixLogger("view", "web", "http"), 81 | webview: wv, 82 | ctl: ctl, 83 | httpProto: proto, 84 | idToTxn: make(map[string]*SerializedTxn), 85 | HttpRequests: util.NewRing(20), 86 | } 87 | ctl.Go(whv.updateHttp) 88 | whv.register() 89 | return whv 90 | } 91 | 92 | type XMLDoc struct { 93 | data []byte `xml:",innerxml"` 94 | } 95 | 96 | func makeBody(h http.Header, body []byte) SerializedBody { 97 | b := SerializedBody{ 98 | Length: len(body), 99 | Text: base64.StdEncoding.EncodeToString(body), 100 | ErrorOffset: -1, 101 | } 102 | 103 | // some errors like XML errors only give a line number 104 | // and not an exact offset 105 | offsetForLine := func(line int) int { 106 | lines := strings.SplitAfterN(b.Text, "\n", line) 107 | return b.Length - len(lines[len(lines)-1]) 108 | } 109 | 110 | var err error 111 | b.RawContentType = h.Get("Content-Type") 112 | if b.RawContentType != "" { 113 | b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0]) 114 | switch b.ContentType { 115 | case "application/xml", "text/xml": 116 | err = xml.Unmarshal(body, new(XMLDoc)) 117 | if err != nil { 118 | if syntaxError, ok := err.(*xml.SyntaxError); ok { 119 | // xml syntax errors only give us a line number, so we 120 | // count to find an offset 121 | b.ErrorOffset = offsetForLine(syntaxError.Line) 122 | } 123 | } 124 | 125 | case "application/json": 126 | err = json.Unmarshal(body, new(json.RawMessage)) 127 | if err != nil { 128 | if syntaxError, ok := err.(*json.SyntaxError); ok { 129 | b.ErrorOffset = int(syntaxError.Offset) 130 | } 131 | } 132 | 133 | case "application/x-www-form-urlencoded": 134 | b.Form, err = url.ParseQuery(string(body)) 135 | } 136 | } 137 | 138 | if err != nil { 139 | b.Error = err.Error() 140 | } 141 | 142 | return b 143 | } 144 | 145 | func (whv *WebHttpView) updateHttp() { 146 | // open channels for incoming http state changes 147 | // and broadcasts 148 | txnUpdates := whv.httpProto.Txns.Reg() 149 | for txn := range txnUpdates { 150 | // XXX: it's not safe for proto.Http and this code 151 | // to be accessing txn and txn.(req/resp) without synchronization 152 | htxn := txn.(*proto.HttpTxn) 153 | 154 | // we haven't processed this transaction yet if we haven't set the 155 | // user data 156 | if htxn.UserCtx == nil { 157 | rawReq, err := proto.DumpRequestOut(htxn.Req.Request, true) 158 | if err != nil { 159 | whv.Error("Failed to dump request: %v", err) 160 | continue 161 | } 162 | 163 | body := makeBody(htxn.Req.Header, htxn.Req.BodyBytes) 164 | whtxn := &SerializedTxn{ 165 | Id: util.RandId(8), 166 | HttpTxn: htxn, 167 | Req: SerializedRequest{ 168 | MethodPath: htxn.Req.Method + " " + htxn.Req.URL.Path, 169 | Raw: base64.StdEncoding.EncodeToString(rawReq), 170 | Params: htxn.Req.URL.Query(), 171 | Header: htxn.Req.Header, 172 | Body: body, 173 | Binary: !utf8.Valid(rawReq), 174 | }, 175 | Start: htxn.Start.Unix(), 176 | ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext), 177 | } 178 | 179 | htxn.UserCtx = whtxn 180 | // XXX: unsafe map access from multiple go routines 181 | whv.idToTxn[whtxn.Id] = whtxn 182 | // XXX: use return value to delete from map so we don't leak memory 183 | whv.HttpRequests.Add(whtxn) 184 | } else { 185 | rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true) 186 | if err != nil { 187 | whv.Error("Failed to dump response: %v", err) 188 | continue 189 | } 190 | 191 | txn := htxn.UserCtx.(*SerializedTxn) 192 | body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes) 193 | txn.Duration = htxn.Duration.Nanoseconds() 194 | txn.Resp = SerializedResponse{ 195 | Status: htxn.Resp.Status, 196 | Raw: base64.StdEncoding.EncodeToString(rawResp), 197 | Header: htxn.Resp.Header, 198 | Body: body, 199 | Binary: !utf8.Valid(rawResp), 200 | } 201 | 202 | payload, err := json.Marshal(txn) 203 | if err != nil { 204 | whv.Error("Failed to serialized txn payload for websocket: %v", err) 205 | } 206 | whv.webview.wsMessages.In() <- payload 207 | } 208 | } 209 | } 210 | 211 | func (whv *WebHttpView) register() { 212 | http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) { 213 | defer func() { 214 | if r := recover(); r != nil { 215 | err := util.MakePanicTrace(r) 216 | whv.Error("Replay failed: %v", err) 217 | http.Error(w, err, 500) 218 | } 219 | }() 220 | 221 | r.ParseForm() 222 | txnid := r.Form.Get("txnid") 223 | if txn, ok := whv.idToTxn[txnid]; ok { 224 | reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw) 225 | if err != nil { 226 | panic(err) 227 | } 228 | whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes) 229 | w.Write([]byte(http.StatusText(200))) 230 | } else { 231 | http.Error(w, http.StatusText(400), 400) 232 | } 233 | }) 234 | 235 | http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) { 236 | defer func() { 237 | if r := recover(); r != nil { 238 | err := util.MakePanicTrace(r) 239 | whv.Error("HTTP web view failed: %v", err) 240 | http.Error(w, err, 500) 241 | } 242 | }() 243 | 244 | pageTmpl, err := assets.Asset("assets/client/page.html") 245 | if err != nil { 246 | panic(err) 247 | } 248 | 249 | tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl))) 250 | 251 | payloadData := SerializedPayload{ 252 | Txns: whv.HttpRequests.Slice(), 253 | UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()}, 254 | } 255 | 256 | payload, err := json.Marshal(payloadData) 257 | if err != nil { 258 | panic(err) 259 | } 260 | 261 | // write the response 262 | if err := tmpl.Execute(w, string(payload)); err != nil { 263 | panic(err) 264 | } 265 | }) 266 | } 267 | 268 | func (whv *WebHttpView) Shutdown() { 269 | } 270 | -------------------------------------------------------------------------------- /src/ngrok/client/views/web/view.go: -------------------------------------------------------------------------------- 1 | // interactive web user interface 2 | package web 3 | 4 | import ( 5 | "github.com/gorilla/websocket" 6 | "net/http" 7 | "ngrok/client/assets" 8 | "ngrok/client/mvc" 9 | "ngrok/log" 10 | "ngrok/proto" 11 | "ngrok/util" 12 | "path" 13 | ) 14 | 15 | type WebView struct { 16 | log.Logger 17 | 18 | ctl mvc.Controller 19 | 20 | // messages sent over this broadcast are sent to all websocket connections 21 | wsMessages *util.Broadcast 22 | } 23 | 24 | func NewWebView(ctl mvc.Controller, addr string) *WebView { 25 | wv := &WebView{ 26 | Logger: log.NewPrefixLogger("view", "web"), 27 | wsMessages: util.NewBroadcast(), 28 | ctl: ctl, 29 | } 30 | 31 | // for now, always redirect to the http view 32 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 33 | http.Redirect(w, r, "/http/in", 302) 34 | }) 35 | 36 | // handle web socket connections 37 | http.HandleFunc("/_ws", func(w http.ResponseWriter, r *http.Request) { 38 | conn, err := websocket.Upgrade(w, r, nil, 1024, 1024) 39 | 40 | if err != nil { 41 | http.Error(w, "Failed websocket upgrade", 400) 42 | wv.Warn("Failed websocket upgrade: %v", err) 43 | return 44 | } 45 | 46 | msgs := wv.wsMessages.Reg() 47 | defer wv.wsMessages.UnReg(msgs) 48 | for m := range msgs { 49 | err := conn.WriteMessage(websocket.TextMessage, m.([]byte)) 50 | if err != nil { 51 | // connection is closed 52 | break 53 | } 54 | } 55 | }) 56 | 57 | // serve static assets 58 | http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { 59 | buf, err := assets.Asset(path.Join("assets", "client", r.URL.Path[1:])) 60 | if err != nil { 61 | wv.Warn("Error serving static file: %s", err.Error()) 62 | http.NotFound(w, r) 63 | return 64 | } 65 | w.Write(buf) 66 | }) 67 | 68 | wv.Info("Serving web interface on %s", addr) 69 | wv.ctl.Go(func() { http.ListenAndServe(addr, nil) }) 70 | return wv 71 | } 72 | 73 | func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView { 74 | return newWebHttpView(wv.ctl, wv, proto) 75 | } 76 | 77 | func (wv *WebView) Shutdown() { 78 | } 79 | -------------------------------------------------------------------------------- /src/ngrok/conn/conn.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "fmt" 8 | vhost "github.com/inconshreveable/go-vhost" 9 | "io" 10 | "math/rand" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "ngrok/log" 15 | "sync" 16 | ) 17 | 18 | type Conn interface { 19 | net.Conn 20 | log.Logger 21 | Id() string 22 | SetType(string) 23 | CloseRead() error 24 | } 25 | 26 | type loggedConn struct { 27 | tcp *net.TCPConn 28 | net.Conn 29 | log.Logger 30 | id int32 31 | typ string 32 | } 33 | 34 | type Listener struct { 35 | net.Addr 36 | Conns chan *loggedConn 37 | } 38 | 39 | func wrapConn(conn net.Conn, typ string) *loggedConn { 40 | switch c := conn.(type) { 41 | case *vhost.HTTPConn: 42 | wrapped := c.Conn.(*loggedConn) 43 | return &loggedConn{wrapped.tcp, conn, wrapped.Logger, wrapped.id, wrapped.typ} 44 | case *loggedConn: 45 | return c 46 | case *net.TCPConn: 47 | wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ} 48 | wrapped.AddLogPrefix(wrapped.Id()) 49 | return wrapped 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) { 56 | // listen for incoming connections 57 | listener, err := net.Listen("tcp", addr) 58 | if err != nil { 59 | return 60 | } 61 | 62 | l = &Listener{ 63 | Addr: listener.Addr(), 64 | Conns: make(chan *loggedConn), 65 | } 66 | 67 | go func() { 68 | for { 69 | rawConn, err := listener.Accept() 70 | if err != nil { 71 | log.Error("Failed to accept new TCP connection of type %s: %v", typ, err) 72 | continue 73 | } 74 | 75 | c := wrapConn(rawConn, typ) 76 | if tlsCfg != nil { 77 | c.Conn = tls.Server(c.Conn, tlsCfg) 78 | } 79 | c.Info("New connection from %v", c.RemoteAddr()) 80 | l.Conns <- c 81 | } 82 | }() 83 | return 84 | } 85 | 86 | func Wrap(conn net.Conn, typ string) *loggedConn { 87 | return wrapConn(conn, typ) 88 | } 89 | 90 | func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) { 91 | var rawConn net.Conn 92 | if rawConn, err = net.Dial("tcp", addr); err != nil { 93 | return 94 | } 95 | 96 | conn = wrapConn(rawConn, typ) 97 | conn.Debug("New connection to: %v", rawConn.RemoteAddr()) 98 | 99 | if tlsCfg != nil { 100 | conn.StartTLS(tlsCfg) 101 | } 102 | 103 | return 104 | } 105 | 106 | func DialHttpProxy(proxyUrl, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) { 107 | // parse the proxy address 108 | var parsedUrl *url.URL 109 | if parsedUrl, err = url.Parse(proxyUrl); err != nil { 110 | return 111 | } 112 | 113 | var proxyAuth string 114 | if parsedUrl.User != nil { 115 | proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(parsedUrl.User.String())) 116 | } 117 | 118 | var proxyTlsConfig *tls.Config 119 | switch parsedUrl.Scheme { 120 | case "http": 121 | proxyTlsConfig = nil 122 | case "https": 123 | proxyTlsConfig = new(tls.Config) 124 | default: 125 | err = fmt.Errorf("Proxy URL scheme must be http or https, got: %s", parsedUrl.Scheme) 126 | return 127 | } 128 | 129 | // dial the proxy 130 | if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil { 131 | return 132 | } 133 | 134 | // send an HTTP proxy CONNECT message 135 | req, err := http.NewRequest("CONNECT", "https://"+addr, nil) 136 | if err != nil { 137 | return 138 | } 139 | 140 | if proxyAuth != "" { 141 | req.Header.Set("Proxy-Authorization", proxyAuth) 142 | } 143 | req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ngrok)") 144 | req.Write(conn) 145 | 146 | // read the proxy's response 147 | resp, err := http.ReadResponse(bufio.NewReader(conn), req) 148 | if err != nil { 149 | return 150 | } 151 | resp.Body.Close() 152 | 153 | if resp.StatusCode != 200 { 154 | err = fmt.Errorf("Non-200 response from proxy server: %s", resp.Status) 155 | return 156 | } 157 | 158 | // upgrade to TLS 159 | conn.StartTLS(tlsCfg) 160 | 161 | return 162 | } 163 | 164 | func (c *loggedConn) StartTLS(tlsCfg *tls.Config) { 165 | c.Conn = tls.Client(c.Conn, tlsCfg) 166 | } 167 | 168 | func (c *loggedConn) Close() (err error) { 169 | if err := c.Conn.Close(); err == nil { 170 | c.Debug("Closing") 171 | } 172 | return 173 | } 174 | 175 | func (c *loggedConn) Id() string { 176 | return fmt.Sprintf("%s:%x", c.typ, c.id) 177 | } 178 | 179 | func (c *loggedConn) SetType(typ string) { 180 | oldId := c.Id() 181 | c.typ = typ 182 | c.ClearLogPrefixes() 183 | c.AddLogPrefix(c.Id()) 184 | c.Info("Renamed connection %s", oldId) 185 | } 186 | 187 | func (c *loggedConn) CloseRead() error { 188 | // XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner 189 | // connection termination. Unfortunately, when I've tried that, I've observed 190 | // failures where the connection was closed *before* flushing its write buffer, 191 | // set with SetLinger() set properly (which it is by default). 192 | return c.tcp.CloseRead() 193 | } 194 | 195 | func Join(c Conn, c2 Conn) (int64, int64) { 196 | var wait sync.WaitGroup 197 | 198 | pipe := func(to Conn, from Conn, bytesCopied *int64) { 199 | defer to.Close() 200 | defer from.Close() 201 | defer wait.Done() 202 | 203 | var err error 204 | *bytesCopied, err = io.Copy(to, from) 205 | if err != nil { 206 | from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err) 207 | } else { 208 | from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id()) 209 | } 210 | } 211 | 212 | wait.Add(2) 213 | var fromBytes, toBytes int64 214 | go pipe(c, c2, &fromBytes) 215 | go pipe(c2, c, &toBytes) 216 | c.Info("Joined with connection %s", c2.Id()) 217 | wait.Wait() 218 | return fromBytes, toBytes 219 | } 220 | -------------------------------------------------------------------------------- /src/ngrok/conn/tee.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | ) 7 | 8 | // conn.Tee is a wraps a conn.Conn 9 | // causing all writes/reads to be tee'd just 10 | // like the unix command such that all data that 11 | // is read and written to the connection through its 12 | // interfaces will also be copied into two dedicated pipes 13 | // used for consuming a copy of the data stream 14 | // 15 | // this is useful for introspecting the traffic flowing 16 | // over a connection without having to tamper with the actual 17 | // code that reads and writes over the connection 18 | // 19 | // NB: the data is Tee'd into a shared-memory io.Pipe which 20 | // has a limited (and small) buffer. If you are not consuming from 21 | // the ReadBuffer() and WriteBuffer(), you are going to block 22 | // your application's real traffic from flowing over the connection 23 | 24 | type Tee struct { 25 | rd io.Reader 26 | wr io.Writer 27 | readPipe struct { 28 | rd *io.PipeReader 29 | wr *io.PipeWriter 30 | } 31 | writePipe struct { 32 | rd *io.PipeReader 33 | wr *io.PipeWriter 34 | } 35 | Conn 36 | } 37 | 38 | func NewTee(conn Conn) *Tee { 39 | c := &Tee{ 40 | rd: nil, 41 | wr: nil, 42 | Conn: conn, 43 | } 44 | 45 | c.readPipe.rd, c.readPipe.wr = io.Pipe() 46 | c.writePipe.rd, c.writePipe.wr = io.Pipe() 47 | 48 | c.rd = io.TeeReader(c.Conn, c.readPipe.wr) 49 | c.wr = io.MultiWriter(c.Conn, c.writePipe.wr) 50 | return c 51 | } 52 | 53 | func (c *Tee) ReadBuffer() *bufio.Reader { 54 | return bufio.NewReader(c.readPipe.rd) 55 | } 56 | 57 | func (c *Tee) WriteBuffer() *bufio.Reader { 58 | return bufio.NewReader(c.writePipe.rd) 59 | } 60 | 61 | func (c *Tee) Read(b []byte) (n int, err error) { 62 | n, err = c.rd.Read(b) 63 | if err != nil { 64 | c.readPipe.wr.Close() 65 | } 66 | return 67 | } 68 | 69 | func (c *Tee) ReadFrom(r io.Reader) (n int64, err error) { 70 | n, err = io.Copy(c.wr, r) 71 | if err != nil { 72 | c.writePipe.wr.Close() 73 | } 74 | return 75 | } 76 | 77 | func (c *Tee) Write(b []byte) (n int, err error) { 78 | n, err = c.wr.Write(b) 79 | if err != nil { 80 | c.writePipe.wr.Close() 81 | } 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /src/ngrok/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | log "github.com/alecthomas/log4go" 5 | "fmt" 6 | ) 7 | 8 | var root log.Logger = make(log.Logger) 9 | 10 | func LogTo(target string, level_name string) { 11 | var writer log.LogWriter = nil 12 | 13 | switch target { 14 | case "stdout": 15 | writer = log.NewConsoleLogWriter() 16 | case "none": 17 | // no logging 18 | default: 19 | writer = log.NewFileLogWriter(target, true) 20 | } 21 | 22 | if writer != nil { 23 | var level = log.DEBUG 24 | 25 | switch level_name { 26 | case "FINEST": 27 | level = log.FINEST 28 | case "FINE": 29 | level = log.FINE 30 | case "DEBUG": 31 | level = log.DEBUG 32 | case "TRACE": 33 | level = log.TRACE 34 | case "INFO": 35 | level = log.INFO 36 | case "WARNING": 37 | level = log.WARNING 38 | case "ERROR": 39 | level = log.ERROR 40 | case "CRITICAL": 41 | level = log.CRITICAL 42 | default: 43 | level = log.DEBUG 44 | } 45 | 46 | root.AddFilter("log", level, writer) 47 | } 48 | } 49 | 50 | type Logger interface { 51 | AddLogPrefix(string) 52 | ClearLogPrefixes() 53 | Debug(string, ...interface{}) 54 | Info(string, ...interface{}) 55 | Warn(string, ...interface{}) error 56 | Error(string, ...interface{}) error 57 | } 58 | 59 | type PrefixLogger struct { 60 | *log.Logger 61 | prefix string 62 | } 63 | 64 | func NewPrefixLogger(prefixes ...string) Logger { 65 | logger := &PrefixLogger{Logger: &root} 66 | 67 | for _, p := range prefixes { 68 | logger.AddLogPrefix(p) 69 | } 70 | 71 | return logger 72 | } 73 | 74 | func (pl *PrefixLogger) pfx(fmtstr string) interface{} { 75 | return fmt.Sprintf("%s %s", pl.prefix, fmtstr) 76 | } 77 | 78 | func (pl *PrefixLogger) Debug(arg0 string, args ...interface{}) { 79 | pl.Logger.Debug(pl.pfx(arg0), args...) 80 | } 81 | 82 | func (pl *PrefixLogger) Info(arg0 string, args ...interface{}) { 83 | pl.Logger.Info(pl.pfx(arg0), args...) 84 | } 85 | 86 | func (pl *PrefixLogger) Warn(arg0 string, args ...interface{}) error { 87 | return pl.Logger.Warn(pl.pfx(arg0), args...) 88 | } 89 | 90 | func (pl *PrefixLogger) Error(arg0 string, args ...interface{}) error { 91 | return pl.Logger.Error(pl.pfx(arg0), args...) 92 | } 93 | 94 | func (pl *PrefixLogger) AddLogPrefix(prefix string) { 95 | if len(pl.prefix) > 0 { 96 | pl.prefix += " " 97 | } 98 | 99 | pl.prefix += "[" + prefix + "]" 100 | } 101 | 102 | func (pl *PrefixLogger) ClearLogPrefixes() { 103 | pl.prefix = "" 104 | } 105 | 106 | // we should never really use these . . . always prefer logging through a prefix logger 107 | func Debug(arg0 string, args ...interface{}) { 108 | root.Debug(arg0, args...) 109 | } 110 | 111 | func Info(arg0 string, args ...interface{}) { 112 | root.Info(arg0, args...) 113 | } 114 | 115 | func Warn(arg0 string, args ...interface{}) error { 116 | return root.Warn(arg0, args...) 117 | } 118 | 119 | func Error(arg0 string, args ...interface{}) error { 120 | return root.Error(arg0, args...) 121 | } 122 | -------------------------------------------------------------------------------- /src/ngrok/main/ngrok/ngrok.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ngrok/client" 5 | ) 6 | 7 | func main() { 8 | client.Main() 9 | } 10 | -------------------------------------------------------------------------------- /src/ngrok/main/ngrokd/ngrokd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ngrok/server" 5 | ) 6 | 7 | func main() { 8 | server.Main() 9 | } 10 | -------------------------------------------------------------------------------- /src/ngrok/msg/conn.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "ngrok/conn" 8 | ) 9 | 10 | func readMsgShared(c conn.Conn) (buffer []byte, err error) { 11 | c.Debug("Waiting to read message") 12 | 13 | var sz int64 14 | err = binary.Read(c, binary.LittleEndian, &sz) 15 | if err != nil { 16 | return 17 | } 18 | c.Debug("Reading message with length: %d", sz) 19 | 20 | buffer = make([]byte, sz) 21 | n, err := c.Read(buffer) 22 | c.Debug("Read message %s", buffer) 23 | 24 | if err != nil { 25 | return 26 | } 27 | 28 | if int64(n) != sz { 29 | err = errors.New(fmt.Sprintf("Expected to read %d bytes, but only read %d", sz, n)) 30 | return 31 | } 32 | 33 | return 34 | } 35 | 36 | func ReadMsg(c conn.Conn) (msg Message, err error) { 37 | buffer, err := readMsgShared(c) 38 | if err != nil { 39 | return 40 | } 41 | 42 | return Unpack(buffer) 43 | } 44 | 45 | func ReadMsgInto(c conn.Conn, msg Message) (err error) { 46 | buffer, err := readMsgShared(c) 47 | if err != nil { 48 | return 49 | } 50 | return UnpackInto(buffer, msg) 51 | } 52 | 53 | func WriteMsg(c conn.Conn, msg interface{}) (err error) { 54 | buffer, err := Pack(msg) 55 | if err != nil { 56 | return 57 | } 58 | 59 | c.Debug("Writing message: %s", string(buffer)) 60 | err = binary.Write(c, binary.LittleEndian, int64(len(buffer))) 61 | 62 | if err != nil { 63 | return 64 | } 65 | 66 | if _, err = c.Write(buffer); err != nil { 67 | return 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /src/ngrok/msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | ) 7 | 8 | var TypeMap map[string]reflect.Type 9 | 10 | func init() { 11 | TypeMap = make(map[string]reflect.Type) 12 | 13 | t := func(obj interface{}) reflect.Type { return reflect.TypeOf(obj).Elem() } 14 | TypeMap["Auth"] = t((*Auth)(nil)) 15 | TypeMap["AuthResp"] = t((*AuthResp)(nil)) 16 | TypeMap["ReqTunnel"] = t((*ReqTunnel)(nil)) 17 | TypeMap["NewTunnel"] = t((*NewTunnel)(nil)) 18 | TypeMap["RegProxy"] = t((*RegProxy)(nil)) 19 | TypeMap["ReqProxy"] = t((*ReqProxy)(nil)) 20 | TypeMap["StartProxy"] = t((*StartProxy)(nil)) 21 | TypeMap["Ping"] = t((*Ping)(nil)) 22 | TypeMap["Pong"] = t((*Pong)(nil)) 23 | } 24 | 25 | type Message interface{} 26 | 27 | type Envelope struct { 28 | Type string 29 | Payload json.RawMessage 30 | } 31 | 32 | // When a client opens a new control channel to the server 33 | // it must start by sending an Auth message. 34 | type Auth struct { 35 | Version string // protocol version 36 | MmVersion string // major/minor software version (informational only) 37 | User string 38 | Password string 39 | OS string 40 | Arch string 41 | ClientId string // empty for new sessions 42 | } 43 | 44 | // A server responds to an Auth message with an 45 | // AuthResp message over the control channel. 46 | // 47 | // If Error is not the empty string 48 | // the server has indicated it will not accept 49 | // the new session and will close the connection. 50 | // 51 | // The server response includes a unique ClientId 52 | // that is used to associate and authenticate future 53 | // proxy connections via the same field in RegProxy messages. 54 | type AuthResp struct { 55 | Version string 56 | MmVersion string 57 | ClientId string 58 | Error string 59 | } 60 | 61 | // A client sends this message to the server over the control channel 62 | // to request a new tunnel be opened on the client's behalf. 63 | // ReqId is a random number set by the client that it can pull 64 | // from future NewTunnel's to correlate then to the requesting ReqTunnel. 65 | type ReqTunnel struct { 66 | ReqId string 67 | Protocol string 68 | 69 | // http only 70 | Hostname string 71 | Subdomain string 72 | HttpAuth string 73 | 74 | // tcp only 75 | RemotePort uint16 76 | } 77 | 78 | // When the server opens a new tunnel on behalf of 79 | // a client, it sends a NewTunnel message to notify the client. 80 | // ReqId is the ReqId from the corresponding ReqTunnel message. 81 | // 82 | // A client may receive *multiple* NewTunnel messages from a single 83 | // ReqTunnel. (ex. A client opens an https tunnel and the server 84 | // chooses to open an http tunnel of the same name as well) 85 | type NewTunnel struct { 86 | ReqId string 87 | Url string 88 | Protocol string 89 | Error string 90 | } 91 | 92 | // When the server wants to initiate a new tunneled connection, it sends 93 | // this message over the control channel to the client. When a client receives 94 | // this message, it must initiate a new proxy connection to the server. 95 | type ReqProxy struct { 96 | } 97 | 98 | // After a client receives a ReqProxy message, it opens a new 99 | // connection to the server and sends a RegProxy message. 100 | type RegProxy struct { 101 | ClientId string 102 | } 103 | 104 | // This message is sent by the server to the client over a *proxy* connection before it 105 | // begins to send the bytes of the proxied request. 106 | type StartProxy struct { 107 | Url string // URL of the tunnel this connection connection is being proxied for 108 | ClientAddr string // Network address of the client initiating the connection to the tunnel 109 | } 110 | 111 | // A client or server may send this message periodically over 112 | // the control channel to request that the remote side acknowledge 113 | // its connection is still alive. The remote side must respond with a Pong. 114 | type Ping struct { 115 | } 116 | 117 | // Sent by a client or server over the control channel to indicate 118 | // it received a Ping. 119 | type Pong struct { 120 | } 121 | -------------------------------------------------------------------------------- /src/ngrok/msg/pack.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | func unpack(buffer []byte, msgIn Message) (msg Message, err error) { 11 | var env Envelope 12 | if err = json.Unmarshal(buffer, &env); err != nil { 13 | return 14 | } 15 | 16 | if msgIn == nil { 17 | t, ok := TypeMap[env.Type] 18 | 19 | if !ok { 20 | err = errors.New(fmt.Sprintf("Unsupported message type %s", env.Type)) 21 | return 22 | } 23 | 24 | // guess type 25 | msg = reflect.New(t).Interface().(Message) 26 | } else { 27 | msg = msgIn 28 | } 29 | 30 | err = json.Unmarshal(env.Payload, &msg) 31 | return 32 | } 33 | 34 | func UnpackInto(buffer []byte, msg Message) (err error) { 35 | _, err = unpack(buffer, msg) 36 | return 37 | } 38 | 39 | func Unpack(buffer []byte) (msg Message, err error) { 40 | return unpack(buffer, nil) 41 | } 42 | 43 | func Pack(payload interface{}) ([]byte, error) { 44 | return json.Marshal(struct { 45 | Type string 46 | Payload interface{} 47 | }{ 48 | Type: reflect.TypeOf(payload).Elem().Name(), 49 | Payload: payload, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/ngrok/proto/http.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "ngrok/conn" 13 | "ngrok/util" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | metrics "github.com/rcrowley/go-metrics" 19 | ) 20 | 21 | type HttpRequest struct { 22 | *http.Request 23 | BodyBytes []byte 24 | } 25 | 26 | type HttpResponse struct { 27 | *http.Response 28 | BodyBytes []byte 29 | } 30 | 31 | type HttpTxn struct { 32 | Req *HttpRequest 33 | Resp *HttpResponse 34 | Start time.Time 35 | Duration time.Duration 36 | UserCtx interface{} 37 | ConnUserCtx interface{} 38 | } 39 | 40 | type Http struct { 41 | Txns *util.Broadcast 42 | reqGauge metrics.Gauge 43 | reqMeter metrics.Meter 44 | reqTimer metrics.Timer 45 | } 46 | 47 | func NewHttp() *Http { 48 | return &Http{ 49 | Txns: util.NewBroadcast(), 50 | reqGauge: metrics.NewGauge(), 51 | reqMeter: metrics.NewMeter(), 52 | reqTimer: metrics.NewTimer(), 53 | } 54 | } 55 | 56 | func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) { 57 | buf := new(bytes.Buffer) 58 | _, err := buf.ReadFrom(r) 59 | return buf.Bytes(), ioutil.NopCloser(buf), err 60 | } 61 | 62 | func (h *Http) GetName() string { return "http" } 63 | 64 | func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn { 65 | tee := conn.NewTee(c) 66 | lastTxn := make(chan *HttpTxn) 67 | go h.readRequests(tee, lastTxn, ctx) 68 | go h.readResponses(tee, lastTxn) 69 | return tee 70 | } 71 | 72 | func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) { 73 | defer close(lastTxn) 74 | 75 | for { 76 | req, err := http.ReadRequest(tee.WriteBuffer()) 77 | if err != nil { 78 | // no more requests to be read, we're done 79 | break 80 | } 81 | 82 | // make sure we read the body of the request so that 83 | // we don't block the writer 84 | _, err = httputil.DumpRequest(req, true) 85 | 86 | h.reqMeter.Mark(1) 87 | if err != nil { 88 | tee.Warn("Failed to extract request body: %v", err) 89 | } 90 | 91 | // golang's ReadRequest/DumpRequestOut is broken. Fix up the request so it works later 92 | req.URL.Scheme = "http" 93 | req.URL.Host = req.Host 94 | 95 | txn := &HttpTxn{Start: time.Now(), ConnUserCtx: connCtx} 96 | txn.Req = &HttpRequest{Request: req} 97 | if req.Body != nil { 98 | txn.Req.BodyBytes, txn.Req.Body, err = extractBody(req.Body) 99 | if err != nil { 100 | tee.Warn("Failed to extract request body: %v", err) 101 | } 102 | } 103 | 104 | lastTxn <- txn 105 | h.Txns.In() <- txn 106 | } 107 | } 108 | 109 | func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) { 110 | for txn := range lastTxn { 111 | resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request) 112 | txn.Duration = time.Since(txn.Start) 113 | h.reqTimer.Update(txn.Duration) 114 | if err != nil { 115 | tee.Warn("Error reading response from server: %v", err) 116 | // no more responses to be read, we're done 117 | break 118 | } 119 | // make sure we read the body of the response so that 120 | // we don't block the reader 121 | _, _ = httputil.DumpResponse(resp, true) 122 | 123 | txn.Resp = &HttpResponse{Response: resp} 124 | // apparently, Body can be nil in some cases 125 | if resp.Body != nil { 126 | txn.Resp.BodyBytes, txn.Resp.Body, err = extractBody(resp.Body) 127 | if err != nil { 128 | tee.Warn("Failed to extract response body: %v", err) 129 | } 130 | } 131 | 132 | h.Txns.In() <- txn 133 | 134 | // XXX: remove web socket shim in favor of a real websocket protocol analyzer 135 | if txn.Req.Header.Get("Upgrade") == "websocket" { 136 | tee.Info("Upgrading to websocket") 137 | var wg sync.WaitGroup 138 | 139 | // shim for websockets 140 | // in order for websockets to work, we need to continue reading all of the 141 | // the bytes in the analyzer so that the joined connections will continue 142 | // sending bytes to each other 143 | wg.Add(2) 144 | go func() { 145 | ioutil.ReadAll(tee.WriteBuffer()) 146 | wg.Done() 147 | }() 148 | 149 | go func() { 150 | ioutil.ReadAll(tee.ReadBuffer()) 151 | wg.Done() 152 | }() 153 | 154 | wg.Wait() 155 | break 156 | } 157 | } 158 | } 159 | 160 | // we have to vendor DumpRequestOut because it's broken and the fix won't be in until at least 1.4 161 | // XXX: remove this all in favor of actually parsing the HTTP traffic ourselves for more transparent 162 | // replay and inspection, regardless of when it gets fixed in stdlib 163 | 164 | // Copyright 2009 The Go Authors. All rights reserved. 165 | // Use of this source code is governed by a BSD-style 166 | // license that can be found in the LICENSE file. 167 | 168 | // One of the copies, say from b to r2, could be avoided by using a more 169 | // elaborate trick where the other copy is made during Request/Response.Write. 170 | // This would complicate things too much, given that these functions are for 171 | // debugging only. 172 | func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { 173 | var buf bytes.Buffer 174 | if _, err = buf.ReadFrom(b); err != nil { 175 | return nil, nil, err 176 | } 177 | if err = b.Close(); err != nil { 178 | return nil, nil, err 179 | } 180 | return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil 181 | } 182 | 183 | // dumpConn is a net.Conn which writes to Writer and reads from Reader 184 | type dumpConn struct { 185 | io.Writer 186 | io.Reader 187 | } 188 | 189 | func (c *dumpConn) Close() error { return nil } 190 | func (c *dumpConn) LocalAddr() net.Addr { return nil } 191 | func (c *dumpConn) RemoteAddr() net.Addr { return nil } 192 | func (c *dumpConn) SetDeadline(t time.Time) error { return nil } 193 | func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } 194 | func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil } 195 | 196 | type neverEnding byte 197 | 198 | func (b neverEnding) Read(p []byte) (n int, err error) { 199 | for i := range p { 200 | p[i] = byte(b) 201 | } 202 | return len(p), nil 203 | } 204 | 205 | // DumpRequestOut is like DumpRequest but includes 206 | // headers that the standard http.Transport adds, 207 | // such as User-Agent. 208 | func DumpRequestOut(req *http.Request, body bool) ([]byte, error) { 209 | save := req.Body 210 | dummyBody := false 211 | if !body || req.Body == nil { 212 | req.Body = nil 213 | if req.ContentLength != 0 { 214 | req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength)) 215 | dummyBody = true 216 | } 217 | } else { 218 | var err error 219 | save, req.Body, err = drainBody(req.Body) 220 | if err != nil { 221 | return nil, err 222 | } 223 | } 224 | 225 | // Since we're using the actual Transport code to write the request, 226 | // switch to http so the Transport doesn't try to do an SSL 227 | // negotiation with our dumpConn and its bytes.Buffer & pipe. 228 | // The wire format for https and http are the same, anyway. 229 | reqSend := req 230 | if req.URL.Scheme == "https" { 231 | reqSend = new(http.Request) 232 | *reqSend = *req 233 | reqSend.URL = new(url.URL) 234 | *reqSend.URL = *req.URL 235 | reqSend.URL.Scheme = "http" 236 | } 237 | 238 | // Use the actual Transport code to record what we would send 239 | // on the wire, but not using TCP. Use a Transport with a 240 | // custom dialer that returns a fake net.Conn that waits 241 | // for the full input (and recording it), and then responds 242 | // with a dummy response. 243 | var buf bytes.Buffer // records the output 244 | pr, pw := io.Pipe() 245 | dr := &delegateReader{c: make(chan io.Reader)} 246 | // Wait for the request before replying with a dummy response: 247 | go func() { 248 | req, _ := http.ReadRequest(bufio.NewReader(pr)) 249 | // THIS IS THE PART THAT'S BROKEN IN THE STDLIB (as of Go 1.3) 250 | if req != nil && req.Body != nil { 251 | ioutil.ReadAll(req.Body) 252 | } 253 | dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\n\r\n") 254 | }() 255 | 256 | t := &http.Transport{ 257 | Dial: func(net, addr string) (net.Conn, error) { 258 | return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil 259 | }, 260 | } 261 | defer t.CloseIdleConnections() 262 | 263 | _, err := t.RoundTrip(reqSend) 264 | 265 | req.Body = save 266 | if err != nil { 267 | return nil, err 268 | } 269 | dump := buf.Bytes() 270 | 271 | // If we used a dummy body above, remove it now. 272 | // TODO: if the req.ContentLength is large, we allocate memory 273 | // unnecessarily just to slice it off here. But this is just 274 | // a debug function, so this is acceptable for now. We could 275 | // discard the body earlier if this matters. 276 | if dummyBody { 277 | if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 { 278 | dump = dump[:i+4] 279 | } 280 | } 281 | return dump, nil 282 | } 283 | 284 | // delegateReader is a reader that delegates to another reader, 285 | // once it arrives on a channel. 286 | type delegateReader struct { 287 | c chan io.Reader 288 | r io.Reader // nil until received from c 289 | } 290 | 291 | func (r *delegateReader) Read(p []byte) (int, error) { 292 | if r.r == nil { 293 | r.r = <-r.c 294 | } 295 | return r.r.Read(p) 296 | } 297 | 298 | // Return value if nonempty, def otherwise. 299 | func valueOrDefault(value, def string) string { 300 | if value != "" { 301 | return value 302 | } 303 | return def 304 | } 305 | 306 | var reqWriteExcludeHeaderDump = map[string]bool{ 307 | "Host": true, // not in Header map anyway 308 | "Content-Length": true, 309 | "Transfer-Encoding": true, 310 | "Trailer": true, 311 | } 312 | -------------------------------------------------------------------------------- /src/ngrok/proto/interface.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "ngrok/conn" 5 | ) 6 | 7 | type Protocol interface { 8 | GetName() string 9 | WrapConn(conn.Conn, interface{}) conn.Conn 10 | } 11 | -------------------------------------------------------------------------------- /src/ngrok/proto/tcp.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "ngrok/conn" 5 | ) 6 | 7 | type Tcp struct{} 8 | 9 | func NewTcp() *Tcp { 10 | return new(Tcp) 11 | } 12 | 13 | func (h *Tcp) GetName() string { return "tcp" } 14 | 15 | func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn { 16 | return c 17 | } 18 | -------------------------------------------------------------------------------- /src/ngrok/server/cli.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | type Options struct { 8 | httpAddr string 9 | httpsAddr string 10 | tunnelAddr string 11 | domain string 12 | tlsCrt string 13 | tlsKey string 14 | logto string 15 | loglevel string 16 | } 17 | 18 | func parseArgs() *Options { 19 | httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable") 20 | httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable") 21 | tunnelAddr := flag.String("tunnelAddr", ":4443", "Public address listening for ngrok client") 22 | domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted") 23 | tlsCrt := flag.String("tlsCrt", "", "Path to a TLS certificate file") 24 | tlsKey := flag.String("tlsKey", "", "Path to a TLS key file") 25 | logto := flag.String("log", "stdout", "Write log messages to this file. 'stdout' and 'none' have special meanings") 26 | loglevel := flag.String("log-level", "DEBUG", "The level of messages to log. One of: DEBUG, INFO, WARNING, ERROR") 27 | flag.Parse() 28 | 29 | return &Options{ 30 | httpAddr: *httpAddr, 31 | httpsAddr: *httpsAddr, 32 | tunnelAddr: *tunnelAddr, 33 | domain: *domain, 34 | tlsCrt: *tlsCrt, 35 | tlsKey: *tlsKey, 36 | logto: *logto, 37 | loglevel: *loglevel, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ngrok/server/control.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "ngrok/conn" 7 | "ngrok/msg" 8 | "ngrok/util" 9 | "ngrok/version" 10 | "runtime/debug" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | pingTimeoutInterval = 30 * time.Second 17 | connReapInterval = 10 * time.Second 18 | controlWriteTimeout = 10 * time.Second 19 | proxyStaleDuration = 60 * time.Second 20 | proxyMaxPoolSize = 10 21 | ) 22 | 23 | type Control struct { 24 | // auth message 25 | auth *msg.Auth 26 | 27 | // actual connection 28 | conn conn.Conn 29 | 30 | // put a message in this channel to send it over 31 | // conn to the client 32 | out chan (msg.Message) 33 | 34 | // read from this channel to get the next message sent 35 | // to us over conn by the client 36 | in chan (msg.Message) 37 | 38 | // the last time we received a ping from the client - for heartbeats 39 | lastPing time.Time 40 | 41 | // all of the tunnels this control connection handles 42 | tunnels []*Tunnel 43 | 44 | // proxy connections 45 | proxies chan conn.Conn 46 | 47 | // identifier 48 | id string 49 | 50 | // synchronizer for controlled shutdown of writer() 51 | writerShutdown *util.Shutdown 52 | 53 | // synchronizer for controlled shutdown of reader() 54 | readerShutdown *util.Shutdown 55 | 56 | // synchronizer for controlled shutdown of manager() 57 | managerShutdown *util.Shutdown 58 | 59 | // synchronizer for controller shutdown of entire Control 60 | shutdown *util.Shutdown 61 | } 62 | 63 | func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) { 64 | var err error 65 | 66 | // create the object 67 | c := &Control{ 68 | auth: authMsg, 69 | conn: ctlConn, 70 | out: make(chan msg.Message), 71 | in: make(chan msg.Message), 72 | proxies: make(chan conn.Conn, 10), 73 | lastPing: time.Now(), 74 | writerShutdown: util.NewShutdown(), 75 | readerShutdown: util.NewShutdown(), 76 | managerShutdown: util.NewShutdown(), 77 | shutdown: util.NewShutdown(), 78 | } 79 | 80 | failAuth := func(e error) { 81 | _ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()}) 82 | ctlConn.Close() 83 | } 84 | 85 | // register the clientid 86 | c.id = authMsg.ClientId 87 | if c.id == "" { 88 | // it's a new session, assign an ID 89 | if c.id, err = util.SecureRandId(16); err != nil { 90 | failAuth(err) 91 | return 92 | } 93 | } 94 | 95 | // set logging prefix 96 | ctlConn.SetType("ctl") 97 | ctlConn.AddLogPrefix(c.id) 98 | 99 | if authMsg.Version != version.Proto { 100 | failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version)) 101 | return 102 | } 103 | 104 | // register the control 105 | if replaced := controlRegistry.Add(c.id, c); replaced != nil { 106 | replaced.shutdown.WaitComplete() 107 | } 108 | 109 | // start the writer first so that the following messages get sent 110 | go c.writer() 111 | 112 | // Respond to authentication 113 | c.out <- &msg.AuthResp{ 114 | Version: version.Proto, 115 | MmVersion: version.MajorMinor(), 116 | ClientId: c.id, 117 | } 118 | 119 | // As a performance optimization, ask for a proxy connection up front 120 | c.out <- &msg.ReqProxy{} 121 | 122 | // manage the connection 123 | go c.manager() 124 | go c.reader() 125 | go c.stopper() 126 | } 127 | 128 | // Register a new tunnel on this control connection 129 | func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) { 130 | for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") { 131 | tunnelReq := *rawTunnelReq 132 | tunnelReq.Protocol = proto 133 | 134 | c.conn.Debug("Registering new tunnel") 135 | t, err := NewTunnel(&tunnelReq, c) 136 | if err != nil { 137 | c.out <- &msg.NewTunnel{Error: err.Error()} 138 | if len(c.tunnels) == 0 { 139 | c.shutdown.Begin() 140 | } 141 | 142 | // we're done 143 | return 144 | } 145 | 146 | // add it to the list of tunnels 147 | c.tunnels = append(c.tunnels, t) 148 | 149 | // acknowledge success 150 | c.out <- &msg.NewTunnel{ 151 | Url: t.url, 152 | Protocol: proto, 153 | ReqId: rawTunnelReq.ReqId, 154 | } 155 | 156 | rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1) 157 | } 158 | } 159 | 160 | func (c *Control) manager() { 161 | // don't crash on panics 162 | defer func() { 163 | if err := recover(); err != nil { 164 | c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack()) 165 | } 166 | }() 167 | 168 | // kill everything if the control manager stops 169 | defer c.shutdown.Begin() 170 | 171 | // notify that manager() has shutdown 172 | defer c.managerShutdown.Complete() 173 | 174 | // reaping timer for detecting heartbeat failure 175 | reap := time.NewTicker(connReapInterval) 176 | defer reap.Stop() 177 | 178 | for { 179 | select { 180 | case <-reap.C: 181 | if time.Since(c.lastPing) > pingTimeoutInterval { 182 | c.conn.Info("Lost heartbeat") 183 | c.shutdown.Begin() 184 | } 185 | 186 | case mRaw, ok := <-c.in: 187 | // c.in closes to indicate shutdown 188 | if !ok { 189 | return 190 | } 191 | 192 | switch m := mRaw.(type) { 193 | case *msg.ReqTunnel: 194 | c.registerTunnel(m) 195 | 196 | case *msg.Ping: 197 | c.lastPing = time.Now() 198 | c.out <- &msg.Pong{} 199 | } 200 | } 201 | } 202 | } 203 | 204 | func (c *Control) writer() { 205 | defer func() { 206 | if err := recover(); err != nil { 207 | c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack()) 208 | } 209 | }() 210 | 211 | // kill everything if the writer() stops 212 | defer c.shutdown.Begin() 213 | 214 | // notify that we've flushed all messages 215 | defer c.writerShutdown.Complete() 216 | 217 | // write messages to the control channel 218 | for m := range c.out { 219 | c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout)) 220 | if err := msg.WriteMsg(c.conn, m); err != nil { 221 | panic(err) 222 | } 223 | } 224 | } 225 | 226 | func (c *Control) reader() { 227 | defer func() { 228 | if err := recover(); err != nil { 229 | c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack()) 230 | } 231 | }() 232 | 233 | // kill everything if the reader stops 234 | defer c.shutdown.Begin() 235 | 236 | // notify that we're done 237 | defer c.readerShutdown.Complete() 238 | 239 | // read messages from the control channel 240 | for { 241 | if msg, err := msg.ReadMsg(c.conn); err != nil { 242 | if err == io.EOF { 243 | c.conn.Info("EOF") 244 | return 245 | } else { 246 | panic(err) 247 | } 248 | } else { 249 | // this can also panic during shutdown 250 | c.in <- msg 251 | } 252 | } 253 | } 254 | 255 | func (c *Control) stopper() { 256 | defer func() { 257 | if r := recover(); r != nil { 258 | c.conn.Error("Failed to shut down control: %v", r) 259 | } 260 | }() 261 | 262 | // wait until we're instructed to shutdown 263 | c.shutdown.WaitBegin() 264 | 265 | // remove ourself from the control registry 266 | controlRegistry.Del(c.id) 267 | 268 | // shutdown manager() so that we have no more work to do 269 | close(c.in) 270 | c.managerShutdown.WaitComplete() 271 | 272 | // shutdown writer() 273 | close(c.out) 274 | c.writerShutdown.WaitComplete() 275 | 276 | // close connection fully 277 | c.conn.Close() 278 | 279 | // shutdown all of the tunnels 280 | for _, t := range c.tunnels { 281 | t.Shutdown() 282 | } 283 | 284 | // shutdown all of the proxy connections 285 | close(c.proxies) 286 | for p := range c.proxies { 287 | p.Close() 288 | } 289 | 290 | c.shutdown.Complete() 291 | c.conn.Info("Shutdown complete") 292 | } 293 | 294 | func (c *Control) RegisterProxy(conn conn.Conn) { 295 | conn.AddLogPrefix(c.id) 296 | 297 | conn.SetDeadline(time.Now().Add(proxyStaleDuration)) 298 | select { 299 | case c.proxies <- conn: 300 | conn.Info("Registered") 301 | default: 302 | conn.Info("Proxies buffer is full, discarding.") 303 | conn.Close() 304 | } 305 | } 306 | 307 | // Remove a proxy connection from the pool and return it 308 | // If not proxy connections are in the pool, request one 309 | // and wait until it is available 310 | // Returns an error if we couldn't get a proxy because it took too long 311 | // or the tunnel is closing 312 | func (c *Control) GetProxy() (proxyConn conn.Conn, err error) { 313 | var ok bool 314 | 315 | // get a proxy connection from the pool 316 | select { 317 | case proxyConn, ok = <-c.proxies: 318 | if !ok { 319 | err = fmt.Errorf("No proxy connections available, control is closing") 320 | return 321 | } 322 | default: 323 | // no proxy available in the pool, ask for one over the control channel 324 | c.conn.Debug("No proxy in pool, requesting proxy from control . . .") 325 | if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil { 326 | return 327 | } 328 | 329 | select { 330 | case proxyConn, ok = <-c.proxies: 331 | if !ok { 332 | err = fmt.Errorf("No proxy connections available, control is closing") 333 | return 334 | } 335 | 336 | case <-time.After(pingTimeoutInterval): 337 | err = fmt.Errorf("Timeout trying to get proxy connection") 338 | return 339 | } 340 | } 341 | return 342 | } 343 | 344 | // Called when this control is replaced by another control 345 | // this can happen if the network drops out and the client reconnects 346 | // before the old tunnel has lost its heartbeat 347 | func (c *Control) Replaced(replacement *Control) { 348 | c.conn.Info("Replaced by control: %s", replacement.conn.Id()) 349 | 350 | // set the control id to empty string so that when stopper() 351 | // calls registry.Del it won't delete the replacement 352 | c.id = "" 353 | 354 | // tell the old one to shutdown 355 | c.shutdown.Begin() 356 | } 357 | -------------------------------------------------------------------------------- /src/ngrok/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | vhost "github.com/inconshreveable/go-vhost" 7 | //"net" 8 | "ngrok/conn" 9 | "ngrok/log" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | NotAuthorized = `HTTP/1.0 401 Not Authorized 16 | WWW-Authenticate: Basic realm="ngrok" 17 | Content-Length: 23 18 | 19 | Authorization required 20 | ` 21 | 22 | NotFound = `HTTP/1.0 404 Not Found 23 | Content-Length: %d 24 | 25 | Tunnel %s not found 26 | ` 27 | 28 | BadRequest = `HTTP/1.0 400 Bad Request 29 | Content-Length: 12 30 | 31 | Bad Request 32 | ` 33 | ) 34 | 35 | // Listens for new http(s) connections from the public internet 36 | func startHttpListener(addr string, tlsCfg *tls.Config) (listener *conn.Listener) { 37 | // bind/listen for incoming connections 38 | var err error 39 | if listener, err = conn.Listen(addr, "pub", tlsCfg); err != nil { 40 | panic(err) 41 | } 42 | 43 | proto := "http" 44 | if tlsCfg != nil { 45 | proto = "https" 46 | } 47 | 48 | log.Info("Listening for public %s connections on %v", proto, listener.Addr.String()) 49 | go func() { 50 | for conn := range listener.Conns { 51 | go httpHandler(conn, proto) 52 | } 53 | }() 54 | 55 | return 56 | } 57 | 58 | // Handles a new http connection from the public internet 59 | func httpHandler(c conn.Conn, proto string) { 60 | defer c.Close() 61 | defer func() { 62 | // recover from failures 63 | if r := recover(); r != nil { 64 | c.Warn("httpHandler failed with error %v", r) 65 | } 66 | }() 67 | 68 | // Make sure we detect dead connections while we decide how to multiplex 69 | c.SetDeadline(time.Now().Add(connReadTimeout)) 70 | 71 | // multiplex by extracting the Host header, the vhost library 72 | vhostConn, err := vhost.HTTP(c) 73 | if err != nil { 74 | c.Warn("Failed to read valid %s request: %v", proto, err) 75 | c.Write([]byte(BadRequest)) 76 | return 77 | } 78 | 79 | // read out the Host header and auth from the request 80 | host := strings.ToLower(vhostConn.Host()) 81 | auth := vhostConn.Request.Header.Get("Authorization") 82 | 83 | // done reading mux data, free up the request memory 84 | vhostConn.Free() 85 | 86 | // We need to read from the vhost conn now since it mucked around reading the stream 87 | c = conn.Wrap(vhostConn, "pub") 88 | 89 | // multiplex to find the right backend host 90 | c.Debug("Found hostname %s in request", host) 91 | tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host)) 92 | if tunnel == nil { 93 | c.Info("No tunnel found for hostname %s", host) 94 | c.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host))) 95 | return 96 | } 97 | 98 | // If the client specified http auth and it doesn't match this request's auth 99 | // then fail the request with 401 Not Authorized and request the client reissue the 100 | // request with basic authdeny the request 101 | if tunnel.req.HttpAuth != "" && auth != tunnel.req.HttpAuth { 102 | c.Info("Authentication failed: %s", auth) 103 | c.Write([]byte(NotAuthorized)) 104 | return 105 | } 106 | 107 | // dead connections will now be handled by tunnel heartbeating and the client 108 | c.SetDeadline(time.Time{}) 109 | 110 | // let the tunnel handle the connection now 111 | tunnel.HandlePublicConnection(c) 112 | } 113 | -------------------------------------------------------------------------------- /src/ngrok/server/main.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "math/rand" 6 | "ngrok/conn" 7 | log "ngrok/log" 8 | "ngrok/msg" 9 | "ngrok/util" 10 | "os" 11 | "runtime/debug" 12 | "time" 13 | ) 14 | 15 | const ( 16 | registryCacheSize uint64 = 1024 * 1024 // 1 MB 17 | connReadTimeout time.Duration = 10 * time.Second 18 | ) 19 | 20 | // GLOBALS 21 | var ( 22 | tunnelRegistry *TunnelRegistry 23 | controlRegistry *ControlRegistry 24 | 25 | // XXX: kill these global variables - they're only used in tunnel.go for constructing forwarding URLs 26 | opts *Options 27 | listeners map[string]*conn.Listener 28 | ) 29 | 30 | func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) { 31 | // fail gracefully if the proxy connection fails to register 32 | defer func() { 33 | if r := recover(); r != nil { 34 | pxyConn.Warn("Failed with error: %v", r) 35 | pxyConn.Close() 36 | } 37 | }() 38 | 39 | // set logging prefix 40 | pxyConn.SetType("pxy") 41 | 42 | // look up the control connection for this proxy 43 | pxyConn.Info("Registering new proxy for %s", regPxy.ClientId) 44 | ctl := controlRegistry.Get(regPxy.ClientId) 45 | 46 | if ctl == nil { 47 | panic("No client found for identifier: " + regPxy.ClientId) 48 | } 49 | 50 | ctl.RegisterProxy(pxyConn) 51 | } 52 | 53 | // Listen for incoming control and proxy connections 54 | // We listen for incoming control and proxy connections on the same port 55 | // for ease of deployment. The hope is that by running on port 443, using 56 | // TLS and running all connections over the same port, we can bust through 57 | // restrictive firewalls. 58 | func tunnelListener(addr string, tlsConfig *tls.Config) { 59 | // listen for incoming connections 60 | listener, err := conn.Listen(addr, "tun", tlsConfig) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | log.Info("Listening for control and proxy connections on %s", listener.Addr.String()) 66 | for c := range listener.Conns { 67 | go func(tunnelConn conn.Conn) { 68 | // don't crash on panics 69 | defer func() { 70 | if r := recover(); r != nil { 71 | tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack()) 72 | } 73 | }() 74 | 75 | tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout)) 76 | var rawMsg msg.Message 77 | if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil { 78 | tunnelConn.Warn("Failed to read message: %v", err) 79 | tunnelConn.Close() 80 | return 81 | } 82 | 83 | // don't timeout after the initial read, tunnel heartbeating will kill 84 | // dead connections 85 | tunnelConn.SetReadDeadline(time.Time{}) 86 | 87 | switch m := rawMsg.(type) { 88 | case *msg.Auth: 89 | NewControl(tunnelConn, m) 90 | 91 | case *msg.RegProxy: 92 | NewProxy(tunnelConn, m) 93 | 94 | default: 95 | tunnelConn.Close() 96 | } 97 | }(c) 98 | } 99 | } 100 | 101 | func Main() { 102 | // parse options 103 | opts = parseArgs() 104 | 105 | // init logging 106 | log.LogTo(opts.logto, opts.loglevel) 107 | 108 | // seed random number generator 109 | seed, err := util.RandomSeed() 110 | if err != nil { 111 | panic(err) 112 | } 113 | rand.Seed(seed) 114 | 115 | // init tunnel/control registry 116 | registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE") 117 | tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile) 118 | controlRegistry = NewControlRegistry() 119 | 120 | // start listeners 121 | listeners = make(map[string]*conn.Listener) 122 | 123 | // load tls configuration 124 | tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey) 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | // listen for http 130 | if opts.httpAddr != "" { 131 | listeners["http"] = startHttpListener(opts.httpAddr, nil) 132 | } 133 | 134 | // listen for https 135 | if opts.httpsAddr != "" { 136 | listeners["https"] = startHttpListener(opts.httpsAddr, tlsConfig) 137 | } 138 | 139 | // ngrok clients 140 | tunnelListener(opts.tunnelAddr, tlsConfig) 141 | } 142 | -------------------------------------------------------------------------------- /src/ngrok/server/metrics.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | gometrics "github.com/rcrowley/go-metrics" 8 | "io/ioutil" 9 | "net/http" 10 | "ngrok/conn" 11 | "ngrok/log" 12 | "os" 13 | "time" 14 | ) 15 | 16 | var metrics Metrics 17 | 18 | func init() { 19 | keenApiKey := os.Getenv("KEEN_API_KEY") 20 | 21 | if keenApiKey != "" { 22 | metrics = NewKeenIoMetrics(60 * time.Second) 23 | } else { 24 | metrics = NewLocalMetrics(30 * time.Second) 25 | } 26 | } 27 | 28 | type Metrics interface { 29 | log.Logger 30 | OpenConnection(*Tunnel, conn.Conn) 31 | CloseConnection(*Tunnel, conn.Conn, time.Time, int64, int64) 32 | OpenTunnel(*Tunnel) 33 | CloseTunnel(*Tunnel) 34 | } 35 | 36 | type LocalMetrics struct { 37 | log.Logger 38 | reportInterval time.Duration 39 | windowsCounter gometrics.Counter 40 | linuxCounter gometrics.Counter 41 | osxCounter gometrics.Counter 42 | otherCounter gometrics.Counter 43 | 44 | tunnelMeter gometrics.Meter 45 | tcpTunnelMeter gometrics.Meter 46 | httpTunnelMeter gometrics.Meter 47 | connMeter gometrics.Meter 48 | lostHeartbeatMeter gometrics.Meter 49 | 50 | connTimer gometrics.Timer 51 | 52 | bytesInCount gometrics.Counter 53 | bytesOutCount gometrics.Counter 54 | 55 | /* 56 | tunnelGauge gometrics.Gauge 57 | tcpTunnelGauge gometrics.Gauge 58 | connGauge gometrics.Gauge 59 | */ 60 | } 61 | 62 | func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics { 63 | metrics := LocalMetrics{ 64 | Logger: log.NewPrefixLogger("metrics"), 65 | reportInterval: reportInterval, 66 | windowsCounter: gometrics.NewCounter(), 67 | linuxCounter: gometrics.NewCounter(), 68 | osxCounter: gometrics.NewCounter(), 69 | otherCounter: gometrics.NewCounter(), 70 | 71 | tunnelMeter: gometrics.NewMeter(), 72 | tcpTunnelMeter: gometrics.NewMeter(), 73 | httpTunnelMeter: gometrics.NewMeter(), 74 | connMeter: gometrics.NewMeter(), 75 | lostHeartbeatMeter: gometrics.NewMeter(), 76 | 77 | connTimer: gometrics.NewTimer(), 78 | 79 | bytesInCount: gometrics.NewCounter(), 80 | bytesOutCount: gometrics.NewCounter(), 81 | 82 | /* 83 | metrics.tunnelGauge = gometrics.NewGauge(), 84 | metrics.tcpTunnelGauge = gometrics.NewGauge(), 85 | metrics.connGauge = gometrics.NewGauge(), 86 | */ 87 | } 88 | 89 | go metrics.Report() 90 | 91 | return &metrics 92 | } 93 | 94 | func (m *LocalMetrics) OpenTunnel(t *Tunnel) { 95 | m.tunnelMeter.Mark(1) 96 | 97 | switch t.ctl.auth.OS { 98 | case "windows": 99 | m.windowsCounter.Inc(1) 100 | case "linux": 101 | m.linuxCounter.Inc(1) 102 | case "darwin": 103 | m.osxCounter.Inc(1) 104 | default: 105 | m.otherCounter.Inc(1) 106 | } 107 | 108 | switch t.req.Protocol { 109 | case "tcp": 110 | m.tcpTunnelMeter.Mark(1) 111 | case "http": 112 | m.httpTunnelMeter.Mark(1) 113 | } 114 | } 115 | 116 | func (m *LocalMetrics) CloseTunnel(t *Tunnel) { 117 | } 118 | 119 | func (m *LocalMetrics) OpenConnection(t *Tunnel, c conn.Conn) { 120 | m.connMeter.Mark(1) 121 | } 122 | 123 | func (m *LocalMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, bytesIn, bytesOut int64) { 124 | m.bytesInCount.Inc(bytesIn) 125 | m.bytesOutCount.Inc(bytesOut) 126 | } 127 | 128 | func (m *LocalMetrics) Report() { 129 | m.Info("Reporting every %d seconds", int(m.reportInterval.Seconds())) 130 | 131 | for { 132 | time.Sleep(m.reportInterval) 133 | buffer, err := json.Marshal(map[string]interface{}{ 134 | "windows": m.windowsCounter.Count(), 135 | "linux": m.linuxCounter.Count(), 136 | "osx": m.osxCounter.Count(), 137 | "other": m.otherCounter.Count(), 138 | "httpTunnelMeter.count": m.httpTunnelMeter.Count(), 139 | "tcpTunnelMeter.count": m.tcpTunnelMeter.Count(), 140 | "tunnelMeter.count": m.tunnelMeter.Count(), 141 | "tunnelMeter.m1": m.tunnelMeter.Rate1(), 142 | "connMeter.count": m.connMeter.Count(), 143 | "connMeter.m1": m.connMeter.Rate1(), 144 | "bytesIn.count": m.bytesInCount.Count(), 145 | "bytesOut.count": m.bytesOutCount.Count(), 146 | }) 147 | 148 | if err != nil { 149 | m.Error("Failed to serialize metrics: %v", err) 150 | continue 151 | } 152 | 153 | m.Info("Reporting: %s", buffer) 154 | } 155 | } 156 | 157 | type KeenIoMetric struct { 158 | Collection string 159 | Event interface{} 160 | } 161 | 162 | type KeenIoMetrics struct { 163 | log.Logger 164 | ApiKey string 165 | ProjectToken string 166 | HttpClient http.Client 167 | Metrics chan *KeenIoMetric 168 | } 169 | 170 | func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics { 171 | k := &KeenIoMetrics{ 172 | Logger: log.NewPrefixLogger("metrics"), 173 | ApiKey: os.Getenv("KEEN_API_KEY"), 174 | ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"), 175 | Metrics: make(chan *KeenIoMetric, 1000), 176 | } 177 | 178 | go func() { 179 | defer func() { 180 | if r := recover(); r != nil { 181 | k.Error("KeenIoMetrics failed: %v", r) 182 | } 183 | }() 184 | 185 | batch := make(map[string][]interface{}) 186 | batchTimer := time.Tick(batchInterval) 187 | 188 | for { 189 | select { 190 | case m := <-k.Metrics: 191 | list, ok := batch[m.Collection] 192 | if !ok { 193 | list = make([]interface{}, 0) 194 | } 195 | batch[m.Collection] = append(list, m.Event) 196 | 197 | case <-batchTimer: 198 | // no metrics to report 199 | if len(batch) == 0 { 200 | continue 201 | } 202 | 203 | payload, err := json.Marshal(batch) 204 | if err != nil { 205 | k.Error("Failed to serialize metrics payload: %v, %v", batch, err) 206 | } else { 207 | for key, val := range batch { 208 | k.Debug("Reporting %d metrics for %s", len(val), key) 209 | } 210 | 211 | k.AuthedRequest("POST", "/events", bytes.NewReader(payload)) 212 | } 213 | batch = make(map[string][]interface{}) 214 | } 215 | } 216 | }() 217 | 218 | return k 219 | } 220 | 221 | func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (resp *http.Response, err error) { 222 | path = fmt.Sprintf("https://api.keen.io/3.0/projects/%s%s", k.ProjectToken, path) 223 | req, err := http.NewRequest(method, path, body) 224 | if err != nil { 225 | return 226 | } 227 | 228 | req.Header.Add("Authorization", k.ApiKey) 229 | 230 | if body != nil { 231 | req.Header.Add("Content-Type", "application/json") 232 | req.ContentLength = int64(body.Len()) 233 | } 234 | 235 | requestStartAt := time.Now() 236 | resp, err = k.HttpClient.Do(req) 237 | 238 | if err != nil { 239 | k.Error("Failed to send metric event to keen.io %v", err) 240 | } else { 241 | k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds()) 242 | defer resp.Body.Close() 243 | if resp.StatusCode != 200 { 244 | bytes, _ := ioutil.ReadAll(resp.Body) 245 | k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes) 246 | } 247 | } 248 | 249 | return 250 | } 251 | 252 | func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) { 253 | } 254 | 255 | func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) { 256 | event := struct { 257 | Keen KeenStruct `json:"keen"` 258 | OS string 259 | ClientId string 260 | Protocol string 261 | Url string 262 | User string 263 | Version string 264 | Reason string 265 | HttpAuth bool 266 | Subdomain bool 267 | TunnelDuration float64 268 | ConnectionDuration float64 269 | BytesIn int64 270 | BytesOut int64 271 | }{ 272 | Keen: KeenStruct{ 273 | Timestamp: start.UTC().Format("2006-01-02T15:04:05.000Z"), 274 | }, 275 | OS: t.ctl.auth.OS, 276 | ClientId: t.ctl.id, 277 | Protocol: t.req.Protocol, 278 | Url: t.url, 279 | User: t.ctl.auth.User, 280 | Version: t.ctl.auth.MmVersion, 281 | HttpAuth: t.req.HttpAuth != "", 282 | Subdomain: t.req.Subdomain != "", 283 | TunnelDuration: time.Since(t.start).Seconds(), 284 | ConnectionDuration: time.Since(start).Seconds(), 285 | BytesIn: in, 286 | BytesOut: out, 287 | } 288 | 289 | k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event} 290 | } 291 | 292 | func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) { 293 | } 294 | 295 | type KeenStruct struct { 296 | Timestamp string `json:"timestamp"` 297 | } 298 | 299 | func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) { 300 | event := struct { 301 | Keen KeenStruct `json:"keen"` 302 | OS string 303 | ClientId string 304 | Protocol string 305 | Url string 306 | User string 307 | Version string 308 | Reason string 309 | Duration float64 310 | HttpAuth bool 311 | Subdomain bool 312 | }{ 313 | Keen: KeenStruct{ 314 | Timestamp: t.start.UTC().Format("2006-01-02T15:04:05.000Z"), 315 | }, 316 | OS: t.ctl.auth.OS, 317 | ClientId: t.ctl.id, 318 | Protocol: t.req.Protocol, 319 | Url: t.url, 320 | User: t.ctl.auth.User, 321 | Version: t.ctl.auth.MmVersion, 322 | //Reason: reason, 323 | Duration: time.Since(t.start).Seconds(), 324 | HttpAuth: t.req.HttpAuth != "", 325 | Subdomain: t.req.Subdomain != "", 326 | } 327 | 328 | k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event} 329 | } 330 | -------------------------------------------------------------------------------- /src/ngrok/server/registry.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/gob" 5 | "fmt" 6 | "net" 7 | "ngrok/cache" 8 | "ngrok/log" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const ( 14 | cacheSaveInterval time.Duration = 10 * time.Minute 15 | ) 16 | 17 | type cacheUrl string 18 | 19 | func (url cacheUrl) Size() int { 20 | return len(url) 21 | } 22 | 23 | // TunnelRegistry maps a tunnel URL to Tunnel structures 24 | type TunnelRegistry struct { 25 | tunnels map[string]*Tunnel 26 | affinity *cache.LRUCache 27 | log.Logger 28 | sync.RWMutex 29 | } 30 | 31 | func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry { 32 | registry := &TunnelRegistry{ 33 | tunnels: make(map[string]*Tunnel), 34 | affinity: cache.NewLRUCache(cacheSize), 35 | Logger: log.NewPrefixLogger("registry", "tun"), 36 | } 37 | 38 | // LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail 39 | // to encode or decode any non-primitive types that haven't been "registered" 40 | // with it. Since we store cacheUrl objects, we need to register them here first 41 | // for the encoding/decoding to work 42 | var urlobj cacheUrl 43 | gob.Register(urlobj) 44 | 45 | // try to load and then periodically save the affinity cache to file, if specified 46 | if cacheFile != "" { 47 | err := registry.affinity.LoadItemsFromFile(cacheFile) 48 | if err != nil { 49 | registry.Error("Failed to load affinity cache %s: %v", cacheFile, err) 50 | } 51 | 52 | registry.SaveCacheThread(cacheFile, cacheSaveInterval) 53 | } else { 54 | registry.Info("No affinity cache specified") 55 | } 56 | 57 | return registry 58 | } 59 | 60 | // Spawns a goroutine the periodically saves the cache to a file. 61 | func (r *TunnelRegistry) SaveCacheThread(path string, interval time.Duration) { 62 | go func() { 63 | r.Info("Saving affinity cache to %s every %s", path, interval.String()) 64 | for { 65 | time.Sleep(interval) 66 | 67 | r.Debug("Saving affinity cache") 68 | err := r.affinity.SaveItemsToFile(path) 69 | if err != nil { 70 | r.Error("Failed to save affinity cache: %v", err) 71 | } else { 72 | r.Info("Saved affinity cache") 73 | } 74 | } 75 | }() 76 | } 77 | 78 | // Register a tunnel with a specific url, returns an error 79 | // if a tunnel is already registered at that url 80 | func (r *TunnelRegistry) Register(url string, t *Tunnel) error { 81 | r.Lock() 82 | defer r.Unlock() 83 | 84 | if r.tunnels[url] != nil { 85 | return fmt.Errorf("The tunnel %s is already registered.", url) 86 | } 87 | 88 | r.tunnels[url] = t 89 | 90 | return nil 91 | } 92 | 93 | func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) { 94 | clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String() 95 | clientId := t.ctl.id 96 | 97 | ipKey := fmt.Sprintf("client-ip-%s:%s", t.req.Protocol, clientIp) 98 | idKey := fmt.Sprintf("client-id-%s:%s", t.req.Protocol, clientId) 99 | return ipKey, idKey 100 | } 101 | 102 | func (r *TunnelRegistry) GetCachedRegistration(t *Tunnel) (url string) { 103 | ipCacheKey, idCacheKey := r.cacheKeys(t) 104 | 105 | // check cache for ID first, because we prefer that over IP which might 106 | // not be specific to a user because of NATs 107 | if v, ok := r.affinity.Get(idCacheKey); ok { 108 | url = string(v.(cacheUrl)) 109 | t.Debug("Found registry affinity %s for %s", url, idCacheKey) 110 | } else if v, ok := r.affinity.Get(ipCacheKey); ok { 111 | url = string(v.(cacheUrl)) 112 | t.Debug("Found registry affinity %s for %s", url, ipCacheKey) 113 | } 114 | return 115 | } 116 | 117 | func (r *TunnelRegistry) RegisterAndCache(url string, t *Tunnel) (err error) { 118 | if err = r.Register(url, t); err == nil { 119 | // we successfully assigned a url, cache it 120 | ipCacheKey, idCacheKey := r.cacheKeys(t) 121 | r.affinity.Set(ipCacheKey, cacheUrl(url)) 122 | r.affinity.Set(idCacheKey, cacheUrl(url)) 123 | } 124 | return 125 | 126 | } 127 | 128 | // Register a tunnel with the following process: 129 | // Consult the affinity cache to try to assign a previously used tunnel url if possible 130 | // Generate new urls repeatedly with the urlFn and register until one is available. 131 | func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) (string, error) { 132 | url := r.GetCachedRegistration(t) 133 | if url == "" { 134 | url = urlFn() 135 | } 136 | 137 | maxAttempts := 5 138 | for i := 0; i < maxAttempts; i++ { 139 | if err := r.RegisterAndCache(url, t); err != nil { 140 | // pick a new url and try again 141 | url = urlFn() 142 | } else { 143 | // we successfully assigned a url, we're done 144 | return url, nil 145 | } 146 | } 147 | 148 | return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts) 149 | } 150 | 151 | func (r *TunnelRegistry) Del(url string) { 152 | r.Lock() 153 | defer r.Unlock() 154 | delete(r.tunnels, url) 155 | } 156 | 157 | func (r *TunnelRegistry) Get(url string) *Tunnel { 158 | r.RLock() 159 | defer r.RUnlock() 160 | return r.tunnels[url] 161 | } 162 | 163 | // ControlRegistry maps a client ID to Control structures 164 | type ControlRegistry struct { 165 | controls map[string]*Control 166 | log.Logger 167 | sync.RWMutex 168 | } 169 | 170 | func NewControlRegistry() *ControlRegistry { 171 | return &ControlRegistry{ 172 | controls: make(map[string]*Control), 173 | Logger: log.NewPrefixLogger("registry", "ctl"), 174 | } 175 | } 176 | 177 | func (r *ControlRegistry) Get(clientId string) *Control { 178 | r.RLock() 179 | defer r.RUnlock() 180 | return r.controls[clientId] 181 | } 182 | 183 | func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) { 184 | r.Lock() 185 | defer r.Unlock() 186 | 187 | oldCtl = r.controls[clientId] 188 | if oldCtl != nil { 189 | oldCtl.Replaced(ctl) 190 | } 191 | 192 | r.controls[clientId] = ctl 193 | r.Info("Registered control with id %s", clientId) 194 | return 195 | } 196 | 197 | func (r *ControlRegistry) Del(clientId string) error { 198 | r.Lock() 199 | defer r.Unlock() 200 | if r.controls[clientId] == nil { 201 | return fmt.Errorf("No control found for client id: %s", clientId) 202 | } else { 203 | r.Info("Removed control registry id %s", clientId) 204 | delete(r.controls, clientId) 205 | return nil 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/ngrok/server/tls.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "io/ioutil" 6 | "ngrok/server/assets" 7 | ) 8 | 9 | func LoadTLSConfig(crtPath string, keyPath string) (tlsConfig *tls.Config, err error) { 10 | fileOrAsset := func(path string, default_path string) ([]byte, error) { 11 | loadFn := ioutil.ReadFile 12 | if path == "" { 13 | loadFn = assets.Asset 14 | path = default_path 15 | } 16 | 17 | return loadFn(path) 18 | } 19 | 20 | var ( 21 | crt []byte 22 | key []byte 23 | cert tls.Certificate 24 | ) 25 | 26 | if crt, err = fileOrAsset(crtPath, "assets/server/tls/snakeoil.crt"); err != nil { 27 | return 28 | } 29 | 30 | if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil { 31 | return 32 | } 33 | 34 | if cert, err = tls.X509KeyPair(crt, key); err != nil { 35 | return 36 | } 37 | 38 | tlsConfig = &tls.Config{ 39 | Certificates: []tls.Certificate{cert}, 40 | } 41 | 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /src/ngrok/server/tunnel.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "ngrok/conn" 9 | "ngrok/log" 10 | "ngrok/msg" 11 | "ngrok/util" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync/atomic" 16 | "time" 17 | ) 18 | 19 | var defaultPortMap = map[string]int{ 20 | "http": 80, 21 | "https": 443, 22 | "smtp": 25, 23 | } 24 | 25 | /** 26 | * Tunnel: A control connection, metadata and proxy connections which 27 | * route public traffic to a firewalled endpoint. 28 | */ 29 | type Tunnel struct { 30 | // request that opened the tunnel 31 | req *msg.ReqTunnel 32 | 33 | // time when the tunnel was opened 34 | start time.Time 35 | 36 | // public url 37 | url string 38 | 39 | // tcp listener 40 | listener *net.TCPListener 41 | 42 | // control connection 43 | ctl *Control 44 | 45 | // logger 46 | log.Logger 47 | 48 | // closing 49 | closing int32 50 | } 51 | 52 | // Common functionality for registering virtually hosted protocols 53 | func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) { 54 | vhost := os.Getenv("VHOST") 55 | if vhost == "" { 56 | vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort) 57 | } 58 | 59 | // Canonicalize virtual host by removing default port (e.g. :80 on HTTP) 60 | defaultPort, ok := defaultPortMap[protocol] 61 | if !ok { 62 | return fmt.Errorf("Couldn't find default port for protocol %s", protocol) 63 | } 64 | 65 | defaultPortSuffix := fmt.Sprintf(":%d", defaultPort) 66 | if strings.HasSuffix(vhost, defaultPortSuffix) { 67 | vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)] 68 | } 69 | 70 | // Canonicalize by always using lower-case 71 | vhost = strings.ToLower(vhost) 72 | 73 | // Register for specific hostname 74 | hostname := strings.ToLower(strings.TrimSpace(t.req.Hostname)) 75 | if hostname != "" { 76 | t.url = fmt.Sprintf("%s://%s", protocol, hostname) 77 | return tunnelRegistry.Register(t.url, t) 78 | } 79 | 80 | // Register for specific subdomain 81 | subdomain := strings.ToLower(strings.TrimSpace(t.req.Subdomain)) 82 | if subdomain != "" { 83 | t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost) 84 | return tunnelRegistry.Register(t.url, t) 85 | } 86 | 87 | // Register for random URL 88 | t.url, err = tunnelRegistry.RegisterRepeat(func() string { 89 | return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost) 90 | }, t) 91 | 92 | return 93 | } 94 | 95 | // Create a new tunnel from a registration message received 96 | // on a control channel 97 | func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) { 98 | t = &Tunnel{ 99 | req: m, 100 | start: time.Now(), 101 | ctl: ctl, 102 | Logger: log.NewPrefixLogger(), 103 | } 104 | 105 | proto := t.req.Protocol 106 | switch proto { 107 | case "tcp": 108 | bindTcp := func(port int) error { 109 | if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil { 110 | err = t.ctl.conn.Error("Error binding TCP listener: %v", err) 111 | return err 112 | } 113 | 114 | // create the url 115 | addr := t.listener.Addr().(*net.TCPAddr) 116 | t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port) 117 | 118 | // register it 119 | if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil { 120 | // This should never be possible because the OS will 121 | // only assign available ports to us. 122 | t.listener.Close() 123 | err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url) 124 | return err 125 | } 126 | 127 | go t.listenTcp(t.listener) 128 | return nil 129 | } 130 | 131 | // use the custom remote port you asked for 132 | if t.req.RemotePort != 0 { 133 | bindTcp(int(t.req.RemotePort)) 134 | return 135 | } 136 | 137 | // try to return to you the same port you had before 138 | cachedUrl := tunnelRegistry.GetCachedRegistration(t) 139 | if cachedUrl != "" { 140 | var port int 141 | parts := strings.Split(cachedUrl, ":") 142 | portPart := parts[len(parts)-1] 143 | port, err = strconv.Atoi(portPart) 144 | if err != nil { 145 | t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart) 146 | } else { 147 | // we have a valid, cached port, let's try to bind with it 148 | if bindTcp(port) != nil { 149 | t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err) 150 | } else { 151 | // success, we're done 152 | return 153 | } 154 | } 155 | } 156 | 157 | // Bind for TCP connections 158 | bindTcp(0) 159 | return 160 | 161 | case "http", "https": 162 | l, ok := listeners[proto] 163 | if !ok { 164 | err = fmt.Errorf("Not listening for %s connections", proto) 165 | return 166 | } 167 | 168 | if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil { 169 | return 170 | } 171 | 172 | default: 173 | err = fmt.Errorf("Protocol %s is not supported", proto) 174 | return 175 | } 176 | 177 | // pre-encode the http basic auth for fast comparisons later 178 | if m.HttpAuth != "" { 179 | m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth)) 180 | } 181 | 182 | t.AddLogPrefix(t.Id()) 183 | t.Info("Registered new tunnel on: %s", t.ctl.conn.Id()) 184 | 185 | metrics.OpenTunnel(t) 186 | return 187 | } 188 | 189 | func (t *Tunnel) Shutdown() { 190 | t.Info("Shutting down") 191 | 192 | // mark that we're shutting down 193 | atomic.StoreInt32(&t.closing, 1) 194 | 195 | // if we have a public listener (this is a raw TCP tunnel), shut it down 196 | if t.listener != nil { 197 | t.listener.Close() 198 | } 199 | 200 | // remove ourselves from the tunnel registry 201 | tunnelRegistry.Del(t.url) 202 | 203 | // let the control connection know we're shutting down 204 | // currently, only the control connection shuts down tunnels, 205 | // so it doesn't need to know about it 206 | // t.ctl.stoptunnel <- t 207 | 208 | metrics.CloseTunnel(t) 209 | } 210 | 211 | func (t *Tunnel) Id() string { 212 | return t.url 213 | } 214 | 215 | // Listens for new public tcp connections from the internet. 216 | func (t *Tunnel) listenTcp(listener *net.TCPListener) { 217 | for { 218 | defer func() { 219 | if r := recover(); r != nil { 220 | log.Warn("listenTcp failed with error %v", r) 221 | } 222 | }() 223 | 224 | // accept public connections 225 | tcpConn, err := listener.AcceptTCP() 226 | 227 | if err != nil { 228 | // not an error, we're shutting down this tunnel 229 | if atomic.LoadInt32(&t.closing) == 1 { 230 | return 231 | } 232 | 233 | t.Error("Failed to accept new TCP connection: %v", err) 234 | continue 235 | } 236 | 237 | conn := conn.Wrap(tcpConn, "pub") 238 | conn.AddLogPrefix(t.Id()) 239 | conn.Info("New connection from %v", conn.RemoteAddr()) 240 | 241 | go t.HandlePublicConnection(conn) 242 | } 243 | } 244 | 245 | func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) { 246 | defer publicConn.Close() 247 | defer func() { 248 | if r := recover(); r != nil { 249 | publicConn.Warn("HandlePublicConnection failed with error %v", r) 250 | } 251 | }() 252 | 253 | startTime := time.Now() 254 | metrics.OpenConnection(t, publicConn) 255 | 256 | var proxyConn conn.Conn 257 | var err error 258 | for i := 0; i < (2 * proxyMaxPoolSize); i++ { 259 | // get a proxy connection 260 | if proxyConn, err = t.ctl.GetProxy(); err != nil { 261 | t.Warn("Failed to get proxy connection: %v", err) 262 | return 263 | } 264 | defer proxyConn.Close() 265 | t.Info("Got proxy connection %s", proxyConn.Id()) 266 | proxyConn.AddLogPrefix(t.Id()) 267 | 268 | // tell the client we're going to start using this proxy connection 269 | startPxyMsg := &msg.StartProxy{ 270 | Url: t.url, 271 | ClientAddr: publicConn.RemoteAddr().String(), 272 | } 273 | 274 | if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil { 275 | proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i) 276 | proxyConn.Close() 277 | } else { 278 | // success 279 | break 280 | } 281 | } 282 | 283 | if err != nil { 284 | // give up 285 | publicConn.Error("Too many failures starting proxy connection") 286 | return 287 | } 288 | 289 | // To reduce latency handling tunnel connections, we employ the following curde heuristic: 290 | // Whenever we take a proxy connection from the pool, replace it with a new one 291 | util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} }) 292 | 293 | // no timeouts while connections are joined 294 | proxyConn.SetDeadline(time.Time{}) 295 | 296 | // join the public and proxy connections 297 | bytesIn, bytesOut := conn.Join(publicConn, proxyConn) 298 | metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut) 299 | } 300 | -------------------------------------------------------------------------------- /src/ngrok/util/broadcast.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type Broadcast struct { 4 | listeners []chan interface{} 5 | reg chan (chan interface{}) 6 | unreg chan (chan interface{}) 7 | in chan interface{} 8 | } 9 | 10 | func NewBroadcast() *Broadcast { 11 | b := &Broadcast{ 12 | listeners: make([]chan interface{}, 0), 13 | reg: make(chan (chan interface{})), 14 | unreg: make(chan (chan interface{})), 15 | in: make(chan interface{}), 16 | } 17 | 18 | go func() { 19 | for { 20 | select { 21 | case l := <-b.unreg: 22 | // remove L from b.listeners 23 | // this operation is slow: O(n) but not used frequently 24 | // unlike iterating over listeners 25 | oldListeners := b.listeners 26 | b.listeners = make([]chan interface{}, 0, len(oldListeners)) 27 | for _, oldL := range oldListeners { 28 | if l != oldL { 29 | b.listeners = append(b.listeners, oldL) 30 | } 31 | } 32 | 33 | case l := <-b.reg: 34 | b.listeners = append(b.listeners, l) 35 | 36 | case item := <-b.in: 37 | for _, l := range b.listeners { 38 | l <- item 39 | } 40 | } 41 | } 42 | }() 43 | 44 | return b 45 | } 46 | 47 | func (b *Broadcast) In() chan interface{} { 48 | return b.in 49 | } 50 | 51 | func (b *Broadcast) Reg() chan interface{} { 52 | listener := make(chan interface{}) 53 | b.reg <- listener 54 | return listener 55 | } 56 | 57 | func (b *Broadcast) UnReg(listener chan interface{}) { 58 | b.unreg <- listener 59 | } 60 | -------------------------------------------------------------------------------- /src/ngrok/util/errors.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | const crashMessage = `panic: %v 9 | 10 | %s 11 | 12 | Oh noes! ngrok crashed! 13 | 14 | Please submit the stack trace and any relevant information to: 15 | github.com/inconshreveable/ngrok/issues` 16 | 17 | func MakePanicTrace(err interface{}) string { 18 | stackBuf := make([]byte, 4096) 19 | n := runtime.Stack(stackBuf, false) 20 | return fmt.Sprintf(crashMessage, err, stackBuf[:n]) 21 | } 22 | 23 | // Runs the given function and converts any panic encountered while doing so 24 | // into an error. Useful for sending to channels that will close 25 | func PanicToError(fn func()) (err error) { 26 | defer func() { 27 | if r := recover(); r != nil { 28 | err = fmt.Errorf("Panic: %v", r) 29 | } 30 | }() 31 | fn() 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /src/ngrok/util/id.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "fmt" 7 | mrand "math/rand" 8 | ) 9 | 10 | func RandomSeed() (seed int64, err error) { 11 | err = binary.Read(rand.Reader, binary.LittleEndian, &seed) 12 | return 13 | } 14 | 15 | // creates a random identifier of the specified length 16 | func RandId(idlen int) string { 17 | b := make([]byte, idlen) 18 | var randVal uint32 19 | for i := 0; i < idlen; i++ { 20 | byteIdx := i % 4 21 | if byteIdx == 0 { 22 | randVal = mrand.Uint32() 23 | } 24 | b[i] = byte((randVal >> (8 * uint(byteIdx))) & 0xFF) 25 | } 26 | return fmt.Sprintf("%x", b) 27 | } 28 | 29 | // like RandId, but uses a crypto/rand for secure random identifiers 30 | func SecureRandId(idlen int) (id string, err error) { 31 | b := make([]byte, idlen) 32 | n, err := rand.Read(b) 33 | 34 | if n != idlen { 35 | err = fmt.Errorf("Only generated %d random bytes, %d requested", n, idlen) 36 | return 37 | } 38 | 39 | if err != nil { 40 | return 41 | } 42 | 43 | id = fmt.Sprintf("%x", b) 44 | return 45 | } 46 | 47 | func SecureRandIdOrPanic(idlen int) string { 48 | id, err := SecureRandId(idlen) 49 | if err != nil { 50 | panic(err) 51 | } 52 | return id 53 | } 54 | -------------------------------------------------------------------------------- /src/ngrok/util/ring.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | type Ring struct { 9 | sync.Mutex 10 | *list.List 11 | capacity int 12 | } 13 | 14 | func NewRing(capacity int) *Ring { 15 | return &Ring{capacity: capacity, List: list.New()} 16 | } 17 | 18 | func (r *Ring) Add(item interface{}) interface{} { 19 | r.Lock() 20 | defer r.Unlock() 21 | 22 | // add new item 23 | r.PushFront(item) 24 | 25 | // remove old item if at capacity 26 | var old interface{} 27 | if r.Len() >= r.capacity { 28 | old = r.Remove(r.Back()) 29 | } 30 | 31 | return old 32 | } 33 | 34 | func (r *Ring) Slice() []interface{} { 35 | r.Lock() 36 | defer r.Unlock() 37 | 38 | i := 0 39 | items := make([]interface{}, r.Len()) 40 | for e := r.Front(); e != nil; e = e.Next() { 41 | items[i] = e.Value 42 | i++ 43 | } 44 | 45 | return items 46 | } 47 | -------------------------------------------------------------------------------- /src/ngrok/util/shutdown.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // A small utility class for managing controlled shutdowns 8 | type Shutdown struct { 9 | sync.Mutex 10 | inProgress bool 11 | begin chan int // closed when the shutdown begins 12 | complete chan int // closed when the shutdown completes 13 | } 14 | 15 | func NewShutdown() *Shutdown { 16 | return &Shutdown{ 17 | begin: make(chan int), 18 | complete: make(chan int), 19 | } 20 | } 21 | 22 | func (s *Shutdown) Begin() { 23 | s.Lock() 24 | defer s.Unlock() 25 | if s.inProgress == true { 26 | return 27 | } else { 28 | s.inProgress = true 29 | close(s.begin) 30 | } 31 | } 32 | 33 | func (s *Shutdown) WaitBegin() { 34 | <-s.begin 35 | } 36 | 37 | func (s *Shutdown) Complete() { 38 | close(s.complete) 39 | } 40 | 41 | func (s *Shutdown) WaitComplete() { 42 | <-s.complete 43 | } 44 | -------------------------------------------------------------------------------- /src/ngrok/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | Proto = "2" 9 | Major = "1" 10 | Minor = "7" 11 | ) 12 | 13 | func MajorMinor() string { 14 | return fmt.Sprintf("%s.%s", Major, Minor) 15 | } 16 | 17 | func Full() string { 18 | return fmt.Sprintf("%s-%s.%s", Proto, Major, Minor) 19 | } 20 | 21 | func Compat(client string, server string) bool { 22 | return client == server 23 | } 24 | --------------------------------------------------------------------------------