├── .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.4 7 | - 1.5 8 | - 1.6 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 | # ngrok - Unified Ingress for Developers 2 | 3 | [https://ngrok.com](https://ngrok.com) 4 | 5 | ## ngrok Community on GitHub 6 | 7 | If you are having an issue with the ngrok cloud service please open an issue on the [ngrok community on GitHub](https://github.com/ngrok/ngrok) 8 | 9 | ## This repository is archived 10 | 11 | This is the GitHub repository for the old v1 version of ngrok which was actively developed from 2013-2016. 12 | 13 | **This repository is archived: ngrok v1 is no longer developed, supported or maintained.** 14 | 15 | Thank you to everyone who contributed to ngrok v1 it in its early days with PRs, issues and feedback. If you wish to continue development on this codebase, please fork it. 16 | 17 | ngrok's cloud service continues to operate and you can sign up for it here: [https://ngrok.com/signup](https://ngrok.com/signup) 18 | 19 | ## What is ngrok? 20 | 21 | ngrok is a globally distributed reverse proxy that secures, protects and accelerates your applications and network services, no matter where you run them. You can think of ngrok as the front door to your applications. ngrok combines your reverse proxy, firewall, API gateway, and global load balancing into one. ngrok can capture and analyze all traffic to your web service for later inspection and replay. 22 | 23 | To use ngrok, sign up at [https://ngrok.com/signup](https://ngrok.com/signup) 24 | 25 | ## ngrok open-source development 26 | ngrok continues to contribute to the open source ecosystem at [https://github.com/ngrok](https://github.com/ngrok) with: 27 | - [The ngrok kubernetes operator](https://github.com/ngrok/kubernetes-ingress-controller) 28 | - [The ngrok agent SDKs](https://ngrok.com/docs/agent-sdks/) for [Python](https://github.com/ngrok/ngrok-python), [JavaScript](https://github.com/ngrok/ngrok-javascript), [Go](https://github.com/ngrok/ngrok-go), [Rust](https://github.com/ngrok/ngrok-rust) and [Java](https://github.com/ngrok/ngrok-java) 29 | 30 | 31 | ## What is ngrok for? 32 | 33 | [What can you do with ngrok?](https://ngrok.com/docs/what-is-ngrok/#what-can-you-do-with-ngrok) 34 | 35 | - Site-to-site Connectivity: Connect securely to APIs and databases in your customers' networks without complex network configuration. 36 | - Developer Previews: Demoing an app from your local machine without deploying it 37 | - Webhook Testing: Developing any services which consume webhooks (HTTP callbacks) by allowing you to replay those requests 38 | - API Gateway: An global gateway-as-a-service that works for API running anywhere with simple CEL-based traffic policy for rate limiting, jwt authentication and more. 39 | - Device Gateway: Run ngrok on your IoT devices to control device APIs from your cloud 40 | - Debug and understand any web service by inspecting the HTTP traffic to it 41 | -------------------------------------------------------------------------------- /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/inconshreveable/ngrok/61d48289f3bf5918aca57cf966e74906bb85e4cd/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 | ngork start-all Start all tunnels defined in config file 26 | ngrok list List tunnel names from config file 27 | ngrok help Print help 28 | ngrok version Print ngrok version 29 | 30 | Examples: 31 | ngrok start www api blog pubsub 32 | ngrok -log=stdout -config=ngrok.yml start ssh 33 | ngrok start-all 34 | ngrok version 35 | 36 | ` 37 | 38 | type Options struct { 39 | config string 40 | logto string 41 | loglevel string 42 | authtoken string 43 | httpauth string 44 | hostname string 45 | protocol string 46 | subdomain string 47 | command string 48 | args []string 49 | } 50 | 51 | func ParseArgs() (opts *Options, err error) { 52 | flag.Usage = func() { 53 | fmt.Fprintf(os.Stderr, usage1, os.Args[0]) 54 | flag.PrintDefaults() 55 | fmt.Fprintf(os.Stderr, usage2) 56 | } 57 | 58 | config := flag.String( 59 | "config", 60 | "", 61 | "Path to ngrok configuration file. (default: $HOME/.ngrok)") 62 | 63 | logto := flag.String( 64 | "log", 65 | "none", 66 | "Write log messages to this file. 'stdout' and 'none' have special meanings") 67 | 68 | loglevel := flag.String( 69 | "log-level", 70 | "DEBUG", 71 | "The level of messages to log. One of: DEBUG, INFO, WARNING, ERROR") 72 | 73 | authtoken := flag.String( 74 | "authtoken", 75 | "", 76 | "Authentication token for identifying an ngrok.com account") 77 | 78 | httpauth := flag.String( 79 | "httpauth", 80 | "", 81 | "username:password HTTP basic auth creds protecting the public tunnel endpoint") 82 | 83 | subdomain := flag.String( 84 | "subdomain", 85 | "", 86 | "Request a custom subdomain from the ngrok server. (HTTP only)") 87 | 88 | hostname := flag.String( 89 | "hostname", 90 | "", 91 | "Request a custom hostname from the ngrok server. (HTTP only) (requires CNAME of your DNS)") 92 | 93 | protocol := flag.String( 94 | "proto", 95 | "http+https", 96 | "The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')") 97 | 98 | flag.Parse() 99 | 100 | opts = &Options{ 101 | config: *config, 102 | logto: *logto, 103 | loglevel: *loglevel, 104 | httpauth: *httpauth, 105 | subdomain: *subdomain, 106 | protocol: *protocol, 107 | authtoken: *authtoken, 108 | hostname: *hostname, 109 | command: flag.Arg(0), 110 | } 111 | 112 | switch opts.command { 113 | case "list": 114 | opts.args = flag.Args()[1:] 115 | case "start": 116 | opts.args = flag.Args()[1:] 117 | case "start-all": 118 | opts.args = flag.Args()[1:] 119 | case "version": 120 | fmt.Println(version.MajorMinor()) 121 | os.Exit(0) 122 | case "help": 123 | flag.Usage() 124 | os.Exit(0) 125 | case "": 126 | err = fmt.Errorf("Error: Specify a local port to tunnel to, or " + 127 | "an ngrok command.\n\nExample: To expose port 80, run " + 128 | "'ngrok 80'") 129 | return 130 | 131 | default: 132 | if len(flag.Args()) > 1 { 133 | err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v", 134 | len(flag.Args()), 135 | flag.Args()) 136 | return 137 | } 138 | 139 | opts.command = "default" 140 | opts.args = flag.Args() 141 | } 142 | 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /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 = defaultInspectAddr 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 | case "start-all": 194 | return 195 | 196 | default: 197 | err = fmt.Errorf("Unknown command: %s", opts.command) 198 | return 199 | } 200 | 201 | return 202 | } 203 | 204 | func defaultPath() string { 205 | user, err := user.Current() 206 | 207 | // user.Current() does not work on linux when cross compiling because 208 | // it requires CGO; use os.Getenv("HOME") hack until we compile natively 209 | homeDir := os.Getenv("HOME") 210 | if err != nil { 211 | log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir) 212 | } else { 213 | homeDir = user.HomeDir 214 | } 215 | 216 | return path.Join(homeDir, ".ngrok") 217 | } 218 | 219 | func normalizeAddress(addr string, propName string) (string, error) { 220 | // normalize port to address 221 | if _, err := strconv.Atoi(addr); err == nil { 222 | addr = ":" + addr 223 | } 224 | 225 | host, port, err := net.SplitHostPort(addr) 226 | if err != nil { 227 | return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error()) 228 | } 229 | 230 | if host == "" { 231 | host = "127.0.0.1" 232 | } 233 | 234 | return fmt.Sprintf("%s:%s", host, port), nil 235 | } 236 | 237 | func validateProtocol(proto, propName string) (err error) { 238 | switch proto { 239 | case "http", "https", "http+https", "tcp": 240 | default: 241 | err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto) 242 | } 243 | 244 | return 245 | } 246 | 247 | func SaveAuthToken(configPath, authtoken string) (err error) { 248 | // empty configuration by default for the case that we can't read it 249 | c := new(Configuration) 250 | 251 | // read the configuration 252 | oldConfigBytes, err := ioutil.ReadFile(configPath) 253 | if err == nil { 254 | // unmarshal if we successfully read the configuration file 255 | if err = yaml.Unmarshal(oldConfigBytes, c); err != nil { 256 | return 257 | } 258 | } 259 | 260 | // no need to save, the authtoken is already the correct value 261 | if c.AuthToken == authtoken { 262 | return 263 | } 264 | 265 | // update auth token 266 | c.AuthToken = authtoken 267 | 268 | // rewrite configuration 269 | newConfigBytes, err := yaml.Marshal(c) 270 | if err != nil { 271 | return 272 | } 273 | 274 | err = ioutil.WriteFile(configPath, newConfigBytes, 0600) 275 | return 276 | } 277 | -------------------------------------------------------------------------------- /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 | func useInsecureSkipVerify() bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /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 | "net" 10 | "ngrok/client/mvc" 11 | "ngrok/conn" 12 | "ngrok/log" 13 | "ngrok/msg" 14 | "ngrok/proto" 15 | "ngrok/util" 16 | "ngrok/version" 17 | "runtime" 18 | "strings" 19 | "sync/atomic" 20 | "time" 21 | ) 22 | 23 | const ( 24 | defaultServerAddr = "ngrokd.ngrok.com:443" 25 | defaultInspectAddr = "127.0.0.1:4040" 26 | pingInterval = 20 * time.Second 27 | maxPongLatency = 15 * time.Second 28 | updateCheckInterval = 6 * time.Hour 29 | BadGateway = ` 30 | 31 |
32 |

Tunnel %s unavailable

33 |

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

34 | ` 35 | ) 36 | 37 | type ClientModel struct { 38 | log.Logger 39 | 40 | id string 41 | tunnels map[string]mvc.Tunnel 42 | serverVersion string 43 | metrics *ClientMetrics 44 | updateStatus mvc.UpdateStatus 45 | connStatus mvc.ConnStatus 46 | protoMap map[string]proto.Protocol 47 | protocols []proto.Protocol 48 | ctl mvc.Controller 49 | serverAddr string 50 | proxyUrl string 51 | authToken string 52 | tlsConfig *tls.Config 53 | tunnelConfig map[string]*TunnelConfiguration 54 | configPath string 55 | } 56 | 57 | func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel { 58 | protoMap := make(map[string]proto.Protocol) 59 | protoMap["http"] = proto.NewHttp() 60 | protoMap["https"] = protoMap["http"] 61 | protoMap["tcp"] = proto.NewTcp() 62 | protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]} 63 | 64 | m := &ClientModel{ 65 | Logger: log.NewPrefixLogger("client"), 66 | 67 | // server address 68 | serverAddr: config.ServerAddr, 69 | 70 | // proxy address 71 | proxyUrl: config.HttpProxy, 72 | 73 | // auth token 74 | authToken: config.AuthToken, 75 | 76 | // connection status 77 | connStatus: mvc.ConnConnecting, 78 | 79 | // update status 80 | updateStatus: mvc.UpdateNone, 81 | 82 | // metrics 83 | metrics: NewClientMetrics(), 84 | 85 | // protocols 86 | protoMap: protoMap, 87 | 88 | // protocol list 89 | protocols: protocols, 90 | 91 | // open tunnels 92 | tunnels: make(map[string]mvc.Tunnel), 93 | 94 | // controller 95 | ctl: ctl, 96 | 97 | // tunnel configuration 98 | tunnelConfig: config.Tunnels, 99 | 100 | // config path 101 | configPath: config.Path, 102 | } 103 | 104 | // configure TLS 105 | if config.TrustHostRootCerts { 106 | m.Info("Trusting host's root certificates") 107 | m.tlsConfig = &tls.Config{} 108 | } else { 109 | m.Info("Trusting root CAs: %v", rootCrtPaths) 110 | var err error 111 | if m.tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil { 112 | panic(err) 113 | } 114 | } 115 | 116 | // configure TLS SNI 117 | m.tlsConfig.ServerName = serverName(m.serverAddr) 118 | m.tlsConfig.InsecureSkipVerify = useInsecureSkipVerify() 119 | 120 | return m 121 | } 122 | 123 | // server name in release builds is the host part of the server address 124 | func serverName(addr string) string { 125 | host, _, err := net.SplitHostPort(addr) 126 | 127 | // should never panic because the config parser calls SplitHostPort first 128 | if err != nil { 129 | panic(err) 130 | } 131 | 132 | return host 133 | } 134 | 135 | // mvc.State interface 136 | func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols } 137 | func (c ClientModel) GetClientVersion() string { return version.MajorMinor() } 138 | func (c ClientModel) GetServerVersion() string { return c.serverVersion } 139 | func (c ClientModel) GetTunnels() []mvc.Tunnel { 140 | tunnels := make([]mvc.Tunnel, 0) 141 | for _, t := range c.tunnels { 142 | tunnels = append(tunnels, t) 143 | } 144 | return tunnels 145 | } 146 | func (c ClientModel) GetConnStatus() mvc.ConnStatus { return c.connStatus } 147 | func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus } 148 | 149 | func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) { 150 | return c.metrics.connMeter, c.metrics.connTimer 151 | } 152 | 153 | func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) { 154 | return c.metrics.bytesInCount, c.metrics.bytesIn 155 | } 156 | 157 | func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) { 158 | return c.metrics.bytesOutCount, c.metrics.bytesOut 159 | } 160 | func (c ClientModel) SetUpdateStatus(updateStatus mvc.UpdateStatus) { 161 | c.updateStatus = updateStatus 162 | c.update() 163 | } 164 | 165 | // mvc.Model interface 166 | func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) { 167 | var localConn conn.Conn 168 | localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) 169 | if err != nil { 170 | c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err) 171 | return 172 | } 173 | 174 | defer localConn.Close() 175 | localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"}) 176 | localConn.Write(payload) 177 | ioutil.ReadAll(localConn) 178 | } 179 | 180 | func (c *ClientModel) Shutdown() { 181 | } 182 | 183 | func (c *ClientModel) update() { 184 | c.ctl.Update(c) 185 | } 186 | 187 | func (c *ClientModel) Run() { 188 | // how long we should wait before we reconnect 189 | maxWait := 30 * time.Second 190 | wait := 1 * time.Second 191 | 192 | for { 193 | // run the control channel 194 | c.control() 195 | 196 | // control only returns when a failure has occurred, so we're going to try to reconnect 197 | if c.connStatus == mvc.ConnOnline { 198 | wait = 1 * time.Second 199 | } 200 | 201 | log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds())) 202 | time.Sleep(wait) 203 | // exponentially increase wait time 204 | wait = 2 * wait 205 | wait = time.Duration(math.Min(float64(wait), float64(maxWait))) 206 | c.connStatus = mvc.ConnReconnecting 207 | c.update() 208 | } 209 | } 210 | 211 | // Establishes and manages a tunnel control connection with the server 212 | func (c *ClientModel) control() { 213 | defer func() { 214 | if r := recover(); r != nil { 215 | log.Error("control recovering from failure %v", r) 216 | } 217 | }() 218 | 219 | // establish control channel 220 | var ( 221 | ctlConn conn.Conn 222 | err error 223 | ) 224 | if c.proxyUrl == "" { 225 | // simple non-proxied case, just connect to the server 226 | ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig) 227 | } else { 228 | ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig) 229 | } 230 | if err != nil { 231 | panic(err) 232 | } 233 | defer ctlConn.Close() 234 | 235 | // authenticate with the server 236 | auth := &msg.Auth{ 237 | ClientId: c.id, 238 | OS: runtime.GOOS, 239 | Arch: runtime.GOARCH, 240 | Version: version.Proto, 241 | MmVersion: version.MajorMinor(), 242 | User: c.authToken, 243 | } 244 | 245 | if err = msg.WriteMsg(ctlConn, auth); err != nil { 246 | panic(err) 247 | } 248 | 249 | // wait for the server to authenticate us 250 | var authResp msg.AuthResp 251 | if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil { 252 | panic(err) 253 | } 254 | 255 | if authResp.Error != "" { 256 | emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error) 257 | c.ctl.Shutdown(emsg) 258 | return 259 | } 260 | 261 | c.id = authResp.ClientId 262 | c.serverVersion = authResp.MmVersion 263 | c.Info("Authenticated with server, client id: %v", c.id) 264 | c.update() 265 | if err = SaveAuthToken(c.configPath, c.authToken); err != nil { 266 | c.Error("Failed to save auth token: %v", err) 267 | } 268 | 269 | // request tunnels 270 | reqIdToTunnelConfig := make(map[string]*TunnelConfiguration) 271 | for _, config := range c.tunnelConfig { 272 | // create the protocol list to ask for 273 | var protocols []string 274 | for proto, _ := range config.Protocols { 275 | protocols = append(protocols, proto) 276 | } 277 | 278 | reqTunnel := &msg.ReqTunnel{ 279 | ReqId: util.RandId(8), 280 | Protocol: strings.Join(protocols, "+"), 281 | Hostname: config.Hostname, 282 | Subdomain: config.Subdomain, 283 | HttpAuth: config.HttpAuth, 284 | RemotePort: config.RemotePort, 285 | } 286 | 287 | // send the tunnel request 288 | if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil { 289 | panic(err) 290 | } 291 | 292 | // save request id association so we know which local address 293 | // to proxy to later 294 | reqIdToTunnelConfig[reqTunnel.ReqId] = config 295 | } 296 | 297 | // start the heartbeat 298 | lastPong := time.Now().UnixNano() 299 | c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) }) 300 | 301 | // main control loop 302 | for { 303 | var rawMsg msg.Message 304 | if rawMsg, err = msg.ReadMsg(ctlConn); err != nil { 305 | panic(err) 306 | } 307 | 308 | switch m := rawMsg.(type) { 309 | case *msg.ReqProxy: 310 | c.ctl.Go(c.proxy) 311 | 312 | case *msg.Pong: 313 | atomic.StoreInt64(&lastPong, time.Now().UnixNano()) 314 | 315 | case *msg.NewTunnel: 316 | if m.Error != "" { 317 | emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error) 318 | c.Error(emsg) 319 | c.ctl.Shutdown(emsg) 320 | continue 321 | } 322 | 323 | tunnel := mvc.Tunnel{ 324 | PublicUrl: m.Url, 325 | LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol], 326 | Protocol: c.protoMap[m.Protocol], 327 | } 328 | 329 | c.tunnels[tunnel.PublicUrl] = tunnel 330 | c.connStatus = mvc.ConnOnline 331 | c.Info("Tunnel established at %v", tunnel.PublicUrl) 332 | c.update() 333 | 334 | default: 335 | ctlConn.Warn("Ignoring unknown control message %v ", m) 336 | } 337 | } 338 | } 339 | 340 | // Establishes and manages a tunnel proxy connection with the server 341 | func (c *ClientModel) proxy() { 342 | var ( 343 | remoteConn conn.Conn 344 | err error 345 | ) 346 | 347 | if c.proxyUrl == "" { 348 | remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig) 349 | } else { 350 | remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig) 351 | } 352 | 353 | if err != nil { 354 | log.Error("Failed to establish proxy connection: %v", err) 355 | return 356 | } 357 | defer remoteConn.Close() 358 | 359 | err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id}) 360 | if err != nil { 361 | remoteConn.Error("Failed to write RegProxy: %v", err) 362 | return 363 | } 364 | 365 | // wait for the server to ack our register 366 | var startPxy msg.StartProxy 367 | if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil { 368 | remoteConn.Error("Server failed to write StartProxy: %v", err) 369 | return 370 | } 371 | 372 | tunnel, ok := c.tunnels[startPxy.Url] 373 | if !ok { 374 | remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url) 375 | return 376 | } 377 | 378 | // start up the private connection 379 | start := time.Now() 380 | localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) 381 | if err != nil { 382 | remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err) 383 | 384 | if tunnel.Protocol.GetName() == "http" { 385 | // try to be helpful when you're in HTTP mode and a human might see the output 386 | badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr) 387 | remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway 388 | Content-Type: text/html 389 | Content-Length: %d 390 | 391 | %s`, len(badGatewayBody), badGatewayBody))) 392 | } 393 | return 394 | } 395 | defer localConn.Close() 396 | 397 | m := c.metrics 398 | m.proxySetupTimer.Update(time.Since(start)) 399 | m.connMeter.Mark(1) 400 | c.update() 401 | m.connTimer.Time(func() { 402 | localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr}) 403 | bytesIn, bytesOut := conn.Join(localConn, remoteConn) 404 | m.bytesIn.Update(bytesIn) 405 | m.bytesOut.Update(bytesOut) 406 | m.bytesInCount.Inc(bytesIn) 407 | m.bytesOutCount.Inc(bytesOut) 408 | }) 409 | c.update() 410 | } 411 | 412 | // Hearbeating to ensure our connection ngrokd is still live 413 | func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) { 414 | lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0) 415 | ping := time.NewTicker(pingInterval) 416 | pongCheck := time.NewTicker(time.Second) 417 | 418 | defer func() { 419 | conn.Close() 420 | ping.Stop() 421 | pongCheck.Stop() 422 | }() 423 | 424 | for { 425 | select { 426 | case <-pongCheck.C: 427 | lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr)) 428 | needPong := lastPong.Sub(lastPing) < 0 429 | pongLatency := time.Since(lastPing) 430 | 431 | if needPong && pongLatency > maxPongLatency { 432 | c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong) 433 | c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds())) 434 | return 435 | } 436 | 437 | case <-ping.C: 438 | err := msg.WriteMsg(conn, &msg.Ping{}) 439 | if err != nil { 440 | conn.Debug("Got error %v when writing PingMsg", err) 441 | return 442 | } 443 | lastPing = time.Now() 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /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 | var ( 6 | rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"} 7 | ) 8 | 9 | func useInsecureSkipVerify() bool { 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /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 | "fmt" 5 | log "github.com/alecthomas/log4go" 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 | --------------------------------------------------------------------------------