├── .gitignore ├── .meteor ├── .gitignore ├── packages └── release ├── LICENSE ├── README.md ├── client ├── index.html ├── scripts │ ├── app.js │ ├── chatCtrl.js │ ├── extern │ │ ├── FileSaver.js │ │ ├── angular-scripts │ │ │ ├── angular-route.js │ │ │ ├── angular-spinner.js │ │ │ ├── angular │ │ │ │ └── angular.js │ │ │ ├── spin.js │ │ │ └── ui-bootstrap-tpls-0.7.0.js │ │ ├── hterm │ │ │ └── hterm.js │ │ ├── indexed.js │ │ ├── ion.rangeSlider.js │ │ ├── paramikojs.js │ │ ├── peer.js │ │ ├── startup.js │ │ └── tab.js │ ├── indexCtrl.js │ ├── localrecordCtrl.js │ ├── masterCtrl.js │ ├── masterTerminal.js │ ├── pbCtrl.js │ ├── playTerminal.js │ ├── playbackCtrl.js │ ├── recordbtnCtrl.js │ ├── remoterecordCtrl.js │ ├── resetPasswordCtrl.js │ ├── scriptsCtrl.js │ ├── services │ │ ├── MasterConnection.js │ │ ├── NuttyConnection.js │ │ ├── NuttySession.js │ │ ├── Player.js │ │ ├── Recorder.js │ │ ├── SlaveConnection.js │ │ ├── cannedscripts.js │ │ ├── compatibility.js │ │ ├── lib │ │ │ └── util.js │ │ ├── loginCtrl.js │ │ ├── nuttyalertCtrl.js │ │ ├── scriptsPaste.js │ │ ├── ssh.js │ │ └── sshext.js │ ├── sharedsessionsCtrl.js │ ├── signinCtrl.js │ ├── slaveCtrl.js │ ├── slaveTerminal.js │ └── tmuxbtnCtrl.js └── styles │ ├── extern │ ├── bootstrap │ │ ├── bootstrap-glyphicons.css │ │ └── bootstrap.css │ ├── ion.rangeSlider.css │ ├── ion.rangeSlider.skinFlat.css │ └── normalize.min.css │ └── main.css ├── packages ├── meteor-broadcast │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── client │ │ └── client.js │ ├── lib │ │ ├── eventemitter.js │ │ └── util.js │ ├── package.js │ ├── server │ │ └── server.js │ └── smart.json └── meteor-pipe │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── client │ └── client.js │ ├── lib │ ├── eventemitter.js │ └── util.js │ ├── package.js │ ├── server │ └── server.js │ └── smart.json ├── private ├── public ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── forgooglebot.html ├── github-btn.html ├── images │ ├── extensionicon.png │ ├── forkme.png │ ├── nutty.png │ ├── sprite-skin-flat.png │ └── twitter.png ├── templates │ ├── chat.html │ ├── connectmodal.html │ ├── demo.html │ ├── masterTerminal.html │ ├── playTerminal.html │ ├── recordButtons.html │ ├── scriptsPaste.html │ ├── signin.html │ ├── slaveTerminal.html │ ├── tmuxButtons.html │ └── username.html └── views │ ├── 404.html │ ├── BigInteger.js │ ├── common.js │ ├── faq.html │ ├── index.html │ ├── kryptos │ ├── PublicKey │ │ └── RSA.js │ └── kryptos.js │ ├── login.html │ ├── master.html │ ├── pb.html │ ├── playback.html │ ├── python_shim.js │ ├── remoterecord.html │ ├── resetPassword.html │ ├── scripts.html │ ├── sharedsessions.html │ ├── sign_ssh_data_worker.js │ ├── slave.html │ └── util.js ├── recording.go ├── server └── server.js ├── smart.json └── smart.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .meteor -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | meteor-pipe 8 | meteor-broadcast 9 | accounts-google 10 | accounts-ui 11 | service-configuration 12 | accounts-base 13 | accounts-password 14 | email 15 | http 16 | jparker:crypto-sha1 17 | jparker:crypto-hmac 18 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.3 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nutty [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/krishnasrinivas/nuttyapp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | https://nutty.io 4 | 5 | ## Advanced use cases 6 | 7 | ### Docker image 8 | 9 | Details here: https://registry.hub.docker.com/u/krishnasrinivas/nuttyapp/ 10 | (docker specific code on docker branch) 11 | 12 | ### Server install 13 | 14 | Nutty server depends on MongoDB please install before proceeding https://www.mongodb.org/downloads 15 | 16 | ``` 17 | $ curl https://install.meteor.com/ | sh 18 | $ git clone https://github.com/krishnasrinivas/nuttyapp.git 19 | $ cd nuttyapp 20 | $ meteor install 21 | Configure authinfo.json (optional, details given below) 22 | $ meteor bundle ../bundle.tgz 23 | $ cd .. 24 | $ tar xzvf bundle.tgz 25 | $ export MONGO_URL=mongodb://localhost/nuttyapp 26 | $ export PORT=80 27 | (or you can run it behind nginx) 28 | $ export ROOT_URL='http://yourserver.com' 29 | $ export MAIL_URL="smtp://user:passwd@smtp.mailgun.org:587" 30 | (get a free account on mailgun) 31 | $ sudo node bundle/main.js 32 | optional (needed for webrtc): $ ./peerjs --port 9000 33 | $ go run recording.go -basedir ./recordings 34 | ``` 35 | 36 | authinfo.json should be put in "nuttyapp/private" directory with the format: 37 | 38 | { 39 | "google": { 40 | "clientId": "googleoauth-clientid-optional", 41 | "secret": "googleoauth-secret-optional" 42 | }, 43 | "webrtc": { 44 | "key": "key from peerjs.com - optional - if you need webrtc" 45 | } 46 | } 47 | 48 | google.clientId and google.secret can be configured if you need google auth signin. 49 | webrtc should be configured if you need WebRTC. For webrtc config details check http://peerjs.com/. 50 | nutty.io's webrtc config looks like this: 51 | 52 | "webrtc": { 53 | "host": "nutty.io", 54 | "port": 9000, 55 | "iceServers": [{ 56 | "url": "stun:stun.l.google.com:19302" 57 | }] 58 | } 59 | 60 | 61 | 62 | LICENSE 63 | ------- 64 | Nutty is released under [Apache License v2](./LICENSE) 65 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | Nutty 3 | 4 | 5 | 6 | 7 | 49 | 50 | 64 | 65 | 66 | 67 |
68 | 69 | -------------------------------------------------------------------------------- /client/scripts/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.element(document).ready(function() { 18 | angular.bootstrap(document, ['nuttyapp']); 19 | }); 20 | -------------------------------------------------------------------------------- /client/scripts/chatCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .directive('nuttyChat', function() { 19 | return { 20 | templateUrl: "templates/chat.html", 21 | scope: true, 22 | restrict: 'E', 23 | replace: true, 24 | link: function(scope, element, attrs, termController) {}, 25 | controller: ['$scope', 'NuttySession', 26 | function($scope, NuttySession) { 27 | var chat; 28 | var sessionid; 29 | var tmpusername; 30 | var user; 31 | var username; 32 | window.chatmsgs = $scope.chatmsgs = []; 33 | 34 | tmpusername = Session.get("tmpusername"); 35 | if (!tmpusername) { 36 | tmpusername = "guest." + Random.hexString(3); 37 | Session.set("tmpusername", tmpusername); 38 | } 39 | $scope.users = NuttySession.users; 40 | $scope.$watch(function() { 41 | return NuttySession.sessionid; 42 | }, function(newval, oldval) { 43 | if (!newval) 44 | return; 45 | if (newval === sessionid) 46 | return; 47 | sessionid = newval; 48 | window.chat = chat = new Meteor.Broadcast(NuttySession.sessionid); 49 | chat.on('data', function(data) { 50 | if (!data) 51 | return; 52 | for (var i = 0; i < $scope.chatmsgs.length; i++) { 53 | if ($scope.chatmsgs[i].msgid === data.msgid) 54 | return; 55 | } 56 | $scope.chatmsgs.push(data); 57 | $scope.$apply(); 58 | setTimeout(function() { 59 | document.getElementById("chat-div").scrollTop = document.getElementById("chat-div").scrollHeight; 60 | }, 0); 61 | }); 62 | }); 63 | $scope.$watch(function() { 64 | return Meteor.userId(); 65 | }, function(newval, oldval) { 66 | if (newval) 67 | $scope.chatwarn = ""; 68 | else 69 | $scope.chatwarn = "Please signin to chat"; 70 | }); 71 | $scope.$watch(function() { 72 | return $scope.notloggedincnt(); 73 | }, function(newval, oldval) { 74 | $scope.guestcntmsg = "Guests : " + $scope.notloggedincnt(); 75 | }); 76 | Deps.autorun(function() { 77 | user = Meteor.user(); 78 | if (user) { 79 | username = user.username; 80 | } else { 81 | username = tmpusername; 82 | } 83 | }); 84 | $scope.notloggedincnt = function() { 85 | var count = 0; 86 | _.each($scope.users, function(element) { 87 | if (!element.username) { 88 | count++; 89 | } 90 | }); 91 | return count; 92 | } 93 | $scope.loggedincnt = function() { 94 | var count = 0; 95 | _.each($scope.users, function(element) { 96 | if (!element.username) 97 | count++; 98 | }); 99 | return count; 100 | } 101 | $scope.chatsubmit = function() { 102 | if (!$scope.msg) 103 | return; 104 | var msgid = Random.id(); 105 | $scope.chatmsgs.push({ 106 | username: username, 107 | msg: $scope.msg, 108 | msgid: msgid 109 | }); 110 | if (chat) { 111 | chat.send({ 112 | username: username, 113 | msg: $scope.msg, 114 | msgid: msgid 115 | }); 116 | } 117 | $scope.msg = ""; 118 | setTimeout(function() { 119 | document.getElementById("chat-div").scrollTop = document.getElementById("chat-div").scrollHeight; 120 | }, 0); 121 | } 122 | } 123 | ] 124 | } 125 | }); 126 | -------------------------------------------------------------------------------- /client/scripts/extern/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 2014-07-25 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: X11/MIT 7 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 12 | 13 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 14 | 15 | window.saveAs = window.saveAs 16 | // IE 10+ (native saveAs) 17 | || (typeof navigator !== "undefined" && 18 | navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) 19 | // Everyone else 20 | || (function(view) { 21 | "use strict"; 22 | // IE <10 is explicitly unsupported 23 | if (typeof navigator !== "undefined" && 24 | /MSIE [1-9]\./.test(navigator.userAgent)) { 25 | return; 26 | } 27 | var 28 | doc = view.document 29 | // only get URL when necessary in case Blob.js hasn't overridden it yet 30 | , get_URL = function() { 31 | return view.URL || view.webkitURL || view; 32 | } 33 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 34 | , can_use_save_link = !view.externalHost && "download" in save_link 35 | , click = function(node) { 36 | var event = doc.createEvent("MouseEvents"); 37 | event.initMouseEvent( 38 | "click", true, false, view, 0, 0, 0, 0, 0 39 | , false, false, false, false, 0, null 40 | ); 41 | node.dispatchEvent(event); 42 | } 43 | , webkit_req_fs = view.webkitRequestFileSystem 44 | , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem 45 | , throw_outside = function(ex) { 46 | (view.setImmediate || view.setTimeout)(function() { 47 | throw ex; 48 | }, 0); 49 | } 50 | , force_saveable_type = "application/octet-stream" 51 | , fs_min_size = 0 52 | // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 for 53 | // the reasoning behind the timeout and revocation flow 54 | , arbitrary_revoke_timeout = 10 55 | , revoke = function(file) { 56 | var revoker = function() { 57 | if (typeof file === "string") { // file is an object URL 58 | get_URL().revokeObjectURL(file); 59 | } else { // file is a File 60 | file.remove(); 61 | } 62 | }; 63 | if (view.chrome) { 64 | revoker(); 65 | } else { 66 | setTimeout(revoker, arbitrary_revoke_timeout); 67 | } 68 | } 69 | , dispatch = function(filesaver, event_types, event) { 70 | event_types = [].concat(event_types); 71 | var i = event_types.length; 72 | while (i--) { 73 | var listener = filesaver["on" + event_types[i]]; 74 | if (typeof listener === "function") { 75 | try { 76 | listener.call(filesaver, event || filesaver); 77 | } catch (ex) { 78 | throw_outside(ex); 79 | } 80 | } 81 | } 82 | } 83 | , FileSaver = function(blob, name) { 84 | // First try a.download, then web filesystem, then object URLs 85 | var 86 | filesaver = this 87 | , type = blob.type 88 | , blob_changed = false 89 | , object_url 90 | , target_view 91 | , dispatch_all = function() { 92 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 93 | } 94 | // on any filesys errors revert to saving with object URLs 95 | , fs_error = function() { 96 | // don't create more object URLs than needed 97 | if (blob_changed || !object_url) { 98 | object_url = get_URL().createObjectURL(blob); 99 | } 100 | if (target_view) { 101 | target_view.location.href = object_url; 102 | } else { 103 | var new_tab = view.open(object_url, "_blank"); 104 | if (new_tab == undefined && typeof safari !== "undefined") { 105 | //Apple do not allow window.open, see http://bit.ly/1kZffRI 106 | view.location.href = object_url 107 | } 108 | } 109 | filesaver.readyState = filesaver.DONE; 110 | dispatch_all(); 111 | revoke(object_url); 112 | } 113 | , abortable = function(func) { 114 | return function() { 115 | if (filesaver.readyState !== filesaver.DONE) { 116 | return func.apply(this, arguments); 117 | } 118 | }; 119 | } 120 | , create_if_not_found = {create: true, exclusive: false} 121 | , slice 122 | ; 123 | filesaver.readyState = filesaver.INIT; 124 | if (!name) { 125 | name = "download"; 126 | } 127 | if (can_use_save_link) { 128 | object_url = get_URL().createObjectURL(blob); 129 | save_link.href = object_url; 130 | save_link.download = name; 131 | click(save_link); 132 | filesaver.readyState = filesaver.DONE; 133 | dispatch_all(); 134 | revoke(object_url); 135 | return; 136 | } 137 | // Object and web filesystem URLs have a problem saving in Google Chrome when 138 | // viewed in a tab, so I force save with application/octet-stream 139 | // http://code.google.com/p/chromium/issues/detail?id=91158 140 | // Update: Google errantly closed 91158, I submitted it again: 141 | // https://code.google.com/p/chromium/issues/detail?id=389642 142 | if (view.chrome && type && type !== force_saveable_type) { 143 | slice = blob.slice || blob.webkitSlice; 144 | blob = slice.call(blob, 0, blob.size, force_saveable_type); 145 | blob_changed = true; 146 | } 147 | // Since I can't be sure that the guessed media type will trigger a download 148 | // in WebKit, I append .download to the filename. 149 | // https://bugs.webkit.org/show_bug.cgi?id=65440 150 | if (webkit_req_fs && name !== "download") { 151 | name += ".download"; 152 | } 153 | if (type === force_saveable_type || webkit_req_fs) { 154 | target_view = view; 155 | } 156 | if (!req_fs) { 157 | fs_error(); 158 | return; 159 | } 160 | fs_min_size += blob.size; 161 | req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { 162 | fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { 163 | var save = function() { 164 | dir.getFile(name, create_if_not_found, abortable(function(file) { 165 | file.createWriter(abortable(function(writer) { 166 | writer.onwriteend = function(event) { 167 | target_view.location.href = file.toURL(); 168 | filesaver.readyState = filesaver.DONE; 169 | dispatch(filesaver, "writeend", event); 170 | revoke(file); 171 | }; 172 | writer.onerror = function() { 173 | var error = writer.error; 174 | if (error.code !== error.ABORT_ERR) { 175 | fs_error(); 176 | } 177 | }; 178 | "writestart progress write abort".split(" ").forEach(function(event) { 179 | writer["on" + event] = filesaver["on" + event]; 180 | }); 181 | writer.write(blob); 182 | filesaver.abort = function() { 183 | writer.abort(); 184 | filesaver.readyState = filesaver.DONE; 185 | }; 186 | filesaver.readyState = filesaver.WRITING; 187 | }), fs_error); 188 | }), fs_error); 189 | }; 190 | dir.getFile(name, {create: false}, abortable(function(file) { 191 | // delete file if it already exists 192 | file.remove(); 193 | save(); 194 | }), abortable(function(ex) { 195 | if (ex.code === ex.NOT_FOUND_ERR) { 196 | save(); 197 | } else { 198 | fs_error(); 199 | } 200 | })); 201 | }), fs_error); 202 | }), fs_error); 203 | } 204 | , FS_proto = FileSaver.prototype 205 | , saveAs = function(blob, name) { 206 | return new FileSaver(blob, name); 207 | } 208 | ; 209 | FS_proto.abort = function() { 210 | var filesaver = this; 211 | filesaver.readyState = filesaver.DONE; 212 | dispatch(filesaver, "abort"); 213 | }; 214 | FS_proto.readyState = FS_proto.INIT = 0; 215 | FS_proto.WRITING = 1; 216 | FS_proto.DONE = 2; 217 | 218 | FS_proto.error = 219 | FS_proto.onwritestart = 220 | FS_proto.onprogress = 221 | FS_proto.onwrite = 222 | FS_proto.onabort = 223 | FS_proto.onerror = 224 | FS_proto.onwriteend = 225 | null; 226 | 227 | return saveAs; 228 | }( 229 | typeof self !== "undefined" && self 230 | || typeof window !== "undefined" && window 231 | || this.content 232 | )); 233 | // `self` is undefined in Firefox for Android content script context 234 | // while `this` is nsIContentFrameMessageManager 235 | // with an attribute `content` that corresponds to the window 236 | 237 | if (typeof module !== "undefined" && module !== null) { 238 | module.exports = saveAs; 239 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { 240 | define([], function() { 241 | return saveAs; 242 | }); 243 | } -------------------------------------------------------------------------------- /client/scripts/extern/angular-scripts/angular-spinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* angular-spinner version 0.2.1 18 | * License: MIT. 19 | * Copyright (C) 2013, Uri Shaked. 20 | */ 21 | 22 | 'use strict'; 23 | 24 | angular.module('angularSpinner', []) 25 | .directive('usSpinner', ['$window', function ($window) { 26 | return { 27 | scope: true, 28 | link: function (scope, element, attr) { 29 | scope.spinner = null; 30 | 31 | function stopSpinner() { 32 | if (scope.spinner) { 33 | scope.spinner.stop(); 34 | scope.spinner = null; 35 | } 36 | } 37 | 38 | scope.$watch(attr.usSpinner, function (options) { 39 | stopSpinner(); 40 | scope.spinner = new $window.Spinner(options); 41 | scope.spinner.spin(element[0]); 42 | }, true); 43 | 44 | scope.$on('$destroy', function () { 45 | stopSpinner(); 46 | }); 47 | } 48 | }; 49 | }]); 50 | -------------------------------------------------------------------------------- /client/scripts/extern/startup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | nuttyapp = angular.module("nuttyapp", ["ui.bootstrap", "ngRoute", "angularSpinner"]); 18 | 19 | nuttyapp.config(['$interpolateProvider', '$routeProvider', '$locationProvider', 20 | function($interpolateProvider, $routeProvider, $locationProvider) { 21 | $interpolateProvider.startSymbol('[['); 22 | $interpolateProvider.endSymbol(']]'); 23 | $locationProvider.html5Mode(true); 24 | 25 | $routeProvider 26 | .when('/', { 27 | templateUrl: 'views/index.html', 28 | controller: 'indexCtrl' 29 | }) 30 | .when('/login', { 31 | templateUrl: 'views/login.html', 32 | controller: 'loginCtrl' 33 | }) 34 | .when('/verify-email/:token', { 35 | templateUrl: 'views/login.html', 36 | controller: 'loginCtrl' 37 | }) 38 | .when('/reset-password/:token', { 39 | templateUrl: 'views/resetPassword.html', 40 | controller: 'resetPasswordCtrl' 41 | }) 42 | .when('/sharedsessions', { 43 | templateUrl: 'views/sharedsessions.html', 44 | controller: 'sharedsessionsCtrl' 45 | }) 46 | .when('/faq', { 47 | templateUrl: 'views/faq.html', 48 | controller: [function() { 49 | ga('send', 'pageview', 'faq'); 50 | }] 51 | }) 52 | .when('/playback', { 53 | templateUrl: 'views/playback.html', 54 | controller: 'playbackCtrl' 55 | }) 56 | .when('/recording/:sessionid', { 57 | template: '' 58 | }) 59 | .when('/share', { 60 | templateUrl: 'views/master.html', 61 | controller: 'masterCtrl' 62 | }) 63 | .when('/scripts', { 64 | templateUrl: 'views/scripts.html', 65 | controller: 'scriptsCtrl' 66 | }) 67 | .when('/websocket/:sessionid', { 68 | templateUrl: 'views/slave.html', 69 | controller: 'slaveCtrl' 70 | }) 71 | .when('/webrtc/:sessionid', { 72 | templateUrl: 'views/slave.html', 73 | controller: 'slaveCtrl' 74 | }) 75 | .otherwise({ 76 | templateUrl: 'views/404.html' 77 | }); 78 | } 79 | ]); 80 | -------------------------------------------------------------------------------- /client/scripts/extern/tab.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * Bootstrap: tab.js v3.1.0 3 | * http://getbootstrap.com/javascript/#tabs 4 | * ======================================================================== 5 | * Copyright 2013 Twitter, Inc. 6 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 7 | * ======================================================================== */ 8 | 9 | 10 | +function ($) { 'use strict'; 11 | 12 | // TAB CLASS DEFINITION 13 | // ==================== 14 | 15 | var Tab = function (element) { 16 | this.element = $(element) 17 | } 18 | 19 | Tab.prototype.show = function () { 20 | var $this = this.element 21 | var $ul = $this.closest('ul:not(.dropdown-menu)') 22 | var selector = $this.data('target') 23 | 24 | if (!selector) { 25 | selector = $this.attr('href') 26 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 27 | } 28 | 29 | if ($this.parent('li').hasClass('active')) return 30 | 31 | var previous = $ul.find('.active:last a')[0] 32 | var e = $.Event('show.bs.tab', { 33 | relatedTarget: previous 34 | }) 35 | 36 | $this.trigger(e) 37 | 38 | if (e.isDefaultPrevented()) return 39 | 40 | var $target = $(selector) 41 | 42 | this.activate($this.parent('li'), $ul) 43 | this.activate($target, $target.parent(), function () { 44 | $this.trigger({ 45 | type: 'shown.bs.tab', 46 | relatedTarget: previous 47 | }) 48 | }) 49 | } 50 | 51 | Tab.prototype.activate = function (element, container, callback) { 52 | var $active = container.find('> .active') 53 | var transition = callback 54 | && $.support.transition 55 | && $active.hasClass('fade') 56 | 57 | function next() { 58 | $active 59 | .removeClass('active') 60 | .find('> .dropdown-menu > .active') 61 | .removeClass('active') 62 | 63 | element.addClass('active') 64 | 65 | if (transition) { 66 | element[0].offsetWidth // reflow for transition 67 | element.addClass('in') 68 | } else { 69 | element.removeClass('fade') 70 | } 71 | 72 | if (element.parent('.dropdown-menu')) { 73 | element.closest('li.dropdown').addClass('active') 74 | } 75 | 76 | callback && callback() 77 | } 78 | 79 | transition ? 80 | $active 81 | .one($.support.transition.end, next) 82 | .emulateTransitionEnd(150) : 83 | next() 84 | 85 | $active.removeClass('in') 86 | } 87 | 88 | 89 | // TAB PLUGIN DEFINITION 90 | // ===================== 91 | 92 | var old = $.fn.tab 93 | 94 | $.fn.tab = function ( option ) { 95 | return this.each(function () { 96 | var $this = $(this) 97 | var data = $this.data('bs.tab') 98 | 99 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 100 | if (typeof option == 'string') data[option]() 101 | }) 102 | } 103 | 104 | $.fn.tab.Constructor = Tab 105 | 106 | 107 | // TAB NO CONFLICT 108 | // =============== 109 | 110 | $.fn.tab.noConflict = function () { 111 | $.fn.tab = old 112 | return this 113 | } 114 | 115 | 116 | // TAB DATA-API 117 | // ============ 118 | 119 | $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { 120 | e.preventDefault() 121 | $(this).tab('show') 122 | }) 123 | 124 | }(jQuery); 125 | -------------------------------------------------------------------------------- /client/scripts/localrecordCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('localrecordCtrl', ['$scope', '$location', '$routeParams', 19 | function($scope, $location, $routeParams) { 20 | if ($routeParams.filename) { 21 | Session.set("filename", $routeParams.filename); 22 | $location.path('/localplay').replace(); 23 | } 24 | } 25 | ]); 26 | -------------------------------------------------------------------------------- /client/scripts/masterCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('masterCtrl', ['$scope', '$modal', '$location','NuttySession', 'ssh', 'NuttyConnection', 'alertBox', 'MasterConnection', 19 | function($scope, $modal, $location, NuttySession, ssh, NuttyConnection, alertBox, MasterConnection) { 20 | var nuttyio = $location.host() === 'nutty.io' || $location.host() === 'www.nutty.io'; 21 | var port = $location.port(); 22 | var portstr = (port === 80 || port === 443) ? '' : ':' + port; 23 | 24 | if (nuttyio) 25 | NuttyConnection.write = ssh.write; 26 | else 27 | NuttyConnection.write = sshext.write; 28 | 29 | if (!localStorage['conntype']) 30 | localStorage['conntype'] = 'websocket'; 31 | 32 | if (!Session.get("autoreload")) { 33 | mixpanel.track("masterterminal"); 34 | ga('send', 'pageview', 'masterterminal'); 35 | Session.set("autoreload", 1); 36 | } 37 | $scope.$watch(function() { 38 | return NuttySession.desc 39 | }, function(newval) { 40 | $scope.desc = NuttySession.desc; 41 | }) 42 | $scope.currentuser = function() { 43 | var user = Meteor.user(); 44 | if (user) { 45 | return user.username; 46 | } else { 47 | return ""; 48 | } 49 | }; 50 | $scope.descsubmit = function() { 51 | NuttySession.setdesc($scope.desc); 52 | mixpanel.track("descsubmit"); 53 | setTimeout(termfocus, 0); 54 | } 55 | $scope.descblur = function() { 56 | $scope.desc = NuttySession.desc; 57 | } 58 | $scope.copysharelink = function() { 59 | mixpanel.track("copysharelink", { 60 | clickedon: "input" 61 | }); 62 | var elem = document.getElementById("sharelinkbox") 63 | elem.focus(); 64 | elem.select(); 65 | } 66 | $scope.$on('$locationChangeStart', function(event, next, current) { 67 | window.location.assign(next); 68 | }); 69 | Deps.autorun(function() { 70 | Meteor.userId(); 71 | setTimeout(function() { 72 | $scope.$apply(); 73 | }, 0); 74 | }); 75 | $scope.MasterConnection = MasterConnection; 76 | MasterConnection.type = localStorage['conntype']; 77 | $scope.$watch('MasterConnection.type', function(newval) { 78 | localStorage['conntype'] = newval; 79 | $scope.sharelink = $location.protocol() + '://' + $location.host() + portstr + '/' + localStorage['conntype'] + '/' + NuttySession.sessionid; 80 | NuttySession.setconntype(newval); 81 | }); 82 | $scope.$watch(function() { 83 | return NuttySession.sessionid; 84 | }, function(newval, oldval) { 85 | if (newval) { 86 | setTimeout(function() { 87 | alertBox.alert("success", "Recording : " + $location.protocol() + '://' + $location.host() + portstr + '/recording/' + NuttySession.sessionid); 88 | $scope.$apply(); 89 | }, 35*1000); 90 | $scope.sharelink = $location.protocol() + '://' + $location.host() + portstr + '/' + localStorage['conntype'] + '/' + NuttySession.sessionid; 91 | } else 92 | $scope.sharelink = "waiting for server..."; 93 | }); 94 | $scope.$watch(function() { 95 | return NuttySession.masterid; 96 | }, function(newval, oldval) { 97 | if (newval) { 98 | NuttySession.setconntype(MasterConnection.type); 99 | } 100 | }); 101 | } 102 | ]); 103 | -------------------------------------------------------------------------------- /client/scripts/pbCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('pbCtrl', ['$scope', '$routeParams', '$http', '$location', 19 | function($scope, $routeParams, $http, $location) { 20 | var ctrl = this; 21 | var play = true; 22 | var sessionid = $routeParams.sessionid; 23 | var rowcol = { 24 | row: 24, 25 | col: 80 26 | }; 27 | hterm.Keyboard.KeyMap.prototype.onZoom_ = function(e, keyDef) { 28 | return hterm.Keyboard.KeyActions.CANCEL; 29 | }; 30 | this.changerowcol = function() { 31 | var termElem; 32 | var outerdivElem; 33 | if (!rowcol.row) 34 | return; 35 | if (!$scope.term) 36 | return; 37 | $scope.term.setFontSize(15); 38 | $scope.term.setHeight(rowcol.row); 39 | $scope.term.setWidth(rowcol.col); 40 | 41 | termElem = $scope.terminalElem; 42 | outerdivElem = termElem.parent(); 43 | 44 | while (1) { 45 | var H = outerdivElem.height(); 46 | var W = outerdivElem.width(); 47 | var h = termElem.height(); 48 | var w = termElem.width(); 49 | if (w < W && h < H) 50 | break; 51 | var fontsize = $scope.term.getFontSize(); 52 | fontsize--; 53 | $scope.term.setFontSize(fontsize); 54 | $scope.term.setHeight(rowcol.row); 55 | $scope.term.setWidth(rowcol.col); 56 | } 57 | 58 | termElem.css({ 59 | left: (outerdivElem.width() - termElem.width()) / 2, 60 | top: (outerdivElem.height() - termElem.height()) / 2 61 | }); 62 | } 63 | $(window).resize(ctrl.changerowcol); 64 | 65 | function _f() { 66 | if ($scope.term && ($scope.term.screenSize.height !== rowcol.row || 67 | $scope.term.screenSize.width !== rowcol.col)) { 68 | ctrl.changerowcol(); 69 | } 70 | setTimeout(_f, 1000); 71 | } 72 | _f(); 73 | var tindex = 0; 74 | var tdelta = 0; 75 | function loop() { 76 | $http({method: 'GET', url: 'http://localhost:9090/recording/' + sessionid + '/' + tindex}). 77 | success(function(data, status, headers, config) { 78 | console.log(data); 79 | tindex++; 80 | setTimeout(loop, 30 * 1000); 81 | }). 82 | error(function() { 83 | console.log("GET error"); 84 | tindex++; 85 | loop(); 86 | }) 87 | } 88 | loop(); 89 | } 90 | ]); 91 | -------------------------------------------------------------------------------- /client/scripts/playbackCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('nuttyapp') 2 | .controller('playbackCtrl', ['$scope', 'NuttySession', '$modal', '$location', 'Player', 3 | function($scope, NuttySession, $modal, $location, Player) { 4 | $scope.recordings = NuttySession.recordings; 5 | ga('send', 'pageview', 'uploads'); 6 | if (Player.playback) { 7 | window.location.pathname = '/playback'; 8 | } 9 | $scope.currentuser = function() { 10 | var user = Meteor.user(); 11 | if (user) { 12 | return user.username; 13 | } else { 14 | return ""; 15 | } 16 | }; 17 | $scope.embed = function(idx) { 18 | var modalInstance = $modal.open({ 19 | template: '', 22 | controller: ['$scope', '$modalInstance', 23 | function($scope, $modalInstance) { 24 | setTimeout(function() { 25 | $('#embedid').focus(); 26 | $('#embedid').select(); 27 | }, 0); 28 | }]}); 29 | } 30 | $scope.datetime = function(idx) { 31 | if ($scope.recordings[idx].createdAt) 32 | return $scope.recordings[idx].createdAt.toString().replace(/ GMT.*/, ''); 33 | else 34 | return ""; 35 | } 36 | $scope.deleterecording = function(idx) { 37 | NuttySession.deleterecording($scope.recordings[idx]._id); 38 | } 39 | $scope.change = function() { 40 | Player.playback = $("#playbackrec")[0].files[0]; 41 | $location.path('/localplay'); 42 | } 43 | $scope.loadmore = function() { 44 | Session.set('itemsLimit', Session.get('itemsLimit') + 20); 45 | } 46 | $scope.loadmorevisible = function() { 47 | if (Session.get('itemsLimit') < 20) 48 | return true; 49 | else 50 | return false; 51 | } 52 | }]); 53 | -------------------------------------------------------------------------------- /client/scripts/remoterecordCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('remoterecordCtrl', ['$scope', 'Compatibility', 19 | function($scope, Compatibility) { 20 | if (Compatibility.browser.browser === "Chrome") 21 | $scope.remoterecordshow = true; 22 | $scope.Compatibility = Compatibility; 23 | } 24 | ]); 25 | -------------------------------------------------------------------------------- /client/scripts/resetPasswordCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('resetPasswordCtrl', ['$scope', '$location', '$routeParams', '$timeout', 19 | function($scope, $location, $routeParams, $timeout) { 20 | $scope.resetpwd = {}; 21 | $scope.resetpwd.close = function() { 22 | $scope.resetpwd.show = false; 23 | } 24 | $scope.resetpwd.reset = function() { 25 | if (!$scope.resetpwd.password) { 26 | $scope.resetpwd.msg = "Empty passwords not allowed"; 27 | $scope.resetpwd.show = true; 28 | return; 29 | } 30 | $scope.resetpwd.spinner = true; 31 | $scope.resetpwd.msg = ""; 32 | $scope.resetpwd.show = false; 33 | Accounts.resetPassword($routeParams.token, $scope.resetpwd.password, function(err) { 34 | $scope.resetpwd.spinner = false; 35 | if (err) { 36 | $scope.resetpwd.msg = err.reason; 37 | $scope.resetpwd.show = true; 38 | $scope.$apply(); 39 | } else { 40 | $location.path('/login'); 41 | } 42 | }); 43 | } 44 | }]); 45 | -------------------------------------------------------------------------------- /client/scripts/scriptsCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('nuttyapp') 2 | .controller('scriptsCtrl', ['$scope', '$modal', 'cannedscripts', 3 | function($scope, $modal, cannedscripts) { 4 | $scope.scripts = cannedscripts.scripts; 5 | $scope.change = function() { 6 | if ($('#script')[0].files[0]) 7 | $scope.desc = $('#script')[0].files[0].name; 8 | else 9 | $scope.desc = ""; 10 | } 11 | $scope.upload = function() { 12 | var reader = new FileReader(); 13 | if (!$('#script')[0].files[0]) { 14 | alert("Please select a script to upload"); 15 | return; 16 | } 17 | if (!$scope.desc) { 18 | alert("Please provide description for the script"); 19 | return; 20 | } 21 | reader.onload = function(e) { 22 | var filecontent = e.target.result; 23 | var userId = Meteor.userId(); 24 | if (!userId) { 25 | alert("User not loggedin"); 26 | return; 27 | } 28 | cannedscripts.insertscript({ 29 | createdAt: new Date, 30 | content: filecontent, 31 | userId: userId, 32 | description: $scope.desc 33 | }); 34 | } 35 | reader.readAsText($('#script')[0].files[0]); 36 | } 37 | $scope.removescript = function(id) { 38 | cannedscripts.removescript(id); 39 | } 40 | $scope.currentuser = function() { 41 | var user = Meteor.user(); 42 | if (user) { 43 | return user.username; 44 | } else { 45 | return ""; 46 | } 47 | }; 48 | } 49 | ]); 50 | -------------------------------------------------------------------------------- /client/scripts/services/MasterConnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('MasterConnection', ['$rootScope', 'NuttySession', '$location', 19 | function($rootScope, NuttySession, $location) { 20 | var websocketactive = false; 21 | var wsocketmaster; 22 | var wrtcmaster; 23 | var wrtcconns = []; 24 | var ondata; 25 | var retobj; 26 | $rootScope.$watch(function() { 27 | return NuttySession.sessionid; 28 | }, function(newval, oldval) { 29 | if (!newval) 30 | return; 31 | Meteor.call('getWebrtcConfig', $location.host(), function(err, webrtcconfig) { 32 | function processinput(data) { 33 | var msg = {}; 34 | if (!data) 35 | return; 36 | if (data.start) { 37 | websocketactive = true; 38 | return; 39 | } 40 | if (data.stop) { 41 | websocketactive = false; 42 | return; 43 | } 44 | if (NuttySession.readonly) { 45 | if (!data.gettermshot) 46 | return; 47 | } 48 | if (data.gettermshot) { 49 | msg.gettermshot = data.gettermshot; 50 | } else if (data.newtmuxsession) { 51 | msg.newtmuxsession = data.newtmuxsession; 52 | } else if (data.data) { 53 | msg.data = data.data; 54 | } else { 55 | return; 56 | } 57 | if (ondata) 58 | ondata(msg); 59 | } 60 | wsocketmaster = new Meteor.PipeClientMaster(NuttySession.sessionid); 61 | wsocketmaster.on('data', function(data) { 62 | if (retobj.type !== 'websocket') 63 | return; 64 | processinput(data); 65 | }); 66 | 67 | if (err) { 68 | console.log("Meteor.call(getWebrtcConfig) returned : " + (err.reason)); 69 | return; 70 | } 71 | 72 | if (!webrtcconfig.host) 73 | webrtcconfig.host = $location.host(); 74 | if (!webrtcconfig.port) 75 | webrtcconfig.port = 9000; 76 | wrtcmaster = new Peer(NuttySession.sessionid, webrtcconfig); 77 | wrtcmaster.on('open', function(connid) { 78 | console.log("Connected to PeerJS server: " + connid); 79 | }); 80 | wrtcmaster.on('error', function(error) { 81 | console.log("PeerJS server disconnected : " + error); 82 | }); 83 | wrtcmaster.on('connection', function(conn) { 84 | wrtcconns.push(conn); 85 | console.log("Got connection from peer"); 86 | conn.on('data', function(data) { 87 | if (retobj.type !== 'webrtc') 88 | return; 89 | processinput(data); 90 | }) 91 | function conndisconnect() { 92 | var idx = wrtcconns.indexOf(conn); 93 | wrtcconns.splice(idx, 1); 94 | } 95 | conn.on('error', conndisconnect); 96 | conn.on('close', conndisconnect); 97 | }); 98 | }); 99 | }); 100 | retobj = { 101 | type: '', 102 | pipe: { 103 | write: function(data) { 104 | var msg = {}; 105 | if (data.data) 106 | msg.data = data.data; 107 | else if (data.settermshot) { 108 | msg.settermshot = data.settermshot; 109 | } 110 | else if (data.setcursorposition) 111 | msg.setcursorposition = data.setcursorposition; 112 | else 113 | return; 114 | if (retobj.type === 'websocket') { 115 | if (!websocketactive) 116 | return; 117 | if (wsocketmaster) { 118 | wsocketmaster.send(msg); 119 | } 120 | } else if (retobj.type === 'webrtc') { 121 | // console.log(wrtcconns); 122 | for (var i = 0; i < wrtcconns.length; i++) { 123 | try { 124 | if (msg.settermshot) { 125 | wrtcconns[i].settermshot = true; 126 | } 127 | if (wrtcconns[i].settermshot) 128 | wrtcconns[i].send(msg); 129 | } catch (ex) { 130 | console.log ("unable to write to peer"); 131 | } 132 | } 133 | } else { 134 | console.log("MasterConnection.type is neither webrtc nor websocket"); 135 | } 136 | }, 137 | ondata: function(cbk) { 138 | ondata = cbk; 139 | } 140 | } 141 | } 142 | window.MasterConnection = retobj; 143 | return retobj; 144 | } 145 | ]); 146 | -------------------------------------------------------------------------------- /client/scripts/services/NuttyConnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('NuttyConnection', ['$rootScope', 'NuttySession', 19 | function($rootScope, NuttySession) { 20 | var retobj = {}; 21 | return retobj; 22 | } 23 | ]); 24 | -------------------------------------------------------------------------------- /client/scripts/services/Player.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('Player', ['$rootScope', 19 | function($rootScope) { 20 | var filestart = 0; 21 | var filereader; 22 | var time = true; 23 | var consoledata = ""; 24 | var delta; 25 | var file; 26 | var write; 27 | var changerowcol; 28 | var replayvar = false; 29 | var timeoutvar; 30 | var timercount = 0; 31 | var timer; 32 | 33 | function start(_file, _write, _changerowcol, _termshot, _curspos) { 34 | file = _file; 35 | write = _write; 36 | changerowcol = _changerowcol; 37 | filereader = new FileReader(); 38 | filereader.onload = function(e) { 39 | 40 | if (time) { 41 | var data = e.target.result; 42 | var length; 43 | var view16 = new Uint16Array(data); 44 | if (view16.length === 0) { 45 | retobj.playvar = false; 46 | clearTimeout(timer); 47 | $rootScope.$apply(); 48 | return; 49 | } 50 | delta = view16[0]; 51 | length = view16[1]; 52 | if (delta === 65535) { 53 | var view8 = new Uint8Array(data); 54 | changerowcol({ 55 | row: view8[2], 56 | col: view8[3] 57 | }); 58 | _play(); 59 | return; 60 | } else if (delta === 65534) { 61 | 62 | } else if (delta === 65533) { 63 | var view8 = new Uint8Array(data); 64 | _curspos({ 65 | row: view8[2], 66 | col: view8[3] 67 | }); 68 | _play(); 69 | return; 70 | } else { 71 | if (delta === 0) 72 | delta = 5; 73 | delta = delta * 10; 74 | } 75 | var blob = file.slice(filestart, filestart + length); 76 | filestart = filestart + length; 77 | time = false; 78 | // readAsBinaryString reads as UTF-8 string 79 | // readAsText reads as UTF-16 80 | // if (delta === 65534) 81 | // filereader.readAsText(blob); 82 | // else 83 | // filereader.readAsBinaryString(blob); 84 | filereader.readAsText(blob); 85 | } else { 86 | consoledata = e.target.result; 87 | if (delta === 65534) { 88 | _termshot(consoledata); 89 | consoledata = ""; 90 | _play(); 91 | } else 92 | timeoutvar = setTimeout(_play, delta); 93 | } 94 | } 95 | } 96 | var _play = function() { 97 | // term.io.writeUTF8(consoledata); 98 | if (retobj.pausevar) 99 | return; 100 | if (consoledata) 101 | write(consoledata); 102 | retobj.progress = Math.floor(filestart / file.size * 100); 103 | safeApply($rootScope); 104 | consoledata = ""; 105 | var blob = file.slice(filestart, filestart + 4); 106 | filestart = filestart + 4; 107 | time = true; 108 | filereader.readAsArrayBuffer(blob); 109 | } 110 | 111 | function play() { 112 | if (!file) 113 | return; 114 | retobj.pausevar = false; 115 | retobj.playvar = true; 116 | timerstart(); 117 | _play(); 118 | } 119 | 120 | function pause() { 121 | if (!file) 122 | return; 123 | retobj.pausevar = true; 124 | retobj.playvar = false; 125 | clearTimeout(timer); 126 | } 127 | 128 | function replay() { 129 | if (!file) 130 | return; 131 | if (timeoutvar) { 132 | clearTimeout(timeoutvar); 133 | timeoutvar = undefined; 134 | } 135 | clearTimeout(timer); 136 | timercount = 0; 137 | filestart = 0; 138 | play(); 139 | } 140 | 141 | function timerstart() { 142 | timer = setTimeout(function() { 143 | timercount++; 144 | var min, sec; 145 | min = Math.floor(timercount / 60); 146 | sec = Math.floor(timercount % 60); 147 | retobj.time = min.toString() + ":" + sec.toString(); 148 | $rootScope.$apply(); 149 | timerstart(); 150 | }, 1000); 151 | } 152 | var retobj = { 153 | start: start, 154 | play: play, 155 | pause: pause, 156 | replay: replay, 157 | progress: 0, 158 | time: "0:0", 159 | pausevar: false, 160 | playvar: false, 161 | playback: undefined 162 | } 163 | window.Player = retobj; 164 | return retobj; 165 | } 166 | ]); 167 | -------------------------------------------------------------------------------- /client/scripts/services/Recorder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | window.buffer = []; 18 | 19 | angular.module('nuttyapp') 20 | .factory('Recorder', ['$rootScope', 'NuttySession', '$http', 21 | function($rootScope, NuttySession, $http) { 22 | var tindex = 0; 23 | var time = 0; 24 | function loop () { 25 | var buffers = JSON.stringify(buffer); 26 | buffer = []; 27 | Meteor.call('recput',NuttySession.sessionid, tindex, buffers, function(err, data) { 28 | if (err) { 29 | console.log("error while recput"); 30 | } 31 | }); 32 | time = 0; 33 | tindex++; 34 | var termshot = term.document_.body.firstChild.firstChild.innerHTML; 35 | buffer.push({ 36 | rowcol: 1, 37 | row: term.screenSize.height, 38 | col: term.screenSize.width 39 | }); 40 | buffer.push({ 41 | settermshot: termshot, 42 | row: term.getCursorRow(), 43 | col: term.getCursorColumn(), 44 | delta: 0 45 | }); 46 | setTimeout(loop, 30 * 1000); 47 | } 48 | setTimeout(loop, 30 * 1000); 49 | function start(recordFileName, create, startcbk) { 50 | } 51 | 52 | function stop() { 53 | } 54 | 55 | function write(obj) { 56 | if (!time) { 57 | time = new Date(); 58 | obj.delta = 0; 59 | buffer.push(obj); 60 | } else { 61 | var time2 = new Date(); 62 | var delta = time2 - time; 63 | time = time2; 64 | obj.delta = delta; 65 | buffer.push(obj) 66 | } 67 | } 68 | 69 | window.onbeforeunload = function() { 70 | var buffers = JSON.stringify(buffer); 71 | buffer = []; 72 | Meteor.call('recput',NuttySession.sessionid, tindex, buffers, function(err, data) { 73 | if (err) { 74 | console.log("error while recput"); 75 | } 76 | }); 77 | return; 78 | } 79 | var retobj = { 80 | start: start, 81 | stop: stop, 82 | write: write 83 | } 84 | return retobj; 85 | } 86 | ]); 87 | -------------------------------------------------------------------------------- /client/scripts/services/SlaveConnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('SlaveConnection', ['$rootScope', 'NuttySession', '$location', 19 | function($rootScope, NuttySession, $location) { 20 | var wsocketslave; 21 | var wrtcslave; 22 | var wrtcconn; 23 | var ondata; 24 | var retobj; 25 | 26 | Meteor._reload.onMigrate("onMigrate", function() { 27 | console.log(arguments); 28 | return [false]; 29 | }); 30 | retobj = { 31 | type: '', 32 | connect: function () { 33 | function processinput(data) { 34 | var msg = {}; 35 | console.log(data); 36 | if (!data) 37 | return; 38 | if (data.data) 39 | msg.data = data.data; 40 | else if (data.settermshot) 41 | msg.settermshot = data.settermshot; 42 | else if (data.setcursorposition) 43 | msg.setcursorposition = data.setcursorposition; 44 | else 45 | return; 46 | if (ondata) 47 | ondata(msg); 48 | } 49 | if (retobj.type === 'websocket') { 50 | wsocketslave = new Meteor.PipeClientSlave(NuttySession.sessionid); 51 | wsocketslave.on('data', processinput); 52 | wsocketslave.on('ready', function() { 53 | setTimeout ( function() { 54 | wsocketslave.send({ 55 | gettermshot: true 56 | }); 57 | }, 1000); 58 | }); 59 | } else if (retobj.type === 'webrtc') { 60 | Meteor.call('getWebrtcConfig', $location.host(), function(err, webrtcconfig) { 61 | if (err) { 62 | console.log("Meteor.call(getWebrtcConfig) returned : " + (err.reason)); 63 | return; 64 | } 65 | if (!webrtcconfig.host) 66 | webrtcconfig.host = $location.host(); 67 | if (!webrtcconfig.port) 68 | webrtcconfig.port = 9000; 69 | wrtcslave = new Peer(webrtcconfig); 70 | wrtcslave.on('open', function(connid) { 71 | console.log("Connected to PeerJS server"); 72 | }); 73 | wrtcslave.on('error', function(error) { 74 | console.log("error from PeerJS server : " + error); 75 | }) 76 | wrtcconn = wrtcslave.connect(NuttySession.sessionid); 77 | wrtcconn.on('open', function() { 78 | console.log('connected to master'); 79 | setTimeout ( function() { 80 | wrtcconn.send({ 81 | gettermshot: true 82 | }); 83 | }, 1000); 84 | }); 85 | wrtcconn.on('error', function(error) { 86 | console.log('error on connection to master'); 87 | }); 88 | wrtcconn.on('close', function() { 89 | console.log('master closed connection'); 90 | }); 91 | wrtcconn.on('data', processinput); 92 | }); 93 | } 94 | }, 95 | pipe: { 96 | write: function(data) { 97 | var msg = {}; 98 | if (data.data) 99 | msg.data = data.data; 100 | else if (data.newtmuxsession) { 101 | msg.newtmuxsession = data.newtmuxsession; 102 | } else if (data.gettermshot) { 103 | msg.gettermshot = data.gettermshot; 104 | } else { 105 | return; 106 | } 107 | if (retobj.type === 'webrtc') { 108 | if (wrtcconn) 109 | wrtcconn.send(msg); 110 | } else if (retobj.type === 'websocket'){ 111 | if (wsocketslave) 112 | wsocketslave.send(msg); 113 | } else { 114 | console.log("SlaveConnection.type is neither websocket nor webrtc"); 115 | } 116 | }, 117 | ondata: function(cbk) { 118 | ondata = cbk; 119 | } 120 | } 121 | } 122 | window.SlaveConnection = retobj; 123 | return retobj; 124 | } 125 | ]); 126 | -------------------------------------------------------------------------------- /client/scripts/services/cannedscripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('cannedscripts', ['$rootScope', 'alertBox', 19 | function($rootScope, alertBox) { 20 | var ScriptsColl = new Meteor.Collection('cannedscripts'); 21 | Meteor.subscribe('ownedcannedscripts'); 22 | var scriptscursor = ScriptsColl.find({}, { 23 | sort: { 24 | createdAt: -1 25 | } 26 | }); 27 | var scripts = []; 28 | window.scripts = scripts; 29 | scriptscursor.observe({ 30 | addedAt: function(doc, atIndex, before) { 31 | if (before) { 32 | scripts.unshift(doc); 33 | } else { 34 | scripts[atIndex] = doc; 35 | } 36 | safeApply($rootScope); 37 | }, 38 | changedAt: function(newdoc, olddoc, atIndex) { 39 | scripts[atIndex] = newdoc; 40 | safeApply($rootScope); 41 | }, 42 | removedAt: function(doc, atIndex) { 43 | scripts.splice(atIndex, 1); 44 | safeApply($rootScope); 45 | }, 46 | movedTo: function(document, fromIndex, toIndex, before) { 47 | console.log("movedTo"); 48 | console.log(fromIndex); 49 | console.log(toIndex); 50 | console.log(before); 51 | } 52 | }); 53 | 54 | function insertscript(doc) { 55 | ScriptsColl.insert(doc); 56 | } 57 | 58 | function removescript(_id) { 59 | ScriptsColl.remove({ 60 | _id: _id 61 | }); 62 | } 63 | 64 | function getscriptcontent(_id, cbk) { 65 | Meteor.call('getscriptcontent', _id, cbk); 66 | } 67 | var retobj = { 68 | insertscript: insertscript, 69 | removescript: removescript, 70 | getscriptcontent: getscriptcontent, 71 | scripts: scripts 72 | } 73 | return retobj; 74 | } 75 | ]); 76 | -------------------------------------------------------------------------------- /client/scripts/services/compatibility.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('Compatibility', ['$rootScope', 19 | function($rootScope) { 20 | var BrowserDetect = { 21 | init: function() { 22 | this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; 23 | this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || "an unknown version"; 24 | this.OS = this.searchString(this.dataOS) || "an unknown OS"; 25 | }, 26 | searchString: function(data) { 27 | for (var i = 0; i < data.length; i++) { 28 | var dataString = data[i].string; 29 | var dataProp = data[i].prop; 30 | this.versionSearchString = data[i].versionSearch || data[i].identity; 31 | if (dataString) { 32 | if (dataString.indexOf(data[i].subString) != -1) 33 | return data[i].identity; 34 | } else if (dataProp) 35 | return data[i].identity; 36 | } 37 | }, 38 | searchVersion: function(dataString) { 39 | var index = dataString.indexOf(this.versionSearchString); 40 | if (index == -1) return; 41 | return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); 42 | }, 43 | dataBrowser: [{ 44 | string: navigator.userAgent, 45 | subString: "Chrome", 46 | identity: "Chrome" 47 | }, { 48 | string: navigator.userAgent, 49 | subString: "OmniWeb", 50 | versionSearch: "OmniWeb/", 51 | identity: "OmniWeb" 52 | }, { 53 | string: navigator.vendor, 54 | subString: "Apple", 55 | identity: "Safari", 56 | versionSearch: "Version" 57 | }, { 58 | prop: window.opera, 59 | identity: "Opera", 60 | versionSearch: "Version" 61 | }, { 62 | string: navigator.vendor, 63 | subString: "iCab", 64 | identity: "iCab" 65 | }, { 66 | string: navigator.vendor, 67 | subString: "KDE", 68 | identity: "Konqueror" 69 | }, { 70 | string: navigator.userAgent, 71 | subString: "Firefox", 72 | identity: "Firefox" 73 | }, { 74 | string: navigator.vendor, 75 | subString: "Camino", 76 | identity: "Camino" 77 | }, { // for newer Netscapes (6+) 78 | string: navigator.userAgent, 79 | subString: "Netscape", 80 | identity: "Netscape" 81 | }, { 82 | string: navigator.userAgent, 83 | subString: "MSIE", 84 | identity: "Explorer", 85 | versionSearch: "MSIE" 86 | }, { 87 | string: navigator.userAgent, 88 | subString: "Gecko", 89 | identity: "Mozilla", 90 | versionSearch: "rv" 91 | }, { // for older Netscapes (4-) 92 | string: navigator.userAgent, 93 | subString: "Mozilla", 94 | identity: "Netscape", 95 | versionSearch: "Mozilla" 96 | }], 97 | dataOS: [{ 98 | string: navigator.platform, 99 | subString: "Win", 100 | identity: "Windows" 101 | }, { 102 | string: navigator.platform, 103 | subString: "Mac", 104 | identity: "Mac" 105 | }, { 106 | string: navigator.userAgent, 107 | subString: "iPhone", 108 | identity: "iPhone/iPod" 109 | }, { 110 | string: navigator.platform, 111 | subString: "Linux", 112 | identity: "Linux" 113 | }, { 114 | string: navigator.platform, 115 | subString: "CrOS", 116 | identity: "ChromeOS" 117 | }] 118 | }; 119 | 120 | BrowserDetect.init(); 121 | 122 | var isMobile = { 123 | Android: function() { 124 | return navigator.userAgent.match(/Android/i); 125 | }, 126 | BlackBerry: function() { 127 | return navigator.userAgent.match(/BlackBerry/i); 128 | }, 129 | iOS: function() { 130 | return navigator.userAgent.match(/iPhone|iPad|iPod/i); 131 | }, 132 | Opera: function() { 133 | return navigator.userAgent.match(/Opera Mini/i); 134 | }, 135 | Windows: function() { 136 | return navigator.userAgent.match(/IEMobile/i); 137 | }, 138 | any: function() { 139 | return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows()); 140 | } 141 | }; 142 | 143 | if (BrowserDetect.browser === "Chrome") 144 | BrowserDetect.incompatible = 0; 145 | else 146 | BrowserDetect.incompatible = 1; 147 | 148 | var retobj = { 149 | browser: BrowserDetect, 150 | ismobile: isMobile.any() 151 | } 152 | window.Compatibility = retobj; 153 | return retobj; 154 | } 155 | ]); 156 | -------------------------------------------------------------------------------- /client/scripts/services/lib/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | safeApply = function(scope) { 18 | var phase = scope.$root.$$phase; 19 | if (phase == '$apply' || phase == '$digest') 20 | return; 21 | else 22 | scope.$apply(); 23 | } 24 | 25 | termfocus = function() { 26 | term.focus(); 27 | } 28 | -------------------------------------------------------------------------------- /client/scripts/services/loginCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('nuttyapp') 2 | .controller('loginCtrl', ['$scope', '$location', '$modal', '$routeParams', '$timeout', 3 | function($scope, $location, $modal, $routeParams, $timeout) { 4 | $scope.alert = {}; 5 | $scope.lalert = {}; 6 | $scope.verification = {}; 7 | $scope.resetpwd = {}; 8 | $scope.showsignupbox = false; 9 | $scope.showloginbox = true; 10 | $scope.showresetpwdbox = false; 11 | $scope.resetpwd.close = function() { 12 | $scope.resetpwd.msg = ""; 13 | $scope.resetpwd.show = false; 14 | } 15 | $scope.resetpwd.reset = function() { 16 | if (!$scope.resetpwd.email) { 17 | $scope.resetpwd.msg = "Please enter your email"; 18 | $scope.resetpwd.show = true; 19 | return; 20 | } 21 | $scope.resetpwd.spinner = true; 22 | $scope.resetpwd.show = false; 23 | Accounts.forgotPassword({ 24 | email: $scope.resetpwd.email 25 | }, function(err) { 26 | if (err) { 27 | $scope.resetpwd.msg = err.reason; 28 | $scope.resetpwd.show = true; 29 | } else { 30 | $scope.resetpwd.email = ""; 31 | $scope.resetpwd.msg = "Please check your email to reset password"; 32 | $scope.resetpwd.show = true; 33 | } 34 | $scope.resetpwd.spinner = false; 35 | $scope.$apply(); 36 | }); 37 | } 38 | $scope.verification.close = function() { 39 | $scope.verification.msg = ""; 40 | $scope.verification.show = false; 41 | } 42 | $scope.alert.close = function() { 43 | $scope.alert.msg = ""; 44 | $scope.alert.show = false; 45 | } 46 | $scope.lalert.close = function() { 47 | $scope.lalert.msg = ""; 48 | $scope.lalert.show = false; 49 | } 50 | $scope.loggedinas = function() { 51 | var user = Meteor.user(); 52 | if (user) { 53 | return user.username; 54 | } else 55 | return ""; 56 | } 57 | $scope.signuppassword = function() { 58 | if (!$scope.password) { 59 | $scope.alert.type = "danger"; 60 | $scope.alert.show = true; 61 | $scope.alert.msg = "Please provide valid password"; 62 | return; 63 | } 64 | Accounts.createUser({ 65 | username: $scope.username, 66 | password: $scope.password, 67 | email: $scope.email 68 | }, function(err) { 69 | $scope.password = ""; 70 | if (err) { 71 | $scope.alert.type = "danger"; 72 | $scope.alert.show = true; 73 | $scope.alert.msg = err.reason; 74 | } 75 | $scope.$apply(); 76 | }) 77 | } 78 | $scope.spinnershow = function() { 79 | return Meteor.loggingIn(); 80 | } 81 | $scope.login = function() { 82 | Meteor.loginWithPassword($scope.lusername, $scope.lpassword, function(err) { 83 | $scope.lpassword = ""; 84 | if (err) { 85 | $scope.lalert.type = "danger"; 86 | $scope.lalert.show = true; 87 | $scope.lalert.msg = err.reason; 88 | } else { 89 | $scope.lalert.show = false; 90 | if ($routeParams.token) { 91 | Accounts.verifyEmail($routeParams.token, function(err) { 92 | if (err) { 93 | $scope.verification.msg = "Unable to verify email"; 94 | } else { 95 | $scope.verification.msg = "Email verified!"; 96 | } 97 | $scope.verification.show = true; 98 | $scope.$apply(); 99 | }); 100 | } 101 | } 102 | $scope.$apply(); 103 | }); 104 | } 105 | $scope.googlelogin = function() { 106 | Meteor.loginWithGoogle(function(err) { 107 | if (err) { 108 | console.log("Error logging in: " + err); 109 | return; 110 | } 111 | if (Meteor.user().username) { 112 | NuttySession.userloggedin(); 113 | $scope.$apply(); 114 | return; 115 | } 116 | var modalInstance = $modal.open({ 117 | templateUrl: 'templates/username.html', 118 | controller: ['$scope', '$modalInstance', 119 | function($scope, $modalInstance) { 120 | $scope.user = { 121 | username: "" 122 | }; 123 | $scope.spinner = { 124 | spin: false 125 | }; 126 | $scope.ok = function() { 127 | $scope.spinner.spin = true; 128 | $scope.error = ""; 129 | console.log("username is : " + $scope.user.username); 130 | Meteor.call('userExists', $scope.user.username, function(err, alreadyexists) { 131 | if (alreadyexists) { 132 | $scope.spinner.spin = false; 133 | $scope.error = "Username " + $scope.user.username + " already exists"; 134 | $scope.$apply(); 135 | } else { 136 | $scope.spinner.spin = false; 137 | Meteor.users.update({ 138 | _id: Meteor.userId() 139 | }, { 140 | $set: { 141 | username: $scope.user.username 142 | } 143 | }, function(err) { 144 | if (err) { 145 | $scope.spinner.spin = false; 146 | $scope.error = "Error, try different username"; 147 | $scope.$apply(); 148 | } else 149 | $modalInstance.close($scope.user.username); 150 | }); 151 | } 152 | }); 153 | } 154 | } 155 | ] 156 | }); 157 | modalInstance.result.then(function(username) { 158 | NuttySession.userloggedin(); 159 | console.log("username is : " + username); 160 | }, function() { 161 | console.log('Modal dismissed at: ' + new Date()); 162 | Meteor.logout(function() { 163 | $scope.$apply(); 164 | }); 165 | }); 166 | }); 167 | } 168 | 169 | if (Meteor.userId() && $routeParams.token) { 170 | Accounts.verifyEmail($routeParams.token, function(err) { 171 | $timeout(function() { 172 | if (err) { 173 | $scope.verification.msg = "Unable to verify email"; 174 | } else { 175 | $scope.verification.msg = "Email verified!"; 176 | } 177 | $scope.verification.show = true; 178 | }, 0); 179 | }); 180 | } 181 | 182 | } 183 | ]); 184 | -------------------------------------------------------------------------------- /client/scripts/services/nuttyalertCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('alertBox', function() { 19 | var retobj = { 20 | show: false, 21 | type: 'danger', 22 | msg: "", 23 | alert: alert, 24 | close: close 25 | } 26 | 27 | function alert(type, msg) { 28 | retobj.type = type; 29 | retobj.msg = msg; 30 | retobj.show = true; 31 | } 32 | 33 | function close() { 34 | retobj.show = false 35 | } 36 | window.alertBox = retobj; 37 | return retobj; 38 | }); 39 | 40 | angular.module('nuttyapp') 41 | .directive('nuttyAlert', function() { 42 | return { 43 | template: "[[alertBox.msg]]", 44 | scope: true, 45 | restrict: 'E', 46 | retplace: true, 47 | controller: ['$scope', 'alertBox', 48 | function($scope, alertBox) { 49 | $scope.alertBox = alertBox; 50 | } 51 | ] 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /client/scripts/services/scriptsPaste.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .directive('scriptsPaste', function() { 19 | return { 20 | templateUrl: "templates/scriptsPaste.html", 21 | scope: {}, 22 | restrict: 'E', 23 | replace: true, 24 | link: function(scope, element, attrs, Ctrl) {}, 25 | controller: ['$scope', 'NuttyConnection', 'alertBox', 'cannedscripts', 26 | function($scope, NuttyConnection, alertBox, cannedscripts) { 27 | $scope.scripts = cannedscripts.scripts; 28 | $scope.selectedscript = {}; 29 | $scope.paste = function() { 30 | if (!$scope.selectedscript.script) { 31 | alertBox.alert("danger", "Please select a script"); 32 | return; 33 | } 34 | cannedscripts.getscriptcontent($scope.selectedscript.script._id, function(err, content) { 35 | if (content) 36 | NuttyConnection.write({ 37 | data: content 38 | }); 39 | }); 40 | } 41 | } 42 | ] 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /client/scripts/services/sshext.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .factory('sshext', ['$rootScope', '$location', 'sshstate', function($rootScope, $location, sshstate) { 19 | var port; 20 | var inputcbk; 21 | var trans; 22 | var channel; 23 | var sshserver; 24 | var sshport; 25 | var username; 26 | var password; 27 | var pvtkey; 28 | var authcbk; 29 | var auth_success; 30 | var nuttyio = $location.host() === 'nutty.io' || $location.host() === 'www.nutty.io'; 31 | 32 | if (nuttyio) { 33 | return {}; 34 | } 35 | 36 | function postMessage(msg) { 37 | msg.type = '_nutty_fromwebpage'; 38 | window.postMessage(msg, window.location.origin); 39 | } 40 | 41 | window.addEventListener('message', function(event) { 42 | if (event.source !== window) 43 | return; 44 | if (event.data.type !== '_nutty_fromcontentscript') 45 | return; 46 | var msg = event.data; 47 | if (!msg) { 48 | log.console("msg from extension is undefined"); 49 | return; 50 | } 51 | if (msg.extensionenabled) { 52 | retobj.appinstalled = true; 53 | $rootScope.$apply(); 54 | } 55 | 56 | if (msg.error) { 57 | sshstate.state = "disconnected"; 58 | switch (-msg.error) { 59 | case 102: 60 | sshstate.error = "connection refused"; 61 | break; 62 | case 105: 63 | sshstate.error = "DNS name resolution failed" 64 | break; 65 | default: 66 | sshstate.error = "connection failure"; 67 | } 68 | $rootScope.$apply() 69 | console.log("error from socket : " + msg.error); 70 | } 71 | if (msg.connected) { 72 | trans = new paramikojs.transport(null); 73 | trans.writeCallback = function (str) { 74 | postMessage({ 75 | data: str 76 | }); 77 | }; 78 | auth_success = function() { 79 | sshstate.state = "authsuccess"; 80 | sshstate.error = ""; 81 | $rootScope.$apply(); 82 | var on_success = function(chan) { 83 | chan.get_pty('linux', 96, 28); 84 | chan.invoke_shell(); 85 | channel = chan; 86 | authcbk(); 87 | }; 88 | trans.open_session(on_success); 89 | } 90 | window.input = function() { 91 | if (!window.term) { 92 | //in case the term is not yet ready 93 | return; 94 | } 95 | try { 96 | var stdin = channel.recv(65536); 97 | } catch (ex) {} 98 | try { 99 | var stderr = channel.recv_stderr(65536); 100 | } catch (ex) {} 101 | if (stdin && inputcbk) { 102 | inputcbk({ 103 | data: stdin 104 | }); 105 | } 106 | if (stderr && inputcbk) { 107 | inputcbk({ 108 | data: stderr 109 | }); 110 | } 111 | } 112 | postMessage({ 113 | tcpconnect: true, 114 | server: sshserver, 115 | port: sshport 116 | }); 117 | } 118 | if (msg.tcpconnected) { 119 | sshstate.state = "authenticating"; 120 | $rootScope.$apply(); 121 | trans.connect (null, null, username, password, pvtkey, auth_success); 122 | } 123 | if (msg.tcpdisconnected) { 124 | sshstate.state = "disconnected"; 125 | $rootScope.$apply(); 126 | console.log ("nutty background app disconnected"); 127 | } 128 | if (msg.data) { 129 | if (!trans) { 130 | console.log("trans is null"); 131 | return; 132 | } 133 | trans.fullBuffer += msg.data; // read data 134 | trans.run(); 135 | } 136 | }); 137 | 138 | var retobj = { 139 | appinstalled: false, 140 | connect: function(_sshserver, _sshport, _username, _password, _pvtkey, _cbk) { 141 | sshstate.error = ""; 142 | sshstate.state = "connecting"; 143 | sshserver = _sshserver; 144 | sshport = _sshport; 145 | username = _username; 146 | password = _password; 147 | pvtkey = _pvtkey; 148 | authcbk = _cbk; 149 | 150 | postMessage({ 151 | connect: true, 152 | }); 153 | }, 154 | restartsession: function(cbk) { 155 | var on_success = function(chan) { 156 | chan.get_pty('linux', term.screenSize.width, term.screenSize.height); 157 | chan.invoke_shell(); 158 | channel = chan; 159 | if (cbk) 160 | cbk(); 161 | }; 162 | trans.open_session(on_success); 163 | }, 164 | ondata: function(cbk) { 165 | inputcbk = cbk; 166 | }, 167 | write: function(msg) { 168 | if (!channel) { 169 | window.location.pathname = '/'; 170 | return; 171 | } 172 | if (!msg) { 173 | console.log("ssh.write(): msg is null") 174 | return; 175 | } 176 | if (msg.newtmuxsession) { 177 | retobj.restartsession(); 178 | } else if (msg.data) { 179 | channel.send(msg.data); 180 | } else if (msg.rowcol) { 181 | channel.resize_pty(msg.col, msg.row); 182 | } else { 183 | postMessage(msg); 184 | } 185 | } 186 | } 187 | window.sshext = retobj; 188 | return retobj; 189 | }]); 190 | -------------------------------------------------------------------------------- /client/scripts/sharedsessionsCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('nuttyapp') 2 | .controller('sharedsessionsCtrl', ['$scope', 'NuttySession', 3 | function($scope, NuttySession) { 4 | $scope.sharedsessions = NuttySession.sharedsessions; 5 | ga('send', 'pageview', 'sharedsessions'); 6 | $scope.currentuser = function() { 7 | var user = Meteor.user(); 8 | if (user) { 9 | return user.username; 10 | } else { 11 | return ""; 12 | } 13 | }; 14 | }]); 15 | -------------------------------------------------------------------------------- /client/scripts/signinCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .directive('nuttySignin', function() { 19 | return { 20 | templateUrl: "templates/signin.html", 21 | scope: true, 22 | restrict: 'E', 23 | replace: true, 24 | link: function(scope, element, attrs, termController) {}, 25 | controller: ['$scope', 'NuttySession', '$modal', '$location', 26 | function($scope, NuttySession, $modal, $location) { 27 | Deps.autorun(function() { 28 | Meteor.userId(); 29 | setTimeout(function() { 30 | $scope.$apply(); 31 | }, 0); 32 | }); 33 | $scope.currentuser = function() { 34 | var user = Meteor.user(); 35 | if (user) { 36 | return user.username; 37 | } else { 38 | return ""; 39 | } 40 | }; 41 | $scope.signintext = function() { 42 | if (Meteor.userId()) { 43 | return "Log Out"; 44 | } else 45 | return "Log In"; 46 | } 47 | $scope.signinout = function($event) { 48 | // $event.stopPropagation(); 49 | // $event.preventDefault(); 50 | if (Meteor.userId()) { 51 | NuttySession.userloggedout(function() { 52 | Meteor.logout(function(err) { 53 | if (err) 54 | console.log("Error logging out in: " + err); 55 | $scope.$apply(); 56 | }); 57 | }); 58 | } else { 59 | if (NuttySession.type) { 60 | var port = $location.port(); 61 | var portstr = (port === 80 || port === 443) ? '' : ':' + port; 62 | window.open($location.protocol() + '://' + $location.host() + portstr + '/login'); 63 | return; 64 | } else { 65 | $location.path('/login'); 66 | return; 67 | } 68 | } 69 | }; 70 | } 71 | ] 72 | } 73 | }); 74 | 75 | angular.module('nuttyapp') 76 | .directive('focusMe', ['$timeout', 77 | function($timeout) { 78 | return { 79 | link: function(scope, element, attrs, model) { 80 | $timeout(function() { 81 | element[0].focus(); 82 | }); 83 | } 84 | }; 85 | } 86 | ]); 87 | -------------------------------------------------------------------------------- /client/scripts/slaveCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .controller('slaveCtrl', ['$scope', '$modal', '$routeParams', 'NuttySession', 'SlaveConnection', 'NuttyConnection', 'Compatibility', '$location', 'alertBox', 19 | function($scope, $modal, $routeParams, NuttySession, SlaveConnection, NuttyConnection, Compatibility, $location, alertBox) { 20 | var clientid = Session.get("clientid"); 21 | var port = $location.port(); 22 | var portstr = (port === 80 || port === 443) ? '' : ':' + port; 23 | $scope.descro = true; 24 | $scope.Compatibility = Compatibility; 25 | if (!clientid) { 26 | clientid = Random.id(); 27 | Session.set("clientid", clientid); 28 | mixpanel.track("slaveterminal"); 29 | ga('send', 'pageview', 'slaveterminal'); 30 | } 31 | 32 | NuttySession.setslave($routeParams.sessionid, clientid); 33 | NuttyConnection.write = SlaveConnection.pipe.write; 34 | $scope.$watch(function() { 35 | return NuttySession.desc; 36 | }, function(newval) { 37 | $scope.desc = NuttySession.desc; 38 | }); 39 | $scope.currentuser = function() { 40 | var user = Meteor.user(); 41 | if (user) { 42 | return user.username; 43 | } else { 44 | return ""; 45 | } 46 | }; 47 | // $scope.slaveshow = function() { 48 | // if (Compatibility.browser.browser === "Chrome" || Compatibility.browser.browser === "Firefox") 49 | // true; 50 | // else 51 | // false; 52 | // } 53 | if (Compatibility.browser.browser === "Chrome" || Compatibility.browser.browser === "Firefox" || Compatibility.browser.browser === "Safari") 54 | $scope.slaveshow = true; 55 | Deps.autorun(function() { 56 | Meteor.userId(); 57 | setTimeout(function() { 58 | $scope.$apply(); 59 | }, 0); 60 | }); 61 | $scope.$watch (function(){ 62 | return NuttySession.sessionid 63 | }, function(newval) { 64 | if (newval) { 65 | setTimeout(function() { 66 | alertBox.alert("success", "Recording : " + $location.protocol() + '://' + $location.host() + portstr + '/recording/' + NuttySession.sessionid); 67 | $scope.$apply(); 68 | }, 35*1000); 69 | if ($location.path().match(/^\/websocket\//)) { 70 | SlaveConnection.type = 'websocket'; 71 | } else if ($location.path().match(/^\/webrtc\//)) { 72 | SlaveConnection.type = 'webrtc'; 73 | if (Compatibility.browser.browser !== "Chrome") { 74 | alertBox.alert("danger", "WebRTC currently supported on Chrome. Try sharing using WebSockets") 75 | alert("WebRTC currently supported on Chrome. Try sharing using WebSockets") 76 | return; 77 | } 78 | } 79 | SlaveConnection.connect(); 80 | } 81 | }) 82 | } 83 | ]); 84 | -------------------------------------------------------------------------------- /client/scripts/tmuxbtnCtrl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('nuttyapp') 18 | .directive('tmuxButtons', function() { 19 | return { 20 | templateUrl: "templates/tmuxButtons.html", 21 | scope: true, 22 | restrict: 'E', 23 | replace: true, 24 | link: function(scope, element, attrs, Ctrl) {}, 25 | controller: ['$scope', 'NuttySession', 'NuttyConnection', 'alertBox', 26 | function($scope, NuttySession, NuttyConnection, alertBox) { 27 | var readonly = NuttySession.readonly; 28 | var tmuxscript = 'TMUXCONF=/tmp/nutty-tmux.conf.`date +%N`\n' + 29 | 'TMUXSCRIPT=/tmp/nutty-tmux.sh.`date +%N`\n' + 30 | 'cat > $TMUXCONF< $TMUXSCRIPT< /dev/null\n' + 42 | 'if [ \\$? == 1 ];\n' + 43 | 'then\n' + 44 | ' echo -e \'\\e[0;31mtmux not found, please install tmux. \\e[0m \'\n' + 45 | ' rm -f $TMUXCONF $TMUXSCRIPT\n' + 46 | ' exit\n' + 47 | 'fi\n' + 48 | 'export TERM=xterm\n' + 49 | 'tmux -L nutty -f $TMUXCONF\n' + 50 | 'if [ \\$? != 0 ];\n' + 51 | 'then\n' + 52 | ' echo tmux is not installed! please install.\n' + 53 | 'fi\n' + 54 | '\n' + 55 | 'rm -f $TMUXCONF $TMUXSCRIPT\n' + 56 | '\n' + 57 | 'EOF\n' + 58 | '\n' + 59 | 'chmod +x $TMUXSCRIPT\n' + 60 | '$TMUXSCRIPT\n'; 61 | 62 | $scope.btn = { 63 | color: "primary", 64 | value: false 65 | }; 66 | $scope.NuttySession = NuttySession; 67 | // if (NuttySession.type === "slave") 68 | // $scope.disabled = "disabled"; 69 | $scope.starttmux = function() { 70 | mixpanel.track("starttmux"); 71 | NuttyConnection.write({ 72 | data: tmuxscript 73 | }); 74 | term.focus(); 75 | } 76 | $scope.splitH = function() { 77 | mixpanel.track("splitH"); 78 | NuttyConnection.write({ 79 | data: String.fromCharCode(2) + '"' 80 | }); 81 | term.focus(); 82 | } 83 | $scope.splitV = function() { 84 | mixpanel.track("splitV"); 85 | NuttyConnection.write({ 86 | data: String.fromCharCode(2) + '%' 87 | }); 88 | term.focus(); 89 | } 90 | $scope.newWindow = function() { 91 | mixpanel.track("newWindow"); 92 | NuttyConnection.write({ 93 | data: String.fromCharCode(2) + 'c' 94 | }); 95 | term.focus(); 96 | } 97 | $scope.newtmuxsession = function() { 98 | mixpanel.track("newtmuxsession"); 99 | NuttyConnection.write({ 100 | newtmuxsession: true 101 | }); 102 | term.focus(); 103 | } 104 | 105 | $scope.resizeL = function() { 106 | NuttyConnection.write({ 107 | data: String.fromCharCode(2, 27, 91, 49, 59, 53, 68) 108 | }); 109 | term.focus(); 110 | } 111 | $scope.resizeR = function() { 112 | NuttyConnection.write({ 113 | data: String.fromCharCode(2, 27, 91, 49, 59, 53, 67) 114 | }); 115 | term.focus(); 116 | } 117 | $scope.resizeU = function() { 118 | NuttyConnection.write({ 119 | data: String.fromCharCode(2, 27, 91, 49, 59, 53, 65) 120 | }); 121 | term.focus(); 122 | } 123 | $scope.resizeD = function() { 124 | NuttyConnection.write({ 125 | data: String.fromCharCode(2, 27, 91, 49, 59, 53, 66) 126 | }); 127 | term.focus(); 128 | } 129 | $scope.markreadonly = function() { 130 | if (NuttySession.type === "master") { 131 | if (!Meteor.userId()) { 132 | alertBox.alert("warning", "Please sign-in"); 133 | term.focus(); 134 | return; 135 | } 136 | mixpanel.track("markreadonly"); 137 | readonly = !readonly; 138 | if (readonly) { 139 | NuttySession.setreadonly(true); 140 | } else { 141 | NuttySession.setreadonly(false); 142 | } 143 | } else { 144 | alertBox.alert("danger", "This can be set only by terminal sharer"); 145 | } 146 | term.focus(); 147 | } 148 | $scope.tooltiptext = function() { 149 | if (readonly) 150 | return "Remote read only access" 151 | else 152 | return "Remote read/write access"; 153 | } 154 | $scope.btntext = function() { 155 | if (readonly) 156 | return "R/O"; 157 | else 158 | return 'R/W'; 159 | } 160 | $scope.$watch(function() { 161 | return NuttySession.readonly; 162 | }, function(newval) { 163 | if (newval) { 164 | $scope.btn.color = "danger"; 165 | readonly = $scope.btn.value = true; 166 | } else { 167 | $scope.btn.color = "primary"; 168 | readonly = $scope.btn.value = false; 169 | } 170 | }); 171 | $scope.$watch("btn.value", function(newval) { 172 | $scope.btn.value = NuttySession.readonly; 173 | }); 174 | } 175 | ] 176 | } 177 | }); 178 | -------------------------------------------------------------------------------- /client/styles/extern/ion.rangeSlider.css: -------------------------------------------------------------------------------- 1 | /* Ion.RangeSlider 2 | // css version 1.9.2 3 | // © 2013-2014 Denis Ineshin | IonDen.com 4 | // ===================================================================================================================*/ 5 | 6 | /* ===================================================================================================================== 7 | // RangeSlider */ 8 | 9 | .irs { 10 | position: relative; display: block; 11 | } 12 | .irs-line { 13 | position: relative; display: block; 14 | overflow: hidden; 15 | } 16 | .irs-line-left, .irs-line-mid, .irs-line-right { 17 | position: absolute; display: block; 18 | top: 0; 19 | } 20 | .irs-line-left { 21 | left: 0; width: 10%; 22 | } 23 | .irs-line-mid { 24 | left: 9%; width: 82%; 25 | } 26 | .irs-line-right { 27 | right: 0; width: 10%; 28 | } 29 | 30 | .irs-diapason { 31 | position: absolute; display: block; 32 | left: 0; width: 100%; 33 | } 34 | .irs-slider { 35 | position: absolute; display: block; 36 | cursor: default; 37 | z-index: 1; 38 | } 39 | .irs-slider.single { 40 | left: 10px; 41 | } 42 | .irs-slider.single:before { 43 | position: absolute; display: block; content: ""; 44 | top: -30%; left: -30%; 45 | width: 160%; height: 160%; 46 | background: rgba(0,0,0,0.0); 47 | } 48 | .irs-slider.from { 49 | left: 100px; 50 | } 51 | .irs-slider.from:before { 52 | position: absolute; display: block; content: ""; 53 | top: -30%; left: -30%; 54 | width: 130%; height: 160%; 55 | background: rgba(0,0,0,0.0); 56 | } 57 | .irs-slider.to { 58 | left: 300px; 59 | } 60 | .irs-slider.to:before { 61 | position: absolute; display: block; content: ""; 62 | top: -30%; left: 0; 63 | width: 130%; height: 160%; 64 | background: rgba(0,0,0,0.0); 65 | } 66 | .irs-slider.last { 67 | z-index: 2; 68 | } 69 | 70 | .irs-min { 71 | position: absolute; display: block; 72 | left: 0; 73 | cursor: default; 74 | } 75 | .irs-max { 76 | position: absolute; display: block; 77 | right: 0; 78 | cursor: default; 79 | } 80 | 81 | .irs-from, .irs-to, .irs-single { 82 | position: absolute; display: block; 83 | top: 0; left: 0; 84 | cursor: default; 85 | white-space: nowrap; 86 | } 87 | 88 | 89 | .irs-grid { 90 | position: absolute; display: none; 91 | bottom: 0; left: 0; 92 | width: 100%; height: 20px; 93 | } 94 | .irs-with-grid .irs-grid { 95 | display: block; 96 | } 97 | .irs-grid-pol { 98 | position: absolute; 99 | top: 0; left: 0; 100 | width: 1px; height: 8px; 101 | background: #000; 102 | } 103 | .irs-grid-pol.small { 104 | height: 4px; 105 | } 106 | .irs-grid-text { 107 | position: absolute; 108 | bottom: 0; left: 0; 109 | width: 100px; 110 | white-space: nowrap; 111 | text-align: center; 112 | font-size: 9px; line-height: 9px; 113 | color: #000; 114 | } 115 | 116 | .irs-disable-mask { 117 | position: absolute; display: block; 118 | top: 0; left: 0; 119 | width: 100%; height: 100%; 120 | cursor: default; 121 | background: rgba(0,0,0,0.0); 122 | z-index: 2; 123 | } 124 | .irs-disabled { 125 | opacity: 0.4; 126 | } 127 | -------------------------------------------------------------------------------- /client/styles/extern/ion.rangeSlider.skinFlat.css: -------------------------------------------------------------------------------- 1 | /* Ion.RangeSlider, Flat UI Skin 2 | // css version 1.9.2 3 | // © 2013-2014 Denis Ineshin | IonDen.com 4 | // ===================================================================================================================*/ 5 | 6 | /* ===================================================================================================================== 7 | // Skin details */ 8 | 9 | .irs-line-mid, 10 | .irs-line-left, 11 | .irs-line-right, 12 | .irs-diapason, 13 | .irs-slider { 14 | background: url(../images/sprite-skin-flat.png) repeat-x; 15 | } 16 | 17 | .irs { 18 | height: 40px; 19 | } 20 | .irs-with-grid { 21 | height: 60px; 22 | } 23 | .irs-line { 24 | height: 12px; top: 25px; 25 | } 26 | .irs-line-left { 27 | height: 12px; 28 | background-position: 0 -30px; 29 | } 30 | .irs-line-mid { 31 | height: 12px; 32 | background-position: 0 0; 33 | } 34 | .irs-line-right { 35 | height: 12px; 36 | background-position: 100% -30px; 37 | } 38 | 39 | .irs-diapason { 40 | height: 12px; top: 25px; 41 | background-position: 0 -60px; 42 | } 43 | 44 | .irs-slider { 45 | width: 16px; height: 18px; 46 | top: 22px; 47 | background-position: 0 -90px; 48 | } 49 | #irs-active-slider, .irs-slider:hover { 50 | background-position: 0 -120px; 51 | } 52 | 53 | .irs-min, .irs-max { 54 | color: #999; 55 | font-size: 10px; line-height: 1.333; 56 | text-shadow: none; 57 | top: 0; padding: 1px 3px; 58 | background: #e1e4e9; 59 | border-radius: 4px; 60 | } 61 | 62 | .irs-from, .irs-to, .irs-single { 63 | color: #fff; 64 | font-size: 10px; line-height: 1.333; 65 | text-shadow: none; 66 | padding: 1px 5px; 67 | background: #ed5565; 68 | border-radius: 4px; 69 | } 70 | .irs-from:after, .irs-to:after, .irs-single:after { 71 | position: absolute; display: block; content: ""; 72 | bottom: -6px; left: 50%; 73 | width: 0; height: 0; 74 | margin-left: -3px; 75 | overflow: hidden; 76 | border: 3px solid transparent; 77 | border-top-color: #ed5565; 78 | } 79 | 80 | 81 | .irs-grid-pol { 82 | background: #e1e4e9; 83 | } 84 | .irs-grid-text { 85 | color: #999; 86 | } 87 | 88 | .irs-disabled { 89 | } 90 | -------------------------------------------------------------------------------- /client/styles/extern/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v1.0.1 | MIT License | git.io/normalize */ 2 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block} 3 | audio,canvas,video{display:inline-block;*display:inline;*zoom:1} 4 | audio:not([controls]){display:none;height:0} 5 | [hidden]{display:none} 6 | html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%} 7 | html,button,input,select,textarea{font-family:sans-serif} 8 | body{margin:0} 9 | a:focus{outline:thin dotted} 10 | a:active,a:hover{outline:0} 11 | h1{font-size:2em;margin:.67em 0} 12 | h2{font-size:1.5em;margin:.83em 0} 13 | h3{font-size:1.17em;margin:1em 0} 14 | h4{font-size:1em;margin:1.33em 0} 15 | h5{font-size:.83em;margin:1.67em 0} 16 | h6{font-size:.75em;margin:2.33em 0} 17 | abbr[title]{border-bottom:1px dotted} 18 | b,strong{font-weight:bold} 19 | blockquote{margin:1em 40px} 20 | dfn{font-style:italic} 21 | mark{background:#ff0;color:#000} 22 | p,pre{margin:1em 0} 23 | code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em} 24 | pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word} 25 | q{quotes:none} 26 | q:before,q:after{content:'';content:none} 27 | small{font-size:80%} 28 | sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} 29 | sup{top:-0.5em} 30 | sub{bottom:-0.25em} 31 | dl,menu,ol,ul{margin:1em 0} 32 | dd{margin:0 0 0 40px} 33 | menu,ol,ul{padding:0 0 0 40px} 34 | nav ul,nav ol{list-style:none;list-style-image:none} 35 | img{border:0;-ms-interpolation-mode:bicubic} 36 | svg:not(:root){overflow:hidden} 37 | figure{margin:0} 38 | form{margin:0} 39 | fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} 40 | legend{border:0;padding:0;white-space:normal;*margin-left:-7px} 41 | button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle} 42 | button,input{line-height:normal} 43 | button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible} 44 | button[disabled],input[disabled]{cursor:default} 45 | input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px} 46 | input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} 47 | input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} 48 | button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} 49 | textarea{overflow:auto;vertical-align:top} 50 | table{border-collapse:collapse;border-spacing:0} -------------------------------------------------------------------------------- /client/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | .fullheight { 6 | height: 100%; 7 | } 8 | .fullwidth { 9 | width: 100%; 10 | } 11 | .row { 12 | /*margin-left: 0px;*/ 13 | margin-right: 0px; 14 | } 15 | .pushdown { 16 | margin-top: 80px; 17 | } 18 | .container { 19 | max-width: 800px; 20 | width: auto; 21 | } 22 | /* Wrapper for page content to push down footer */ 23 | #wrap { 24 | min-height: 100%; 25 | height: auto; 26 | /* Negative indent footer by its height */ 27 | margin: 0 auto -60px; 28 | /* Pad bottom by footer height */ 29 | padding: 0 0 60px; 30 | } 31 | 32 | /* Set the fixed height of the footer here */ 33 | #footer { 34 | height: 60px; 35 | background-color: #f5f5f5; 36 | } 37 | 38 | .rotate { 39 | -webkit-transform: rotate(90deg); 40 | -moz-transform: rotate(90deg); 41 | -o-transform: rotate(90deg); 42 | /* filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1.5); */ 43 | -ms-transform: rotate(90deg); 44 | } 45 | .glyphicon-lg { 46 | font-size: 22px 47 | } 48 | .glyphicon-exlg { 49 | font-size: 27px 50 | } 51 | .green { 52 | color: green; 53 | } 54 | .red { 55 | color: red; 56 | } 57 | .blue { 58 | color: #428bca; 59 | } 60 | .faq { 61 | background-color: #eeeeee; 62 | padding: 30px; 63 | margin-bottom: 30px; 64 | margin-top: 30px; 65 | } 66 | .nuttychat { 67 | padding: 10px; 68 | border: 1px solid #ccc; 69 | background-color: #fff; 70 | -webkit-border-radius: 4px; 71 | -moz-border-radius: 4px; 72 | border-radius: 4px; 73 | -webkit-box-shadow: 0 5px 25px #666; 74 | -moz-box-shadow: 0 5px 25px #666; 75 | box-shadow: 0 5px 25px #666; 76 | } 77 | .nuttychat .chat { 78 | overflow: auto; 79 | -ms-overflow-x: hidden; 80 | overflow-x: hidden; 81 | height: 200px; 82 | margin-bottom: 5px; 83 | border: 1px solid #cccccc; 84 | overflow-y: auto; 85 | } 86 | .nav-pills > li > a { 87 | border-radius: 0px; 88 | } 89 | .container .jumbotron { 90 | border-radius: 0px; 91 | } 92 | .btn { 93 | border-radius: 0px; 94 | } 95 | .panel { 96 | border-radius: 0px; 97 | } 98 | 99 | .center { 100 | text-align: center; 101 | } 102 | 103 | .navbar { 104 | border: 0px; 105 | border-radius: 0px; 106 | } 107 | /* make sidebar nav vertical */ 108 | @media (min-width: 768px) { 109 | .sidebar-nav .navbar .navbar-collapse { 110 | padding: 0; 111 | max-height: none; 112 | } 113 | .sidebar-nav .navbar ul { 114 | float: none; 115 | display: block; 116 | } 117 | .sidebar-nav .navbar li { 118 | float: none; 119 | display: block; 120 | } 121 | .sidebar-nav .navbar li a { 122 | padding-top: 12px; 123 | padding-bottom: 12px; 124 | } 125 | } 126 | 127 | .vertical-container { 128 | display: -webkit-flex; 129 | display: flex; 130 | } 131 | .vertically-centered { 132 | margin: auto; 133 | } 134 | 135 | .content { 136 | min-height: 100%; 137 | border-right: 4px solid #428bca; 138 | } 139 | 140 | .nutty-ssh-creds { 141 | border: 2px solid grey; 142 | border-radius: 4px; 143 | padding: 20px; 144 | box-shadow: 0 5px 25px #666; 145 | margin: 40px; 146 | } 147 | 148 | .pad0 { 149 | padding: 0px; 150 | } 151 | 152 | .navbar-default .navbar-nav > .active > a, 153 | .navbar-default .navbar-nav > .active > a:hover, 154 | .navbar-default .navbar-nav > .active > a:focus { 155 | color: white; 156 | background-color: #428bca; 157 | } 158 | 159 | .serverlist { 160 | margin-top: 50px; 161 | /* border: 2px solid grey; 162 | border-radius: 4px;*/ 163 | } 164 | 165 | .nuttysidebar { 166 | min-height: 100%; 167 | } 168 | 169 | .termpanel { 170 | padding-top: 50px; 171 | } 172 | 173 | .socialbtns { 174 | width: 200px; 175 | margin: 0 auto; 176 | margin-bottom: 20px; 177 | } 178 | 179 | .installinstr { 180 | margin-top: 20px; 181 | } 182 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Krishna Srinivas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/README.md: -------------------------------------------------------------------------------- 1 | meteor-broadcast 2 | =========== 3 | 4 | This package enables realtime communication between meteor clients without mongo. 5 | Not added to atmosphere 6 | 7 | The similar sounding meteor-pipe package is used when you need communication between a master and multiple slaves. (i.e slave can talk to master but not other slaves) 8 | 9 | Usage 10 | ===== 11 | 12 | On server: 13 | if (Meteor.isServer) { 14 | var msgserver = new Meteor.Broadcast(); 15 | } 16 | 17 | on clients do: 18 | var client = new Meteor.Broadcast ("sharedcode"); 19 | 20 | ("sharedcode" can be any string that is pre-agreed between clients.) 21 | 22 | You can send data like this: 23 | client.send(Object); 24 | 25 | You can receive data from other clients like this: 26 | client.on('data', function (data) { 27 | console.log (data); 28 | } 29 | 30 | Warning : Note that this package uses the undocumented Meteor.connection.registerStore API on client to receive server published data without using local Meteor.Collection on the client. 31 | 32 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/client/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Krishna Srinivas 3 | * MIT License 4 | */ 5 | 6 | Meteor.Broadcast = function (sharecode) { 7 | EventEmitter.call(this); 8 | var streamName = '_meteor_peerbroadcast_'; 9 | var self = this; 10 | var clientid = Random.id(); 11 | 12 | Meteor.connection.registerStore (streamName, { 13 | update: function (data) { 14 | self.emit('data', data.fields); 15 | } 16 | }); 17 | 18 | Meteor.subscribe (streamName, sharecode, clientid, { 19 | onError : function (e) { 20 | self.emit('error', e); 21 | }, 22 | onReady : function () { 23 | self.emit('ready'); 24 | } 25 | }); 26 | 27 | this.send = function (data) { 28 | Meteor.call(streamName, sharecode, clientid, data); 29 | } 30 | } 31 | 32 | 33 | util.inherits(Meteor.Broadcast, EventEmitter); 34 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/lib/eventemitter.js: -------------------------------------------------------------------------------- 1 | /* taken from PeerJS project. MIT Licensed */ 2 | 3 | /** 4 | * Light EventEmitter. Ported from Node.js/events.js 5 | * Eric Zhang 6 | */ 7 | 8 | /** 9 | * EventEmitter class 10 | * Creates an object with event registering and firing methods 11 | */ 12 | EventEmitter = function EventEmitter() { 13 | // Initialise required storage variables 14 | this._events = {}; 15 | } 16 | 17 | var isArray = Array.isArray; 18 | 19 | 20 | EventEmitter.prototype.addListener = function(type, listener, scope, once) { 21 | if ('function' !== typeof listener) { 22 | throw new Error('addListener only takes instances of Function'); 23 | } 24 | 25 | // To avoid recursion in the case that type == "newListeners"! Before 26 | // adding it to the listeners, first emit "newListeners". 27 | this.emit('newListener', type, typeof listener.listener === 'function' ? 28 | listener.listener : listener); 29 | 30 | if (!this._events[type]) { 31 | // Optimize the case of one listener. Don't need the extra array object. 32 | this._events[type] = listener; 33 | } else if (isArray(this._events[type])) { 34 | 35 | // If we've already got an array, just append. 36 | this._events[type].push(listener); 37 | 38 | } else { 39 | // Adding the second element, need to change to array. 40 | this._events[type] = [this._events[type], listener]; 41 | } 42 | return this; 43 | }; 44 | 45 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 46 | 47 | EventEmitter.prototype.once = function(type, listener, scope) { 48 | if ('function' !== typeof listener) { 49 | throw new Error('.once only takes instances of Function'); 50 | } 51 | 52 | var self = this; 53 | function g() { 54 | self.removeListener(type, g); 55 | listener.apply(this, arguments); 56 | }; 57 | 58 | g.listener = listener; 59 | self.on(type, g); 60 | 61 | return this; 62 | }; 63 | 64 | EventEmitter.prototype.removeListener = function(type, listener, scope) { 65 | if ('function' !== typeof listener) { 66 | throw new Error('removeListener only takes instances of Function'); 67 | } 68 | 69 | // does not use listeners(), so no side effect of creating _events[type] 70 | if (!this._events[type]) return this; 71 | 72 | var list = this._events[type]; 73 | 74 | if (isArray(list)) { 75 | var position = -1; 76 | for (var i = 0, length = list.length; i < length; i++) { 77 | if (list[i] === listener || 78 | (list[i].listener && list[i].listener === listener)) 79 | { 80 | position = i; 81 | break; 82 | } 83 | } 84 | 85 | if (position < 0) return this; 86 | list.splice(position, 1); 87 | if (list.length == 0) 88 | delete this._events[type]; 89 | } else if (list === listener || 90 | (list.listener && list.listener === listener)) 91 | { 92 | delete this._events[type]; 93 | } 94 | 95 | return this; 96 | }; 97 | 98 | 99 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 100 | 101 | 102 | EventEmitter.prototype.removeAllListeners = function(type) { 103 | if (arguments.length === 0) { 104 | this._events = {}; 105 | return this; 106 | } 107 | 108 | // does not use listeners(), so no side effect of creating _events[type] 109 | if (type && this._events && this._events[type]) this._events[type] = null; 110 | return this; 111 | }; 112 | 113 | EventEmitter.prototype.listeners = function(type) { 114 | if (!this._events[type]) this._events[type] = []; 115 | if (!isArray(this._events[type])) { 116 | this._events[type] = [this._events[type]]; 117 | } 118 | return this._events[type]; 119 | }; 120 | 121 | EventEmitter.prototype.emit = function(type) { 122 | var type = arguments[0]; 123 | var handler = this._events[type]; 124 | if (!handler) return false; 125 | 126 | if (typeof handler == 'function') { 127 | switch (arguments.length) { 128 | // fast cases 129 | case 1: 130 | handler.call(this); 131 | break; 132 | case 2: 133 | handler.call(this, arguments[1]); 134 | break; 135 | case 3: 136 | handler.call(this, arguments[1], arguments[2]); 137 | break; 138 | // slower 139 | default: 140 | var l = arguments.length; 141 | var args = new Array(l - 1); 142 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 143 | handler.apply(this, args); 144 | } 145 | return true; 146 | 147 | } else if (isArray(handler)) { 148 | var l = arguments.length; 149 | var args = new Array(l - 1); 150 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 151 | 152 | var listeners = handler.slice(); 153 | for (var i = 0, l = listeners.length; i < l; i++) { 154 | listeners[i].apply(this, args); 155 | } 156 | return true; 157 | } else { 158 | return false; 159 | } 160 | }; 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/lib/util.js: -------------------------------------------------------------------------------- 1 | util = { 2 | inherits: function(ctor, superCtor) { 3 | ctor.super_ = superCtor; 4 | ctor.prototype = Object.create(superCtor.prototype, { 5 | constructor: { 6 | value: ctor, 7 | enumerable: false, 8 | writable: true, 9 | configurable: true 10 | } 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "realtime communication between meteor clients without mongo - broadcast messages" 3 | }); 4 | 5 | Package.on_use(function (api) { 6 | api.add_files(['lib/util.js', 'lib/eventemitter.js', 'server/server.js'], 'server'); 7 | api.add_files(['lib/util.js', 'lib/eventemitter.js', 'client/client.js'], 'client'); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/server/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Krishna Srinivas 3 | * MIT License 4 | */ 5 | 6 | Meteor.Broadcast = function() { 7 | EventEmitter.call(this); 8 | var streamName = '_meteor_peerbroadcast_'; 9 | var methods = {}; 10 | var peerbroadcastserver = this; 11 | var messages = {}; 12 | 13 | Meteor.publish(streamName, function (sharecode, clientid) { 14 | var self = this; 15 | var pushData = function (_clientid, data) { 16 | if (_clientid === clientid) { 17 | return; 18 | } 19 | var id = Random.id(); 20 | self.added (streamName, id, data); 21 | }; 22 | 23 | peerbroadcastserver.on(sharecode, pushData); 24 | if (!messages[sharecode]) { 25 | messages[sharecode] = []; 26 | } 27 | 28 | this.onStop(function () { 29 | peerbroadcastserver.off(sharecode, pushData); 30 | if (!peerbroadcastserver._events[sharecode]) { 31 | delete messages[sharecode]; 32 | } 33 | }); 34 | 35 | this.ready(); 36 | _.each(messages[sharecode], function(element) { 37 | self.added (streamName, Random.id(), element); 38 | }); 39 | }); 40 | 41 | methods[streamName] = function (sharecode, _clientid, data) { 42 | if (Meteor.userId()) 43 | data.username = Meteor.user().username; 44 | messages[sharecode].push(data); 45 | if (messages[sharecode].length >= 11) 46 | messages[sharecode].shift(); 47 | peerbroadcastserver.emit(sharecode, _clientid, data); 48 | } 49 | 50 | Meteor.methods (methods); 51 | } 52 | 53 | util.inherits(Meteor.Broadcast, EventEmitter); 54 | -------------------------------------------------------------------------------- /packages/meteor-broadcast/smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broadcast", 3 | "description": "realtime communication between meteor clients without mongo - broadcast messages", 4 | "homepage": "https://github.com/krishnasrinivas/meteor-broadcast", 5 | "author": "Krishna Srinivas ", 6 | "version": "0.1.1", 7 | "git": "https://github.com/krishnasrinivas/meteor-broadcast.git" 8 | } 9 | -------------------------------------------------------------------------------- /packages/meteor-pipe/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/meteor-pipe/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Krishna Srinivas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/meteor-pipe/README.md: -------------------------------------------------------------------------------- 1 | meteor-pipe 2 | =========== 3 | 4 | This package enables realtime communication between meteor clients without mongo. 5 | Not added to atmosphere 6 | 7 | Usage 8 | ===== 9 | 10 | On server: 11 | if (Meteor.isServer) { 12 | var pipeserver = new Meteor.PipeServer(); 13 | } 14 | 15 | on one client do: 16 | var master = new Meteor.PipeClientMaster ("random-string"); 17 | 18 | on another client do: 19 | var slave = new Meteor.PipeClientSlave ("random-string"); 20 | 21 | ("random-string" can be any string that is pre-agreed between clients.) 22 | 23 | This establishes connection between 'master' and 'slave' 24 | You can send data like this: 25 | master.send(Object) 26 | or 27 | slave.send(Object) 28 | 29 | You can receive data like this: 30 | master.on('data', function (data) { 31 | console.log (data); 32 | } 33 | 34 | or 35 | 36 | slave.on('data', function (data) { 37 | console.log (data); 38 | } 39 | 40 | Note that for you can have multiple 'slave' connections to a single 'master' in which case when you do master.send() it reaches all the slaves. However if you do slave.send() it reaches only the master. Data is mux'ed on the server. 41 | 42 | Warning : Note that this package uses the undocumented Meteor.connection.registerStore API on client to receive server published data without using local Meteor.Collection on the client. 43 | 44 | -------------------------------------------------------------------------------- /packages/meteor-pipe/client/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Krishna Srinivas 3 | * MIT License 4 | */ 5 | 6 | Meteor.PipeClientMaster = function (sharecode) { 7 | EventEmitter.call(this); 8 | var streamName = '_meteor_pipe_'; 9 | var self = this; 10 | 11 | Meteor.connection.registerStore (streamName, { 12 | update: function (data) { 13 | self.emit('data', data.fields); 14 | } 15 | }); 16 | 17 | Meteor.subscribe (streamName, sharecode, 'master', { 18 | onError : function (e) { 19 | self.emit('error', e); 20 | }, 21 | onReady : function () { 22 | self.emit('ready'); 23 | } 24 | }); 25 | 26 | this.send = function (data) { 27 | Meteor.call(streamName, sharecode, 'master', data); 28 | } 29 | } 30 | 31 | Meteor.PipeClientSlave = function (sharecode) { 32 | EventEmitter.call(this); 33 | var streamName = '_meteor_pipe_'; 34 | var self = this; 35 | 36 | Meteor.connection.registerStore (streamName, { 37 | update: function (data) { 38 | self.emit('data', data.fields); 39 | } 40 | }); 41 | 42 | Meteor.subscribe (streamName, sharecode, 'slave', { 43 | onError : function (e) { 44 | self.emit('error', e); 45 | }, 46 | onReady : function () { 47 | self.emit('ready'); 48 | } 49 | }); 50 | 51 | this.send = function (data) { 52 | Meteor.call(streamName, sharecode, 'slave', data); 53 | } 54 | } 55 | 56 | util.inherits(Meteor.PipeClientMaster, EventEmitter); 57 | util.inherits(Meteor.PipeClientSlave, EventEmitter); 58 | -------------------------------------------------------------------------------- /packages/meteor-pipe/lib/eventemitter.js: -------------------------------------------------------------------------------- 1 | /* taken from PeerJS project. MIT Licensed */ 2 | 3 | /** 4 | * Light EventEmitter. Ported from Node.js/events.js 5 | * Eric Zhang 6 | */ 7 | 8 | /** 9 | * EventEmitter class 10 | * Creates an object with event registering and firing methods 11 | */ 12 | EventEmitter = function EventEmitter() { 13 | // Initialise required storage variables 14 | this._events = {}; 15 | } 16 | 17 | var isArray = Array.isArray; 18 | 19 | 20 | EventEmitter.prototype.addListener = function(type, listener, scope, once) { 21 | if ('function' !== typeof listener) { 22 | throw new Error('addListener only takes instances of Function'); 23 | } 24 | 25 | // To avoid recursion in the case that type == "newListeners"! Before 26 | // adding it to the listeners, first emit "newListeners". 27 | this.emit('newListener', type, typeof listener.listener === 'function' ? 28 | listener.listener : listener); 29 | 30 | if (!this._events[type]) { 31 | // Optimize the case of one listener. Don't need the extra array object. 32 | this._events[type] = listener; 33 | } else if (isArray(this._events[type])) { 34 | 35 | // If we've already got an array, just append. 36 | this._events[type].push(listener); 37 | 38 | } else { 39 | // Adding the second element, need to change to array. 40 | this._events[type] = [this._events[type], listener]; 41 | } 42 | return this; 43 | }; 44 | 45 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 46 | 47 | EventEmitter.prototype.once = function(type, listener, scope) { 48 | if ('function' !== typeof listener) { 49 | throw new Error('.once only takes instances of Function'); 50 | } 51 | 52 | var self = this; 53 | function g() { 54 | self.removeListener(type, g); 55 | listener.apply(this, arguments); 56 | }; 57 | 58 | g.listener = listener; 59 | self.on(type, g); 60 | 61 | return this; 62 | }; 63 | 64 | EventEmitter.prototype.removeListener = function(type, listener, scope) { 65 | if ('function' !== typeof listener) { 66 | throw new Error('removeListener only takes instances of Function'); 67 | } 68 | 69 | // does not use listeners(), so no side effect of creating _events[type] 70 | if (!this._events[type]) return this; 71 | 72 | var list = this._events[type]; 73 | 74 | if (isArray(list)) { 75 | var position = -1; 76 | for (var i = 0, length = list.length; i < length; i++) { 77 | if (list[i] === listener || 78 | (list[i].listener && list[i].listener === listener)) 79 | { 80 | position = i; 81 | break; 82 | } 83 | } 84 | 85 | if (position < 0) return this; 86 | list.splice(position, 1); 87 | if (list.length == 0) 88 | delete this._events[type]; 89 | } else if (list === listener || 90 | (list.listener && list.listener === listener)) 91 | { 92 | delete this._events[type]; 93 | } 94 | 95 | return this; 96 | }; 97 | 98 | 99 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 100 | 101 | 102 | EventEmitter.prototype.removeAllListeners = function(type) { 103 | if (arguments.length === 0) { 104 | this._events = {}; 105 | return this; 106 | } 107 | 108 | // does not use listeners(), so no side effect of creating _events[type] 109 | if (type && this._events && this._events[type]) this._events[type] = null; 110 | return this; 111 | }; 112 | 113 | EventEmitter.prototype.listeners = function(type) { 114 | if (!this._events[type]) this._events[type] = []; 115 | if (!isArray(this._events[type])) { 116 | this._events[type] = [this._events[type]]; 117 | } 118 | return this._events[type]; 119 | }; 120 | 121 | EventEmitter.prototype.emit = function(type) { 122 | var type = arguments[0]; 123 | var handler = this._events[type]; 124 | if (!handler) return false; 125 | 126 | if (typeof handler == 'function') { 127 | switch (arguments.length) { 128 | // fast cases 129 | case 1: 130 | handler.call(this); 131 | break; 132 | case 2: 133 | handler.call(this, arguments[1]); 134 | break; 135 | case 3: 136 | handler.call(this, arguments[1], arguments[2]); 137 | break; 138 | // slower 139 | default: 140 | var l = arguments.length; 141 | var args = new Array(l - 1); 142 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 143 | handler.apply(this, args); 144 | } 145 | return true; 146 | 147 | } else if (isArray(handler)) { 148 | var l = arguments.length; 149 | var args = new Array(l - 1); 150 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 151 | 152 | var listeners = handler.slice(); 153 | for (var i = 0, l = listeners.length; i < l; i++) { 154 | listeners[i].apply(this, args); 155 | } 156 | return true; 157 | } else { 158 | return false; 159 | } 160 | }; 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /packages/meteor-pipe/lib/util.js: -------------------------------------------------------------------------------- 1 | util = { 2 | inherits: function(ctor, superCtor) { 3 | ctor.super_ = superCtor; 4 | ctor.prototype = Object.create(superCtor.prototype, { 5 | constructor: { 6 | value: ctor, 7 | enumerable: false, 8 | writable: true, 9 | configurable: true 10 | } 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/meteor-pipe/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "realtime communication between meteor clients without mongo" 3 | }); 4 | 5 | Package.on_use(function (api) { 6 | api.add_files(['lib/util.js', 'lib/eventemitter.js', 'server/server.js'], 'server'); 7 | api.add_files(['lib/util.js', 'lib/eventemitter.js', 'client/client.js'], 'client'); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/meteor-pipe/server/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Krishna Srinivas 3 | * MIT License 4 | */ 5 | 6 | Meteor.PipeServer = function() { 7 | var pipeserver = this; 8 | var tomaster = new EventEmitter(); 9 | var frommaster = new EventEmitter(); 10 | var streamName = '_meteor_pipe_'; 11 | var methods = {}; 12 | 13 | Meteor.publish(streamName, function (sharecode, type) { 14 | var self = this; 15 | var sendstart = false; 16 | 17 | var pushData = function (data) { 18 | var id = Random.id(); 19 | self.added (streamName, id, data); 20 | }; 21 | if (type === 'master') { 22 | tomaster.on(sharecode, pushData); 23 | if (frommaster._events[sharecode]) 24 | sendstart = true; 25 | } else if (type === 'slave') { 26 | frommaster.on (sharecode, pushData); 27 | sendstart = true; 28 | } else { 29 | //err 30 | } 31 | this.onStop(function () { 32 | if (type === 'master') { 33 | tomaster.off(sharecode, pushData); 34 | } else if (type === 'slave') { 35 | frommaster.off (sharecode, pushData); 36 | if (!frommaster._events[sharecode]) { 37 | tomaster.emit(sharecode, {stop: true}); 38 | } 39 | } 40 | }); 41 | 42 | this.ready(); 43 | if (sendstart) 44 | tomaster.emit(sharecode, {start:true}); 45 | }); 46 | 47 | methods[streamName] = function (sharecode, type, data) { 48 | if (type === 'master') { 49 | frommaster.emit (sharecode, data); 50 | } else if (type === 'slave') { 51 | tomaster.emit (sharecode, data); 52 | } 53 | } 54 | 55 | Meteor.methods (methods); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /packages/meteor-pipe/smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pipe", 3 | "description": "realtime communication between meteor clients without mongo", 4 | "homepage": "https://github.com/krishnasrinivas/meteor-pipe", 5 | "author": "Krishna Srinivas ", 6 | "version": "0.1.1", 7 | "git": "https://github.com/krishnasrinivas/meteor-pipe.git" 8 | } 9 | -------------------------------------------------------------------------------- /private: -------------------------------------------------------------------------------- 1 | ../private -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/forgooglebot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nutty - Terminal sharing using browser 6 | 7 | 8 | 9 | 10 |
11 | 12 | Fork me on GitHub 13 | 14 | 15 | 16 |
17 |
18 | 19 |

20 |

1. What is nutty?

21 | Nutty is a browser tool using which you can share your local desktop terminal console with a remote user. It does not matter if you or the remote user is behind firewall. 22 |

23 | 24 |

25 |

2. What advantages does it provide over tmux/screen?

26 | tmux/screen works well when two users on the same machine want to share terminals. When you want to share the terminal with a remote user nutty will help. 27 |

28 | 29 |

30 |

3. What is nutty's License?

31 | Nutty is licened under Apache License Version 2.0 32 |

33 | 34 |

35 |

4. In the 3rd step of installation why does it require root access?

36 |
curl -s https://raw.github.com/krishnasrinivas/nuttyapp/master/public/install.sh | sudo sh
37 | This is a Chrome limitation. Bug-237873 had been raised which has been fixed in Chrome-34. So you do not need root permission to install nutty script for Chrome-34+ (Stable Chrome-34 has not been released yet) 38 |

39 | 40 |

41 |

5. Is nutty like ajaxterm?

42 | Not at all. Ajaxterm lets you login to a server using a browser. Nutty is a terminal sharing tool 43 |

44 | 45 |

46 |

6. How do I share terminal using nutty?

47 | Follow the install steps. After install click the icon beside the browser bar. It will open a terminal window inside your Chrome browser and also give you a unique URL which you can share with anyone. That person can access your terminal using the URL even if you and him/her are behind firewall. 48 |

49 | 50 |

51 |

7. After sharing terminal, does nutty.io server proxy the data between peers?

52 | You can share terminal with peers using either Websocket or WebRTC. If you choose the Websocket option nutty.io server proxies the data between peers. If you choose the WebRTC option the terminal sharing between peers happen with out any proxy, i.e it is entirely peer-to-peer. 53 |

54 | 55 |

56 |

8. What is WebRTC

57 | WebRTC enables peer-to-peer real-time communication in the browser. More info here. Nutty makes use of peerjs.com WebRTC library. 58 |

59 | 60 |

61 |

9. Is the terminal sharing encrypted?

62 | Yes. Websocket data transfer uses https-TLS encryption. WebRTC data traffic is encrypted by the browser by default. 63 |

64 | 65 |

66 |

10. If I record terminal in nutty and upload, does it store passwords?

67 | No. Because passwords are not echoed back to the terminal. 68 |

69 | 70 |

71 |

11. What browsers are supported?

72 | For now sharing has to be done using Chrome Web Browser. In future Firefox will be supported. 73 | For viewing only, nutty has been tested on Chrome, it is known to work on FF and Safari. 74 |

75 | 76 |

77 |

12. How can Chrome access terminal device? Isn't Chrome sandboxed?

78 | Nutty uses Chrome's Native Messaging API to provide terminal access to the browser. Nutty extension is restricted to the https://nutty.io domain. Hence other websites will not be able to access/misuse it. 79 |

80 | 81 |

82 |

Does nutty work on mobile devices?

83 | Yes. A "sharee" can view the shared terminal on mobile devices using mobile browsers. Ability to type commands from mobile devices do not work yet. 84 |

85 | 86 |

87 |

14. I would like to run nutty server on a machine inside my private network(or on EC2), is that possible?

88 | Yes. You can download the server code and host your own private nutty server. Please follow these instructions: https://github.com/krishnasrinivas/nuttyapp/blob/master/README.md 89 |

90 | 91 |
92 | 93 |
94 |
95 | 96 | Twitter : https://twitter.com/nuttydotio 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /public/images/extensionicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/images/extensionicon.png -------------------------------------------------------------------------------- /public/images/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/images/forkme.png -------------------------------------------------------------------------------- /public/images/nutty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/images/nutty.png -------------------------------------------------------------------------------- /public/images/sprite-skin-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/images/sprite-skin-flat.png -------------------------------------------------------------------------------- /public/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnasrinivas/nuttyapp/c5d68273fe812fa2add93597394661b444a2c375/public/images/twitter.png -------------------------------------------------------------------------------- /public/templates/chat.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 23 | 24 |
25 |
26 | 32 |
33 |
34 |
35 |
36 |
[[chatmsg.username]] : [[chatmsg.msg]]
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 |
47 |
[[user.username]] 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /public/templates/connectmodal.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /public/templates/demo.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/templates/masterTerminal.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /public/templates/playTerminal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /public/templates/recordButtons.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 |
38 | 39 |
40 |
-------------------------------------------------------------------------------- /public/templates/scriptsPaste.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 |
9 | 10 |
11 |
12 |
-------------------------------------------------------------------------------- /public/templates/signin.html: -------------------------------------------------------------------------------- 1 |
2 | [[signintext()]] 3 | 4 |
[[currentuser()]] 5 |
6 |
7 | -------------------------------------------------------------------------------- /public/templates/slaveTerminal.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /public/templates/tmuxButtons.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | W 8 |
9 | 10 | 11 | [[btntext()]] 12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /public/templates/username.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /public/views/404.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Page not found

5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /public/views/common.js: -------------------------------------------------------------------------------- 1 | function paramikojs() { } 2 | paramikojs = { 3 | MSG_DISCONNECT : 1, 4 | MSG_IGNORE : 2, 5 | MSG_UNIMPLEMENTED : 3, 6 | MSG_DEBUG : 4, 7 | MSG_SERVICE_REQUEST : 5, 8 | MSG_SERVICE_ACCEPT : 6, 9 | 10 | MSG_KEXINIT : 20, 11 | MSG_NEWKEYS : 21, 12 | 13 | MSG_USERAUTH_REQUEST : 50, 14 | MSG_USERAUTH_FAILURE : 51, 15 | MSG_USERAUTH_SUCCESS : 52, 16 | MSG_USERAUTH_BANNER : 53, 17 | 18 | MSG_USERAUTH_PK_OK : 60, 19 | 20 | MSG_USERAUTH_INFO_REQUEST : 60, 21 | MSG_USERAUTH_INFO_RESPONSE : 61, 22 | 23 | MSG_GLOBAL_REQUEST : 80, 24 | MSG_REQUEST_SUCCESS : 81, 25 | MSG_REQUEST_FAILURE : 82, 26 | 27 | MSG_CHANNEL_OPEN : 90, 28 | MSG_CHANNEL_OPEN_SUCCESS : 91, 29 | MSG_CHANNEL_OPEN_FAILURE : 92, 30 | MSG_CHANNEL_WINDOW_ADJUST : 93, 31 | MSG_CHANNEL_DATA : 94, 32 | MSG_CHANNEL_EXTENDED_DATA : 95, 33 | MSG_CHANNEL_EOF : 96, 34 | MSG_CHANNEL_CLOSE : 97, 35 | MSG_CHANNEL_REQUEST : 98, 36 | MSG_CHANNEL_SUCCESS : 99, 37 | MSG_CHANNEL_FAILURE : 100, 38 | 39 | // for debugging: 40 | MSG_NAMES : { 41 | 1: 'disconnect', 42 | 2: 'ignore', 43 | 3: 'unimplemented', 44 | 4: 'debug', 45 | 5: 'service-request', 46 | 6: 'service-accept', 47 | 20: 'kexinit', 48 | 21: 'newkeys', 49 | 30: 'kex30', 50 | 31: 'kex31', 51 | 32: 'kex32', 52 | 33: 'kex33', 53 | 34: 'kex34', 54 | 50: 'userauth-request', 55 | 51: 'userauth-failure', 56 | 52: 'userauth-success', 57 | 53: 'userauth--banner', 58 | 60: 'userauth-60(pk-ok/info-request)', 59 | 61: 'userauth-info-response', 60 | 80: 'global-request', 61 | 81: 'request-success', 62 | 82: 'request-failure', 63 | 90: 'channel-open', 64 | 91: 'channel-open-success', 65 | 92: 'channel-open-failure', 66 | 93: 'channel-window-adjust', 67 | 94: 'channel-data', 68 | 95: 'channel-extended-data', 69 | 96: 'channel-eof', 70 | 97: 'channel-close', 71 | 98: 'channel-request', 72 | 99: 'channel-success', 73 | 100: 'channel-failure' 74 | }, 75 | 76 | // authentication request return codes: 77 | AUTH_SUCCESSFUL : 0, 78 | AUTH_PARTIALLY_SUCCESSFUL : 1, 79 | AUTH_FAILED : 2, 80 | 81 | // channel request failed reasons: 82 | OPEN_SUCCEEDED : 0, 83 | OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED : 1, 84 | OPEN_FAILED_CONNECT_FAILED : 2, 85 | OPEN_FAILED_UNKNOWN_CHANNEL_TYPE : 3, 86 | OPEN_FAILED_RESOURCE_SHORTAGE : 4, 87 | 88 | CONNECTION_FAILED_CODE : { 89 | 1: 'Administratively prohibited', 90 | 2: 'Connect failed', 91 | 3: 'Unknown channel type', 92 | 4: 'Resource shortage' 93 | }, 94 | 95 | DISCONNECT_SERVICE_NOT_AVAILABLE : 7, 96 | DISCONNECT_AUTH_CANCELLED_BY_USER : 13, 97 | DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE : 14 98 | }; 99 | -------------------------------------------------------------------------------- /public/views/faq.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 |

What is nutty?

7 | Nutty is a browser tool using which you can connect to a machine using SSH and share the terminal session with a remote user. It does not matter if you or the remote user is behind firewall. 8 |

9 |

10 |

What is nutty's License?

11 | Nutty is licened under Apache License Version 2.0 12 |

13 |

14 |

How do I share terminal using nutty?

15 | Login onto an ssh server using username/password (or a private ssh key). It will open a terminal window inside your Chrome browser and also give you a unique URL which you can share with anyone. That person can access your terminal using the URL even if you and him/her are behind firewall. 16 |

17 |

18 |

Where can I report issues with Nutty?

19 | https://github.com/krishnasrinivas/nuttyapp/issues/new 20 |

21 |

22 |

How do I enable SSH server on Mac OSX?

23 | Go to ‘System Preferences’. Under ‘Internet & Networking’ there is a ‘Sharing’ icon. Run that. In the list that appears, check the ‘Remote Login’ option. 24 | Or on the command line do: 'systemsetup -setremotelogin on' 25 |

26 |

27 |

Will my SSH password or Private Key be accessed by nutty server?

28 | No. SSH connection is made directly from your Chrome browser to the SSH server (using nutty's Chrome app) Hence nutty.io server will never access your password or Private Key. 29 |

30 |

31 |

Is my SSH credentials stored on nutty server when I save it?

32 | No. If you save by clicking "Save" the SSH credentials it will be stored on your localdisk in the HTML5 filesystem. 33 |

34 |

35 |

If I share terminal and close my browser tab will a remote user still be able to access the terminal?

36 | No. 37 |

38 |

39 |

How do I copy/paste in the terminal?

40 | Copy: Ctrl-C
41 | Paste: Ctrl-Shift-V 42 |

43 |

44 |

After sharing terminal, does nutty.io server proxy the data between peers?

45 | You can share terminal with peers using either Websocket or WebRTC. If you choose the Websocket option http://nutty.io server proxies the data between peers. If you choose the WebRTC option the terminal sharing between peers happen with out any proxy, i.e it is entirely peer-to-peer. 46 |

47 |

48 |

What is WebRTC?

49 | WebRTC enables peer-to-peer real-time communication in the browser. More info here. Nutty makes use of peerjs.com WebRTC library. 50 |

51 |

52 |

Is the terminal sharing encrypted?

53 | Yes. Websocket data transfer uses https-TLS encryption. WebRTC data traffic is encrypted by default by the browser. 54 |

55 |

56 |

How can I access recording of a session?

57 | Visit https://nutty.io/recording/<SESSIONID> 58 |

59 |

60 |

Are passwords also stored when terminal session is recorded?

61 | No. Because passwords are not echoed back to the terminal. 62 |

63 |

64 |

What browsers are supported?

65 | For now sharing has to be done using Chrome Web Browser. In future Firefox will be supported. 66 | For viewing only, nutty has been tested on Chrome, it is known to work on FF and Safari. 67 |

68 |

69 |

Does nutty work on mobile devices?

70 | Yes. A "sharee" can view the shared terminal on mobile devices using mobile browsers. Ability to type commands from mobile devices do not work yet. 71 |

72 |

73 |

I would like to run nutty server on a machine inside my private network(or on EC2), is that possible?

74 | Yes. You can download the server code and host your own private nutty server. Please follow these instructions: https://github.com/krishnasrinivas/nuttyapp/blob/master/README.md 75 |

76 |
77 |
78 |
79 |
80 |
81 | 115 |
116 |
117 |
118 | -------------------------------------------------------------------------------- /public/views/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

SSH-Terminal sharing using Browser!

6 |
7 |
8 |
Demo
9 |
10 |
11 | 12 |

Supported only on Chrome. Firefox support coming soon.

13 |
14 |
15 | Install nutty app and extension: 16 |
    17 |
  1. Install 18 | 19 | Nutty-App 20 | 21 |
  2. 22 |
  3. 23 | Install Nutty-extension 24 |
  4. 25 |
26 | And then click the nutty icon beside address bar to enable "Connect" button 27 |
28 |
29 |
Note: SSH password/pvtkey will not be transmitted to the nutty server as
SSH connection is established directly between browser and SSH server

30 |
31 |
32 |
33 |
Install Nutty
34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 |
Or
56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 | [[loginerrshow]] 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
[[$index+1]][[server.username]]@[[server.host]]:[[server.port]]
80 |
81 |
82 |
83 |
84 |
85 | 118 |
119 |
120 |
121 | -------------------------------------------------------------------------------- /public/views/kryptos/PublicKey/RSA.js: -------------------------------------------------------------------------------- 1 | kryptos.publicKey.RSA = function() { 2 | 3 | } 4 | 5 | kryptos.publicKey.RSA.prototype = { 6 | construct : function(n, e, d) { 7 | this.n = n; 8 | this.e = e; 9 | this.d = d; 10 | 11 | return this; 12 | }, 13 | 14 | sign : function(m, K) { 15 | return [m.modPow(this.d, this.n), '']; 16 | }, 17 | 18 | verify : function(m, sig) { 19 | var s = sig[0]; // HACK - We should use the previous line instead, but 20 | // this is more compatible and we're going to replace 21 | // the Crypto.PublicKey API soon anyway. 22 | return s.modPow(this.e, this.n).equals(m); 23 | }, 24 | 25 | generate : function() { 26 | alert('NOT_IMPLEMENTED'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /public/views/kryptos/kryptos.js: -------------------------------------------------------------------------------- 1 | kryptos = function() {}; 2 | kryptos.prototype = {}; 3 | 4 | kryptos.cipher = {}; 5 | kryptos.hash = {}; 6 | kryptos.protocol = {}; 7 | kryptos.publicKey = {}; 8 | kryptos.random = {}; 9 | kryptos.random.Fortuna = {}; 10 | kryptos.random.OSRNG = {}; 11 | kryptos.util = {}; 12 | 13 | kryptos.toByteArray = function(str) { 14 | function charToUint(chr) { return chr.charCodeAt(0) } 15 | return str.split('').map(charToUint); 16 | }; 17 | 18 | kryptos.fromByteArray = function(data) { 19 | function uintToChar(uint) { return String.fromCharCode(uint) } 20 | return data.map(uintToChar).join(''); 21 | }; 22 | 23 | kryptos.bytesToWords = function(bytes) { 24 | for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8) { 25 | words[b >>> 5] |= (bytes[i] & 0xFF) << (24 - b % 32); 26 | } 27 | return words; 28 | }; 29 | 30 | kryptos.wordsToBytes = function(words) { 31 | for (var bytes = [], b = 0; b < words.length * 32; b += 8) { 32 | bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF); 33 | } 34 | return bytes; 35 | }; 36 | -------------------------------------------------------------------------------- /public/views/master.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 | 13 | 15 |
16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /public/views/pb.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /public/views/playback.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |

Number of uploaded recordings : [[recordings.length]]

8 |

Not loggedin

9 | 10 | 11 | 12 | 13 | 17 | 20 | 23 | 24 | 25 |
[[$index+1]] 14 | [[recording.sessionid]] 15 | 16 | 18 | [[datetime($index)]] 19 | 21 | 22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /public/views/python_shim.js: -------------------------------------------------------------------------------- 1 | var binascii = { 2 | hexlify : function(str, padding) { 3 | var result = ""; 4 | padding = padding || ''; 5 | for (var x = 0; x < str.length; ++x) { 6 | var c = str.charCodeAt(x).toString(16); 7 | result += (c.length == 1 ? '0' : '') + c + padding; 8 | } 9 | return result; 10 | }, 11 | 12 | unhexlify : function(str) { 13 | var result = ""; 14 | for (var x = 0; x < str.length; x += 2) { 15 | result += String.fromCharCode(parseInt(str.charAt(x) + str.charAt(x + 1), 16)); 16 | } 17 | return result; 18 | } 19 | }; 20 | 21 | var base64 = { 22 | encodestring : function(input) { 23 | return window.btoa(input); 24 | }, 25 | 26 | decodestring : function(input) { 27 | return window.atob(input); 28 | } 29 | }; 30 | 31 | /* 32 | This is a dumbed down version of the python function only doing a couple formats and big-endian only currently. 33 | todo: allow for unlimited arguments 34 | */ 35 | var struct = { 36 | pack : function(fmt, v) { 37 | var type = fmt[1]; 38 | var result = ""; 39 | switch (type) { 40 | case 'Q': 41 | var ff = new BigInteger('ff', 16); 42 | result += String.fromCharCode(v.shiftRight(56).and(ff)); 43 | result += String.fromCharCode(v.shiftRight(48).and(ff)); 44 | result += String.fromCharCode(v.shiftRight(40).and(ff)); 45 | result += String.fromCharCode(v.shiftRight(32).and(ff)); 46 | result += String.fromCharCode(v.shiftRight(24).and(ff)); 47 | result += String.fromCharCode(v.shiftRight(16).and(ff)); 48 | result += String.fromCharCode(v.shiftRight(8).and(ff)); 49 | result += String.fromCharCode(v.and(ff)); 50 | break; 51 | case 'I': 52 | result += String.fromCharCode(v >>> 24 & 0xff); 53 | result += String.fromCharCode(v >>> 16 & 0xff); 54 | result += String.fromCharCode(v >>> 8 & 0xff); 55 | // fall through 56 | case 'B': 57 | result += String.fromCharCode(v & 0xff); 58 | break; 59 | } 60 | 61 | return result; 62 | }, 63 | 64 | unpack : function(fmt, str) { 65 | var type = fmt[1]; 66 | var result = []; 67 | var index = 0; 68 | var v = 0; 69 | switch (type) { 70 | case 'Q': 71 | v = new BigInteger("0", 10); 72 | 73 | v = v.add(new BigInteger(str.charCodeAt(0).toString(), 10).shiftLeft(56)); 74 | v = v.add(new BigInteger(str.charCodeAt(1).toString(), 10).shiftLeft(48)); 75 | v = v.add(new BigInteger(str.charCodeAt(2).toString(), 10).shiftLeft(40)); 76 | v = v.add(new BigInteger(str.charCodeAt(3).toString(), 10).shiftLeft(32)); 77 | v = v.add(new BigInteger(str.charCodeAt(4).toString(), 10).shiftLeft(24)); 78 | v = v.add(new BigInteger(str.charCodeAt(5).toString(), 10).shiftLeft(16)); 79 | v = v.add(new BigInteger(str.charCodeAt(6).toString(), 10).shiftLeft(8)); 80 | v = v.add(new BigInteger(str.charCodeAt(7).toString(), 10).shiftLeft(0)); 81 | result.push(v); 82 | break; 83 | case 'I': 84 | v += str.charCodeAt(0) << 24 >>> 0; 85 | v += str.charCodeAt(1) << 16 >>> 0; 86 | v += str.charCodeAt(2) << 8 >>> 0; 87 | index += 3; 88 | // fall through 89 | case 'B': 90 | v += str.charCodeAt(0 + index) << 0 >>> 0; 91 | result.push(v); 92 | break; 93 | } 94 | 95 | return result; 96 | } 97 | }; 98 | 99 | var sys = { 100 | 'browser' : navigator.userAgent.toLowerCase().indexOf('chrome') != -1 ? 'chrome' : 'mozilla', 101 | 'platform' : navigator.platform.toLowerCase().indexOf('linux') != -1 ? 'linux' : 102 | (navigator.platform.toLowerCase().indexOf('mac') != -1 ? 'darwin' : 'win32') 103 | }; 104 | -------------------------------------------------------------------------------- /public/views/remoterecord.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | Supported Browser: Chrome-29+ 7 | 8 |
Your Browser: [[Compatibility.browser.browser]]-[[Compatibility.browser.version]] 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /public/views/resetPassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 |
Enter new password
11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 | 32 | [[resetpwd.msg]] 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /public/views/scripts.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |

Canned support Scripts:

7 |

Not loggedin

8 | 9 |
10 |
11 |
12 | Choose file to upload: 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 | Description: 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |

30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
[[$index+1]][[script.description]]
41 | 42 |
43 |
44 |
45 |
46 |
47 | 81 |
82 |
83 |
84 | -------------------------------------------------------------------------------- /public/views/sharedsessions.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |

Number of Sessions shared : [[sharedsessions.length]]

7 |

Not loggedin

8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 |
[[$index+1]]https://nutty.io/[[sharedsession.conntype]]/[[sharedsession.sessionid]] 13 | [[sharedsession.desc]]
18 |
19 |
20 |
21 |
22 |
23 | 67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /public/views/sign_ssh_data_worker.js: -------------------------------------------------------------------------------- 1 | importScripts('kryptos/kryptos.js', 2 | 'kryptos/PublicKey/RSA.js', 3 | 'common.js', 4 | 'python_shim.js', 5 | 'BigInteger.js', 6 | 'util.js'); 7 | 8 | onmessage = function(event) { 9 | var rsa = new kryptos.publicKey.RSA().construct(new BigInteger(event.data.n, 10), 10 | new BigInteger(event.data.e, 10), 11 | new BigInteger(event.data.d, 10)); 12 | var inflated = paramikojs.util.inflate_long(event.data.pkcs1imified, true); 13 | postMessage(rsa.sign(inflated, '')[0].toString()); 14 | }; 15 | -------------------------------------------------------------------------------- /public/views/slave.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 | Supported Browser: Chrome-29 21 | 22 |
Your Browser: [[Compatibility.browser.browser]]-[[Compatibility.browser.version]] 23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /recording.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 krishna.srinivas@gmail.com All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "flag" 22 | "io/ioutil" 23 | "log" 24 | "net/http" 25 | "os" 26 | "time" 27 | 28 | "github.com/stretchr/goweb" 29 | "github.com/stretchr/goweb/context" 30 | ghttp "github.com/stretchr/goweb/http" 31 | ) 32 | 33 | func nuttyPut(c context.Context) error { 34 | sessionid := c.PathValue("sessionid") 35 | tindex := c.PathValue("tindex") 36 | 37 | log.Println("PUT ", sessionid, " ", tindex) 38 | 39 | data, dataErr := c.RequestBody() 40 | if dataErr != nil { 41 | log.Println(dataErr) 42 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, dataErr.Error()) 43 | } 44 | dataErr = os.MkdirAll(basedir+sessionid, 0700) 45 | if dataErr != nil { 46 | log.Println(dataErr) 47 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, "Unable to create directory") 48 | } 49 | dataErr = ioutil.WriteFile(basedir+sessionid+"/"+tindex, data, 0600) 50 | if dataErr != nil { 51 | log.Println(dataErr) 52 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, "Unable to WriteFile") 53 | } 54 | rjsonBytes, err := json.Marshal(struct { 55 | tindex string `json:"end"` 56 | }{ 57 | tindex: tindex, 58 | }) 59 | if err != nil { 60 | log.Fatalln(err) 61 | } 62 | dataErr = ioutil.WriteFile(basedir+sessionid+"/rec.json", rjsonBytes, 0600) 63 | if dataErr != nil { 64 | log.Println(dataErr) 65 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, "Unable to WriteFile") 66 | } 67 | return goweb.API.Respond(c, 200, nil, nil) 68 | } 69 | 70 | func nuttyGet(c context.Context) error { 71 | sessionid := c.PathValue("sessionid") 72 | tindex := c.PathValue("tindex") 73 | 74 | log.Println("GET ", sessionid, " ", tindex) 75 | 76 | bs, err := ioutil.ReadFile(basedir + sessionid + "/" + tindex) 77 | if err != nil { 78 | log.Println(err) 79 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, "Unable to ReadFile") 80 | } 81 | return goweb.Respond.With(c, 200, bs) 82 | } 83 | 84 | func nuttyGetLength(c context.Context) error { 85 | sessionid := c.PathValue("sessionid") 86 | bs, err := ioutil.ReadFile(basedir + sessionid + "/rec.json") 87 | if err != nil { 88 | log.Println(err) 89 | return goweb.API.RespondWithError(c, http.StatusInternalServerError, "Unable to ReadFile") 90 | } 91 | return goweb.Respond.With(c, 200, bs) 92 | } 93 | 94 | // get current working directory 95 | var currentWorkingDirectory string 96 | 97 | func init() { 98 | var err error 99 | currentWorkingDirectory, err = os.Getwd() 100 | if err != nil { 101 | log.Fatalln(err) 102 | } 103 | } 104 | 105 | var ( 106 | basedir string 107 | address string 108 | ) 109 | 110 | func main() { 111 | flag.StringVar(&basedir, "basedir", currentWorkingDirectory, "basedir") 112 | flag.StringVar(&address, "address", ":9090", "address") 113 | flag.Parse() 114 | 115 | basedir = basedir + "/" 116 | goweb.Map(ghttp.MethodPut, "/recording/{sessionid}/{tindex}", nuttyPut) 117 | goweb.Map(ghttp.MethodGet, "/recording/{sessionid}/rec.json", nuttyGetLength) 118 | goweb.Map(ghttp.MethodGet, "/recording/{sessionid}/{tindex}", nuttyGet) 119 | goweb.MapBefore(func(c context.Context) error { 120 | c.HttpResponseWriter().Header().Set("Access-Control-Allow-Origin", "*") 121 | c.HttpResponseWriter().Header().Set("Access-Control-Allow-Headers", "Content-Type") 122 | c.HttpResponseWriter().Header().Set("Access-Control-Allow-Methods", "PUT,GET") 123 | c.HttpResponseWriter().Header().Set("Content-Type", "application/json") 124 | return nil 125 | }) 126 | 127 | s := &http.Server{ 128 | Addr: address, 129 | Handler: goweb.DefaultHttpHandler(), 130 | ReadTimeout: 10 * time.Second, 131 | WriteTimeout: 10 * time.Second, 132 | MaxHeaderBytes: 1 << 20, 133 | } 134 | 135 | log.Println("Starting server") 136 | log.Fatal(s.ListenAndServe()) 137 | } 138 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "winston": {}, 4 | "crypto-hmac": {}, 5 | "crypto-sha1": {}, 6 | "crypto-md5": {}, 7 | "crypto-base64": {}, 8 | "crypto-base": {"version":"3.1.2.1"}, 9 | "knox": {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /smart.lock: -------------------------------------------------------------------------------- 1 | { 2 | "meteor": {}, 3 | "dependencies": { 4 | "basePackages": { 5 | "winston": {}, 6 | "crypto-hmac": {}, 7 | "crypto-sha1": {}, 8 | "crypto-md5": {}, 9 | "crypto-base64": {}, 10 | "crypto-base": { 11 | "version": "3.1.2.1" 12 | }, 13 | "knox": {} 14 | }, 15 | "packages": { 16 | "winston": { 17 | "git": "https://github.com/tomrogers3/meteor-winston.git", 18 | "tag": "v0.0.3", 19 | "commit": "60dc02303ca003ef05254f94fbb9bfd40915d5d3" 20 | }, 21 | "crypto-hmac": { 22 | "git": "https://github.com/oortcloud/meteor-crypto-hmac.git", 23 | "tag": "v3.1.2.0", 24 | "commit": "7c9d1b96c682982cb75f2fd6c75fc7663b5b361e" 25 | }, 26 | "crypto-sha1": { 27 | "git": "https://github.com/dandv/meteor-crypto-sha1.git", 28 | "tag": "v3.1.2.1", 29 | "commit": "f5c87d44aa4bdda6b33c7e2c21ea396d9d0f436d" 30 | }, 31 | "crypto-md5": { 32 | "git": "https://github.com/oortcloud/meteor-crypto-md5.git", 33 | "tag": "v3.1.2.2", 34 | "commit": "75bbb3eeace302122d3554a6212a6fe92553fa18" 35 | }, 36 | "crypto-base64": { 37 | "git": "https://github.com/dandv/meteor-crypto-base64.git", 38 | "tag": "v3.1.2.0", 39 | "commit": "03e523e80faaf7b10191ea50493414bec7cd0e3d" 40 | }, 41 | "crypto-base": { 42 | "git": "https://github.com/oortcloud/meteor-crypto-base.git", 43 | "tag": "v3.1.2.1", 44 | "commit": "5df8d82a0595553415bbfae2930c6ecbeece5794" 45 | }, 46 | "knox": { 47 | "git": "https://github.com/ewindso/meteor-knox.git", 48 | "tag": "v0.8.5.2", 49 | "commit": "62c778c40f89884cb10cfd2097e614e01ca8b8cc" 50 | } 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------