diff --git a/Makefile b/Makefile index a078754..43dfc61 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,6 @@ image: push-image: docker push $(IMAGE) - .PHONY: push-develop push-develop: docker tag $(IMAGE) $(IMAGE):develop diff --git a/README.md b/README.md index 74a3b27..5772110 100644 --- a/README.md +++ b/README.md @@ -130,23 +130,14 @@ After you exec some commands, you will see the inputs and outputs under the ### Real-time sharing -```bash -docker run -dti --restart always --name container-web-tty \ - -p 8080:8080 \ - -e WEB_TTY_SHARE=true \ - -v /var/run/docker.sock:/var/run/docker.sock \ - wrfly/container-web-tty -``` - -By enabling this feature, you can share the container's inputs and outputs -with others via the share link (click the container's image to get the link). +You can always share the container's inputs and outputs with others via the exec +link, just share the `/exec/` to them! -#### Collaborate +### Collaborate ```bash docker run -dti --restart always --name container-web-tty \ -p 8080:8080 \ - -e WEB_TTY_SHARE=true \ -e WEB_TTY_COLLABORATE=true \ -v /var/run/docker.sock:/var/run/docker.sock \ wrfly/container-web-tty @@ -155,7 +146,7 @@ docker run -dti --restart always --name container-web-tty \ By enabling this feature, once you exec into the container, you can share your process with others, that means anyone got the shareable link would type the command to the tty you are working on. You can edit the same file, type the same code, in the -same TTY! (P.S. Only the first exec process would be shared to others) +same TTY! Just share the exec link to your friend! ## Options @@ -172,8 +163,7 @@ GLOBAL OPTIONS: --docker-host value docker host path (default: "/var/run/docker.sock") --docker-ps value docker ps options --enable-audit, --audit enable audit the container outputs (default: false) - --enable-collaborate, --clb shared terminal can write to the same TTY (default: false) - --enable-share, --share enable share the container's terminal (default: false) + --enable-collaborate, --clb collaborate on the same TTY process (default: false) --grpc-auth value grpc auth token (default: "password") --grpc-port value grpc server port, -1 for disable the grpc server (default: -1) --grpc-proxy value grpc proxy address, in the format of http://127.0.0.1:8080 or socks5://127.0.0.1:1080 diff --git a/config/config.go b/config/config.go index 9a82883..3ea6fff 100644 --- a/config/config.go +++ b/config/config.go @@ -51,8 +51,7 @@ type ServerConfig struct { WSOrigin string Term string `default:"xterm"` ShowLocation bool - EnableShare bool - Collaborate bool + Collaborate bool // audit EnableAudit bool diff --git a/container/docker/slave.go b/container/docker/slave.go index 5871ee7..e80f67a 100644 --- a/container/docker/slave.go +++ b/container/docker/slave.go @@ -4,6 +4,7 @@ import ( "time" apiTypes "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" ) // execInjector implement webtty.Slave @@ -35,13 +36,12 @@ func (enj *execInjector) Read(p []byte) (n int, err error) { } func (enj *execInjector) Write(p []byte) (n int, err error) { - // logrus.Debugf("input: %v\n", p) + logrus.Debugf("input: %v\n", p) return enj.hResp.Conn.Write(p) } func (enj *execInjector) Exit() error { - enj.Write([]byte{3}) // ^C - enj.Write([]byte{4}) // ^D + enj.Write([]byte{3, 13, 4, 13}) // ^C, ^D, enter close(enj.activeChan) return enj.hResp.Conn.Close() } diff --git a/go.mod b/go.mod index 8b6cd8f..e2ff0cf 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/spf13/pflag v1.0.3 // indirect github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0 - github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4 + github.com/wrfly/pubsub v0.0.0-20200314104228-47828c5578b6 github.com/yudai/gotty v2.0.0-alpha.3+incompatible golang.org/x/net v0.0.0-20190326090315-15845e8f865b golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect diff --git a/go.sum b/go.sum index 5d64631..d2e7bd9 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04 github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0 h1:Zy3Chk4CvwAqJ6YgQym6hJQEk4JE7iPbHRfxkru6Zn0= github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0/go.mod h1:cmmFTD+MLlrDa3/EO3gjeKLKUhHiYP3cgaaJamIS1NU= -github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4 h1:G8zs08Ln8gek2TpeM1dgYFlUepEK6JNCM966Sf1kT+8= -github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4/go.mod h1:WFtPVb6GumrLEVcAHZPSbjZmvWgenDSi4ceFW7k1r30= +github.com/wrfly/pubsub v0.0.0-20200314104228-47828c5578b6 h1:iuI+7TJcnnKB3WH08PmK5Y4c66Tf2XNXmnvLpIFFP30= +github.com/wrfly/pubsub v0.0.0-20200314104228-47828c5578b6/go.mod h1:WFtPVb6GumrLEVcAHZPSbjZmvWgenDSi4ceFW7k1r30= github.com/yudai/gotty v2.0.0-alpha.3+incompatible h1:eUFSuV4B2g+Rj+PS3HxhvOGEu2klWRzsl/7z7T/NUJQ= github.com/yudai/gotty v2.0.0-alpha.3+incompatible/go.mod h1:QBg0hL6VTVdqQk0qoBYk631EHLRH+XtR4wtbVi64UJ4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= diff --git a/main.go b/main.go index 8f28b68..b82deb1 100644 --- a/main.go +++ b/main.go @@ -128,13 +128,6 @@ func main() { Usage: "enable container restart", Destination: &conf.Server.Control.Restart, }, - &cli.BoolFlag{ - Name: "enable-share", - Aliases: []string{"share"}, - EnvVars: util.EnvVars("share"), - Usage: "enable share the container's terminal", - Destination: &conf.Server.EnableShare, - }, &cli.BoolFlag{ Name: "enable-collaborate", Aliases: []string{"clb"}, diff --git a/resources/clipboard.min.js b/resources/clipboard.min.js deleted file mode 100644 index 7a4fde6..0000000 --- a/resources/clipboard.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * clipboard.js v2.0.1 - * https://zenorocha.github.io/clipboard.js - * - * Licensed MIT © Zeno Rocha - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])}); \ No newline at end of file diff --git a/resources/list.html b/resources/list.html index 84b6eab..9ebd736 100644 --- a/resources/list.html +++ b/resources/list.html @@ -6,7 +6,6 @@ {{ .title }} - @@ -38,17 +37,9 @@ {{ range $i, $e := .containers }} - {{ printf "%.12s" .ID }} + {{ printf "%.12s" .ID }} - {{- if $share -}} - - {{ printf .Image }} - - {{- else -}} - - {{ printf .Image }} - - {{- end -}} + {{ printf .Image }} {{ printf .Command }} {{ printf .Name }} @@ -74,21 +65,6 @@ - \ No newline at end of file diff --git a/route/asset/asset.go b/route/asset/asset.go index 11f7f1a..9b4ba6a 100644 --- a/route/asset/asset.go +++ b/route/asset/asset.go @@ -11,7 +11,6 @@ Files: /favicon.png /index.html /js - /js/clipboard.min.js /js/control.js /js/gotty-bundle.js /list.html @@ -472,242 +471,6 @@ var _file_8 = &file{ } var _compress_bytes_9 = []byte("" + - "\x78\x9c\xe4\x5a\xdb\x8e\xe3\x38\x73\xbe\xdf\xa7\x90\x75\xa1" + - "\x21\xb7\xb9\x1a\xf7\xe6\x84\x91\x97\x31\x1a\x8d\x5e\xfc\x1b" + - "\xcc\xec\x0c\xa6\x3b\x40\xfe\x38\x46\x83\x2d\x95\x6d\xfe\x2d" + - "\x93\x5e\x8a\xea\xc3\xda\x7a\xa0\xbc\x46\x9e\x2c\xe0\x41\x12" + - "\x65\xcb\x33\x3b\xc0\x06\x01\x92\x8b\x99\xa6\x78\x28\x56\x15" + - "\xab\x8a\x55\x1f\xfd\xf6\xfb\xc9\x77\xd1\xf7\x51\x5e\xf2\xdd" + - "\x83\x64\xaa\x48\xff\x56\x45\x4f\x3f\xa6\xd3\xf4\xd2\x74\x6f" + - "\xb4\xde\x55\xd9\xdb\xb7\xbf\x83\x90\x4a\xe6\x1b\x96\xae\xb9" + - "\xde\xd4\x0f\x29\x97\x6f\xc3\x25\x66\xae\xf9\xf7\x9e\xe7\x20" + - "\x2a\x28\xa2\x0f\xbf\xdc\x45\xff\xf5\x9f\xd1\xbf\x83\x90\xd1" + - "\x67\xb3\xf0\xbb\xe8\xfb\xb7\xdf\x4d\x56\xb5\xc8\x35\x97\x02" + - "\x69\x02\x78\x1f\xcb\x87\xbf\x41\xae\x63\x4a\xf5\xeb\x0e\xe4" + - "\x2a\x82\x97\x9d\x54\xba\x4a\x92\x93\x91\xad\x2c\xea\x12\xe6" + - "\xee\x4f\xea\xe7\x51\x40\x38\x8b\x5b\x9a\xfd\xe4\x02\x56\x5c" + - "\x40\x92\xb8\xbf\x29\xdb\x16\x73\xd7\x44\x8b\x25\x01\x9c\x9d" + - "\xdb\x77\xee\xff\xa6\xd7\xad\x68\xff\x72\x6b\xf7\xd0\xc7\x3d" + - "\x0d\xd2\x1b\x5e\x91\x4e\x1c\xbc\x57\xa0\x6b\x25\xa2\x5e\x40" + - "\xbc\x6f\xdb\x11\x20\x89\xf7\x7c\x85\xc4\x42\x2e\xb1\x9f\x68" + - "\xda\xad\x1c\xb3\x27\xa6\x22\x45\x4d\x17\xdd\xf3\x4c\x92\x32" + - "\x9b\x5c\x12\x3f\x98\xed\x9b\x66\xe6\x17\x69\xb3\x28\x67\x65" + - "\x89\x54\xbb\x96\x28\xd2\xb7\x01\x13\x95\x96\x74\x32\xed\xfb" + - "\x1a\x43\x5b\xd0\x7d\x47\x03\xd2\x2d\xd5\x04\xd2\x9c\x0a\x02" + - "\x29\xa7\x21\xc7\xed\x36\x0d\x81\xb4\x08\x46\x88\x20\x12\xef" + - "\x21\x95\xa6\x89\x0f\x87\x8f\x56\x7f\xa9\x53\xea\x27\x25\x77" + - "\xa0\xf4\xab\x9d\xb6\xcf\xa5\x58\xf1\x75\xad\xd8\x43\x09\x56" + - "\x0a\x51\x6f\xc1\x7f\x4d\xc9\x1a\x74\x26\x1b\x6c\xe8\x8b\xc1" + - "\xce\x8e\x4b\x9d\x24\x3a\xbd\xbf\x87\xea\x83\x3b\xee\x53\xfd" + - "\xda\x4d\x59\x5d\xea\x26\x1b\x19\x0c\x84\x2c\x90\x20\x31\x8b" + - "\x89\xc0\x44\x98\xed\x24\x1d\xda\x9e\x9f\xe8\x25\xd9\x29\xa9" + - "\xa5\x31\x86\x74\xc3\xaa\x8f\xcf\xa2\x95\xc9\x29\xdb\x2c\x30" + - "\x34\x76\x34\x8e\x09\x20\x48\x2b\xfa\x77\xb8\x41\x8b\x90\x22" + - "\x11\x4e\x08\x49\x14\xe1\xb3\xde\xd0\x19\xc9\xf1\x5e\xd1\x85" + - "\x26\x02\xfd\x13\x5e\x12\x49\x73\xf2\x24\x79\x11\x4d\x27\x94" + - "\x22\x4e\x47\xcc\x57\xce\x65\xca\x76\xbb\xf2\x15\x01\x51\x38" + - "\x93\x38\x49\x90\xee\x6c\x9e\xe3\x06\x4d\xc9\x91\x23\xd5\x15" + - "\x44\x95\x56\x3c\xd7\xf1\xac\x33\x3b\x3f\xc8\x57\x68\x82\x74" + - "\xc4\x45\xa5\x99\xc8\xad\xb9\x63\xac\x37\x4a\x3e\x47\x02\x9e" + - "\xa3\xbb\xd7\x1d\xdc\x28\x25\x15\x8a\xaf\x99\x10\x52\x47\x46" + - "\xe6\x88\x45\x79\xc9\xaa\x2a\x62\x55\xc4\x3a\xab\x8e\xb1\x35" + - "\x27\x39\x6a\x34\x47\x67\xa7\xb3\xbd\x3f\xaa\x4c\x37\x0d\x32" + - "\xa6\x39\x26\xec\xed\xeb\xf6\x41\x96\x49\x12\x57\xb6\x71\x3c" + - "\x90\x72\x0d\x8a\x69\xa9\xe6\x63\x5b\xba\x99\xa1\x2d\x0c\xf8" + - "\xf9\xc2\x76\x3a\xcd\xa5\xa8\xb4\xaa\x73\x2d\x15\xa5\xb4\xeb" + - "\x9f\xb4\xed\xde\x26\xe6\x2d\x6f\x59\xb7\x21\x09\xfc\x26\x70" + - "\x74\xed\x34\xbe\x92\x0a\x39\x83\x9e\xce\xc4\x4f\x90\x96\x20" + - "\xd6\x7a\x33\x13\x17\x17\xde\x46\x28\x2c\xc4\x72\x26\xd3\xde" + - "\x39\x68\xf8\x71\x38\x4c\x2e\x89\x4c\x43\x57\x32\x3e\x1d\x3f" + - "\xb1\xb2\x86\x98\x8b\x48\x26\x09\x92\xe9\xb3\xe2\xda\x8f\x61" + - "\x72\xce\x25\x65\xfa\x08\xaf\x44\xe2\xa6\x39\x8e\x50\xe0\xbc" + - "\xba\x8d\x47\x49\xa2\x11\xf4\x42\x1b\xc7\x91\xb6\x8f\x48\x4c" + - "\xa0\x69\x10\x26\xec\x8c\xd0\x80\xf7\xc2\x05\x44\x8d\x89\xf9" + - "\x9b\x2a\xa8\x64\xf9\x04\x1f\x77\x66\x46\x65\x0e\xdf\x76\x73" + - "\xc1\xf5\x2d\x94\xe0\x69\xb4\x2c\x71\xa4\xc9\x62\xff\x08\xaf" + - "\x59\x3c\x5c\x18\x13\x2b\x72\xe8\xea\x46\x7f\x9a\x32\xb5\xae" + - "\xb7\x20\x74\xe5\x75\xfb\xcf\xd3\x24\xe9\xbc\xaa\x1b\x5c\x4c" + - "\x97\xf3\xf0\x23\xdb\x37\x33\xcb\x07\xb3\xd4\xa8\xf6\x0d\xc7" + - "\x5c\x2e\x85\x66\x5c\x80\xa2\xba\x6f\xbb\x21\xd8\x72\xad\xed" + - "\x80\x6f\xb9\x6e\xcd\xd4\x1a\x34\xd5\xbe\xe1\x3b\xe1\xc5\x76" + - "\xc1\x4b\xdb\xa1\xf8\x7a\x6d\x17\xfb\x96\xeb\xae\xac\x1e\xa0" + - "\xb8\x33\xf3\xe3\xb8\x69\x88\xd3\xc0\x40\x47\x23\x0a\xe8\x36" + - "\x99\x07\x64\x7e\x66\x8f\xf6\x9e\xea\xb9\x4a\x92\x60\xf8\xce" + - "\x76\x21\xdc\x6d\xd2\xaf\x3a\xab\x62\x7b\x9e\x40\x63\xa5\x8d" + - "\x53\x16\x32\xb7\x6a\x4c\xdb\xc6\x4d\x09\xf6\x7b\x0d\xfa\x4a" + - "\x6b\xc5\x1f\x6a\x0d\x28\x2e\xb8\x8a\xf1\xcc\x9b\xc0\x56\x3e" + - "\x81\x63\xcc\x49\xbc\x62\x8f\xf0\x17\x26\x8a\x12\xd4\x35\x2b" + - "\xcb\x07\x96\x3f\xd2\xb1\x08\x1f\xae\x6c\x4e\x96\xd2\xe1\x61" + - "\xa5\xac\x28\x6e\x9e\x40\xe8\xf7\xbc\xd2\x20\x40\xa1\x38\x2f" + - "\x79\xfe\x18\x9f\xdd\x13\x1f\x0e\x93\x69\x3f\x6a\x04\xe9\xc5" + - "\xcb\x15\x30\x0d\x5e\x38\x14\x1b\x3d\x33\x05\x2c\xc6\xc3\x05" + - "\x69\xa5\x5f\x4b\x48\x57\x52\xe8\x5b\xfe\x3b\xd0\xf8\xf2\xc7" + - "\x9d\x8e\x47\xe7\x3c\x48\x55\x80\xa2\xf1\x74\x7c\x78\xc7\x8a" + - "\x82\x8b\xf5\xd9\xf1\x2d\x53\x6b\x2e\xce\x2f\x97\x15\xb7\xb6" + - "\x1c\xb3\x87\x4a\x96\xb5\x86\xd1\x79\x0b\x98\xc7\x8a\xaf\x37" + - "\x3a\xce\xe2\x12\x56\x3a\x5e\xd2\xf8\x87\x77\xef\xde\xbd\xdb" + - "\xbd\xc4\x33\x17\xaa\x9e\xb9\x28\xe4\x73\xba\x63\x6b\xf8\xeb" + - "\xc7\xd5\xaa\x02\x7d\x38\x9c\x3d\xf5\x2a\x57\xb2\x2c\xef\xe4" + - "\x6e\x36\xc6\x94\x96\x3b\x2a\x2e\xe2\xdd\xcb\x09\x2f\x03\x63" + - "\x51\xc0\x0a\x29\xca\xd7\x98\xc4\x27\xfa\xb5\x46\x49\x3b\x5b" + - "\x27\xc7\x87\xbe\xdb\x81\x28\xae\x37\xbc\x2c\xd0\x60\x21\x1e" + - "\x71\x2e\x34\x25\xb2\x4d\x1b\xf0\xe8\xf4\x5c\xee\x5e\xcd\xd4" + - "\xc0\x43\x7a\x23\x3c\xe7\x83\x81\x69\x99\x0b\x7a\xc8\xa0\x5b" + - "\xfe\x8d\x86\x79\x6a\xeb\xa2\x2e\xcb\xf3\xce\x63\x46\x8f\x14" + - "\x77\x8e\x93\xf3\xaa\xea\x7c\xc0\x52\x3b\x8a\x10\x2e\x70\x9c" + - "\xd3\xc0\xd7\xb4\xec\x22\xd1\x79\x1d\xb7\x5d\x67\x63\x90\x0b" + - "\xe9\x33\xad\x5e\xf7\xba\xf7\x51\x78\x81\xfc\x5a\x6e\xb7\x4c" + - "\x78\x89\x5c\x14\xc7\x4d\xce\x74\xbe\x31\xb7\x91\xa6\x93\xcb" + - "\xc6\x0e\x6d\xac\xca\x3e\x43\x55\x97\x1a\xe9\x7e\xeb\xb0\xff" + - "\x64\x7b\xed\xe5\xf3\xc1\xde\xfe\x45\x7a\x1e\x57\x75\x9e\x43" + - "\x55\xc5\x59\x0c\x26\x61\x8a\xc9\xde\xed\x9c\x05\x5c\x10\x63" + - "\xae\xd9\x89\x7a\x88\x8f\xfd\x59\x78\x25\x90\xbc\x04\xa6\xba" + - "\x60\xef\xc6\x86\x7d\xe9\x03\xf7\x52\xe2\x26\x50\xdc\x60\xce" + - "\xd9\x4b\xc2\xed\xe2\x6f\x02\xff\x95\xae\x64\x5e\x57\x08\x13" + - "\xef\xef\x6b\x08\x6f\x64\x6f\x2d\x57\x65\xf9\x99\x89\x35\x54" + - "\xc1\x61\x15\x50\x69\x25\x5f\xcf\x6d\x36\x08\xda\xed\x1a\xe6" + - "\xf9\xab\x40\xff\x69\x77\xb8\xb5\x9a\x78\xc6\x57\xee\xec\xef" + - "\xdb\xbb\x9c\xb8\x81\x09\xa5\x61\x7f\x92\xc4\x79\xad\x8f\x7b" + - "\x83\xf4\xd7\xa5\xbe\x6f\x7e\x11\x4f\xac\xe4\x45\xd4\xb2\x1c" + - "\x59\x29\x49\x64\x12\x6b\xe0\x7a\x03\x2a\x72\xf4\x23\x69\x5a" + - "\xb5\x8e\xdf\xe0\xc6\x96\x33\x23\x37\x58\xb0\x53\xa7\x0a\xed" + - "\x3d\x69\xa0\x0a\x6d\xb3\xf3\x4e\x68\xf7\x39\xd1\x87\x43\x5b" + - "\xa1\x9a\x1a\xc1\x0d\x53\x4a\xf5\x3c\xae\x85\x4b\xf0\x8a\x38" + - "\x53\x48\x63\x7c\x38\x5c\x9a\x75\xa9\x90\x05\x98\x44\xfe\x0b" + - "\x82\x79\x06\x42\xc1\x58\xe4\xc6\x7c\x54\x7f\x83\x8d\x56\x9d" + - "\x98\xd4\x2b\xac\xd5\xa2\x36\x85\xd1\xe0\xa2\xaf\x4c\xea\x59" + - "\xc4\xf8\x0f\x6c\xc9\xda\x75\x69\xf4\xa9\x04\x56\x81\xdd\xbd" + - "\x8f\xff\xb6\x36\x01\x56\x44\x72\x15\xf5\x94\xfb\x65\x2d\x63" + - "\xb5\x3e\xe1\x0b\x1d\x33\xd6\x11\xc5\x87\xc3\x17\x98\xfe\x36" + - "\xae\xff\x2a\xeb\x28\x67\xe2\x3f\xde\xe8\x28\xaf\x75\x64\x5c" + - "\x3c\x5a\x29\xb9\x8d\xc0\x69\xae\x8a\x9e\xb9\xde\x84\x12\x19" + - "\x2b\x19\x91\xa4\x7a\xe3\xb3\xa3\xfb\x36\x77\x6c\xbe\x6c\x45" + - "\x6e\x5a\xd3\x2c\x31\xd1\x0d\xc2\xb3\xbe\x0e\x64\xa6\x8c\x3e" + - "\xae\x40\xbb\xa4\x5c\xb6\x3d\xd6\xa0\x92\x64\x02\x49\x32\x39" + - "\xb5\xfb\xf8\x03\xaf\x2a\x2e\xd6\x91\x82\xdf\x6a\xae\xa0\x88" + - "\x3a\x5f\x8b\xad\xd2\x27\x79\x6a\xea\x4a\xb1\x46\xe7\x8a\xc6" + - "\x5b\xc8\xa5\xe8\xd7\x45\xdb\xba\xd2\xd1\x83\x31\xae\x5b\xbb" + - "\xb0\xa3\xb3\x12\x48\x9c\xa1\x71\xb7\xe1\x6a\x94\xc4\xcf\x5d" + - "\xe1\x69\x88\xe4\xd6\xce\x8d\xdd\x7b\x1d\x29\x2f\x65\x3f\x68" + - "\x2e\xdb\x60\x02\x1f\x4c\xf0\x92\xf4\xc3\xac\x1d\x1e\xe5\xea" + - "\x67\xae\x2a\x7d\x5e\x30\x12\xfd\xe5\xee\xc3\x7b\xef\x3b\xee" + - "\xe3\x5a\x96\x3e\x94\x12\x63\x01\xbf\x7a\x86\x62\xdc\x74\x07" + - "\xd3\xb2\xdc\xa7\xbb\x27\x09\xac\xad\xbd\xf6\x3e\xde\x0e\x02" + - "\xed\x68\x5a\x61\xa6\x37\x4d\xbf\x03\x3f\xda\xe1\x4a\x29\xf6" + - "\x1a\x20\x1c\x2b\xa9\x6e\x58\xbe\x69\xa1\x8d\xc1\xad\x37\xce" + - "\x4c\x33\xce\xce\x37\x11\x3e\xcb\xf9\x80\x77\x76\xc4\x7b\x8d" + - "\xba\x5b\xff\x41\x16\xaf\xc4\x8d\x5a\x10\x22\xa7\x02\xfd\x23" + - "\x26\x35\x15\xe8\x1f\x42\xbf\x90\xcd\x11\x38\x12\xe0\x21\x78" + - "\xdf\x88\x9e\x61\xba\x97\x22\x1b\xc5\x70\x5c\x8c\x81\xc3\xc1" + - "\xdd\x31\x40\xf7\x0d\xf6\xd0\x12\x92\x0b\xbd\x3c\x1c\xec\x1f" + - "\xba\x58\x62\x9c\xee\xea\x6a\x83\xf6\x2b\x91\x01\xc9\xf5\x4b" + - "\x26\x1a\x97\xf0\x34\x44\x8a\x1c\x4e\xe8\x07\x1e\x8a\xf7\x2a" + - "\x95\xab\x95\x29\xd2\x31\x01\x0f\xf9\x08\xd2\x79\xa0\x93\xd3" + - "\x15\x3c\x2d\xb0\x25\xd3\x7b\x0a\x2e\xa1\xb2\x34\xa5\x51\x07" + - "\x31\x09\x4a\x76\x0c\xa7\x01\x5d\x2c\xd3\xaa\xe4\x39\xb8\x13" + - "\xe9\xe8\x92\x4b\x4c\x04\x45\x68\x44\x48\x6c\x85\x5b\x2c\xb1" + - "\x5b\x88\x30\x91\x74\x4a\x14\x15\x2d\x86\xb1\x92\x0a\xc9\x99" + - "\xfc\x49\xcd\xe4\xc5\x05\xb6\x20\xe6\x4a\xb4\xac\x5b\x74\x52" + - "\xbf\x10\xc0\xb3\x20\x88\x35\x44\xae\x56\xd9\xf0\x4c\x3c\xdc" + - "\x77\xca\x00\x91\x54\x2c\xf4\x92\x28\xba\x58\x1a\xa7\x95\x49" + - "\x02\xb8\xc5\x53\x38\x9d\x12\x46\x65\xcb\x0b\xff\x89\xcd\xf8" + - "\xc5\x05\x96\x0b\x6e\xb8\x98\x50\x0a\x49\xe2\x3f\xd2\x7b\xf7" + - "\xa9\xdc\xf1\x98\xde\x8e\x29\xe5\x09\xcc\xcd\x4e\x54\x65\x05" + - "\x94\xa0\x21\xb2\xfb\x5a\x86\x1b\xd2\x9b\x93\x38\x0d\xb2\x5f" + - "\x83\xf9\xa6\x98\x08\xf4\xa3\xf9\xef\xf2\x7f\x06\xf0\x73\xc0" + - "\xcd\x38\xe8\xa7\xfe\x30\x24\x37\x0c\x18\x7f\x36\x4e\x38\x74" + - "\x69\x7f\x0f\x05\xe4\x3e\xc3\x0a\x14\x88\xbc\xa5\x69\x14\x1f" + - "\x6d\x58\x25\xde\x98\x20\x0b\x22\xe2\x82\x6b\xce\x4a\x5e\x41" + - "\x11\xfd\x10\x55\xf5\x0e\x14\xc2\x83\x19\x66\x7f\x73\x99\xfb" + - "\x73\x9d\x40\x98\x37\xb5\xc8\x7e\x88\x02\xf6\xbd\x73\x9d\x41" + - "\xcf\x62\xde\xb1\x38\x36\x37\x49\x4c\x65\x64\xcc\xe9\xcc\x05" + - "\x68\x58\x8b\xe0\x65\xa7\xa0\xaa\x0c\x39\x7b\x51\xf8\xa4\xf1" + - "\x01\x22\xb3\xda\xdc\x06\xbd\x7a\x48\x64\xd4\x17\x5f\xb4\x3b" + - "\x98\xf0\xd5\x47\x25\x8f\xe4\x39\x24\x02\x41\x92\x84\xa8\xdc" + - "\x3e\x80\x2d\xb3\xbd\x4b\xc5\xf5\x00\x64\xbf\x24\x2d\x2e\x98" + - "\x4d\xa6\x64\x08\xc7\x4f\x9b\x06\x13\x48\x12\xe4\xf7\xa8\x40" + - "\x7f\x6a\x49\x7f\x5c\xcd\x47\x7b\xad\x6e\x32\x63\x47\x96\x8b" + - "\xfb\x7b\x0a\xc1\xe9\xd6\xa1\x3f\xc7\x05\xd3\xec\x87\xee\x5d" + - "\xe8\x87\xf8\x42\x1b\x1f\x86\x61\x1a\x26\xba\xeb\x17\x86\xe8" + - "\x91\x0f\xec\x25\x55\x08\x30\xa9\xa8\x42\x02\x93\x15\x55\x48" + - "\x62\x52\xfc\x1f\xc2\x8e\x37\xff\x1f\xb1\xe3\x1d\x3d\xf3\x34" + - "\xa6\x6d\x96\xea\xe0\x63\xc0\x33\x27\x23\x73\xdf\x08\x7a\xb3" + - "\xeb\x5e\x9d\xd6\x43\xf3\x04\x8c\x7d\xc6\x61\x8a\x64\xdc\x5f" + - "\x94\x47\x18\xb4\xe1\x29\x2d\x6d\xea\x71\x5d\xf2\xfc\x11\x69" + - "\x4c\x64\x2b\x58\x8e\x80\x68\x4c\x36\x08\xfe\x57\x80\xe8\x11" + - "\x63\x6b\xb1\xe9\x79\xdb\x70\xe8\x80\x8f\xdf\x57\x01\x6e\xed" + - "\x2b\x89\x51\x1a\x6e\x6c\xde\x36\x06\x34\xee\x8e\x41\xeb\x71" + - "\x0a\x16\x69\x4e\x7b\x54\xa3\x5d\x7d\x8a\xca\xd1\xfe\x5d\x95" + - "\x16\x28\x00\xd1\xf1\x3c\xf8\xc8\x06\x19\x5d\x57\x1f\x07\x27" + - "\x33\x86\xc7\xb8\x84\xc6\x66\x42\x76\xd3\xd2\xe7\x90\xd4\x5c" + - "\x8d\x01\xec\x44\x5a\x90\x6d\xc4\xab\x21\x95\xdd\xd1\x07\x60" + - "\x8a\xfc\xea\xae\xa9\xc9\x10\xd6\x4c\x83\x53\x99\x29\x2c\xf3" + - "\x5a\x29\x10\x5e\x87\x33\x0f\xdc\xf8\xb8\x77\xd5\xd5\xa6\x23" + - "\xdd\x21\x68\x77\x32\x04\xcf\x51\xd9\x0a\x83\x46\xc0\x25\xfb" + - "\x90\x12\x9c\xa4\x6b\xdb\xde\xee\x7c\x4c\xcb\xf4\xf4\x0a\x1f" + - "\x9e\x52\x87\x44\x01\xf1\xf8\x96\x9d\xd0\x84\x48\x4f\x60\x63" + - "\x63\x6a\xe9\x92\xf3\x0e\xe0\xd1\x27\xab\xcf\x20\x87\x9d\x52" + - "\x6b\xd4\x41\x22\xda\x96\x67\xd0\xde\x0a\x9d\x7d\xfc\x56\x83" + - "\x7a\x75\xe0\x94\x34\x57\xc2\xc9\x16\x63\xd0\xe1\x80\x3d\x6d" + - "\x67\xe8\x3f\x0c\x62\xb5\x56\x95\xfa\x79\x68\xfc\xa0\xce\x1c" + - "\xed\x57\x56\xb5\x00\xeb\xb2\x0d\x31\xbc\xba\xad\x77\x26\xc7" + - "\x83\xe2\xcf\x8e\x2f\x0b\x87\xe2\x10\x8b\x99\x2c\x09\xd0\xd8" + - "\xd5\xbe\x81\x67\xcf\x17\x7a\x99\x69\x22\xe8\x64\x32\xd4\xb8" + - "\x87\x56\x3b\xde\xfa\x37\x75\x5f\xe0\xa1\x50\xd9\x82\x8a\x24" + - "\xf9\x1a\x09\xeb\x70\x44\x58\x18\x03\x1a\xd4\x05\x91\xb0\x6e" + - "\xdb\x1d\xe3\x19\x83\xca\xad\xbb\x1b\x67\x36\xa9\x6d\xf1\xae" + - "\x09\xa5\x72\x76\x94\xb9\x05\xc1\x6b\xcb\x74\xbe\x81\xca\xac" + - "\xf0\x4d\x73\x5d\xb4\x97\xfc\x4c\x53\x9d\xee\x98\xf1\x63\x53" + - "\xa9\x37\xfe\x6d\xfb\x9d\x05\x9b\x7a\xa4\xad\xcb\x04\x7d\xa9" + - "\x9f\x24\x93\xf6\x19\xa4\xaf\x7d\x3d\x79\x77\x6c\x8a\x9e\x4c" + - "\x98\xa9\x76\x0a\xed\x5a\xad\x69\x1f\x0e\x2a\xdd\xca\xdf\x3f" + - "\x8c\xf4\x56\x23\x9d\x72\xa4\xef\x19\x1e\x1e\xb9\x3e\x1a\x68" + - "\xbe\x58\xc5\x1c\x43\x45\x44\x12\xe5\xd8\x67\x94\xfb\x72\xc4" + - "\x5e\xc2\x7d\x3d\x3a\x3b\x8f\x59\x08\xc2\x88\xfa\x16\xd4\xc2" + - "\x2d\x08\x6b\x7f\x8f\x8c\x98\xc2\xaa\xf5\xe1\xf1\x7b\xf1\x68" + - "\xef\xae\x78\xb2\x8f\x25\x3d\xb7\x63\xbf\x0c\x12\x73\xe9\x10" + - "\x75\x3b\xb7\x35\x5a\x7c\x86\x00\x3a\xf5\x1a\xe3\xfa\x74\x3c" + - "\x42\x5d\x99\x1c\x04\x63\x72\x0c\x8b\x6c\xd9\x6e\x0c\x12\x69" + - "\xf3\x94\x40\xea\x06\xe3\x13\x18\x27\x48\xb3\xba\xd5\x02\xef" + - "\xc5\xd1\x9d\x44\x19\x12\xed\x33\x34\x60\x72\x3c\x9c\x24\xb2" + - "\xe5\x41\x60\x67\xe8\x8c\x0a\xf4\xf7\xa1\x03\xaa\x63\xf7\x03" + - "\xeb\x66\x63\xbf\xf5\xe8\x11\xeb\x24\x19\x54\x8c\x01\x22\x96" + - "\x24\x97\x34\x84\xa6\xed\x6f\x7e\x3c\x20\x36\xf2\xd3\x9f\x93" + - "\x1f\xe2\x68\xe9\x60\x36\xcf\x77\x67\x7c\xe1\xde\x28\x5e\xb8" + - "\x94\xa3\x83\xda\x96\x26\xf9\x10\x87\x43\x37\x30\x84\xe5\xdc" + - "\x30\x4e\x92\xd8\x85\x53\x93\x10\x1b\x3a\x16\x5e\xf7\x21\xf6" + - "\x70\x00\x8f\x33\x2e\xa6\x4b\x6c\x7f\xf8\xe3\xcc\x60\x44\x13" + - "\xa7\x06\x72\x38\x0c\x14\xe2\x64\x30\x34\x56\x62\x6c\x7d\xcb" + - "\x66\x0b\x74\x5a\x06\xbf\xaa\x8b\xe6\x4b\x91\xd2\xdf\xb0\x36" + - "\x8a\xdd\xde\xbc\xbf\xb9\xbe\x8b\xbb\x83\xf8\x95\x6d\x01\xeb" + - "\xee\x0d\xc8\xe4\x36\xf6\xe2\x99\x41\x59\x41\x64\x56\xfc\xf2" + - "\xeb\xa7\x7f\x3d\x5a\x70\x38\xc4\x77\x37\xff\x76\x77\xf5\xf9" + - "\xe6\xea\x88\x52\x8b\xe3\x9c\xc5\xe0\x67\xc2\x24\x4b\x5f\x7c" + - "\xf6\xf5\xcf\x64\xc8\x35\xfb\xc7\x28\xfb\x00\x85\xa6\xc4\x73" + - "\xe8\x0f\x07\x13\x4b\xd1\x3f\x53\x8d\x6d\xd9\x0b\xd5\x18\xa1" + - "\xf6\xc7\xcc\x99\x44\x08\x84\x86\xc2\x15\x40\x31\x36\xb7\x83" + - "\x57\x88\xaf\x3e\x46\x9f\xc6\x88\x3a\xfe\x95\x80\xe3\x10\xcf" + - "\x94\x17\xc1\xd8\xe0\xb5\xa3\x5e\xd9\xea\xe2\xf4\x35\x8d\x48" + - "\x13\xbe\xdc\x42\x65\x58\x95\xdd\xd9\xf6\x3f\x87\x81\x41\xe0" + - "\x5e\xe2\x06\xcf\xfe\x3b\x00\x00\xff\xff\x1f\xab\x07\x8d") - -var _file_9 = &file{ - fileInfo: &fileInfo{ - name: "clipboard.min.js", - isDir: false, - size: 10662, - mode: os.FileMode(0), - mTime: time.Unix(-62135596800, 0), - cType: "application/javascript", - }, - path: "/js/clipboard.min.js", - dirP: "/js", - sPath: "/js/clipboard.min.js", - id: 9, - cb: _compress_bytes_9, -} - -var _compress_bytes_10 = []byte("" + "\x78\x9c\x94\x53\x41\x8f\xd3\x3c\x10\xbd\xf7\x57\xbc\xaf\x97" + "\xba\xea\x2a\xad\x3e\x71\x40\x14\x1f\x58\x09\x09\x21\xd8\x45" + "\xb4\x07\x24\xc4\xc1\x75\xa6\x89\x17\xd7\xee\xda\xe3\x65\x2b" + @@ -738,7 +501,7 @@ var _compress_bytes_10 = []byte("" + "\xa8\x81\x56\xac\x6b\x08\x0a\xc1\x87\x3c\x94\x5c\xbf\x4b\xf6" + "\x4f\xcb\x51\xf3\x2b\x00\x00\xff\xff\x54\x02\x16\x01") -var _file_10 = &file{ +var _file_9 = &file{ fileInfo: &fileInfo{ name: "control.js", isDir: false, @@ -750,11 +513,11 @@ var _file_10 = &file{ path: "/js/control.js", dirP: "/js", sPath: "/js/control.js", - id: 10, - cb: _compress_bytes_10, + id: 9, + cb: _compress_bytes_9, } -var _compress_bytes_11 = []byte("" + +var _compress_bytes_10 = []byte("" + "\x78\x9c\xcc\xfd\x6b\x76\xe3\x38\xb6\x20\x0a\xff\xef\x29\xf4" + "\x1f\x9a\x55\xa5\x24\x53\xb0\x4c\x52\xd4\x3b\x18\x6e\xa5\x65" + "\x67\xba\x2b\x32\x22\x8e\xed\xc8\x3a\xd5\x4a\x55\x2c\x5a\x82" + @@ -9564,7 +9327,7 @@ var _compress_bytes_11 = []byte("" + "\x34\x98\x1e\xb5\x19\x25\x26\xf5\xca\x9f\xff\x2f\x00\x00\xff" + "\xff\x00\x27\x28\xd9") -var _file_11 = &file{ +var _file_10 = &file{ fileInfo: &fileInfo{ name: "gotty-bundle.js", isDir: false, @@ -9576,85 +9339,68 @@ var _file_11 = &file{ path: "/js/gotty-bundle.js", dirP: "/js", sPath: "/js/gotty-bundle.js", - id: 11, - cb: _compress_bytes_11, + id: 10, + cb: _compress_bytes_10, } -var _compress_bytes_12 = []byte("" + - "\x78\x9c\xa4\x57\xdd\x72\xdb\x36\x13\xbd\xd7\x53\x6c\x10\x7f" + - "\x1f\xa9\xb1\x45\x5a\x76\xfe\xc6\x21\xd9\xf1\x24\xbd\x70\x27" + - "\xd3\xc9\xc4\xed\x75\x07\x02\x57\x12\x12\x08\x60\x01\x48\xb6" + - "\x46\xe5\xbb\x77\x40\x90\x14\xa9\x1f\x4b\x6d\xaf\x44\x00\x67" + - "\xcf\x9e\x5d\x2c\x16\xd0\x66\x33\x82\x0b\x66\x05\xdc\xa5\x10" + - "\x31\x25\xad\x56\x02\x46\x65\x09\xd5\x82\x99\xab\xa7\x2f\x8a" + - "\x51\xcb\x95\xac\x10\x42\xb1\xee\x2a\xd5\x58\x4d\xfb\xaf\x51" + - "\x59\x0e\x92\x57\xb9\x62\x76\x5d\x20\xcc\xed\x42\x64\x83\xc4" + - "\xff\x0c\x92\x39\xd2\x3c\x1b\x00\x24\x96\x5b\x81\xd9\x66\x03" + - "\x51\xf5\x05\x65\x99\xc4\x7e\xce\xad\x0a\x2e\x7f\x80\x46\x91" + - "\x12\xce\x94\x24\xe0\xa8\x52\xc2\x17\x74\x86\x71\x21\x67\x04" + - "\xe6\x1a\xa7\x29\x89\xa7\x74\xe5\x00\x91\x9b\xdb\x31\x34\x76" + - "\x2d\xd0\xcc\x11\x6d\x8b\x66\xc6\xc4\x82\x1b\x1b\x31\x63\x08" + - "\xc4\x95\x81\x61\x9a\x17\x16\x8c\x66\x29\x89\xbf\x9b\x98\x09" + - "\x5e\x4c\x14\xd5\x79\xb4\xe0\x32\xfa\x6e\x48\x96\xc4\x1e\x93" + - "\x0d\x92\xd8\xcb\x1f\x24\x13\x95\xaf\x2b\xf3\x9c\xaf\x80\x09" + - "\x6a\x4c\x4a\x2c\x9d\x08\x84\x15\xea\x5b\x58\x8c\x26\xa3\xf1" + - "\xf8\xba\x92\x74\x00\x34\x72\x34\xf5\xa2\x4b\x85\x9b\x6b\x46" + - "\x6e\xdc\x24\x69\x3b\xa3\xbb\xc3\x0a\xd2\x10\x32\x25\x96\x0b" + - "\x39\x26\xd9\x27\x25\x2d\xe5\x12\x35\x3c\x7c\x4e\x62\x3b\x3f" + - "\x61\x71\x43\xb2\x07\x97\xce\x33\xa0\xb7\x8e\x7c\xb1\xa0\x32" + - "\x3f\x03\xfc\x86\x64\xbf\xd2\xc5\x39\xb4\x6f\x49\xf6\xf0\x75" + - "\x1f\xe7\x6a\x8a\x4f\x77\x8a\xae\x2c\x5f\xe6\x7a\x47\xb2\x06" + - "\x7b\x98\x11\x65\x7e\x92\xe4\x3d\xc9\x1e\x2d\xb5\x4b\x73\x5c" + - "\x14\xb3\x22\xfa\x59\x56\x1b\x7d\x8a\xed\x03\xc9\xee\x99\x13" + - "\x74\x84\xce\x29\x1a\xf5\x48\x92\xb8\xbb\xcf\xce\xaa\x53\x07" + - "\x49\xdc\x29\x93\x24\xce\xf9\x2a\x1b\x1c\xa9\x2e\x57\x9c\x2f" + - "\x54\x57\x53\xbb\x5b\x31\xa0\xa9\x9c\x21\x5c\xf0\x2b\xb8\xc0" + - "\xb6\x05\x54\xc5\x64\xfa\x71\x26\x56\x3b\x3c\x9f\x42\x88\x7f" + - "\x42\xb8\x50\x39\x5c\x70\xb8\x19\xc2\x78\xe8\x82\x69\x64\x00" + - "\x73\xe7\xe0\x86\x6c\x33\xbf\x5b\x0b\xf9\x6e\xfd\x42\x75\xf8" + - "\x53\x82\xcf\xc8\x80\x4b\xab\xa0\xd5\x40\xfa\xc6\x00\x09\x6d" + - "\x4e\xb4\x43\xc7\x9b\x0d\x14\x9a\x4b\x3b\x05\xf2\xbf\x68\x7c" + - "\x63\x08\x44\x0f\x9f\xa1\x2c\x09\xac\xa8\x58\x62\x4a\x5c\x97" + - "\xa9\x67\x2c\xd5\x33\xb4\x29\xf9\x63\x22\xa8\xfc\x41\xb2\x63" + - "\xb6\x49\x4c\x77\x24\xc7\x36\x3f\x56\xa8\x4d\xd7\x7b\x31\xc4" + - "\x9b\x36\xc4\x4a\x8e\x3b\x7b\x50\x96\xf0\x17\x78\x7b\x6b\xd7" + - "\xc7\xe3\x7c\x4d\xb6\x5c\xc5\x9a\x40\x4e\x2d\x1d\xb5\x9d\x6a" + - "\x64\xf1\xd9\xa6\x24\xae\x88\x8e\x67\xa3\x13\x6b\xeb\xfe\xcc" + - "\x30\x51\x98\x7f\x1d\xe1\x5e\x54\x07\x64\x9c\x23\x61\xef\xb8" + - "\x1c\x50\x70\xdb\x53\x50\x37\xad\xdd\xd8\xb7\xd3\xfb\x9e\xf6" + - "\x18\xdf\xf4\x18\x5d\x67\x3b\x14\xd2\xb6\x20\x85\x9a\x99\xa3" + - "\x5b\xf0\xd3\x54\x09\xa1\x9e\xd2\xf1\xff\x2d\xe5\x22\x1d\x5f" + - "\xef\xd5\x63\xe3\x6c\x86\x16\x1c\x55\x4f\x78\xed\xfd\x8c\x3d" + - "\xdb\x0b\xe3\x6d\x7f\x6b\xbe\x9a\x26\x29\x5c\xe6\xf8\xec\x67" + - "\xae\x0f\x66\xe4\x60\x3b\x3e\xb9\x11\xef\x7a\xfe\xbe\x28\xf6" + - "\x88\x7a\x85\x7a\x77\x2b\xba\x0b\xff\x61\xdb\xdf\xf7\xbc\xb9" + - "\x1e\x8e\x8d\xa7\xc8\x77\xf4\x23\xfc\xbb\x3d\xfd\xa4\xa7\x0f" + - "\x87\xaa\x99\x4f\x41\x69\xcf\xf3\x68\xa9\xb6\xfe\xf3\x5e\x88" + - "\xdd\xca\x06\x48\x26\x4b\x6b\x95\x6c\xe4\x1a\x07\xaf\x6e\x1d" + - "\x6d\x93\xd8\xaf\x39\xd5\xbe\x67\xee\x71\xab\xe2\x9f\x50\xab" + - "\xc2\x31\xab\xe2\x24\xf1\x37\x34\x3d\xd9\xa7\xa8\x35\xd6\xba" + - "\x6b\xc3\x7d\x07\x27\xcf\xf3\xc9\xdb\xaf\x05\x75\x30\x49\xdc" + - "\xbb\xbb\x0e\xdd\x88\xdd\xab\x71\xff\x69\xe7\xdf\xb5\x3b\x8f" + - "\xba\x16\xe8\x89\x56\x54\x43\xdb\x59\x21\x05\x89\x4f\xf0\xa9" + - "\x19\xff\xf2\x18\x06\x91\x6b\xc1\xc1\x15\x6c\x6a\x19\xae\xf9" + - "\xde\xc1\x74\x29\xab\xcb\x1e\x42\xab\xf9\x6c\x86\x7a\xd8\x02" + - "\x00\x34\xda\xa5\x96\x50\xaf\x44\x13\x6a\xf0\xf7\x6f\x0f\x91" + - "\xc6\x42\x50\x86\x61\x10\xbf\x0e\xae\x82\x60\x08\x97\x2d\x64" + - "\x86\xf6\xde\x5a\xcd\x27\x4b\x8b\x61\x70\xa0\xdd\x07\xc3\x8f" + - "\x35\xbd\xcf\x4f\x59\x8f\xb7\xcf\x57\x25\xc3\xc0\x2c\x19\x43" + - "\x63\x82\xab\x8e\x3e\xdc\x2a\x63\x4a\x1a\x25\x30\xe2\x72\xaa" + - "\xc2\xc0\xbf\x56\xee\x82\x2b\xc0\x88\x56\xdf\xad\x8f\x3e\xf0" + - "\x37\x17\x71\x05\x73\x4a\x8e\x81\x7c\x24\x35\xae\xce\xc9\xc7" + - "\x41\x8d\xc5\x88\x09\xa4\xfa\x11\x05\x56\x9e\xc2\x96\x85\x0a" + - "\xd4\x36\x24\xfe\x52\xac\x5e\xf3\x21\xb9\xf4\x9e\x2e\xc9\x10" + - "\x98\x2a\x38\xe6\xaf\x48\x8d\xf7\x61\x77\x5f\xe8\xbe\x42\xdc" + - "\x53\xdd\xfd\xe3\xf8\x3b\x00\x00\xff\xff\x94\xcd\xa8\x2f") +var _compress_bytes_11 = []byte("" + + "\x78\x9c\x9c\x96\x6f\x6f\xfa\x36\x10\xc7\x9f\xf3\x2a\x6e\x16" + + "\x9b\x7e\x3f\x69\x89\x0b\x6d\xd7\x6a\x72\x32\x55\xeb\x1e\x20" + + "\x55\x53\xb5\xbe\x80\xc9\x38\x06\xdc\x3a\x36\xb3\x0d\x6d\x85" + + "\xf2\xde\xa7\xcb\x1f\x48\x02\x34\x6c\x8f\x20\xe7\xaf\x3f\x77" + + "\x3e\xdf\x9d\xbc\xdb\x45\x30\x16\x41\xc3\xaf\x09\xc4\xc2\x9a" + + "\xe0\xac\x86\xa8\x28\xa0\x5c\xf0\x2b\xfb\xfe\x64\x05\x0f\xca" + + "\x9a\x52\xa1\xad\x68\xaf\x72\x27\x4b\x73\xf5\x2f\x2a\x8a\x11" + + "\xfb\x21\xb3\x22\x7c\xae\x25\xac\x42\xae\xd3\x11\xab\x7e\x46" + + "\x6c\x25\x79\x96\x8e\x00\x58\x50\x41\xcb\x74\xb7\x83\xb8\xfc" + + "\x07\x45\xc1\x68\x65\xc3\x55\xad\xcc\x1b\x38\xa9\x13\xa2\x84" + + "\x35\x04\x10\x95\x10\x95\xf3\xa5\xa4\x6b\xb3\x24\xb0\x72\x72" + + "\x91\x10\xba\xe0\x5b\x14\xc4\x68\xeb\x6d\xf4\xe1\x53\x4b\xbf" + + "\x92\x32\xec\xd5\xc2\x7b\xaa\x95\x0f\xb1\xf0\x9e\x00\x4d\x47" + + "\x8c\x56\xf1\x8c\xd8\xdc\x66\x9f\x25\x20\x53\x5b\x10\x9a\x7b" + + "\x9f\x90\xc0\xe7\x5a\xc2\x56\xba\x6b\xc8\xa3\x79\x34\x99\x5c" + + "\x95\x3e\x4e\x88\x22\xc4\xd4\x8b\x78\x36\xb4\x35\x5f\xf8\xdd" + + "\x9c\xfa\x60\x71\xed\xcf\x52\xd2\x00\x85\xd5\x9b\xdc\x4c\x48" + + "\xfa\xbb\x35\x81\x2b\x23\x1d\xcc\x1e\x19\x0d\xab\x81\x1d\x53" + + "\x92\xce\x30\x3f\x17\x48\xaf\x11\x9e\xe7\xdc\x64\x17\x88\x6f" + + "\x48\xfa\x27\xcf\x2f\xc1\xde\x92\x74\xf6\x7c\xac\xc3\x22\x51" + + "\x8b\x5e\x15\x15\xc5\xd7\xac\x5f\x48\xda\x68\x4f\x13\xa5\xc9" + + "\x06\x21\x77\x24\x7d\x09\x3c\x6c\xfc\xf9\xa0\x44\xd0\xf1\x1f" + + "\xa6\xbc\xe8\x21\xda\x3d\x49\x1f\x04\x06\x74\x06\x87\x11\x45" + + "\x1d\x08\xa3\xed\x7b\xc6\x5d\xad\x3a\x60\xb4\x55\x26\x8c\x66" + + "\x6a\x9b\x8e\xce\x54\x17\x16\xe7\x17\xd5\xd5\xd4\xee\x21\x18" + + "\x70\xdc\x2c\x25\x8c\xd5\xcf\x30\x96\xfb\x9e\x2e\x8b\xc9\x77" + + "\xcf\xc9\x82\x43\xbd\x5a\xc0\x37\xf9\x0f\x7c\xcb\x6d\x06\x63" + + "\x05\xd3\xef\x30\xf9\x8e\x87\x69\xc2\x00\x81\x7d\x30\x25\x87" + + "\xcc\xf7\x6b\x21\xeb\xd7\x2f\x94\xdd\x9c\x10\xf9\x21\x05\x28" + + "\x13\x2c\xec\x63\x20\xdd\xcd\x00\x8c\x37\x2d\x2a\xe9\x6e\x07" + + "\x6b\xa7\x4c\x58\x00\xf9\x31\x9e\x4c\x3d\x81\x78\xf6\x08\x45" + + "\x41\x60\xcb\xf5\x46\x26\x04\x67\x46\x6d\x09\xdc\x2d\x65\x48" + + "\xc8\xdf\x73\xcd\xcd\x1b\x49\xcf\xed\x65\x94\xf7\xe2\xa5\x21" + + "\x1b\x38\xc1\x74\x7f\x82\xd2\x21\xb6\x16\xfa\x6c\xf9\xd8\x1b" + + "\x2f\xa0\x5d\x77\x68\x75\xf7\xf5\x79\x07\xf3\x05\xc4\x9b\x0e" + + "\x11\x5b\xb4\xc4\x9d\xcd\xac\xb6\x4b\x7f\x36\xb9\xbf\x2d\xac" + + "\xd6\xf6\x3d\x99\xfc\x14\xb8\xd2\xc9\xe4\xea\x28\xb7\x8d\xb3" + + "\xa5\x0c\x80\xa8\x4e\xe0\xb5\xf7\xff\x93\xe6\xdb\x6e\x9a\x9f" + + "\x7d\x93\x14\x65\x32\xf9\x51\x59\xae\x4e\x66\xe4\xe4\x5c\x89" + + "\x8e\xba\x38\xeb\x0f\x96\xb6\xbf\x27\x2b\x5e\xa4\xdb\x4a\xd7" + + "\xbf\x8a\xf6\xc2\x69\xd7\xc7\xed\x7e\xc2\xdb\x5d\xc7\x1b\x0e" + + "\xa3\x7d\x11\xc5\xd5\x68\x3a\xc3\xef\x0f\xa7\x41\x4f\xf7\x47" + + "\x57\x5f\x41\xac\xab\x38\x2f\x81\xbb\x50\xfd\x7d\xd0\xba\x3f" + + "\xec\x00\xd8\x7c\x13\x82\x35\x4d\xb8\x1e\xe5\xe5\xf8\x74\x81" + + "\xd1\x6a\x0d\xa3\xae\x9a\xff\x88\x6d\xd7\xff\x05\x6d\xd7\x48" + + "\xb6\xeb\x41\xf0\x5f\xd2\x77\xc2\x1e\x42\x3b\x59\xc7\x5d\x6f" + + "\x3c\x76\x30\x50\x9e\xb5\xee\xab\x31\xbe\x17\xb5\x34\x8c\x76" + + "\x86\xf0\xa9\xd1\xde\x9e\xf1\xcc\x0b\xa7\xd6\x01\xbc\x13\x09" + + "\xa1\xaf\x9e\xd6\x2f\xae\xf8\xd5\x93\x94\xd1\x6a\x15\x5f\x27" + + "\x15\x14\x9f\x29\xf8\x7c\xfa\x37\x00\x00\xff\xff\x72\x73\xbc" + + "\xe4") -var _file_12 = &file{ +var _file_11 = &file{ fileInfo: &fileInfo{ name: "list.html", isDir: false, - size: 3288, + size: 2469, mode: os.FileMode(0), mTime: time.Unix(-62135596800, 0), cType: "text/html; charset=utf-8", @@ -9662,15 +9408,15 @@ var _file_12 = &file{ path: "/list.html", dirP: "/", sPath: "/list.html", - id: 12, - cb: _compress_bytes_12, + id: 11, + cb: _compress_bytes_11, } func init() { fs = []*file{ _file_0, _file_1, _file_2, _file_3, _file_4, _file_5, _file_6, _file_7, _file_8, _file_9, - _file_10, _file_11, _file_12, + _file_10, _file_11, } root = &data{ diff --git a/route/exec.go b/route/exec.go index 13aa915..490686a 100644 --- a/route/exec.go +++ b/route/exec.go @@ -15,13 +15,39 @@ import ( "github.com/yudai/gotty/webtty" ) +func (server *Server) handleExecRedirect(c *gin.Context) { + containerID := c.Param("cid") + execID := server.setContainerID(containerID) + if query := c.Request.URL.RawQuery; query != "" { + c.Redirect(302, "/exec/"+execID+"?"+c.Request.URL.RawQuery) + } else { + c.Redirect(302, "/exec/"+execID) + } +} + func (server *Server) handleExec(c *gin.Context, counter *counter) { - cInfo := server.containerCli.GetInfo(c.Request.Context(), c.Param("id")) - server.generateHandleWS(c.Request.Context(), counter, cInfo). + execID := c.Param("eid") + containerID, ok := server.getContainerID(execID) + if !ok { + c.String(http.StatusBadRequest, fmt.Sprintf("exec id %s not found", execID)) + return + } + + server.m.RLock() + masterTTY, ok := server.masters[execID] + server.m.RUnlock() + if ok { // exec ID exist, use the same master + log.Infof("using exist master for exec %s", execID) + server.processShare(c, execID, masterTTY) + return + } + + cInfo := server.containerCli.GetInfo(c.Request.Context(), containerID) + server.generateHandleWS(c.Request.Context(), execID, counter, cInfo). ServeHTTP(c.Writer, c.Request) } -func (server *Server) generateHandleWS(ctx context.Context, counter *counter, container types.Container) http.HandlerFunc { +func (server *Server) generateHandleWS(ctx context.Context, execID string, counter *counter, container types.Container) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if container.Shell == "" { log.Errorf("cannot find a valid shell in container [%s]", container.ID) @@ -60,7 +86,7 @@ func (server *Server) generateHandleWS(ctx context.Context, counter *counter, co cctx, timeoutCancel := context.WithCancel(ctx) defer timeoutCancel() - err = server.processTTY(cctx, timeoutCancel, conn, container) + err = server.processTTY(cctx, execID, timeoutCancel, conn, container) switch err { case ctx.Err(): closeReason = "cancelation" @@ -76,13 +102,13 @@ func (server *Server) generateHandleWS(ctx context.Context, counter *counter, co } } -func (server *Server) processTTY(ctx context.Context, timeoutCancel context.CancelFunc, +func (server *Server) processTTY(ctx context.Context, execID string, timeoutCancel context.CancelFunc, conn *websocket.Conn, container types.Container) error { arguments, err := server.readInitMessage(conn) if err != nil { return err } - log.Debugf("exec container: %s, params: %s", container.ID, arguments) + log.Debugf("exec container: %s, params: [%s]", container.ID[:7], arguments) q, err := parseQuery(strings.TrimSpace(arguments)) if err != nil { @@ -99,7 +125,12 @@ func (server *Server) processTTY(ctx context.Context, timeoutCancel context.Canc if err != nil { return fmt.Errorf("exec container error: %s", err) } - defer containerTTY.Exit() + defer func() { + log.Infof("container %s exit", container.ID[:7]) + if err := containerTTY.Exit(); err != nil { + log.Warnf("exit container err: %s", err) + } + }() // handle timeout tout := server.options.IdleTime @@ -130,24 +161,25 @@ func (server *Server) processTTY(ctx context.Context, timeoutCancel context.Canc opts := []webtty.Option{ webtty.WithWindowTitle(titleBuf), webtty.WithPermitWrite(), - // webtty.WithReconnect(10), // not work.... } - wrapper := &wsWrapper{conn} shareID := fmt.Sprintf("%s-%d", container.ID, time.Now().UnixNano()) masterTTY, err := types.NewMasterTTY(ctx, containerTTY, shareID) if err != nil { return err } - server.mMux.Lock() - if _, ok := server.masters[container.ID]; !ok { - server.masters[container.ID] = masterTTY - } - server.mMux.Unlock() + + server.m.Lock() + server.masters[execID] = masterTTY + server.m.Unlock() + defer func() { - server.mMux.Lock() - delete(server.masters, container.ID) - server.mMux.Unlock() + // if master dead, all slaves dead + server.m.Lock() + masterTTY.Close() + delete(server.masters, execID) + delete(server.execs, execID) + server.m.Unlock() }() if server.options.EnableAudit { @@ -158,7 +190,8 @@ func (server *Server) processTTY(ctx context.Context, timeoutCancel context.Canc }) } - log.Infof("new web tty for container: %s", container.ID) + log.Infof("new web tty for container: %s", container.ID[:7]) + wrapper := &wsWrapper{conn} tty, err := webtty.New(wrapper, masterTTY, opts...) if err != nil { return fmt.Errorf("failed to create webtty: %s", err) diff --git a/route/handler.go b/route/handler.go index 71e8222..b79d8f4 100644 --- a/route/handler.go +++ b/route/handler.go @@ -4,49 +4,15 @@ import ( "bytes" "encoding/json" "fmt" - "net/http" "net/url" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" - "github.com/yudai/gotty/webtty" "github.com/wrfly/container-web-tty/types" - "github.com/wrfly/container-web-tty/util" ) -func (server *Server) handleWSIndex(c *gin.Context) { - cInfo := server.containerCli.GetInfo(c.Request.Context(), c.Param("id")) - titleVars := server.titleVariables( - []string{"server"}, - map[string]map[string]interface{}{ - "server": map[string]interface{}{ - "containerName": cInfo.Name, - "containerID": cInfo.ID, - }, - }, - ) - - titleBuf := new(bytes.Buffer) - err := titleTemplate.Execute(titleBuf, titleVars) - if err != nil { - c.Error(err) - } - - indexVars := map[string]interface{}{ - "title": titleBuf.String(), - } - - indexBuf := new(bytes.Buffer) - err = indexTemplate.Execute(indexBuf, indexVars) - if err != nil { - c.Error(err) - } - - c.Writer.Write(indexBuf.Bytes()) -} - func (server *Server) handleAuthToken(c *gin.Context) { c.Header("Content-Type", "application/javascript") // @TODO hashing? @@ -87,7 +53,6 @@ func (server *Server) handleListContainers(c *gin.Context) { "containers": server.containerCli.List(c.Request.Context()), "control": server.options.Control, "loc": server.options.ShowLocation, - "share": server.options.EnableShare, } listBuf := new(bytes.Buffer) @@ -137,91 +102,23 @@ func (server *Server) handleRestartContainer(c *gin.Context) { server.handleContainerActions(c, "restart") } -func (server *Server) handleLogs(c *gin.Context) { - ctx := c.Request.Context() - - conn, err := server.upgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - c.String(http.StatusInternalServerError, "server error: %s", err) - return - } - defer conn.Close() - - initArg, err := server.readInitMessage(conn) - if err != nil { - c.String(http.StatusBadRequest, "read init message error: %s", err) - return - } - - q, err := parseQuery(initArg) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - follow := true - if v := q.Get("follow"); v != "1" && v != "" { - follow = false - } - tail := "10" - if v := q.Get("tail"); v != "" { - tail = v - } - opts := types.LogOptions{ - ID: c.Param("id"), - Follow: follow, - Tail: tail, - } - - container := server.containerCli.GetInfo(ctx, opts.ID) - - log.Debugf("get logs of container: %s", container.ID) - logsReadCloser, err := server.containerCli.Logs(ctx, opts) - if err != nil { - c.String(http.StatusInternalServerError, "get logs error: %s", err) - return - } - defer logsReadCloser.Close() - - titleBuf, err := server.makeTitleBuff(container) - if err != nil { - c.String(http.StatusInternalServerError, "failed to fill window title template: %s", err) - return - } - - tty, err := webtty.New( - &wsWrapper{conn}, - newSlave(util.NopRWCloser(logsReadCloser), false), - []webtty.Option{ - webtty.WithWindowTitle(titleBuf), - webtty.WithPermitWrite(), // can type "enter" - }..., - ) - if err != nil { - c.String(http.StatusInternalServerError, "failed to create webtty: %s", err) - return - } - - if err := tty.Run(ctx); err != nil { - if err != webtty.ErrMasterClosed && err != webtty.ErrSlaveClosed { - log.Errorf("failed to run webtty: %s", err) - } - } -} - -func (server *Server) terminalPage(c *gin.Context) { server.handleWSIndex(c) } - -func (server *Server) makeTitleBuff(c types.Container) ([]byte, error) { - location := "127.0.0.1" +func (server *Server) makeTitleBuff(c types.Container, extra ...string) ([]byte, error) { + location := "localhost" if c.LocServer != "" { location = c.LocServer } + cName := c.Name + if len(extra) != 0 { + cName = extra[0] + " " + c.Name + } + titleVars := server.titleVariables( []string{"server"}, map[string]map[string]interface{}{ "server": map[string]interface{}{ "containerLoc": location, - "containerName": c.Name, + "containerName": cName, "containerID": c.ID, }, }, diff --git a/route/id.go b/route/id.go new file mode 100644 index 0000000..af27666 --- /dev/null +++ b/route/id.go @@ -0,0 +1,18 @@ +package route + +import "github.com/wrfly/container-web-tty/util" + +func (server *Server) getContainerID(execID string) (string, bool) { + server.m.Lock() + containerID, ok := server.execs[execID] + server.m.Unlock() + return containerID, ok +} + +func (server *Server) setContainerID(containerID string) string { + execID := util.ID(containerID) + server.m.Lock() + server.execs[execID] = containerID + server.m.Unlock() + return execID +} diff --git a/route/log.go b/route/log.go new file mode 100644 index 0000000..25f436b --- /dev/null +++ b/route/log.go @@ -0,0 +1,83 @@ +package route + +import ( + "net/http" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "github.com/yudai/gotty/webtty" + + "github.com/wrfly/container-web-tty/types" + "github.com/wrfly/container-web-tty/util" +) + +func (server *Server) handleLogs(c *gin.Context) { + ctx := c.Request.Context() + + conn, err := server.upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + c.String(http.StatusInternalServerError, "server error: %s", err) + return + } + defer conn.Close() + + initArg, err := server.readInitMessage(conn) + if err != nil { + c.String(http.StatusBadRequest, "read init message error: %s", err) + return + } + + q, err := parseQuery(initArg) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + follow := true + if v := q.Get("follow"); v != "1" && v != "" { + follow = false + } + tail := "10" + if v := q.Get("tail"); v != "" { + tail = v + } + opts := types.LogOptions{ + ID: c.Param("cid"), + Follow: follow, + Tail: tail, + } + + container := server.containerCli.GetInfo(ctx, opts.ID) + + log.Debugf("get logs of container: %s", container.ID) + logsReadCloser, err := server.containerCli.Logs(ctx, opts) + if err != nil { + c.String(http.StatusInternalServerError, "get logs error: %s", err) + return + } + defer logsReadCloser.Close() + + titleBuf, err := server.makeTitleBuff(container) + if err != nil { + c.String(http.StatusInternalServerError, "failed to fill window title template: %s", err) + return + } + + tty, err := webtty.New( + &wsWrapper{conn}, + newSlave(util.NopRWCloser(logsReadCloser)), + []webtty.Option{ + webtty.WithWindowTitle(titleBuf), + webtty.WithPermitWrite(), // can type "enter" + }..., + ) + if err != nil { + c.String(http.StatusInternalServerError, "failed to create webtty: %s", err) + return + } + + if err := tty.Run(ctx); err != nil { + if err != webtty.ErrMasterClosed && err != webtty.ErrSlaveClosed { + log.Errorf("failed to run webtty: %s", err) + } + } +} diff --git a/route/route.go b/route/route.go index 82269fe..2e24961 100644 --- a/route/route.go +++ b/route/route.go @@ -32,8 +32,11 @@ type Server struct { srv *http.Server hostname string + // execID -> containerID + execs map[string]string + // execID -> process masters map[string]*types.MasterTTY - mMux sync.RWMutex + m sync.RWMutex } var ( @@ -66,7 +69,7 @@ func init() { panic(err) } - titleFormat := "{{ .containerName }} - {{ printf \"%.8s\" .containerID }}@{{ .containerLoc }}" + titleFormat := "{{ .containerName }}@{{ .containerLoc }}" titleTemplate, err = noesctmpl.New("title").Parse(titleFormat) if err != nil { log.Fatal(err) @@ -92,6 +95,7 @@ func New(containerCli container.Cli, options config.ServerConfig) (*Server, erro return &Server{ options: options, containerCli: containerCli, + execs: make(map[string]string, 500), masters: make(map[string]*types.MasterTTY, 50), hostname: h, @@ -136,18 +140,13 @@ func (server *Server) Run(ctx context.Context, options ...RunOption) error { // exec counter := newCounter(server.options.IdleTime) - router.GET("/exec/:id/", server.terminalPage) - router.GET("/exec/:id/"+"ws", func(c *gin.Context) { server.handleExec(c, counter) }) - - if server.options.EnableShare { - // share screen - router.GET("/share/:id/", server.terminalPage) - router.GET("/share/:id/ws", func(c *gin.Context) { server.handleShare(c) }) - } + router.GET("/e/:cid/", server.handleExecRedirect) // containerID + router.GET("/exec/:eid/", server.handleWSIndex) // execID + router.GET("/exec/:eid/"+"ws", func(c *gin.Context) { server.handleExec(c, counter) }) // logs - router.GET("/logs/:id/", server.terminalPage) - router.GET("/logs/:id/"+"ws", func(c *gin.Context) { server.handleLogs(c) }) + router.GET("/logs/:cid/", server.handleWSIndex) + router.GET("/logs/:cid/"+"ws", func(c *gin.Context) { server.handleLogs(c) }) ctl := server.options.Control if ctl.Enable { diff --git a/route/share.go b/route/share.go index fbd6e20..446e235 100644 --- a/route/share.go +++ b/route/share.go @@ -6,35 +6,36 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" + "github.com/wrfly/container-web-tty/types" "github.com/yudai/gotty/webtty" ) -func (server *Server) handleShare(c *gin.Context) { - ctx := c.Request.Context() - cid := c.Param("id") - cInfo := server.containerCli.GetInfo(ctx, cid) - +func (server *Server) processShare(c *gin.Context, execID string, masterTTY *types.MasterTTY) { conn, err := server.upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Errorf("upgrade ws error: %s", err) return } defer conn.Close() - // note: must read the init message // although it's useless in this situation server.readInitMessage(conn) - server.mMux.RLock() - masterTTY, ok := server.masters[cInfo.ID] - server.mMux.RUnlock() + ctx := c.Request.Context() + containerID, ok := server.getContainerID(execID) if !ok { - log.Error("share terminal error, master not found") - conn.WriteMessage(websocket.CloseMessage, []byte("master container not found, exit")) + log.Error("share terminal error, exec not found") + conn.WriteMessage(websocket.CloseMessage, + []byte("exec container not found, exit")) return } - titleBuf, err := server.makeTitleBuff(cInfo) + cInfo := server.containerCli.GetInfo(ctx, containerID) + var titleExtra = "[READONLY]" + if server.options.Collaborate { + titleExtra = "[SLAVE]" + } + titleBuf, err := server.makeTitleBuff(cInfo, titleExtra) if err != nil { e := fmt.Sprintf("failed to fill window title template: %s", err) conn.WriteMessage(websocket.CloseMessage, []byte(e)) @@ -42,15 +43,18 @@ func (server *Server) handleShare(c *gin.Context) { return } - fork := masterTTY.Fork(ctx, server.options.Collaborate) - defer fork.Close() + master := masterTTY.Fork(ctx, true) + defer master.Close() + + ttyOptions := []webtty.Option{webtty.WithWindowTitle(titleBuf)} + if server.options.Collaborate { + ttyOptions = append(ttyOptions, webtty.WithPermitWrite()) + } tty, err := webtty.New( &wsWrapper{conn}, - newSlave(fork, true), - []webtty.Option{ - webtty.WithWindowTitle(titleBuf), - webtty.WithPermitWrite()}..., + newSlave(master), + ttyOptions..., ) if err != nil { e := fmt.Sprintf("failed to create webtty: %s", err) @@ -59,7 +63,8 @@ func (server *Server) handleShare(c *gin.Context) { return } - if err := tty.Run(ctx); err != nil && err != webtty.ErrMasterClosed { + err = tty.Run(ctx) + if err != nil && err != webtty.ErrMasterClosed { e := fmt.Sprintf("failed to run webtty: %s", err) log.Error(e) } diff --git a/route/slave_wrapper.go b/route/slave_wrapper.go index e9a8430..0ceae8d 100644 --- a/route/slave_wrapper.go +++ b/route/slave_wrapper.go @@ -30,50 +30,6 @@ func (sw *slaveWrapper) Read(p []byte) (n int, err error) { return sw.master.Read(p) } -func newSlave(master io.ReadWriteCloser, share bool) webtty.Slave { - // pr, pw := io.Pipe() - // go func() { - // defer pr.Close() - // defer pw.Close() - // defer master.Close() - - // bs := make([]byte, 2048) - // for { - // n, err := master.Read(bs) - // if err != nil { - // return - // } - // if share { - // pw.Write(bs[:n]) - // continue - // } - // panic(1) - // if n <= 1 { - // continue - // } - - // if n >= 2 && bs[n-2] == 13 { // \r\n - // pw.Write(bs[:n]) - // continue - // } - - // // only \n or a long log string contains \n - // s, e := 0, 0 - // for e < n { - // x := bytes.IndexByte(bs[e:n], 10) - // if x == -1 { - // break - // } - // s = e - // e += x - // pw.Write(bs[s:e]) - // pw.Write([]byte{13, 10}) - // e++ // skip this \n - // } - // } - // }() - - return &slaveWrapper{ - master: master, - } +func newSlave(master io.ReadWriteCloser) webtty.Slave { + return &slaveWrapper{master: master} } diff --git a/route/ws.go b/route/ws.go new file mode 100644 index 0000000..a5dd50f --- /dev/null +++ b/route/ws.go @@ -0,0 +1,51 @@ +package route + +import ( + "bytes" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +func (server *Server) handleWSIndex(c *gin.Context) { + var ( + containerID = c.Param("cid") + execID = c.Param("eid") + _foundExecID bool + ) + if containerID == "" { + containerID, _foundExecID = server.getContainerID(execID) + if !_foundExecID { + c.String(http.StatusBadRequest, fmt.Sprintf("exec id %s not found", execID)) + return + } + } + cInfo := server.containerCli.GetInfo(c.Request.Context(), containerID) + titleVars := server.titleVariables( + []string{"server"}, + map[string]map[string]interface{}{ + "server": map[string]interface{}{ + "containerName": cInfo.Name, + }, + }, + ) + + titleBuf := new(bytes.Buffer) + err := titleTemplate.Execute(titleBuf, titleVars) + if err != nil { + c.Error(err) + } + + indexVars := map[string]interface{}{ + "title": titleBuf.String(), + } + + indexBuf := new(bytes.Buffer) + err = indexTemplate.Execute(indexBuf, indexVars) + if err != nil { + c.Error(err) + } + + c.Writer.Write(indexBuf.Bytes()) +} diff --git a/types/tty.go b/types/tty.go index 867ba58..a337efe 100644 --- a/types/tty.go +++ b/types/tty.go @@ -23,9 +23,17 @@ type SlaveTTY struct { tty TTY readOnly bool + + masterOutputs []byte } func (s *SlaveTTY) Read(p []byte) (int, error) { + if len(s.masterOutputs) != 0 { + copy(p[:len(s.masterOutputs)], s.masterOutputs) + s.masterOutputs = nil + return len(p), nil + } + bs := <-s.ps.Read() // logrus.Debugf("slave tty read: %s", bs) copy(p[:len(bs)], bs) @@ -47,18 +55,18 @@ func (s *SlaveTTY) Close() error { type MasterTTY struct { TTY - id string - pubC pubsub.PubChan + id string + pubC pubsub.PubChan + outputs []byte // previous outputs } func (m *MasterTTY) Read(p []byte) (n int, err error) { n, err = m.TTY.Read(p) // read from tty // logrus.Debugf("read from container: %s", p[:n]) - // publish to all - if err := m.pubC.Write(p[:n]); err != nil { - panic(err) - } + // publish to all, ignore the error + m.pubC.Write(p[:n]) + return } @@ -78,23 +86,40 @@ func (m *MasterTTY) Fork(ctx context.Context, collaborate bool) *SlaveTTY { if err != nil { panic(err) // shouldn't happen } + outputs := make([]byte, len(m.outputs)) + copy(outputs, m.outputs) return &SlaveTTY{ tty: m.TTY, ps: pubsub, // options readOnly: !collaborate, + // previous outputs from master + masterOutputs: outputs, } } -func NewMasterTTY(ctx context.Context, t TTY, shareID string) (*MasterTTY, error) { - pubChan, err := globalPubSuber.Pub(ctx, shareID) +func NewMasterTTY(ctx context.Context, t TTY, execID string) (*MasterTTY, error) { + pubsub, err := globalPubSuber.PubSub(ctx, execID) if err != nil { return nil, err } - return &MasterTTY{ - TTY: t, - id: shareID, - pubC: pubChan, - }, nil + master := &MasterTTY{ + TTY: t, + id: execID, + pubC: pubsub, + outputs: make([]byte, 1e3), + } + + go func() { + for output := range pubsub.Read() { + master.outputs = append(master.outputs, output...) + // master.outputs = append(master.outputs, '\n') + if len(master.outputs) > 1e3 { + master.outputs = master.outputs[len(master.outputs)-1e3:] + } + } + }() + + return master, nil } diff --git a/util/id.go b/util/id.go new file mode 100644 index 0000000..b4e72c3 --- /dev/null +++ b/util/id.go @@ -0,0 +1,28 @@ +package util + +import ( + "crypto/md5" + "encoding/base64" + "math/rand" + "time" +) + +const _min = 15 + +// ID returns a base64 encoded id +func ID(salt string) string { + if len(salt) < _min { + salt = string(md5.New().Sum([]byte(salt))) + } + + b64 := make([]byte, base64.StdEncoding.EncodedLen(_min)) + base64.StdEncoding.Encode(b64, []byte(salt[:_min])) + rand.New(rand.NewSource(time.Now().UnixNano())). + Shuffle(len(b64), func(i, j int) { + x := b64[i] + b64[i] = b64[j] + b64[j] = x + }) + + return string(b64) +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..7c0497c --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,11 @@ +package util + +import ( + "fmt" + "testing" +) + +func TestID(t *testing.T) { + fmt.Println(ID("hello-world-1234-qwer")) + fmt.Println(ID("11")) +}