├── .editorconfig ├── .gitignore ├── .jshintrc ├── COPYING ├── Gruntfile.js ├── Makefile ├── README.md ├── npm-shrinkwrap.json ├── package.json ├── rel └── server.js └── src ├── css ├── app.css └── app.less ├── img └── icons.png ├── index.html ├── js ├── app │ ├── presenters │ │ ├── app.js │ │ ├── capture.js │ │ ├── capture │ │ │ └── session.js │ │ ├── presenter.js │ │ ├── project.js │ │ ├── stream.js │ │ └── stream │ │ │ ├── details.js │ │ │ └── summary.js │ ├── services │ │ ├── frida.js │ │ ├── messagebus.js │ │ ├── navigation.js │ │ ├── ospy.js │ │ ├── service.js │ │ ├── settings.js │ │ └── storage.js │ ├── utils.js │ └── views │ │ ├── app.js │ │ ├── capture.js │ │ ├── capture │ │ └── session.js │ │ ├── project.js │ │ ├── stream.js │ │ ├── stream │ │ ├── details.js │ │ └── summary.js │ │ ├── template.js │ │ └── view.js ├── config.js ├── lib │ ├── deferred.js │ ├── events.js │ ├── extend.js │ └── lru.js └── main.js ├── templates ├── project.html └── session.html └── vendor ├── lcss.js ├── less.js ├── require.js └── text.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | [**.js] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [**.css] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [**.html] 21 | indent_style = space 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | *.swp 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "es5": true, 9 | "forin": true, 10 | "immed": true, 11 | "indent": 4, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "strict": true, 16 | "sub": true, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "white": false, 21 | "globals": { 22 | "define": true, 23 | "module": true, 24 | "exports": true, 25 | "require": true, 26 | "console": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | wxWindows Library Licence, Version 3.1 2 | ====================================== 3 | 4 | Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al 5 | 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this licence document, but changing it is not allowed. 8 | 9 | WXWINDOWS LIBRARY LICENCE 10 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 11 | 12 | This library is free software; you can redistribute it and/or modify it 13 | under the terms of the GNU Library General Public Licence as published by 14 | the Free Software Foundation; either version 2 of the Licence, or (at your 15 | option) any later version. 16 | 17 | This library is distributed in the hope that it will be useful, but WITHOUT 18 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 20 | Licence for more details. 21 | 22 | You should have received a copy of the GNU Library General Public Licence 23 | along with this software, usually in a file named COPYING.LIB. If not, 24 | write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 25 | Floor, Boston, MA 02110-1301 USA. 26 | 27 | EXCEPTION NOTICE 28 | 29 | 1. As a special exception, the copyright holders of this library give 30 | permission for additional uses of the text contained in this release of the 31 | library as licenced under the wxWindows Library Licence, applying either 32 | version 3.1 of the Licence, or (at your option) any later version of the 33 | Licence as published by the copyright holders of version 3.1 of the Licence 34 | document. 35 | 36 | 2. The exception is that you may use, copy, link, modify and distribute 37 | under your own terms, binary object code versions of works based on the 38 | Library. 39 | 40 | 3. If you copy code from files distributed under the terms of the GNU 41 | General Public Licence or the GNU Library General Public Licence into a 42 | copy of this library, as this licence permits, the exception does not apply 43 | to the code that you add in this way. To avoid misleading anyone as to the 44 | status of such modified files, you must delete this exception notice from 45 | such code and/or adjust the licensing conditions notice accordingly. 46 | 47 | 4. If you write modifications of your own for this library, it is your 48 | choice whether to permit this exception to apply to your modifications. If 49 | you do not wish that, you must delete the exception notice from such code 50 | and/or adjust the licensing conditions notice accordingly. 51 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | grunt.initConfig({ 6 | requirejs: { 7 | rel: { 8 | options: { 9 | modules: [{ 10 | name: 'main' 11 | }], 12 | dir: "build/www", 13 | appDir: "src", 14 | baseUrl: ".", 15 | paths: { 16 | 'lcss': "vendor/lcss", 17 | 'text': "vendor/text", 18 | 'deferred': "js/lib/deferred", 19 | 'events': "js/lib/events", 20 | 'extend': "js/lib/extend", 21 | 'lru': "js/lib/lru", 22 | 'app': "js/app", 23 | 'main': "js/main", 24 | 'css': "css" 25 | }, 26 | 27 | optimize: "uglify2", 28 | optimizeAllPluginResources: true, 29 | optimizeCss: 'none', /* LESS compiler takes care of this */ 30 | stubModules: [ 'lcss', 'text' ], 31 | removeCombined: true, 32 | 33 | almond: true, 34 | replaceRequireScript: [{ 35 | files: ["build/www/index.html"], 36 | module: 'main', 37 | modulePath: '/js/main' 38 | }], 39 | 40 | fileExclusionRegExp: /^\..+$/, 41 | pragmas: { productionExclude: true }, 42 | preserveLicenseComments: false, 43 | useStrict: false 44 | } 45 | } 46 | }, 47 | jshint: { 48 | all: [ 49 | 'Gruntfile.js', 50 | 'src/js/*.js', 51 | 'src/js/app/**/*.js', 52 | 'rel/*.js' 53 | ], 54 | options: { 55 | jshintrc: ".jshintrc" 56 | } 57 | } 58 | }); 59 | 60 | grunt.loadNpmTasks('grunt-contrib-jshint'); 61 | grunt.loadNpmTasks('grunt-requirejs'); 62 | 63 | grunt.registerTask('build', ['requirejs', 'trim']); 64 | grunt.registerTask('lint', ['jshint']); 65 | grunt.registerTask('default', ['lint']); 66 | 67 | grunt.registerTask('trim', "Trim for packaging.", function() { 68 | var fs = require('fs'); 69 | var path = require('path'); 70 | var targets = grunt.config(['requirejs']); 71 | 72 | function rmTreeSync(name) { 73 | if (!fs.existsSync(name)) { 74 | return; 75 | } 76 | 77 | if (fs.statSync(name).isDirectory()) { 78 | fs.readdirSync(name).forEach(function(file) { 79 | rmTreeSync(path.join(name, file)); 80 | }); 81 | fs.rmdirSync(name); 82 | } else { 83 | fs.unlinkSync(name); 84 | } 85 | } 86 | 87 | for (var target in targets) { 88 | if (targets.hasOwnProperty(target)) { 89 | var dir = targets[target].options.dir; 90 | 91 | var htmlFilepath = path.join(dir, "index.html"); 92 | var htmlDoc = grunt.file.read(htmlFilepath); 93 | grunt.file.write(htmlFilepath, htmlDoc. 94 | replace(/"\/css\/app\.css"/, "\"/app.css\""). 95 | replace(/"\/js\/config\.js"/, "\"/config.js\""). 96 | replace(/"\/js\/main\.js"/, "\"/app.js\"") 97 | ); 98 | 99 | grunt.file.copy(path.join(dir, "css", "app.css"), path.join(dir, "app.css")); 100 | grunt.file.copy(path.join(dir, "js", "config.js"), path.join(dir, "config.js")); 101 | grunt.file.copy(path.join(dir, "js", "main.js"), path.join(dir, "app.js")); 102 | 103 | rmTreeSync(path.join(dir, "build.txt")); 104 | rmTreeSync(path.join(dir, "css")); 105 | rmTreeSync(path.join(dir, "js")); 106 | rmTreeSync(path.join(dir, "templates")); 107 | rmTreeSync(path.join(dir, "vendor")); 108 | } 109 | } 110 | }); 111 | }; 112 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: lint build 2 | 3 | lint: 4 | @grunt lint 5 | 6 | build: 7 | @grunt build 8 | 9 | deploy: build 10 | rsync -rltDzv --delete build/www/ ospy@ospy.org:/home/ospy/www/ --exclude '.*' 11 | 12 | .PHONY: all lint build deploy 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CloudSpy 2 | ======== 3 | 4 | This is a proof-of-concept web app built on top of Frida. 5 | 6 | To get started: 7 | 8 | npm install 9 | node rel/server.js 10 | 11 | Then point your web browser at http://127.0.0.1:8000/ 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ospy", 3 | "version": "1.0.0", 4 | "description": "oSpy", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/frida/ospy" 8 | }, 9 | "keywords": [ 10 | "frida" 11 | ], 12 | "author": "Ole André Vadla Ravnås", 13 | "license": "GPL", 14 | "readmeFilename": "README.md", 15 | "devDependencies": { 16 | "base62": "~0.1.1", 17 | "future": "~2.3.1", 18 | "grunt": "0.4.x", 19 | "grunt-contrib-jshint": "0.1.x", 20 | "grunt-requirejs": "0.3.x", 21 | "less": "~1.3.3", 22 | "mongodb": "~1.2.13", 23 | "restify": "~2.3.2", 24 | "websocket": "~1.0.8" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rel/server.js: -------------------------------------------------------------------------------- 1 | /*global __dirname:false, process:false*/ 2 | (function () { 3 | 'use strict'; 4 | 5 | var EventEmitter = require('events').EventEmitter; 6 | var Future = require('future'); 7 | var WebSocketServer = require('websocket').server; 8 | var base62 = require('base62'); 9 | var fs = require('fs'); 10 | var mongo = require('mongodb'); 11 | var path = require('path'); 12 | var restify = require('restify'); 13 | var url = require('url'); 14 | var util = require('util'); 15 | 16 | var Server = function Server(options) { 17 | var server, 18 | wsServer, 19 | database = null, 20 | collection = {}, 21 | projects = {}, 22 | saveTimer = null, 23 | saving = null; 24 | 25 | var initialize = function initialize() { 26 | server = restify.createServer(); 27 | wsServer = new WebSocketServer({ 28 | httpServer: server 29 | }); 30 | wsServer.on('request', onWebSocketRequest); 31 | 32 | server.use(restify.acceptParser(server.acceptable)); 33 | server.use(restify.gzipResponse()); 34 | server.use(function (req, res, next) { 35 | var u = url.parse(req.url); 36 | var filepath = path.join(options.staticDir, u.pathname); 37 | fs.exists(filepath, function(exists) { 38 | if (!exists) { 39 | u.pathname = "/index.html"; 40 | req.url = url.format(u); 41 | req._url = u; 42 | req._path = u.pathname; 43 | } 44 | next(); 45 | }); 46 | }); 47 | server.get(/.*/, restify.serveStatic({ 48 | 'directory': options.staticDir, 49 | 'default': "index.html" 50 | })); 51 | }; 52 | 53 | this.dispose = function dispose() { 54 | if (saveTimer !== null) { 55 | clearInterval(saveTimer); 56 | saveTimer = null; 57 | } 58 | 59 | wsServer.removeListener('request', onWebSocketRequest); 60 | wsServer.shutDown(); 61 | server.close(); 62 | 63 | Object.keys(projects).forEach(function (projectId) { 64 | projects[projectId].shutDown(); 65 | }); 66 | 67 | saveAll().when(function () { 68 | projects = {}; 69 | collection = {}; 70 | if (database !== null) { 71 | database.close(); 72 | database = null; 73 | } 74 | 75 | wsServer = null; 76 | server = null; 77 | }); 78 | }; 79 | 80 | this.start = function start() { 81 | var future = Future.create(this); 82 | 83 | var db = new mongo.Db('ospy', new mongo.Server(options.database.host, options.database.port, {}), {w: 1}); 84 | db.open(function (error, client) { 85 | if (error) { 86 | future.fulfill(error); 87 | return; 88 | } 89 | database = client; 90 | 91 | client.collection('projects', function (error, c) { 92 | if (error) { 93 | future.fulfill(error); 94 | return; 95 | } 96 | collection.projects = c; 97 | 98 | client.collection('applications', function (error, c) { 99 | if (error) { 100 | future.fulfill(error); 101 | return; 102 | } 103 | collection.applications = c; 104 | 105 | server.listen(options.port, function () { 106 | console.log("%s listening at %s, serving static content from %s", server.name, server.url, options.staticDir); 107 | saveTimer = setInterval(saveAll, 5 * 60 * 1000); 108 | future.fulfill(null); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | return future; 115 | }; 116 | 117 | var onWebSocketRequest = function onWebSocketRequest(req) { 118 | var match = req.resourceURL.pathname.match(/^\/channel\/projects\/([^\/]+)$/); 119 | if (match !== null) { 120 | getProject(match[1]).when(function (error, project) { 121 | if (error) { 122 | req.reject(404, "Project Not Found"); 123 | } else { 124 | var connection = req.accept(null, req.origin); 125 | var session = new Session({connection: connection}); 126 | session.join(project); 127 | } 128 | }); 129 | } else { 130 | req.reject(404, "Handler Not Found"); 131 | } 132 | }; 133 | 134 | var getProject = function getProject(id) { 135 | var future = Future.create(this), 136 | projectId, 137 | project; 138 | if (id === 'undefined') { 139 | projectId = base62.encode(Math.floor(Math.random() * 0xffffffff)); 140 | project = new Project(projectId, { 141 | persisted: false, 142 | database: options.database 143 | }); 144 | projects[projectId] = project; 145 | console.log("New temporary project:", projectId); 146 | console.log("Projects alive now:", Object.keys(projects).length); 147 | project.once('suspendable', function () { 148 | delete projects[projectId]; 149 | console.log("Projects alive now:", Object.keys(projects).length); 150 | }); 151 | future.fulfill(null, project); 152 | } else { 153 | project = projects[id]; 154 | if (project) { 155 | future.fulfill(null, project); 156 | } else { 157 | project = new Project(id, { 158 | persisted: true, 159 | database: options.database 160 | }); 161 | project.load().when(function (error) { 162 | if (error) { 163 | future.fulfill(error); 164 | return; 165 | } 166 | if (!projects[id]) { 167 | projects[id] = project; 168 | project.once('suspendable', function () { 169 | delete projects[id]; 170 | console.log("Projects alive now:", Object.keys(projects).length); 171 | }); 172 | console.log("Loaded published project:", id); 173 | console.log("Projects alive now:", Object.keys(projects).length); 174 | } 175 | future.fulfill(null, projects[id]); 176 | }); 177 | } 178 | } 179 | return future; 180 | }; 181 | 182 | var saveAll = function saveAll() { 183 | if (saving !== null) { 184 | return saving; 185 | } 186 | 187 | var future = Future.create(this); 188 | saving = future; 189 | 190 | var pending = Object.keys(projects).length; 191 | var errors = 0; 192 | var onComplete = function onComplete(error) { 193 | if (error) { 194 | errors++; 195 | } 196 | pending--; 197 | if (pending === 0) { 198 | saving = null; 199 | if (errors) { 200 | future.fulfill("save failed"); 201 | } else { 202 | future.fulfill(null); 203 | } 204 | } 205 | }.bind(this); 206 | 207 | pending++; 208 | 209 | Object.keys(projects).forEach(function (projectId) { 210 | projects[projectId].save().when(onComplete); 211 | }); 212 | 213 | onComplete(null); 214 | 215 | return future; 216 | }.bind(this); 217 | 218 | var Project = function Project(id, options) { 219 | var shuttingDown = false, 220 | applications = {}, 221 | sessions = [], 222 | saving = null, 223 | suspendTimer = null; 224 | 225 | EventEmitter.call(this); 226 | 227 | applications['ospy:stream'] = new Stream(this); 228 | 229 | this.shutDown = function shutDown() { 230 | if (suspendTimer !== null) { 231 | clearTimeout(suspendTimer); 232 | suspendTimer = null; 233 | } 234 | shuttingDown = true; 235 | }; 236 | 237 | this.load = function load() { 238 | var future = Future.create(this); 239 | collection.projects.findOne({'_id': id}, function (error, doc) { 240 | if (error) { 241 | future.fulfill(error); 242 | return; 243 | } else if (doc === null) { 244 | future.fulfill("not found"); 245 | return; 246 | } 247 | var pending = Object.keys(applications).length; 248 | var errors = 0; 249 | var onComplete = function onComplete(error) { 250 | if (error) { 251 | errors++; 252 | } 253 | pending--; 254 | if (pending === 0) { 255 | if (errors) { 256 | future.fulfill("load failed"); 257 | } else { 258 | future.fulfill(null); 259 | } 260 | } 261 | }; 262 | Object.keys(applications).forEach(function (appName) { 263 | applications[appName].load().when(onComplete); 264 | }); 265 | }); 266 | return future; 267 | }; 268 | this.save = function save() { 269 | if (saving !== null) { 270 | return saving; 271 | } 272 | 273 | var future = Future.create(this); 274 | saving = future; 275 | 276 | if (options.persisted) { 277 | collection.projects.insert({ 278 | '_id': id, 279 | 'date_created': new Date() 280 | }, function (error) { 281 | if (error && error.code !== 11000) { 282 | saving = null; 283 | future.fulfill(error); 284 | return; 285 | } 286 | var pending = Object.keys(applications).length; 287 | var errors = 0; 288 | var onComplete = function onComplete(error) { 289 | if (error) { 290 | errors++; 291 | } 292 | pending--; 293 | if (pending === 0) { 294 | saving = null; 295 | if (errors) { 296 | future.fulfill("save failed"); 297 | } else { 298 | future.fulfill(null); 299 | } 300 | } 301 | }; 302 | Object.keys(applications).forEach(function (appName) { 303 | applications[appName].save().when(onComplete); 304 | }); 305 | }); 306 | } else { 307 | saving = null; 308 | future.fulfill("not published"); 309 | } 310 | 311 | return future; 312 | }; 313 | 314 | this.join = function join(session) { 315 | if (suspendTimer !== null) { 316 | clearTimeout(suspendTimer); 317 | suspendTimer = null; 318 | } 319 | 320 | sessions.push(session); 321 | Object.keys(applications).forEach(function (appName) { 322 | applications[appName].onJoin(session); 323 | }); 324 | }; 325 | this.leave = function leave(session) { 326 | sessions.splice(sessions.indexOf(session), 1); 327 | Object.keys(applications).forEach(function (appName) { 328 | applications[appName].onLeave(session); 329 | }); 330 | 331 | if (!shuttingDown && sessions.length === 0) { 332 | suspendTimer = setTimeout(function considerSuspend() { 333 | suspendTimer = null; 334 | this.save().when(function () { 335 | if (sessions.length === 0) { 336 | this.emit('suspendable'); 337 | } 338 | }); 339 | }.bind(this), 5000); 340 | } 341 | }; 342 | this.receive = function receive(stanza, session) { 343 | if (stanza.to === "/") { 344 | if (stanza.name === '.publish') { 345 | if (!options.persisted) { 346 | options.persisted = true; 347 | session.receive({ 348 | id: stanza.id, 349 | from: "/", 350 | name: '+result', 351 | payload: { 352 | _id: id 353 | } 354 | }); 355 | } else { 356 | session.receive({ 357 | id: stanza.id, 358 | from: "/", 359 | name: '+error', 360 | payload: { 361 | error: "already published" 362 | } 363 | }); 364 | } 365 | } 366 | } else { 367 | var match = stanza.to.match(/^\/applications\/([^\/]+)$/); 368 | if (match !== null) { 369 | var app = applications[match[1]]; 370 | if (app) { 371 | app.onStanza(stanza, session); 372 | } 373 | } 374 | } 375 | }; 376 | this.broadcast = function broadcast(stanza) { 377 | for (var i = 0; i !== sessions.length; i++) { 378 | sessions[i].receive(stanza); 379 | } 380 | }; 381 | 382 | Object.defineProperty(this, 'id', {value: id}); 383 | }; 384 | util.inherits(Project, EventEmitter); 385 | 386 | var Stream = function Stream(project) { 387 | var appId = 'ospy:stream', 388 | appAddress = "/applications/" + appId, 389 | items = [], 390 | itemById = {}, 391 | lastId = 0; 392 | 393 | this.load = function load() { 394 | var future = Future.create(this); 395 | collection.applications.findOne({ 396 | project: project.id, 397 | application: appId, 398 | }, function (error, doc) { 399 | if (error) { 400 | future.fulfill(error); 401 | return; 402 | } 403 | if (doc !== null) { 404 | items = doc.state['items']; 405 | lastId = doc.state['last_id']; 406 | addToIndex(items); 407 | } else { 408 | items = []; 409 | lastId = 0; 410 | } 411 | future.fulfill(null); 412 | }); 413 | return future; 414 | }; 415 | this.save = function save() { 416 | var future = Future.create(this); 417 | collection.applications.findOne({ 418 | project: project.id, 419 | application: appId, 420 | }, function (error, doc) { 421 | if (error) { 422 | future.fulfill(error); 423 | return; 424 | } 425 | if (doc === null) { 426 | doc = { 427 | project: project.id, 428 | application: appId, 429 | }; 430 | } 431 | doc.state = { 432 | 'items': items, 433 | 'last_id': lastId 434 | }; 435 | collection.applications.save(doc, function (error) { 436 | future.fulfill(error); 437 | }); 438 | }); 439 | return future; 440 | }; 441 | 442 | this.onJoin = function onJoin(session) { 443 | send(session, '+sync', {total: items.length}); 444 | }; 445 | this.onLeave = function onLeave(/*session*/) { 446 | }; 447 | this.onStanza = function onStanza(stanza, session) { 448 | switch (stanza.name) { 449 | case '+clear': 450 | items = []; 451 | broadcast('+sync', {total: items.length}); 452 | break; 453 | case '+add': 454 | (function () { 455 | var newItems = stanza.payload.items.map(function (item) { 456 | return { 457 | _id: lastId++, 458 | timestamp: JSON.parse(JSON.stringify(new Date())), 459 | event: item.event, 460 | payload: item.payload 461 | }; 462 | }); 463 | items.push.apply(items, newItems); 464 | addToIndex(newItems); 465 | broadcast('+update', {total: items.length}); 466 | }).call(this); 467 | break; 468 | case '.get': 469 | (function () { 470 | var result = []; 471 | var requestedItems = stanza.payload.items; 472 | for (var i = 0; i !== requestedItems.length; i++) { 473 | var item = itemById[requestedItems[i]._id]; 474 | if (item) { 475 | result.push(item); 476 | } else { 477 | session.receive({ 478 | id: stanza.id, 479 | from: appAddress, 480 | name: '+error', 481 | payload: {} 482 | }); 483 | return; 484 | } 485 | } 486 | session.receive({ 487 | id: stanza.id, 488 | from: appAddress, 489 | name: '+result', 490 | payload: result 491 | }); 492 | }).call(this); 493 | break; 494 | case '.get-at': 495 | (function () { 496 | var result = []; 497 | var requestedIndexes = stanza.payload.indexes; 498 | for (var i = 0; i !== requestedIndexes.length; i++) { 499 | var item = items[requestedIndexes[i]]; 500 | if (item) { 501 | result.push(item); 502 | } else { 503 | session.receive({ 504 | id: stanza.id, 505 | from: appAddress, 506 | name: '+error', 507 | payload: {} 508 | }); 509 | return; 510 | } 511 | } 512 | session.receive({ 513 | id: stanza.id, 514 | from: appAddress, 515 | name: '+result', 516 | payload: result 517 | }); 518 | }).call(this); 519 | break; 520 | case '.get-range': 521 | (function () { 522 | var startIndex = stanza.payload['start_index'], 523 | limit = stanza.payload['limit'], 524 | result; 525 | result = items.slice(startIndex, startIndex + limit); 526 | session.receive({ 527 | id: stanza.id, 528 | from: appAddress, 529 | name: '+result', 530 | payload: result 531 | }); 532 | }).call(this); 533 | break; 534 | } 535 | }; 536 | 537 | var addToIndex = function addToIndex(items) { 538 | items.forEach(function (item) { 539 | itemById[item._id] = item; 540 | }); 541 | }; 542 | 543 | var send = function send(session, name, payload) { 544 | session.receive({ 545 | from: appAddress, 546 | name: name, 547 | payload: payload 548 | }); 549 | }; 550 | var broadcast = function broadcast(name, payload) { 551 | project.broadcast({ 552 | from: appAddress, 553 | name: name, 554 | payload: payload 555 | }); 556 | }; 557 | }; 558 | 559 | var Session = function Session(options) { 560 | var connection = options.connection, 561 | project; 562 | 563 | this.join = function join(p) { 564 | project = p; 565 | p.join(this); 566 | }; 567 | this.receive = function receive(stanza) { 568 | connection.sendUTF(JSON.stringify(stanza)); 569 | }; 570 | 571 | var initialize = function initialize() { 572 | connection.once('close', onConnectionClose); 573 | connection.on('message', onConnectionMessage); 574 | }; 575 | var onConnectionClose = function onConnectionClose() { 576 | project.leave(this); 577 | connection.removeListener('message', onConnectionMessage); 578 | }.bind(this); 579 | var onConnectionMessage = function onConnectionMessage(message) { 580 | if (message.type === 'utf8' && message.utf8Data.length !== 0) { 581 | try { 582 | var stanza = JSON.parse(message.utf8Data); 583 | project.receive(stanza, this); 584 | } catch (e) { 585 | console.log("Error processing message. Closing connection."); 586 | connection.close(); 587 | } 588 | } 589 | }.bind(this); 590 | 591 | initialize(); 592 | }; 593 | 594 | initialize(); 595 | }; 596 | 597 | 598 | var server = new Server({ 599 | port: 8000, 600 | staticDir: path.join(__dirname, "..", "src"), 601 | database: { 602 | host: "127.0.0.1", 603 | port: 27017 604 | } 605 | }); 606 | server.start().when(function (error) { 607 | if (error) { 608 | console.log("Failed to start server:", error); 609 | server.dispose(); 610 | return; 611 | } 612 | console.log("Server started!"); 613 | console.log("Hit ENTER to stop."); 614 | process.stdin.resume(); 615 | process.stdin.setEncoding('utf8'); 616 | process.stdin.on('data', function () { 617 | process.stdin.pause(); 618 | console.log("Stopping server."); 619 | server.dispose(); 620 | server = null; 621 | }); 622 | }); 623 | })(); 624 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | /* This is a placeholder */ 2 | -------------------------------------------------------------------------------- /src/css/app.less: -------------------------------------------------------------------------------- 1 | .box-sizing(@sizing) { 2 | -webkit-box-sizing: @sizing; 3 | -moz-box-sizing: @sizing; 4 | box-sizing: @sizing; 5 | } 6 | 7 | .background-size(@size) { 8 | -webkit-background-size: @size; 9 | -moz-background-size: @size; 10 | -o-background-size: @size; 11 | background-size: @size; 12 | } 13 | 14 | .background-gradient(@start, @stop) { 15 | background: mix(@start, @stop, 50%); 16 | 17 | background: -moz-linear-gradient(top, @start 0%, @stop 100%); 18 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, @start), color-stop(100%, @stop)); /* Chrome, Safari4+ */ 19 | background: -webkit-linear-gradient(top, @start 0%, @stop 100%); /* Chrome10+, Safari5.1+ */ 20 | background: -o-linear-gradient(top, @start 0%, @stop 100%); 21 | background: -ms-linear-gradient(top, @start 0%, @stop 100%); 22 | background: linear-gradient(to bottom, @start 0%, @stop 100%); 23 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)", @start, @stop)); 24 | } 25 | 26 | .user-select(@select) { 27 | -webkit-user-select: @select; 28 | -moz-user-select: @select; 29 | -ms-user-select: @select; 30 | -o-user-select: @select; 31 | user-select: @select; 32 | } 33 | 34 | .transition(@transition) { 35 | -webkit-transition: @transition; 36 | -moz-transition: @transition; 37 | -o-transition: @transition; 38 | transition: @transition; 39 | } 40 | 41 | body { 42 | padding: 0; 43 | margin: 0; 44 | font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif; 45 | font-size: 11px; 46 | 47 | cursor: default; 48 | .user-select(none); 49 | 50 | -webkit-font-smoothing: antialiased; 51 | } 52 | table { 53 | border-collapse: collapse; 54 | .box-sizing(border-box); 55 | outline: none; 56 | } 57 | th, td { 58 | padding: 5px 3px 4px 3px; 59 | .box-sizing(border-box); 60 | } 61 | th { 62 | border: 1px solid #dedfe1; 63 | .background-gradient(#f1f1f1, #f7f8f9); 64 | text-align: left; 65 | font-weight: normal; 66 | } 67 | td { 68 | border: 1px solid #a0a0a0; 69 | background-color: white; 70 | } 71 | tr.selected { 72 | background-color: #4d94fd; 73 | color: white; 74 | td { 75 | background-color: inherit; 76 | color: inherit; 77 | } 78 | } 79 | 80 | .icon { 81 | display: inline-block; 82 | background: url(/img/icons.png) no-repeat top left; 83 | } 84 | .icon-info { background-position: 0 -102px; width: 16px; height: 16px; } 85 | .icon-warning { background-position: 0 -174px; width: 16px; height: 16px; } 86 | .icon-error { background-position: 0 -48px; width: 16px; height: 16px; } 87 | .icon-event { background-position: 0 -66px; width: 16px; height: 16px; } 88 | .icon-connected { background-position: 0 0; width: 22px; height: 22px; } 89 | .icon-disconnected { background-position: 0 -24px; width: 22px; height: 22px; } 90 | .icon-listening { background-position: 0 -120px; width: 16px; height: 16px; } 91 | .icon-incoming { background-position: 0 -84px; width: 16px; height: 16px; } 92 | .icon-outgoing { background-position: 0 -138px; width: 16px; height: 16px; } 93 | .icon-search { background-position: 0 -156px; width: 16px; height: 16px; } 94 | 95 | .app-content { 96 | padding: 6px; 97 | background-color: #bdd1ea; color: black; 98 | .box-sizing(border-box); 99 | } 100 | .project-actions { 101 | margin-bottom: 5px; 102 | } 103 | .attach-form { 104 | width: 400px; 105 | padding: 10px; 106 | border: 1px solid #ccc; 107 | margin-bottom: 5px; 108 | background-color: #f7f7f7; 109 | text-align: center; 110 | } 111 | .session { 112 | width: 370px; 113 | padding: 10px; 114 | border: 1px solid #ccc; 115 | margin-bottom: 10px; 116 | background-color: #f7f7f7; 117 | word-wrap: break-word; 118 | .status { 119 | display: inline-block; 120 | width: 100px; 121 | padding: 3px; 122 | margin-right: 2px; 123 | background-color: #ccc; 124 | font-weight: bold; 125 | text-align: center; 126 | } 127 | } 128 | .stream { 129 | position: relative; 130 | width: 100%; 131 | padding: 10px; 132 | border: 1px solid #ccc; 133 | background-color: #f7f7f7; 134 | .box-sizing(border-box); 135 | .stream-status { 136 | position: absolute; 137 | top: 10px; right: 10px; 138 | } 139 | .stream-actions { 140 | margin-bottom: 10px; 141 | } 142 | .stream-summary { 143 | table { 144 | width: 100%; 145 | } 146 | th:nth-child(1) { 147 | text-align: center; 148 | } 149 | td:nth-child(1) { 150 | min-width: 41px; 151 | text-align: center; 152 | } 153 | th:nth-child(2) { 154 | min-width: 36px; 155 | text-align: center; 156 | } 157 | td:nth-child(2) { 158 | min-width: 36px; 159 | padding-right: 0; padding-left: 0; 160 | text-align: center; 161 | } 162 | td:nth-child(3) { 163 | min-width: 70px; 164 | } 165 | td:nth-child(4) { 166 | min-width: 97px; 167 | } 168 | td:nth-child(5) { 169 | width: 100%; 170 | } 171 | .content { 172 | height: 190px; 173 | margin-bottom: 10px; 174 | overflow-y: scroll; 175 | } 176 | } 177 | .stream-details { 178 | height: 200px; 179 | padding: 2px; 180 | overflow-y: scroll; 181 | background-color: #808080; 182 | font-family: "Lucida Console", "Lucida Sans Typewriter", Monaco, "Bitstream Vera Sans Mono", monospace; 183 | font-weight: bold; 184 | .user-select(auto); 185 | .item-data { 186 | margin-bottom: 10px; 187 | } 188 | .prefix { 189 | margin-right: 5px; 190 | } 191 | .prefix-incoming { 192 | color: #8ae899; 193 | } 194 | .prefix-outgoing { 195 | color: #9cb7d1; 196 | } 197 | } 198 | } 199 | .notification { 200 | position: fixed; 201 | top: 40px; left: 20px; 202 | padding: 10px; 203 | border: 1px solid #ccc; 204 | background-color: #f7f7f7; 205 | text-align: center; 206 | font-weight: bold; 207 | } 208 | .notification-error { 209 | color: red; 210 | } 211 | -------------------------------------------------------------------------------- /src/img/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frida/cloudspy/79f68201660a0bbee9d68c5d8cc60277337e7121/src/img/icons.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | oSpy 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/js/app/presenters/app.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter', 'app/views/project', 'app/presenters/project'], function (Presenter, ProjectView, ProjectPresenter) { 2 | 'use strict'; 3 | 4 | var App = Presenter.define({ 5 | initialize: function initialize() { 6 | this.view.resume(); 7 | this.view.update({ 8 | loading: true, 9 | error: null 10 | }); 11 | this._currentPage = null; 12 | this._pages = {}; 13 | this._onLocationUpdated = this._onLocationUpdated.bind(this); 14 | this.services.bus.on('services.navigation:location-updated', this._onLocationUpdated); 15 | this._onLocationUpdated(this.services.navigation.location); 16 | }, 17 | dispose: function dispose() { 18 | var pages = this._pages; 19 | this._pages = {}; 20 | for (var pageId in pages) { 21 | if (this._pages.hasOwnProperty(pageId)) { 22 | var page = this._pages[pageId]; 23 | page.dispose(); 24 | page.view.dispose(); 25 | } 26 | } 27 | this.services.bus.off('services.navigation:location-updated', this._onLocationUpdated); 28 | App['__super__'].dispose.call(this); 29 | }, 30 | suspendPage: function suspendPage(page) { 31 | var deletable = null; 32 | for (var pageId in this._pages) { 33 | if (this._pages.hasOwnProperty(pageId)) { 34 | var p = this._pages[pageId]; 35 | if (p === page) { 36 | if (page.suspend()) { 37 | page.dispose(); 38 | page.view.dispose(); 39 | deletable = pageId; 40 | } 41 | break; 42 | } 43 | } 44 | } 45 | 46 | if (deletable !== null) { 47 | delete this._pages[deletable]; 48 | if (deletable === this._currentPage) { 49 | this._currentPage = null; 50 | this.view.update({ 51 | loading: true, 52 | error: null 53 | }); 54 | this.services.navigation.update([]); 55 | } 56 | } 57 | }, 58 | _onLocationUpdated: function _onLocationUpdated(location) { 59 | var pageId = location.join('/'), 60 | page = this._pages[pageId]; 61 | 62 | if (!page) { 63 | if (location.length === 0) { 64 | page = new ProjectPresenter(new ProjectView(), this.services, this, undefined); 65 | } else if (location.length === 2 && location[0] === 'p' && location[1]) { 66 | page = new ProjectPresenter(new ProjectView(), this.services, this, location[1]); 67 | } 68 | } 69 | 70 | if (this._pages[this._currentPage] && this._pages[this._currentPage].view.suspend) { 71 | this._pages[this._currentPage].view.suspend(); 72 | } 73 | 74 | if (page) { 75 | var request, 76 | deletable = [], 77 | id, 78 | p, 79 | i; 80 | 81 | this._currentPage = pageId; 82 | this._pages[pageId] = page; 83 | this.view.update({ 84 | loading: true, 85 | error: null 86 | }); 87 | this.view.show(page.view); 88 | 89 | request = page.resume(); 90 | request.done(function () { 91 | if (this._currentPage === pageId) { 92 | this.view.update({ 93 | loading: false, 94 | error: null 95 | }).done(function pageUpdated() { 96 | if (page.view.resume) { 97 | page.view.resume(); 98 | } 99 | }); 100 | } 101 | }.bind(this)); 102 | request.fail(function () { 103 | if (this._currentPage === pageId) { 104 | if (pageId) { 105 | this.services.navigation.update([]); 106 | } else { 107 | page.dispose(); 108 | page.view.dispose(); 109 | delete this._pages[pageId]; 110 | this._currentPage = null; 111 | this.view.update({ 112 | loading: false, 113 | error: "An error occurred. Please try again later." 114 | }); 115 | } 116 | } 117 | }.bind(this)); 118 | 119 | for (id in this._pages) { 120 | if (this._pages.hasOwnProperty(id)) { 121 | p = this._pages[id]; 122 | if (id !== pageId && p.suspend()) { 123 | deletable.push(id); 124 | } 125 | } 126 | } 127 | 128 | for (i = 0; i < deletable.length; i++) { 129 | id = deletable[i]; 130 | p = this._pages[id]; 131 | delete this._pages[id]; 132 | p.dispose(); 133 | p.view.dispose(); 134 | } 135 | } else { 136 | this.services.navigation.update([]); 137 | } 138 | } 139 | }); 140 | 141 | return App; 142 | }); 143 | -------------------------------------------------------------------------------- /src/js/app/presenters/capture.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter', 'app/views/capture/session', 'app/presenters/capture/session', 'deferred'], function (Presenter, SessionView, SessionPresenter, Deferred) { 2 | 'use strict'; 3 | 4 | var Capture = Presenter.define({ 5 | initialize: function initialize(project) { 6 | this._project = project; 7 | this._sessions = []; 8 | this._previouslySelectedDevice = null; 9 | this._items = []; 10 | this._itemsTimer = null; 11 | this._loading = new Deferred(); 12 | 13 | this._onDeviceSelected = this._onDeviceSelected.bind(this); 14 | this._onAttach = this._onAttach.bind(this); 15 | this.view.events.on('device-selected', this._onDeviceSelected); 16 | this.view.events.on('attach', this._onAttach); 17 | 18 | this._handler = { 19 | enable: function enable() { 20 | this.view.update({'frida-enabled': true}); 21 | }, 22 | disable: function disable() { 23 | this.view.update({'frida-enabled': false}); 24 | if (this._loading.state() === 'pending') { 25 | this.view.flushUpdates(); 26 | this._loading.resolve(); 27 | } 28 | }, 29 | update: function update() { 30 | this.view.update({devices: this.services.frida.devices}); 31 | if (this._loading.state() === 'pending') { 32 | this.view.flushUpdates(); 33 | this._loading.resolve(); 34 | } 35 | } 36 | }; 37 | this.services.frida.addHandler(this._handler, this); 38 | }, 39 | dispose: function dispose() { 40 | this.services.frida.removeHandler(this._handler); 41 | 42 | this.view.events.off('attach', this._onAttach); 43 | this.view.events.off('device-selected', this._onDeviceSelected); 44 | 45 | for (var i = 0; i !== this._sessions.length; i++) { 46 | var session = this._sessions[i]; 47 | session.dispose(); 48 | session.view.dispose(); 49 | } 50 | 51 | window.clearTimeout(this._itemsTimer); 52 | 53 | Capture['__super__'].dispose.call(this); 54 | }, 55 | load: function load() { 56 | return this._loading.promise(); 57 | }, 58 | _onDeviceSelected: function _onDeviceSelected(deviceId) { 59 | this.services.frida.enumerateProcesses(deviceId).done(function updateProcesses(processes) { 60 | this.view.update({processes: processes}); 61 | this._previouslySelectedDevice = deviceId; 62 | }.bind(this)).fail(function (error) { 63 | this.view.displayError(error); 64 | if (this._previouslySelectedDevice !== null) { 65 | var fallbackDevice = this._previouslySelectedDevice; 66 | this._previouslySelectedDevice = null; 67 | this.view.selectDevice(fallbackDevice); 68 | } else { 69 | this.view.update({processes: []}); 70 | } 71 | }.bind(this)); 72 | }, 73 | _onAttach: function _onAttach(deviceId, processId) { 74 | var presenter = null; 75 | for (var i = 0; i !== this._sessions.length && presenter === null; i++) { 76 | var p = this._sessions[i]; 77 | if (p.deviceId === deviceId && p.processId === processId) { 78 | presenter = p; 79 | } 80 | } 81 | if (presenter === null) { 82 | var view = new SessionView(); 83 | presenter = new SessionPresenter(view, this.services, this, deviceId, processId); 84 | this._sessions.push(presenter); 85 | this.view.addSession(view); 86 | } else { 87 | presenter.attach(); 88 | } 89 | }, 90 | _deleteSession: function _deleteSession(session) { 91 | this.view.removeSession(session.view); 92 | this._sessions.splice(this._sessions.indexOf(session), 1); 93 | session.dispose(); 94 | session.view.dispose(); 95 | }, 96 | _onEvent: function _onEvent(session, event, payload) { 97 | var data; 98 | if (payload) { 99 | data = payload.toString('base64'); 100 | } else { 101 | data = null; 102 | } 103 | var item = { 104 | event: event, 105 | payload: data 106 | }; 107 | this._items.push(item); 108 | 109 | if (this._itemsTimer === null) { 110 | this._itemsTimer = window.setTimeout(function deliverItems() { 111 | this._itemsTimer = null; 112 | this._project.stream.add(this._items); 113 | this._items = []; 114 | }.bind(this), 50); 115 | } 116 | } 117 | }); 118 | 119 | return Capture; 120 | }); 121 | -------------------------------------------------------------------------------- /src/js/app/presenters/capture/session.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter'], function (Presenter) { 2 | 'use strict'; 3 | 4 | var Session = Presenter.define({ 5 | initialize: function initialize(parent, deviceId, processId) { 6 | this._parent = parent; 7 | 8 | this._onDetach = this._onDetach.bind(this); 9 | this._onClose = this._onClose.bind(this); 10 | 11 | Object.defineProperty(this, 'deviceId', {value: deviceId}); 12 | Object.defineProperty(this, 'processId', {value: processId}); 13 | 14 | this._session = this.services.frida.getSession(deviceId, processId); 15 | 16 | this.view.update({ 17 | attached: false, 18 | error: null 19 | }); 20 | this._handler = { 21 | attach: function handleAttach() { 22 | this.view.update({attached: true}); 23 | }, 24 | detach: function handleDetach(error) { 25 | this.view.update({ 26 | attached: false, 27 | error: error 28 | }); 29 | }, 30 | event: function handleEvent(event, payload) { 31 | parent._onEvent(this, event, payload); 32 | } 33 | }; 34 | this._session.addHandler(this._handler, this); 35 | 36 | this.view.events.on('detach', this._onDetach); 37 | this.view.events.on('close', this._onClose); 38 | }, 39 | dispose: function dispose() { 40 | this.view.events.off('detach', this._onDetach); 41 | this.view.events.off('close', this._onClose); 42 | 43 | this._session.removeHandler(this._handler); 44 | 45 | Session['__super__'].dispose.call(this); 46 | }, 47 | attach: function attach() { 48 | this._session.attach(); 49 | }, 50 | _onDetach: function _onDetach() { 51 | this._session.detach(); 52 | }, 53 | _onClose: function _onClose() { 54 | this._parent._deleteSession(this); 55 | } 56 | }); 57 | 58 | return Session; 59 | }); 60 | -------------------------------------------------------------------------------- /src/js/app/presenters/presenter.js: -------------------------------------------------------------------------------- 1 | define(['extend'], function (extend) { 2 | 'use strict'; 3 | 4 | var Presenter = function Presenter(view, services) { 5 | if (view) { 6 | this.view = view; 7 | } else { 8 | this.view = null; 9 | } 10 | this.services = services; 11 | this.initialize.apply(this, [].slice.call(arguments, 2)); 12 | }; 13 | Presenter.prototype = { 14 | initialize: function initialize() {}, 15 | dispose: function dispose() {} 16 | }; 17 | Presenter.define = extend; 18 | 19 | return Presenter; 20 | }); 21 | -------------------------------------------------------------------------------- /src/js/app/presenters/project.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter', 'app/presenters/capture', 'app/presenters/stream', 'deferred'], function (Presenter, Capture, Stream, Deferred) { 2 | 'use strict'; 3 | 4 | var Project = Presenter.define({ 5 | initialize: function initialize(app, id) { 6 | this._project = this.services.ospy.get(id); 7 | this._loading = new Deferred(); 8 | 9 | this.capture = new Capture(this.view.capture, this.services, this._project); 10 | this.stream = new Stream(this.view.stream, this.services, this._project); 11 | 12 | this._handler = { 13 | join: function handleJoin() { 14 | Deferred.when(this.view.update({published: !!id}), this.capture.load(), this.stream.load()).done(function () { 15 | this._loading.resolve(); 16 | }.bind(this)).fail(function () { 17 | this._loading.reject(); 18 | }.bind(this)); 19 | }, 20 | leave: function handleLeave() { 21 | if (this._loading.state() === 'pending') { 22 | this._loading.reject(); 23 | } else { 24 | app.suspendPage(this); 25 | } 26 | } 27 | }; 28 | this._project.addHandler(this._handler, this); 29 | 30 | this._onPublish = this._onPublish.bind(this); 31 | this.view.events.on('publish', this._onPublish); 32 | }, 33 | dispose: function dispose() { 34 | this.view.events.off('publish', this._onPublish); 35 | 36 | this._project.removeHandler(this._handler); 37 | 38 | this.stream.dispose(); 39 | this.capture.dispose(); 40 | 41 | Project['__super__'].dispose.call(this); 42 | }, 43 | resume: function resume() { 44 | return this._loading.promise(); 45 | }, 46 | suspend: function suspend() { 47 | return true; 48 | }, 49 | _onPublish: function _onPublish() { 50 | this._project.publish().done(function (project) { 51 | this.services.navigation.update(['p', project._id]); 52 | }.bind(this)); 53 | } 54 | }); 55 | 56 | return Project; 57 | }); 58 | -------------------------------------------------------------------------------- /src/js/app/presenters/stream.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter', 'app/presenters/stream/summary', 'app/presenters/stream/details', 'deferred'], function (Presenter, Summary, Details, Deferred) { 2 | 'use strict'; 3 | 4 | var Stream = Presenter.define({ 5 | initialize: function initialize(project) { 6 | this._stream = project.stream; 7 | this._loading = new Deferred(); 8 | 9 | this.summary = new Summary(this.view.summary, this.services, this, this._stream); 10 | this.details = new Details(this.view.details, this.services, this, this._stream); 11 | 12 | this._handler = { 13 | update: function handleUpdate(data, isPartial) { 14 | if (!isPartial) { 15 | if (this._loading.state() === 'pending') { 16 | this._loading.resolve(); 17 | } 18 | } 19 | 20 | if ('total' in data) { 21 | this.view.update({total: data.total}); 22 | } 23 | } 24 | }; 25 | this._stream.addHandler(this._handler, this); 26 | 27 | this._onClear = this._onClear.bind(this); 28 | this.view.events.on('clear', this._onClear); 29 | }, 30 | dispose: function dispose() { 31 | this.view.events.off('clear', this._onClear); 32 | 33 | this._stream.removeHandler(this._handler); 34 | 35 | Stream['__super__'].dispose.call(this); 36 | }, 37 | load: function load() { 38 | return this._loading.promise(); 39 | }, 40 | _onClear: function _onClear() { 41 | this._stream.clear(); 42 | } 43 | }); 44 | 45 | return Stream; 46 | }); 47 | -------------------------------------------------------------------------------- /src/js/app/presenters/stream/details.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter'], function (Presenter) { 2 | 'use strict'; 3 | 4 | var Details = Presenter.define({ 5 | initialize: function initialize(parent, stream) { 6 | this._parent = parent; 7 | this._stream = stream; 8 | this._handler = { 9 | update: function handleUpdate(data, isPartial) { 10 | if (!isPartial) { 11 | this.view.clear(); 12 | } 13 | } 14 | }; 15 | this._stream.addHandler(this._handler, this); 16 | }, 17 | dispose: function dispose() { 18 | this._stream.removeHandler(this._handler); 19 | 20 | Details['__super__'].dispose.call(this); 21 | }, 22 | load: function load(indexes) { 23 | this._stream.getAt(indexes).done(function (items) { 24 | this.view.clear(); 25 | this.view.add(items); 26 | }.bind(this)); 27 | } 28 | }); 29 | 30 | return Details; 31 | }); 32 | -------------------------------------------------------------------------------- /src/js/app/presenters/stream/summary.js: -------------------------------------------------------------------------------- 1 | define(['app/presenters/presenter'], function (Presenter) { 2 | 'use strict'; 3 | 4 | var Summary = Presenter.define({ 5 | initialize: function initialize(parent, stream) { 6 | this._parent = parent; 7 | this._stream = stream; 8 | this._loaded = false; 9 | this._lastGetSeq = 0; 10 | 11 | this.view.presenter = this; 12 | 13 | this._handler = { 14 | update: function handleUpdate() { 15 | this._loaded = true; 16 | this.view.render(); 17 | } 18 | }; 19 | this._stream.addHandler(this._handler, this); 20 | 21 | this._onSelect = this._onSelect.bind(this); 22 | this.view.events.on('select', this._onSelect); 23 | }, 24 | dispose: function dispose() { 25 | this.view.events.off('select', this._onSelect); 26 | 27 | this._stream.removeHandler(this._handler); 28 | 29 | Summary['__super__'].dispose.call(this); 30 | }, 31 | getTotal: function getTotal() { 32 | if (this._loaded) { 33 | return this._stream.state.total; 34 | } else { 35 | return -1; 36 | } 37 | }, 38 | getItems: function getItems(startIndex, limit) { 39 | this._lastGetSeq++; 40 | var seq = this._lastGetSeq; 41 | return this._stream.getCachedRange(startIndex, limit, function (error, result) { 42 | if (!error && result.source === 'server' && seq === this._lastGetSeq) { 43 | this.view.render(); 44 | } 45 | }.bind(this)); 46 | }, 47 | _onSelect: function _onSelect(indexes) { 48 | indexes = indexes.slice(0).sort(function (a, b) { 49 | return a - b; 50 | }); 51 | this._parent.details.load(indexes); 52 | } 53 | }); 54 | 55 | return Summary; 56 | }); 57 | -------------------------------------------------------------------------------- /src/js/app/services/frida.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service', 'app/utils'], function (Service, utils) { 2 | 'use strict'; 3 | 4 | var FRIDA_MIME_TYPE = 'application/x-vnd-frida'; 5 | 6 | var Frida = Service.define({ 7 | initialize: function initialize() { 8 | this.devices = null; 9 | this._state = 'disabled'; 10 | this._plugin = null; 11 | this._handlers = []; 12 | this._timer = null; 13 | this._sessions = {}; 14 | this._updateDevices = this._updateDevices.bind(this); 15 | this._onDetach = this._onDetach.bind(this); 16 | this._onMessage = this._onMessage.bind(this); 17 | }, 18 | dispose: function dispose() { 19 | window.clearInterval(this._timer); 20 | }, 21 | addHandler: function addHandler(handler, context) { 22 | context = context || handler; 23 | this._handlers.push(utils.bind(handler, context)); 24 | switch (this._state) { 25 | case 'disabled': 26 | if (pluginIsInstalled()) { 27 | this._loadPlugin(); 28 | this._state = 'enabled'; 29 | this._executeHandlers('enable'); 30 | this._updateDevices(); 31 | } else { 32 | if (handler['disable']) { 33 | handler['disable'].call(context, 'missing-plugin'); 34 | } 35 | } 36 | if (this._timer === null) { 37 | this._timer = window.setInterval(function checkPluginState() { 38 | if (pluginIsInstalled()) { 39 | if (this._state === 'disabled') { 40 | this._loadPlugin(); 41 | this._state = 'enabled'; 42 | this._executeHandlers('enable'); 43 | this._updateDevices(); 44 | } 45 | } else { 46 | if (this._state === 'enabled') { 47 | this.devices = null; 48 | this._unloadPlugin(); 49 | this._state = 'disabled'; 50 | this._executeHandlers('disable', 'missing-plugin'); 51 | } 52 | } 53 | }.bind(this), 1000); 54 | } 55 | break; 56 | case 'enabled': 57 | if (handler['enable']) { 58 | handler['enable'].call(context); 59 | } 60 | if (handler['update'] && this.devices !== null) { 61 | handler['update'].call(context); 62 | } 63 | break; 64 | } 65 | }, 66 | removeHandler: function removeHandler(handler) { 67 | this._handlers = this._handlers.filter(function (h) { 68 | return h !== handler; 69 | }); 70 | }, 71 | enumerateProcesses: function enumerateProcesses(deviceId) { 72 | try { 73 | return this._plugin.enumerateProcesses(deviceId); 74 | } catch (e) { 75 | this._onCrash(e); 76 | return new Deferred().reject('plugin crashed').promise(); 77 | } 78 | }, 79 | getSession: function getSession(deviceId, processId) { 80 | var id = sessionId(deviceId, processId); 81 | var session = this._sessions[id]; 82 | if (!session) { 83 | session = new Session(this, this._plugin, deviceId, processId); 84 | this._sessions[id] = session; 85 | } 86 | return session; 87 | }, 88 | _executeHandlers: function _executeHandlers(name) { 89 | var args = [].slice.call(arguments, 1); 90 | this._handlers.forEach(function executeHandler(handler) { 91 | if (handler[name]) { 92 | handler[name].apply(handler, args); 93 | } 94 | }); 95 | }, 96 | _loadPlugin: function _loadPlugin() { 97 | this._plugin = document.createElement('embed'); 98 | this._plugin.style.position = 'fixed'; 99 | this._plugin.style.top = "-1px"; 100 | this._plugin.style.left = "-1px"; 101 | this._plugin.style.width = "1px"; 102 | this._plugin.style.height = "1px"; 103 | this._plugin.type = FRIDA_MIME_TYPE; 104 | document.body.appendChild(this._plugin); 105 | this._plugin.addEventListener('devices-changed', this._updateDevices); 106 | this._plugin.addEventListener('detach', this._onDetach); 107 | this._plugin.addEventListener('message', this._onMessage); 108 | }, 109 | _unloadPlugin: function _unloadPlugin() { 110 | this._plugin.removeEventListener('message', this._onMessage); 111 | this._plugin.removeEventListener('detach', this._onDetach); 112 | this._plugin.removeEventListener('devices-changed', this._updateDevices); 113 | document.body.removeChild(this._plugin); 114 | this._plugin = null; 115 | }, 116 | _updateDevices: function _updateDevices() { 117 | try { 118 | this._plugin.enumerateDevices().done(function (devices) { 119 | this.devices = devices; 120 | this._executeHandlers('update'); 121 | }.bind(this)); 122 | } catch (e) { 123 | this._onCrash(e); 124 | } 125 | }, 126 | _onCrash: function _onCrash() { 127 | this.devices = null; 128 | this._unloadPlugin(); 129 | this._state = 'disabled'; 130 | this._executeHandlers('disable', 'plugin-crashed'); 131 | }, 132 | _onDetach: function _onDetach(deviceId, processId) { 133 | var session = this._sessions[sessionId(deviceId, processId)]; 134 | if (session) { 135 | session.detach(); 136 | } 137 | }, 138 | _onMessage: function _onMessage(deviceId, processId, message, data) { 139 | var session = this._sessions[sessionId(deviceId, processId)]; 140 | if (session) { 141 | session._onMessage(message, data); 142 | } 143 | }, 144 | _deleteSession: function _deleteSession(session) { 145 | for (var id in this._sessions) { 146 | if (this._sessions.hasOwnProperty(id)) { 147 | var s = this._sessions[id]; 148 | if (s === session) { 149 | delete this._sessions[id]; 150 | return; 151 | } 152 | } 153 | } 154 | } 155 | }); 156 | 157 | var Session = function Session(service, plugin, deviceId, processId) { 158 | var state = 'detached', 159 | handlers = []; 160 | 161 | this.addHandler = function addHandler(handler, context) { 162 | handlers.push(utils.bind(handler, context)); 163 | switch (state) { 164 | case 'detached': 165 | this.attach(); 166 | break; 167 | case 'attaching': 168 | break; 169 | case 'attached': 170 | if (handler['attach']) { 171 | handler['attach'].call(context); 172 | } 173 | break; 174 | } 175 | }; 176 | this.removeHandler = function removeHandler(handler) { 177 | handlers = handlers.filter(function (h) { 178 | return h !== handler; 179 | }); 180 | if (handlers.length === 0) { 181 | this.detach(); 182 | service._deleteSession(this); 183 | } 184 | }; 185 | this.attach = function attach() { 186 | if (state === 'detached') { 187 | var rawScript = Script.toString(); 188 | var rawBody = rawScript.substring(rawScript.indexOf("{") + 1, rawScript.lastIndexOf("}")); 189 | var operation; 190 | try { 191 | operation = plugin.attachTo(deviceId, processId, rawBody); 192 | } catch (e) { 193 | service._onCrash(e); 194 | return; 195 | } 196 | state = 'attaching'; 197 | operation.done(function handleAttachSuccess() { 198 | if (state === 'attaching') { 199 | state = 'attached'; 200 | executeHandlers('attach'); 201 | } 202 | }); 203 | operation.fail(function handleAttachFailure(reason) { 204 | if (state === 'attaching') { 205 | state = 'detached'; 206 | executeHandlers('detach', reason); 207 | } 208 | }); 209 | } 210 | }; 211 | this.detach = function detach() { 212 | if (state !== 'detached') { 213 | try { 214 | plugin.detachFrom(deviceId, processId); 215 | } catch (e) { 216 | service._onCrash(e); 217 | } 218 | state = 'detached'; 219 | executeHandlers('detach', null); 220 | } 221 | }; 222 | this._onMessage = function _onMessage(message, data) { 223 | if (message.type === 'send') { 224 | executeHandlers('event', message.payload, data); 225 | } else { 226 | console.log("_onMessage", message, data); 227 | } 228 | }; 229 | 230 | var executeHandlers = function executeHandlers(name) { 231 | var args = [].slice.call(arguments, 1); 232 | handlers.forEach(function executeHandler(handler) { 233 | if (handler[name]) { 234 | handler[name].apply(handler, args); 235 | } 236 | }); 237 | }; 238 | }; 239 | 240 | var Script = function Script() { 241 | /*global Interceptor:false, Memory:false, Module:false, Process:false, send:false */ 242 | /*jshint bitwise:false*/ 243 | 244 | var AF_INET = 2; 245 | 246 | var connect = null; 247 | 248 | if (Process.platform === 'darwin') { 249 | connect = Module.findExportByName('libSystem.B.dylib', 'connect$UNIX2003'); 250 | if (!connect) { 251 | connect = Module.findExportByName('libSystem.B.dylib', 'connect'); 252 | } 253 | } 254 | 255 | if (connect) { 256 | Interceptor.attach(connect, { 257 | onEnter: function(args) { 258 | var fd = args[0].toInt32(); 259 | var sockAddr = args[1]; 260 | var family; 261 | if (Process.platform === 'windows') { 262 | family = Memory.readU8(sockAddr); 263 | } else { 264 | family = Memory.readU8(sockAddr.add(1)); 265 | } 266 | if (family === AF_INET) { 267 | var port = (Memory.readU8(sockAddr.add(2)) << 8) | Memory.readU8(sockAddr.add(3)); 268 | var ip = 269 | Memory.readU8(sockAddr.add(4)) + "." + 270 | Memory.readU8(sockAddr.add(5)) + "." + 271 | Memory.readU8(sockAddr.add(6)) + "." + 272 | Memory.readU8(sockAddr.add(7)); 273 | send({ 274 | name: 'connect', 275 | type: 'event', 276 | properties: { 277 | fd: fd, 278 | ip: ip, 279 | port: port 280 | } 281 | }); 282 | } 283 | } 284 | }); 285 | } 286 | 287 | if (Process.platform === 'darwin') { 288 | var cryptorCreate = Module.findExportByName("libcommonCrypto.dylib", "CCCryptorCreate"); 289 | var cryptorCreateFromData = Module.findExportByName("libcommonCrypto.dylib", "CCCryptorCreateFromData"); 290 | var cryptorRelease = Module.findExportByName("libcommonCrypto.dylib", "CCCryptorRelease"); 291 | var cryptorUpdate = Module.findExportByName("libcommonCrypto.dylib", "CCCryptorUpdate"); 292 | 293 | var kCCSuccess = 0; 294 | 295 | var ccOperation = { 296 | 0: 'kCCEncrypt', 297 | 1: 'kCCDecrypt' 298 | }; 299 | var kCCEncrypt = 0; 300 | var kCCDecrypt = 1; 301 | 302 | var ccAlgorithm = { 303 | 0: 'kCCAlgorithmAES128', 304 | 1: 'kCCAlgorithmDES', 305 | 2: 'kCCAlgorithm3DES', 306 | 3: 'kCCAlgorithmCAST', 307 | 4: 'kCCAlgorithmRC4', 308 | 5: 'kCCAlgorithmRC2' 309 | }; 310 | 311 | if (cryptorCreate && cryptorCreateFromData && cryptorRelease && cryptorUpdate) { 312 | 313 | var cryptors = {}; 314 | var lastCryptorId = 1; 315 | 316 | var createCryptor = function createCryptor(op, alg) { 317 | return { 318 | id: lastCryptorId++, 319 | op: op, 320 | alg: alg, 321 | properties: { 322 | handle: null, 323 | op: ccOperation[op] || 'kCCInvalid', 324 | alg: ccAlgorithm[alg] || 'kCCAlgorithmInvalid', 325 | } 326 | }; 327 | }; 328 | 329 | var registerCryptor = function registerCryptor(cryptor, handle) { 330 | cryptor.properties.handle = "0x" + handle.toString(16); 331 | cryptors[handle.toString()] = cryptor; 332 | }; 333 | 334 | Interceptor.attach(cryptorCreate, { 335 | onEnter: function (args) { 336 | this.cryptor = createCryptor(args[0].toInt32(), args[1].toInt32()); 337 | this.cryptorRef = args[6]; 338 | }, 339 | onLeave: function (retval) { 340 | if (retval.toInt32() === kCCSuccess) { 341 | var handle = Memory.readPointer(this.cryptorRef); 342 | registerCryptor(this.cryptor, handle); 343 | send({ 344 | name: 'CCCryptorCreate', 345 | type: 'event', 346 | properties: this.cryptor.properties 347 | }); 348 | } 349 | } 350 | }); 351 | Interceptor.attach(cryptorCreateFromData, { 352 | onEnter: function (args) { 353 | if (this.depth === 0) { 354 | this.cryptor = createCryptor(args[0].toInt32(), args[1].toInt32()); 355 | this.cryptorRef = args[8]; 356 | } 357 | }, 358 | onLeave: function (retval) { 359 | if (this.depth === 0 && retval.toInt32() === kCCSuccess) { 360 | var handle = Memory.readPointer(this.cryptorRef); 361 | registerCryptor(this.cryptor, handle); 362 | send({ 363 | name: 'CCCryptorCreateFromData', 364 | type: 'event', 365 | properties: this.cryptor.properties 366 | }); 367 | } 368 | } 369 | }); 370 | 371 | Interceptor.attach(cryptorRelease, { 372 | onEnter: function (args) { 373 | send({ 374 | name: 'CCCryptorRelease', 375 | type: 'event', 376 | properties: { 377 | handle: "0x" + args[0].toString(16) 378 | } 379 | }); 380 | delete cryptors[args[0].toString()]; 381 | } 382 | }); 383 | 384 | Interceptor.attach(cryptorUpdate, { 385 | onEnter: function (args) { 386 | this.cryptor = cryptors[args[0].toString()]; 387 | if (this.cryptor) { 388 | if (this.cryptor.op === kCCEncrypt) { 389 | this.data = Memory.readByteArray(args[1], args[2].toInt32()); 390 | } else { 391 | this.dataOut = args[3]; 392 | this.dataOutMoved = args[5]; 393 | } 394 | } else { 395 | send({ 396 | name: 'CCCryptorUpdate', 397 | type: 'warning', 398 | properties: { 399 | message: "unknown cryptor handle: 0x" + args[0].toString(16) 400 | } 401 | }); 402 | } 403 | }, 404 | onLeave: function (retval) { 405 | if (retval.toInt32() === kCCSuccess && this.cryptor) { 406 | var cryptor = this.cryptor, 407 | data; 408 | if (cryptor.op === kCCEncrypt) { 409 | data = this.data; 410 | } else { 411 | data = Memory.readByteArray(this.dataOut, Memory.readPointer(this.dataOutMoved).toInt32()); 412 | } 413 | send({ 414 | name: 'CCCryptorUpdate', 415 | type: (cryptor.op === kCCDecrypt) ? 'incoming' : 'outgoing', 416 | properties: cryptor.properties, 417 | dataLength: data.length 418 | }, data); 419 | } 420 | } 421 | }); 422 | } 423 | } 424 | }; 425 | 426 | var pluginIsInstalled = function pluginIsInstalled() { 427 | var mimeTypes = window.navigator.mimeTypes; 428 | for (var i = 0; i !== mimeTypes.length; i++) { 429 | if (mimeTypes[i].type === FRIDA_MIME_TYPE) { 430 | return true; 431 | } 432 | } 433 | return false; 434 | }; 435 | 436 | var sessionId = function sessionId(deviceId, processId) { 437 | return deviceId + "|" + processId; 438 | }; 439 | 440 | return Frida; 441 | }); 442 | -------------------------------------------------------------------------------- /src/js/app/services/messagebus.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service', 'events'], function (Service, Events) { 2 | 'use strict'; 3 | 4 | var MessageBus = Service.define({ 5 | initialize: function initialize() { 6 | this._private.events = new Events(); 7 | }, 8 | on: function on(type, callback) { 9 | this._private.events.on(type, callback); 10 | }, 11 | off: function off(type, callback) { 12 | this._private.events.off(type, callback); 13 | }, 14 | post: function post(type, message) { 15 | this._private.events.trigger(type, message); 16 | } 17 | }); 18 | 19 | return MessageBus; 20 | }); 21 | -------------------------------------------------------------------------------- /src/js/app/services/navigation.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service'], function (Service) { 2 | 'use strict'; 3 | 4 | var Navigation = Service.define({ 5 | initialize: function initialize() { 6 | if (hasPushState) { 7 | if (window.location.hash.indexOf("#!/") === 0) { 8 | this.location = getHashPath(); 9 | window.location.replace(getRoot() + unparsePath(this.location)); 10 | } else { 11 | this.location = getPath(); 12 | window.location.hash = ""; 13 | } 14 | window.addEventListener('popstate', this, false); 15 | } else { 16 | if (window.location.pathname.length > 1) { 17 | this.location = getLocationPath(); 18 | window.location.replace(getRoot() + "/#!" + unparsePath(this.location)); 19 | } else { 20 | this.location = getPath(); 21 | } 22 | window.addEventListener('hashchange', this, false); 23 | } 24 | 25 | this._onAnchorClick = this._onAnchorClick.bind(this); 26 | document.addEventListener('click', this._onAnchorClick, false); 27 | }, 28 | dispose: function dispose() { 29 | document.removeEventListener('click', this._onAnchorClick, false); 30 | if (hasPushState) { 31 | window.removeEventListener('popstate', this, false); 32 | } else { 33 | window.removeEventListener('hashchange', this, false); 34 | } 35 | Navigation['__super__'].dispose.call(this); 36 | }, 37 | url: function url(path) { 38 | return hasPushState ? unparsePath(path) : "#!" + unparsePath(path); 39 | }, 40 | update: function update(path) { 41 | if (unparsePath(path) !== unparsePath(this.location)) { 42 | if (hasPushState) { 43 | window.history.pushState(null, null, unparsePath(path)); 44 | this.location = path.slice(0); 45 | this.services.bus.post('services.navigation:location-updated', this.location); 46 | } else { 47 | window.location.hash = "#!" + unparsePath(path); 48 | } 49 | } 50 | }, 51 | handleEvent: function handleEvent(event) { 52 | if (event.type === 'popstate' || event.type === 'hashchange') { 53 | var location = getPath(); 54 | if (unparsePath(location) !== unparsePath(this.location)) { 55 | this.location = location; 56 | this.services.bus.post('services.navigation:location-updated', this.location); 57 | } 58 | } 59 | }, 60 | _onAnchorClick: function _onAnchorClick(event) { 61 | var current = event.target, 62 | anchor = null; 63 | 64 | while (current && anchor === null) { 65 | if (current.tagName === "A") { 66 | anchor = current; 67 | } 68 | current = current.parentElement; 69 | } 70 | 71 | if (anchor !== null) { 72 | var href = anchor.getAttribute('href'); 73 | if (href && href.indexOf("/") === 0) { 74 | try { 75 | this.update(parsePath(href)); 76 | } catch (e) { 77 | console.error("Navigation#update failed:", e.stack); 78 | } 79 | event.preventDefault(); 80 | } 81 | } 82 | } 83 | }); 84 | 85 | var hasPushState = window.history && window.history.pushState; 86 | 87 | var getRoot = function getRoot() { 88 | return window.location.protocol + "//" + window.location.host; 89 | }; 90 | 91 | var getPath = function getPath() { 92 | return hasPushState ? getLocationPath() : getHashPath(); 93 | }; 94 | 95 | var getHashPath = function getHashPath() { 96 | if (window.location.hash.indexOf("#!/") === 0) { 97 | return parsePath(window.location.hash.substring(2)); 98 | } else { 99 | return []; 100 | } 101 | }; 102 | 103 | var getLocationPath = function getLocationPath() { 104 | if (window.location.pathname.length > 1) { 105 | return parsePath(window.location.pathname); 106 | } else { 107 | return []; 108 | } 109 | }; 110 | 111 | var parsePath = function parsePath(path) { 112 | var components = path.split("/"), 113 | result = []; 114 | for (var i = 0; i < components.length; i++) { 115 | if (components[i] !== "") { 116 | result.push(decodeURIComponent(components[i])); 117 | } 118 | } 119 | return result; 120 | }; 121 | 122 | var unparsePath = function unparsePath(path) { 123 | return "/" + path.join("/"); 124 | }; 125 | 126 | return Navigation; 127 | }); 128 | -------------------------------------------------------------------------------- /src/js/app/services/ospy.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service', 'app/utils', 'deferred', 'lru'], function (Service, utils, Deferred, LRUCache) { 2 | 'use strict'; 3 | 4 | var OSpy = Service.define({ 5 | initialize: function initialize() { 6 | this._projects = {}; 7 | }, 8 | dispose: function dispose() { 9 | for (var id in this._projects) { 10 | if (this._projects.hasOwnProperty(id)) { 11 | this._projects[id].dispose(); 12 | } 13 | } 14 | }, 15 | get: function get(id) { 16 | var project = this._projects[id]; 17 | if (!project) { 18 | project = new Project(this, id); 19 | this._projects[id] = project; 20 | } 21 | return project; 22 | }, 23 | _delete: function _delete(id) { 24 | var project = this._projects[id]; 25 | project.dispose(); 26 | delete this._projects[id]; 27 | } 28 | }); 29 | 30 | var Project = function Project(service, id) { 31 | var state = 'not-joined', 32 | socket = null, 33 | requests = {}, 34 | lastRequestId = 0, 35 | stream = new Stream(this), 36 | handlers = []; 37 | 38 | this.dispose = function dispose() { 39 | }; 40 | 41 | this.addHandler = function addHandler(handler, context) { 42 | context = context || handler; 43 | handlers.push(utils.bind(handler, context)); 44 | switch (state) { 45 | case 'not-joined': 46 | state = 'joining'; 47 | socket = new window.WebSocket("ws://" + service.services.settings.get('ospy.host') + "/channel/projects/" + id); 48 | socket.onopen = onSocketOpen; 49 | socket.onclose = onSocketClose; 50 | socket.onmessage = onSocketMessage; 51 | break; 52 | case 'joining': 53 | break; 54 | case 'joined': 55 | if (handler['join']) { 56 | handler['join'].call(context); 57 | } 58 | break; 59 | } 60 | }; 61 | this.removeHandler = function removeHandler(handler) { 62 | handlers = handlers.filter(function (h) { 63 | return h !== handler; 64 | }); 65 | if (handlers.length === 0 && state !== 'not-joined') { 66 | socket.onopen = null; 67 | socket.onclose = null; 68 | socket.onmessage = null; 69 | socket.close(); 70 | socket = null; 71 | state = 'not-joined'; 72 | service._delete(id); 73 | } 74 | }; 75 | 76 | this.publish = function publish() { 77 | return this._request({ 78 | to: "/", 79 | name: '.publish', 80 | payload: {} 81 | }); 82 | }; 83 | 84 | Object.defineProperty(this, 'stream', { 85 | get: function get() { return stream; } 86 | }); 87 | 88 | this._request = function _request(stanza) { 89 | var deferred = new Deferred(); 90 | var id = lastRequestId++; 91 | stanza.id = id; 92 | requests[id] = deferred; 93 | this._send(stanza); 94 | return deferred; 95 | }; 96 | 97 | this._send = function _send(stanza) { 98 | socket.send(JSON.stringify(stanza)); 99 | }; 100 | 101 | var onSocketOpen = function onSocketOpen() { 102 | executeHandlers('join'); 103 | }; 104 | var onSocketClose = function onSocketClose() { 105 | socket.onopen = null; 106 | socket.onclose = null; 107 | socket.onmessage = null; 108 | socket = null; 109 | state = 'not-joined'; 110 | executeHandlers('leave'); 111 | }; 112 | var onSocketMessage = function onSocketMessage(event) { 113 | var stanza = JSON.parse(event.data); 114 | if (stanza.hasOwnProperty('id')) { 115 | var deferred = requests[stanza.id]; 116 | if (stanza.name === '+result') { 117 | deferred.resolve(stanza.payload); 118 | } else if (stanza.name === '+error') { 119 | deferred.reject(stanza.payload); 120 | } 121 | } else { 122 | if (stanza.from === "/applications/ospy:stream") { 123 | stream._handleStanza(stanza); 124 | } else { 125 | console.log("onSocketMessage", stanza); 126 | } 127 | } 128 | }; 129 | 130 | var executeHandlers = function executeHandlers(name) { 131 | var args = [].slice.call(arguments, 1); 132 | handlers.forEach(function executeHandler(handler) { 133 | if (handler[name]) { 134 | handler[name].apply(handler, args); 135 | } 136 | }); 137 | }; 138 | }; 139 | 140 | var Stream = function Stream(project) { 141 | var state = null, 142 | itemsCache = null, 143 | rangeCache = null, 144 | pending = null, 145 | handlers = []; 146 | 147 | this.addHandler = function addHandler(handler, context) { 148 | context = context || handler; 149 | handler = utils.bind(handler, context); 150 | if (state !== null) { 151 | if (handler['update']) { 152 | handler['update'].call(context, state, false); 153 | } 154 | } 155 | handlers.push(handler); 156 | }; 157 | this.removeHandler = function removeHandler(handler) { 158 | handlers = handlers.filter(function (h) { 159 | return h !== handler; 160 | }); 161 | }; 162 | 163 | this.clear = function clear() { 164 | send('+clear', {}); 165 | }; 166 | this.add = function add(items) { 167 | send('+add', {items: items}); 168 | }; 169 | this.get = function get(items) { 170 | var deferred = new Deferred(); 171 | 172 | var result = []; 173 | var missing = []; 174 | for (var i = 0; i !== items.length; i++) { 175 | var item = itemsCache.get(items[i]._id); 176 | if (item) { 177 | result.push(item); 178 | } else { 179 | result.push(null); 180 | missing.push(items[i]); 181 | } 182 | } 183 | 184 | if (missing.length === 0) { 185 | deferred.resolve(result); 186 | } else { 187 | var req = request('.get', {items: missing}); 188 | req.done(function (remaining) { 189 | for (var i = 0; i !== result.length; i++) { 190 | if (result[i] === null) { 191 | var item = remaining.shift(); 192 | itemsCache.put(item._id, item); 193 | result[i] = item; 194 | } 195 | } 196 | deferred.resolve(result); 197 | }); 198 | req.fail(function (error) { 199 | deferred.reject(error); 200 | }); 201 | } 202 | 203 | return deferred.promise(); 204 | }; 205 | this.getAt = function getAt(indexes) { 206 | var deferred = new Deferred(); 207 | 208 | var result = []; 209 | var missing = []; 210 | for (var i = 0; i !== indexes.length; i++) { 211 | var item = rangeCache.get(indexes[i]); 212 | if (item) { 213 | result.push(item); 214 | } else { 215 | result.push(null); 216 | missing.push(indexes[i]); 217 | } 218 | } 219 | 220 | if (missing.length === 0) { 221 | deferred.resolve(result); 222 | } else { 223 | var req = request('.get-at', {indexes: missing}); 224 | req.done(function (remaining) { 225 | for (var i = 0; i !== result.length; i++) { 226 | if (result[i] === null) { 227 | var item = remaining.shift(); 228 | rangeCache.put(indexes[i], item); 229 | result[i] = item; 230 | } 231 | } 232 | deferred.resolve(result); 233 | }); 234 | req.fail(function (error) { 235 | deferred.reject(error); 236 | }); 237 | } 238 | 239 | return deferred.promise(); 240 | }; 241 | this.getRange = function getRange(startIndex, limit) { 242 | var deferred = new Deferred(); 243 | 244 | this.getCachedRange(startIndex, limit, function (error, result) { 245 | if (error) { 246 | deferred.reject(error); 247 | } else { 248 | deferred.resolve(result.items); 249 | } 250 | }); 251 | 252 | return deferred.promise(); 253 | }; 254 | this.getCachedRange = function getCachedRange(startIndex, limit, callback) { 255 | var result = [], 256 | i, 257 | item; 258 | 259 | for (i = startIndex; i !== startIndex + limit; i++) { 260 | item = rangeCache.get(i); 261 | if (item) { 262 | result.push(item); 263 | } else { 264 | break; 265 | } 266 | } 267 | startIndex += result.length; 268 | limit -= result.length; 269 | 270 | if (startIndex < state.total && limit > 0) { 271 | _getRange(startIndex, Math.max(limit, 20)).done(function (remainder) { 272 | result.push.apply(result, remainder.slice(0, limit)); 273 | for (i = 0; i !== remainder.length; i++) { 274 | item = remainder[i]; 275 | itemsCache.put(item._id, item); 276 | rangeCache.put(startIndex + i, item); 277 | } 278 | if (callback) { 279 | callback(null, { 280 | items: result, 281 | source: 'server' 282 | }); 283 | } 284 | }).fail(function (error) { 285 | if (callback) { 286 | callback(error, null); 287 | } 288 | }); 289 | } else { 290 | if (callback) { 291 | callback(null, { 292 | items: result, 293 | source: 'cache' 294 | }); 295 | } 296 | } 297 | 298 | return result; 299 | }; 300 | 301 | Object.defineProperty(this, 'state', { 302 | get: function get() { return state; } 303 | }); 304 | 305 | this._handleStanza = function _handleStanza(stanza) { 306 | switch (stanza.name) { 307 | case '+sync': 308 | state = stanza.payload; 309 | itemsCache = new LRUCache(1000); 310 | rangeCache = new LRUCache(1000); 311 | pending = {}; 312 | executeHandlers('update', state, false); 313 | break; 314 | case '+update': 315 | utils.update(state, stanza.payload); 316 | executeHandlers('update', state, true); 317 | break; 318 | default: 319 | console.log("Unhandled stream stanza", stanza); 320 | break; 321 | } 322 | }; 323 | 324 | var _getRange = function _getRange(startIndex, limit) { 325 | var id = startIndex + ":" + limit; 326 | var req = pending[id]; 327 | if (!req) { 328 | req = request('.get-range', { 329 | 'start_index': startIndex, 330 | 'limit': limit 331 | }).pipe(function (items) { 332 | delete pending[id]; 333 | return items; 334 | }, function (error) { 335 | delete pending[id]; 336 | return error; 337 | }); 338 | pending[id] = req; 339 | } 340 | return req; 341 | }; 342 | 343 | var request = function request(name, payload) { 344 | return project._request({ 345 | to: "/applications/ospy:stream", 346 | name: name, 347 | payload: payload 348 | }); 349 | }; 350 | var send = function send(name, payload) { 351 | project._send({ 352 | to: "/applications/ospy:stream", 353 | name: name, 354 | payload: payload 355 | }); 356 | }; 357 | 358 | var executeHandlers = function executeHandlers(name) { 359 | var args = [].slice.call(arguments, 1); 360 | handlers.forEach(function executeHandler(handler) { 361 | if (handler[name]) { 362 | handler[name].apply(handler, args); 363 | } 364 | }); 365 | }; 366 | }; 367 | 368 | return OSpy; 369 | }); 370 | -------------------------------------------------------------------------------- /src/js/app/services/service.js: -------------------------------------------------------------------------------- 1 | define(['extend'], function (extend) { 2 | 'use strict'; 3 | 4 | var Service = function Service(services) { 5 | this._private = {}; 6 | this.services = services; 7 | this.initialize.apply(this, [].slice.call(arguments, 1)); 8 | }; 9 | Service.prototype = { 10 | initialize: function initialize() {}, 11 | dispose: function dispose() {} 12 | }; 13 | Service.define = extend; 14 | 15 | return Service; 16 | }); 17 | -------------------------------------------------------------------------------- /src/js/app/services/settings.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service'], function (Service) { 2 | 'use strict'; 3 | 4 | var lookup = function lookup(properties, key) { 5 | var path = key.split('.'); 6 | var value = properties; 7 | for (var i = 0; i < path.length; i++) { 8 | if (value === undefined) { 9 | return undefined; 10 | } 11 | value = value[path[i]]; 12 | } 13 | return value; 14 | }; 15 | 16 | var Settings = Service.define({ 17 | initialize: function initialize(sessionSettings, defaults) { 18 | this.get = function get(key) { 19 | var scope = (sessionSettings.indexOf(key) > 0) ? 'session' : 'local'; 20 | var result = this.services.storage.load(scope, key); 21 | if (result !== undefined) { 22 | return result; 23 | } else { 24 | return lookup(defaults, key); 25 | } 26 | }; 27 | this.put = function put(key, value) { 28 | var scope = (sessionSettings.indexOf(key) > 0) ? 'session' : 'local'; 29 | this.services.storage.store(scope, key, value); 30 | 31 | var updates = {}; 32 | updates[key] = value; 33 | this.services.bus.post('services.settings:settings-updated', updates); 34 | }; 35 | } 36 | }); 37 | 38 | return Settings; 39 | }); 40 | -------------------------------------------------------------------------------- /src/js/app/services/storage.js: -------------------------------------------------------------------------------- 1 | define(['app/services/service'], function (Service) { 2 | 'use strict'; 3 | 4 | var getStorage = function getStorage(scope) { 5 | if (scope === 'local') { 6 | return window.localStorage; 7 | } else { 8 | return window.sessionStorage; 9 | } 10 | }; 11 | 12 | var Storage = Service.define({ 13 | store: function store(scope, key, value) { 14 | var storage = getStorage(scope); 15 | if (value !== null) { 16 | storage.setItem(key, JSON.stringify(value)); 17 | } else { 18 | storage.removeItem(key); 19 | } 20 | }, 21 | load: function load(scope, key) { 22 | var value = getStorage(scope).getItem(key); 23 | return (value !== null) ? JSON.parse(value) : undefined; 24 | } 25 | }); 26 | 27 | return Storage; 28 | }); 29 | -------------------------------------------------------------------------------- /src/js/app/utils.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 'use strict'; 3 | 4 | return { 5 | update: function update(target, updates) { 6 | Object.keys(updates).forEach(function (key) { 7 | var value = updates[key]; 8 | if (value === null) { 9 | delete target[key]; 10 | } else if ((typeof value !== 'object') || (value instanceof Array)) { 11 | target[key] = value; 12 | } else if (target[key] == null) { 13 | target[key] = value; 14 | } else { 15 | update(target[key], value); 16 | } 17 | }); 18 | }, 19 | bind: function bind(callbacks, context) { 20 | Object.keys(callbacks).forEach(function (key) { 21 | var value = callbacks[key]; 22 | if (typeof value === 'function') { 23 | callbacks[key] = value.bind(context); 24 | } 25 | }); 26 | return callbacks; 27 | } 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /src/js/app/views/app.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view'], function (View) { 2 | 'use strict'; 3 | 4 | var App = View.define({ 5 | initialize: function initialize() { 6 | this._page = this.element.querySelector("[data-view='page']"); 7 | }, 8 | resume: function resume() { 9 | this.element.style.display = 'block'; 10 | }, 11 | show: function show(view) { 12 | var container = this._page; 13 | while (container.firstElementChild) { 14 | container.removeChild(container.firstElementChild); 15 | } 16 | if (view) { 17 | container.appendChild(view.element); 18 | } 19 | } 20 | }); 21 | 22 | return App; 23 | }); 24 | -------------------------------------------------------------------------------- /src/js/app/views/capture.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view'], function (View) { 2 | 'use strict'; 3 | 4 | var Capture = View.define({ 5 | initialize: function initialize(project) { 6 | this._project = project; 7 | 8 | this._devices = this.element.querySelector("[data-view='devices']"); 9 | this._processes = this.element.querySelector("[data-view='processes']"); 10 | this._sessions = this.element.querySelector("[data-view='sessions']"); 11 | 12 | this._connect(this._devices, 'change', this._onDeviceChange); 13 | this._connect("[data-action='refresh']", 'click', this._onRefreshClick); 14 | this._connect("[data-action='attach']", 'click', this._onAttachClick); 15 | }, 16 | update: function update(properties) { 17 | var container; 18 | 19 | if ('devices' in properties) { 20 | var devices = properties['devices']; 21 | container = this._devices; 22 | while (container.firstElementChild) { 23 | container.removeChild(container.firstElementChild); 24 | } 25 | devices.forEach(function addDevice(device) { 26 | var element = document.createElement('option'); 27 | element.textContent = device.name; 28 | element.value = device.id; 29 | container.appendChild(element); 30 | }); 31 | this.events.trigger('device-selected', devices[0].id); 32 | } 33 | 34 | if ('processes' in properties) { 35 | container = this._processes; 36 | while (container.firstElementChild) { 37 | container.removeChild(container.firstElementChild); 38 | } 39 | properties['processes'].forEach(function addProcess(process) { 40 | var element = document.createElement('option'); 41 | element.textContent = process.name; 42 | element.value = process.pid; 43 | container.appendChild(element); 44 | }); 45 | } 46 | 47 | Capture['__super__'].update.call(this, properties); 48 | }, 49 | selectDevice: function selectDevice(deviceId) { 50 | var children = this._devices.children; 51 | for (var i = 0; i !== children.length; i++) { 52 | if (parseInt(children[i].value, 10) === deviceId) { 53 | this._devices.selectedIndex = i; 54 | this.events.trigger('device-selected', deviceId); 55 | break; 56 | } 57 | } 58 | }, 59 | addSession: function addSession(view) { 60 | this._sessions.appendChild(view.element); 61 | this._project.render(); 62 | }, 63 | removeSession: function removeSession(view) { 64 | this._sessions.removeChild(view.element); 65 | this._project.render(); 66 | }, 67 | displayError: function displayError(error) { 68 | var element = document.createElement('div'); 69 | element.classList.add('notification'); 70 | element.classList.add('notification-error'); 71 | element.textContent = error; 72 | this.element.appendChild(element); 73 | window.setTimeout(function expireNotification() { 74 | this.element.removeChild(element); 75 | }.bind(this), 5000); 76 | }, 77 | _onDeviceChange: function _onDeviceChange() { 78 | this.events.trigger('device-selected', parseInt(this._devices.value, 10)); 79 | }, 80 | _onRefreshClick: function _onRefreshClick() { 81 | this.events.trigger('device-selected', parseInt(this._devices.value, 10)); 82 | }, 83 | _onAttachClick: function _onAttachClick() { 84 | this.events.trigger('attach', parseInt(this._devices.value, 10), parseInt(this._processes.value, 10)); 85 | } 86 | }); 87 | 88 | return Capture; 89 | }); 90 | -------------------------------------------------------------------------------- /src/js/app/views/capture/session.js: -------------------------------------------------------------------------------- 1 | define(['app/views/template', 'text!templates/session.html'], function (Template, template) { 2 | 'use strict'; 3 | 4 | var Session = Template.define({ 5 | initialize: function initialize() { 6 | Session['__super__'].initialize.call(this, '/session', template); 7 | 8 | this._connect("button.detach", 'click', this._onDetachClick); 9 | this._connect("button.close", 'click', this._onCloseClick); 10 | }, 11 | _onDetachClick: function _onDetachClick() { 12 | this.events.trigger('detach'); 13 | }, 14 | _onCloseClick: function _onCloseClick() { 15 | this.events.trigger('close'); 16 | } 17 | }); 18 | 19 | return Session; 20 | }); 21 | -------------------------------------------------------------------------------- /src/js/app/views/project.js: -------------------------------------------------------------------------------- 1 | define(['app/views/template', 'app/views/capture', 'app/views/stream', 'text!templates/project.html'], function (Template, Capture, Stream, template) { 2 | 'use strict'; 3 | 4 | var Project = Template.define({ 5 | initialize: function initialize() { 6 | Project['__super__'].initialize.call(this, '/project', template); 7 | 8 | this.capture = new Capture(this.element.querySelector("[data-view='capture']"), this); 9 | this.stream = new Stream(this.element.querySelector("[data-view='stream']")); 10 | 11 | this._connect("[data-action='publish']", 'click', this._onPublishBtnClick); 12 | }, 13 | dispose: function dispose() { 14 | this.stream.dispose(); 15 | this.capture.dispose(); 16 | 17 | Project['__super__'].dispose.call(this); 18 | }, 19 | resume: function resume() { 20 | this.render(); 21 | }, 22 | render: function render() { 23 | this.stream.resume(); 24 | }, 25 | _onPublishBtnClick: function _onPublishBtnClick() { 26 | this.events.trigger('publish'); 27 | } 28 | }); 29 | 30 | return Project; 31 | }); 32 | -------------------------------------------------------------------------------- /src/js/app/views/stream.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view', 'app/views/stream/summary', 'app/views/stream/details'], function (View, Summary, Details) { 2 | 'use strict'; 3 | 4 | var Stream = View.define({ 5 | initialize: function initialize() { 6 | this.summary = new Summary(this.element.querySelector("[data-view='summary']")); 7 | this.details = new Details(this.element.querySelector("[data-view='details']")); 8 | 9 | this._connect("[data-action='clear']", 'click', this._onClearClick); 10 | }, 11 | resume: function resume() { 12 | this.summary.resume(); 13 | this.details.resume(); 14 | }, 15 | _onClearClick: function _onClearClick() { 16 | this.events.trigger('clear'); 17 | } 18 | }); 19 | 20 | return Stream; 21 | }); 22 | -------------------------------------------------------------------------------- /src/js/app/views/stream/details.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view'], function (View) { 2 | 'use strict'; 3 | 4 | var Details = View.define({ 5 | initialize: function initialize() { 6 | this._connect(window, 'resize', this._updateHeight); 7 | }, 8 | resume: function resume() { 9 | this._updateHeight(); 10 | }, 11 | clear: function clear() { 12 | var content = this.element; 13 | while (content.firstChild) { 14 | content.removeChild(content.firstChild); 15 | } 16 | }, 17 | add: function add(items) { 18 | items.forEach(function (item) { 19 | var ev = item.event, 20 | prefix, 21 | container, 22 | line, 23 | i; 24 | if (ev.type === 'incoming' || ev.type === 'outgoing') { 25 | prefix = document.createElement('span'); 26 | prefix.classList.add('prefix'); 27 | prefix.classList.add('prefix-' + ev.type); 28 | prefix.textContent = (ev.type === 'incoming') ? "<<" : ">>"; 29 | 30 | container = document.createElement('div'); 31 | container.classList.add('item-data'); 32 | 33 | line = document.createElement('div'); 34 | line.appendChild(prefix.cloneNode(true)); 35 | line.appendChild(document.createTextNode(".¸¸.· #" + item._id)); 36 | container.appendChild(line); 37 | 38 | var asciiLines = window.atob(item.payload).split("\n"); 39 | for (i = 0; i !== asciiLines.length; i++) { 40 | line = document.createElement('div'); 41 | line.appendChild(prefix.cloneNode(true)); 42 | line.appendChild(document.createTextNode(asciiLines[i])); 43 | container.appendChild(line); 44 | } 45 | 46 | this.element.appendChild(container); 47 | } 48 | }.bind(this)); 49 | }, 50 | _updateHeight: function _updateHeight() { 51 | var windowHeight = window.innerHeight; 52 | var appHeight = parseInt(window.getComputedStyle(window.document.body.firstElementChild).height, 10); 53 | var ourHeight = parseInt(window.getComputedStyle(this.element).height, 10); 54 | this.element.style.height = Math.max(ourHeight + windowHeight - appHeight, 200) + "px"; 55 | } 56 | }); 57 | 58 | return Details; 59 | }); 60 | -------------------------------------------------------------------------------- /src/js/app/views/stream/summary.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view'], function (View) { 2 | 'use strict'; 3 | 4 | var appendColumn = function appendColumn(row, content) { 5 | var col = document.createElement('td'); 6 | col.textContent = content; 7 | row.appendChild(col); 8 | return col; 9 | }; 10 | 11 | var ctrlModifier, ctrlKeyCode; 12 | if (navigator.platform === 'MacIntel') { 13 | ctrlModifier = 'metaKey'; 14 | ctrlKeyCode = 91; 15 | } else { 16 | ctrlModifier = 'ctrlKey'; 17 | ctrlKeyCode = 17; 18 | } 19 | 20 | var Summary = View.define({ 21 | initialize: function initialize() { 22 | this._headers = this.element.querySelector(".headers"); 23 | this._viewport = this.element.querySelector(".content"); 24 | this._content = this._viewport.querySelector("table"); 25 | 26 | this._connect(this._viewport, 'scroll', this._onScroll); 27 | 28 | this._connect(this._content, 'mousewheel', this._onMouseWheel); 29 | this._connect(this._content, 'click', this._onClick); 30 | this._connect(this._content, 'dblclick', this._onDoubleClick); 31 | this._connect(this._content, 'keydown', this._onKeyDown); 32 | this._connect(this._content, 'keyup', this._onKeyUp); 33 | 34 | this._connect(window, 'resize', this._updateHeaders); 35 | 36 | this._reset(); 37 | }, 38 | resume: function resume() { 39 | this._updateHeaders(); 40 | this.render(); 41 | }, 42 | render: function render() { 43 | var modified = false, 44 | partial = false, 45 | total = this.presenter.getTotal(), 46 | items, 47 | row, 48 | i; 49 | 50 | if (total <= 0) { 51 | this._reset(); 52 | while (this._content.firstElementChild) { 53 | this._content.removeChild(this._content.firstElementChild); 54 | } 55 | } else { 56 | if (this._rowHeight === null) { 57 | items = this.presenter.getItems(this._topIndex, 1); 58 | if (items.length === 0) { 59 | return; 60 | } 61 | row = this._createRow(items[0], this._topIndex); 62 | this._content.appendChild(row); 63 | this._rowHeight = row.offsetHeight; 64 | if (this._rowHeight === 0) { 65 | // Too early, we are probably not visible 66 | this._rowHeight = null; 67 | this._content.removeChild(row); 68 | return; 69 | } 70 | this._rowsPerPage = Math.round(this._viewport.clientHeight / this._rowHeight); 71 | this._bottomIndex = 1; 72 | this._updateHeaders(); 73 | modified = true; 74 | } 75 | 76 | var extra = 1; 77 | var topIndex = Math.max(Math.floor(this._viewport.scrollTop / this._rowHeight) - extra, 0); 78 | var bottomIndex = Math.min(extra + topIndex + this._rowsPerPage + extra, total); 79 | 80 | var fillAbove = this._topIndex - topIndex; 81 | if (fillAbove > 0) { 82 | if (topIndex < this._topIndex - this._rowsPerPage) { 83 | while (this._bottomIndex > this._topIndex) { 84 | if (this._content.firstElementChild) { 85 | this._content.removeChild(this._content.firstElementChild); 86 | } 87 | this._bottomIndex--; 88 | } 89 | this._topIndex = bottomIndex; 90 | this._bottomIndex = bottomIndex; 91 | fillAbove = this._topIndex - topIndex; 92 | } 93 | 94 | items = this.presenter.getItems(topIndex, fillAbove); 95 | for (i = items.length - 1; i >= 0; i--) { 96 | row = this._createRow(items[i], topIndex + i); 97 | this._content.insertBefore(row, this._content.firstElementChild); 98 | this._topIndex--; 99 | } 100 | this._content.style.marginTop = (topIndex * this._rowHeight) + "px"; 101 | modified = true; 102 | partial = items.length !== fillAbove; 103 | } 104 | 105 | if (!partial) { 106 | var fillBelow = bottomIndex - this._bottomIndex; 107 | if (fillBelow > 0) { 108 | if (topIndex > this._bottomIndex + this._rowsPerPage) { 109 | while (this._bottomIndex > this._topIndex) { 110 | if (this._content.firstElementChild) { 111 | this._content.removeChild(this._content.firstElementChild); 112 | } 113 | this._bottomIndex--; 114 | } 115 | this._topIndex = topIndex; 116 | this._bottomIndex = topIndex; 117 | this._content.style.marginTop = (this._topIndex * this._rowHeight) + "px"; 118 | fillBelow = bottomIndex - this._bottomIndex; 119 | } 120 | 121 | var previousBottomIndex = this._bottomIndex; 122 | items = this.presenter.getItems(this._bottomIndex, fillBelow); 123 | for (i = 0; i !== items.length; i++) { 124 | row = this._createRow(items[i], previousBottomIndex + i); 125 | this._content.appendChild(row); 126 | this._bottomIndex++; 127 | } 128 | modified = true; 129 | partial = items.length !== fillBelow; 130 | } 131 | } 132 | 133 | if (!partial) { 134 | var trimAbove = topIndex - this._topIndex; 135 | if (trimAbove > 0) { 136 | for (i = 0; i < trimAbove; i++) { 137 | if (this._content.firstElementChild) { 138 | this._content.removeChild(this._content.firstElementChild); 139 | } else { 140 | break; 141 | } 142 | } 143 | this._topIndex = topIndex; 144 | this._content.style.marginTop = (this._topIndex * this._rowHeight) + "px"; 145 | modified = true; 146 | } 147 | 148 | var trimBelow = this._bottomIndex - bottomIndex; 149 | if (trimBelow > 0) { 150 | for (i = 0; i < trimBelow; i++) { 151 | if (this._content.lastElementChild) { 152 | this._content.removeChild(this._content.lastElementChild); 153 | } else { 154 | break; 155 | } 156 | } 157 | this._bottomIndex = bottomIndex; 158 | modified = true; 159 | } 160 | } 161 | 162 | this._content.style.marginBottom = ((total - this._bottomIndex) * this._rowHeight) + "px"; 163 | } 164 | 165 | if (modified && this._scrollPosition === 'end') { 166 | this._scrollToEnd(); 167 | } 168 | }, 169 | _reset: function _reset() { 170 | this._rowHeight = null; 171 | this._rowsPerPage = null; 172 | this._topIndex = 0; 173 | this._bottomIndex = 0; 174 | this._scrollPosition = 'end'; 175 | this._selected = []; 176 | this._shiftPressed = false; 177 | this._ctrlPressed = false; 178 | this._content.style.marginTop = "0px"; 179 | this._content.style.marginBottom = "0px"; 180 | }, 181 | _createRow: function _createRow(item, index) { 182 | var ev = item.event; 183 | 184 | var row = document.createElement('tr'); 185 | row.classList.add('item'); 186 | if (this._selected.indexOf(index) !== -1) { 187 | row.classList.add('selected'); 188 | } 189 | 190 | appendColumn(row, item._id); 191 | 192 | var type = appendColumn(row, ''); 193 | var icon = document.createElement('div'); 194 | icon.classList.add('icon'); 195 | icon.classList.add('icon-' + ev.type); 196 | type.appendChild(icon); 197 | 198 | var timestamp = new Date(item.timestamp).toTimeString(); 199 | timestamp = timestamp.substring(0, timestamp.indexOf(" ")); 200 | appendColumn(row, timestamp); 201 | 202 | appendColumn(row, ev.name); 203 | 204 | var keys = Object.keys(ev.properties); 205 | keys.sort(); 206 | appendColumn(row, keys.map(function (key) { 207 | return key + "=" + ev.properties[key]; 208 | }).join(" ")); 209 | 210 | return row; 211 | }, 212 | _updateHeaders: function _updateHeaders() { 213 | var first = this._content.firstElementChild; 214 | if (first !== null) { 215 | var widths = [], 216 | children = first.children, 217 | i; 218 | for (i = 0; i !== children.length; i++) { 219 | var w = window.getComputedStyle(children[i]).width; 220 | if (w === 'auto') { 221 | return; 222 | } 223 | widths.push(parseInt(w, 10)); 224 | } 225 | 226 | children = this._headers.firstElementChild.firstElementChild.children; 227 | for (i = 0; i !== children.length; i++) { 228 | if (i < children.length - 1) { 229 | children[i].style.width = widths[i] + "px"; 230 | } else { 231 | children[i].style.width = 'auto'; 232 | } 233 | } 234 | } 235 | }, 236 | _scrollToEnd: function _scrollToEnd() { 237 | var el = this._viewport; 238 | el.scrollTop = el.scrollHeight - el.clientHeight; 239 | this._scrollPosition = 'end'; 240 | }, 241 | _rowIndex: function _rowIndex(row) { 242 | var distance = parseInt(this._content.style.marginTop, 10) + row.offsetTop; 243 | return Math.floor(distance / this._rowHeight); 244 | }, 245 | _selectAll: function _selectAll() { 246 | var total = this.presenter.getTotal(); 247 | if (total > 0) { 248 | this._selected = []; 249 | for (var i = 0; i !== total; i++) { 250 | this._selected.push(i); 251 | } 252 | this._selected.reverse(); 253 | this._updateSelected(); 254 | this.events.trigger('select', this._selected); 255 | } 256 | }, 257 | _selectWithKeyboard: function _selectWithKeyboard(index) { 258 | if (this._shiftPressed) { 259 | this._selectTo(index); 260 | } else { 261 | this._selected = [index]; 262 | } 263 | this._updateSelected(); 264 | this._scrollToLastSelected(); 265 | this.events.trigger('select', this._selected); 266 | }, 267 | _selectWithMouse: function _selectWithMouse(index) { 268 | if (this._shiftPressed) { 269 | this._selectTo(index); 270 | } else { 271 | if (!this._ctrlPressed) { 272 | this._selected = []; 273 | } 274 | if (this._selected.indexOf(index) === -1) { 275 | this._selected.push(index); 276 | } else { 277 | this._selected.splice(this._selected.indexOf(index), 1); 278 | } 279 | } 280 | this._updateSelected(); 281 | this.events.trigger('select', this._selected); 282 | }, 283 | _selectTo: function _selectTo(index) { 284 | var firstSelected, 285 | lastSelected, 286 | increment, 287 | i; 288 | 289 | lastSelected = index; 290 | if (this._selected.length > 0) { 291 | firstSelected = this._selected[0]; 292 | } else { 293 | firstSelected = lastSelected; 294 | } 295 | increment = lastSelected >= firstSelected ? 1 : -1; 296 | 297 | this._selected = []; 298 | for (i = firstSelected; i !== lastSelected + increment; i += increment) { 299 | this._selected.push(i); 300 | } 301 | }, 302 | _updateSelected: function _updateSelected() { 303 | var children = this._content.children; 304 | for (var i = 0; i !== children.length; i++) { 305 | var child = children[i]; 306 | if (this._selected.indexOf(this._rowIndex(child)) !== -1) { 307 | child.classList.add('selected'); 308 | } else { 309 | child.classList.remove('selected'); 310 | } 311 | } 312 | }, 313 | _scrollToLastSelected: function _scrollToLastSelected() { 314 | var viewportTop = this._viewport.scrollTop; 315 | var viewportBottom = viewportTop + this._viewport.clientHeight; 316 | var rowTop = this._selected[this._selected.length - 1] * this._rowHeight; 317 | var rowBottom = rowTop + this._rowHeight; 318 | if (rowTop < viewportTop) { 319 | this._viewport.scrollTop = rowTop; 320 | } else if (rowBottom > viewportBottom) { 321 | this._viewport.scrollTop = rowBottom - this._viewport.clientHeight + 1; 322 | } 323 | }, 324 | _onScroll: function _onScroll() { 325 | this._scrollPosition = this._computeScrollPosition(); 326 | this.render(); 327 | }, 328 | _computeScrollPosition: function _computeScrollPosition() { 329 | var el = this._viewport; 330 | if (el.scrollTop === el.scrollHeight - el.clientHeight) { 331 | return 'end'; 332 | } else if (el.scrollTop === 0) { 333 | return 'start'; 334 | } 335 | return 'middle'; 336 | }, 337 | _onMouseWheel: function _onMouseWheel(event) { 338 | var dY = event.wheelDeltaY; 339 | if (dY !== 0 && this._rowHeight !== null) { 340 | var distance; 341 | if (dY % 120 === 0) { 342 | distance = (dY / 120) * -1; 343 | } else { 344 | distance = (dY < 0) ? 1 : -1; 345 | } 346 | var viewportTop = this._viewport.scrollTop; 347 | this._viewport.scrollTop = (Math.floor(viewportTop / this._rowHeight) + distance) * this._rowHeight; 348 | event.stopPropagation(); 349 | event.preventDefault(); 350 | } 351 | }, 352 | _onClick: function _onClick(event) { 353 | if (event.button === 0) { 354 | var row = null, 355 | element = event.target; 356 | while (element && element !== this.element) { 357 | if (element.tagName === 'TR' && element.classList.contains('item')) { 358 | row = element; 359 | break; 360 | } 361 | element = element.parentElement; 362 | } 363 | 364 | if (row !== null) { 365 | this._selectWithMouse(this._rowIndex(row)); 366 | event.stopPropagation(); 367 | event.preventDefault(); 368 | } 369 | } 370 | }, 371 | _onDoubleClick: function _onDoubleClick() { 372 | this._selectAll(); 373 | }, 374 | _onKeyDown: function _onKeyDown(event) { 375 | var index; 376 | switch (event.keyCode) { 377 | case 33: // Page up 378 | case 34: // Page down 379 | case 38: // Arrow up 380 | case 40: // Arrow down 381 | if (this._selected.length > 0) { 382 | index = this._selected[this._selected.length - 1]; 383 | switch (event.keyCode) { 384 | case 33: 385 | index -= this._rowsPerPage; 386 | break; 387 | case 34: 388 | index += this._rowsPerPage; 389 | break; 390 | case 38: 391 | index--; 392 | break; 393 | case 40: 394 | index++; 395 | break; 396 | } 397 | index = Math.max(Math.min(index, this.presenter.getTotal() - 1), 0); 398 | this._selectWithKeyboard(index); 399 | event.stopPropagation(); 400 | event.preventDefault(); 401 | } 402 | break; 403 | case 36: // Home 404 | case 35: // End 405 | if (this._selected.length > 0) { 406 | index = (event.keyCode === 36) ? 0 : this.presenter.getTotal() - 1; 407 | this._selectWithKeyboard(index); 408 | event.stopPropagation(); 409 | event.preventDefault(); 410 | } 411 | break; 412 | case 65: // 'A' 413 | if (event[ctrlModifier]) { 414 | this._selectAll(); 415 | event.stopPropagation(); 416 | event.preventDefault(); 417 | } 418 | break; 419 | case 16: 420 | this._shiftPressed = true; 421 | break; 422 | default: 423 | if (event.keyCode === ctrlKeyCode) { 424 | this._ctrlPressed = true; 425 | } 426 | break; 427 | } 428 | }, 429 | _onKeyUp: function _onKeyUp(event) { 430 | if (event.keyCode === 16) { 431 | this._shiftPressed = false; 432 | } else if (event.keyCode === ctrlKeyCode) { 433 | this._ctrlPressed = false; 434 | } 435 | } 436 | }); 437 | 438 | return Summary; 439 | }); 440 | -------------------------------------------------------------------------------- /src/js/app/views/template.js: -------------------------------------------------------------------------------- 1 | define(['app/views/view'], function (View) { 2 | 'use strict'; 3 | 4 | var parseHtml = function parseHtml(string) { 5 | var container = document.createElement('div'); 6 | container.innerHTML = string; 7 | return container.childNodes[0]; 8 | }; 9 | 10 | var Template = View.define({ 11 | initialize: function initialize(id, template) { 12 | var element = Template._cache[id]; 13 | if (!element) { 14 | element = Template._cache[id] = parseHtml(template); 15 | } 16 | this.element = element.cloneNode(true); 17 | }, 18 | dispose: function dispose() { 19 | if (this.element.parentElement) { 20 | this.element.parentElement.removeChild(this.element); 21 | } 22 | Template['__super__'].dispose.call(this); 23 | } 24 | }); 25 | Template._cache = {}; 26 | 27 | return Template; 28 | }); 29 | -------------------------------------------------------------------------------- /src/js/app/views/view.js: -------------------------------------------------------------------------------- 1 | define(['deferred', 'events', 'extend'], function(Deferred, Events, extend) { 2 | 'use strict'; 3 | 4 | var View = function View(element) { 5 | this.element = element; 6 | this.events = new Events(); 7 | this._updates = {}; 8 | this._updatesTimer = null; 9 | this._updatesDeferred = null; 10 | this._connections = []; 11 | 12 | this.initialize.apply(this, [].slice.call(arguments, 1)); 13 | }; 14 | View.prototype = { 15 | initialize: function initialize() {}, 16 | dispose: function dispose() { 17 | this._disconnectAll(); 18 | clearTimeout(this._updatesTimer); 19 | }, 20 | update: function update(properties) { 21 | merge(this._updates, properties); 22 | if (this._updatesTimer === null) { 23 | this._updatesDeferred = new Deferred(); 24 | this._updatesTimer = setTimeout(this.flushUpdates.bind(this), 10); 25 | } 26 | return this._updatesDeferred.promise(); 27 | }, 28 | flushUpdates: function flushUpdates() { 29 | deepUpdate(this.element.children, this._updates); 30 | this._updates = {}; 31 | if (this._updatesTimer !== null) { 32 | clearTimeout(this._updatesTimer); 33 | this._updatesTimer = null; 34 | } 35 | if (this._updatesDeferred !== null) { 36 | this._updatesDeferred.resolve(); 37 | this._updatesDeferred = null; 38 | } 39 | }, 40 | _connect: function _connect(target, event, callback) { 41 | var result = null; 42 | if (typeof target === 'string') { 43 | result = this.element.querySelector(target); 44 | if (result === null) { 45 | throw new Error("No element matching " + target); 46 | } 47 | } else { 48 | result = target; 49 | } 50 | 51 | var connection = { 52 | element: result, 53 | event: event, 54 | callback: callback.bind(this) 55 | }; 56 | connection.element.addEventListener(connection.event, connection.callback, false); 57 | this._connections.push(connection); 58 | }, 59 | _disconnectAll: function _disconnectAll() { 60 | for (var i = 0; i < this._connections.length; i++) { 61 | var connection = this._connections[i]; 62 | connection.element.removeEventListener(connection.event, connection.callback, false); 63 | } 64 | this._connections = []; 65 | }, 66 | _show: function _show() { 67 | this.element.style.display = ""; 68 | }, 69 | _hide: function _hide() { 70 | this.element.style.display = "none"; 71 | } 72 | }; 73 | View.define = extend; 74 | 75 | var deepUpdate = function deepUpdate(elements, properties) { 76 | for (var i = 0; i < elements.length; i++) { 77 | var element = elements[i]; 78 | update(element, properties); 79 | if (!element.hasAttribute('data-view')) { 80 | deepUpdate(element.children, properties); 81 | } 82 | } 83 | }; 84 | 85 | var update = function update(element, properties) { 86 | for (var i = 0; i < element.attributes.length; i++) { 87 | var attribute = element.attributes[i]; 88 | var match = attribute.name.match(/^data-(show|hide|visible|invisible|text|html|value|checked|unchecked|enabled|disabled|(attr)-([a-z\-]+)|(class)-([a-z\-]+))$/); 89 | if (match != null) { 90 | var value = lookup(properties, attribute.value); 91 | if (value !== undefined) { 92 | switch (match[1]) { 93 | case 'show': 94 | element.style.display = value ? '' : 'none'; 95 | break; 96 | case 'hide': 97 | element.style.display = value ? 'none' : ''; 98 | break; 99 | case 'visible': 100 | element.style.visibility = value ? '' : 'hidden'; 101 | break; 102 | case 'invisible': 103 | element.style.visibility = value ? 'hidden' : ''; 104 | break; 105 | case 'text': 106 | element.textContent = value; 107 | break; 108 | case 'html': 109 | element.innerHTML = value; 110 | break; 111 | case 'value': 112 | element.value = value; 113 | break; 114 | case 'checked': 115 | element.checked = value; 116 | break; 117 | case 'unchecked': 118 | element.checked = !value; 119 | break; 120 | case 'enabled': 121 | element.disabled = !value; 122 | break; 123 | case 'disabled': 124 | element.disabled = value; 125 | break; 126 | default: 127 | if (match[2] === 'attr') { 128 | element.setAttribute(match[3], value); 129 | } else if (match[4] === 'class') { 130 | if (element.classList) { 131 | element.classList[value ? 'add' : 'remove'](match[5]); 132 | } else { 133 | var className = match[5], 134 | regex = new RegExp('(^|\\s+)' + className + '(\\s+|$)'), 135 | hasClass = regex.test(element.className); 136 | if (hasClass && !value) { 137 | element.className = element.className.replace(hasClass, ' ').trim(); 138 | } else if (value && !hasClass) { 139 | element.className = element.className + ' ' + className; 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | }; 148 | 149 | var merge = function merge(target, updates) { 150 | Object.keys(updates).forEach(function (key) { 151 | target[key] = updates[key]; 152 | }); 153 | }; 154 | 155 | var lookup = function lookup(properties, key) { 156 | var path = key.split('.'); 157 | var value = properties; 158 | for (var i = 0; i < path.length; i++) { 159 | if (value === undefined) { 160 | return undefined; 161 | } else if (value === null) { 162 | return null; 163 | } 164 | value = value[path[i]]; 165 | } 166 | return value; 167 | }; 168 | 169 | return View; 170 | }); 171 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | window.AppVersion = "development"; 2 | window.AppConfig = { 3 | ospy: { 4 | host: "ospy.dev:8000" 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/js/lib/deferred.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 'use strict'; 3 | 4 | var Deferred = function Deferred() { 5 | var state = 'pending'; 6 | var doneCallbacks = [], 7 | failCallbacks = [], 8 | alwaysCallbacks = []; 9 | var result = []; 10 | 11 | this.promise = function promise(candidate) { 12 | candidate = candidate || {}; 13 | candidate.state = function getState() { 14 | return state; 15 | }; 16 | 17 | var storeCallbacks = function storeCallbacks(shouldExecuteImmediately, holder) { 18 | return function() { 19 | if (state === 'pending') { 20 | holder.push.apply(holder, arguments); 21 | } 22 | if (shouldExecuteImmediately()) { 23 | execute(null, arguments, result); 24 | } 25 | return candidate; 26 | }; 27 | }; 28 | var pipe = function pipe(doneFilter, failFilter) { 29 | var deferred = new Deferred(); 30 | var filter = function filter(target, source, filterFunc) { 31 | if (filterFunc) { 32 | target(function() { 33 | var val = filterFunc.apply(null, arguments); 34 | if (isPromise(val)) { 35 | val.done(function () { deferred.resolve.apply(this, arguments); }); 36 | val.fail(function () { deferred.reject.apply(this, arguments); }); 37 | } else { 38 | source(val); 39 | } 40 | }); 41 | } else { 42 | target(function() { 43 | source.apply(null, arguments); 44 | }); 45 | } 46 | }; 47 | filter(candidate.done, deferred.resolve, doneFilter); 48 | filter(candidate.fail, deferred.reject, failFilter); 49 | return deferred; 50 | }; 51 | candidate.done = storeCallbacks(function() {return state === 'resolved';}, doneCallbacks); 52 | candidate.fail = storeCallbacks(function() {return state === 'rejected';}, failCallbacks); 53 | candidate.always = storeCallbacks(function() {return state !== 'pending';}, alwaysCallbacks); 54 | candidate.pipe = pipe; 55 | candidate.then = pipe; 56 | return candidate; 57 | }; 58 | this.promise(this); 59 | 60 | var close = function close(finalState, callbacks) { 61 | return function() { 62 | if (state === 'pending') { 63 | state = finalState; 64 | result = [].slice.call(arguments); 65 | execute(null, callbacks.concat(alwaysCallbacks), result); 66 | } 67 | return this; 68 | }; 69 | }; 70 | var closeWith = function closeWith(finalState, callbacks) { 71 | return function(context) { 72 | if (state === 'pending') { 73 | state = finalState; 74 | result = [].slice.call(arguments, 1); 75 | execute(context, callbacks.concat(alwaysCallbacks), result); 76 | } 77 | return this; 78 | }; 79 | }; 80 | this.resolve = close('resolved', doneCallbacks); 81 | this.resolveWith = closeWith('resolved', doneCallbacks); 82 | this.reject = close('rejected', failCallbacks); 83 | this.rejectWith = closeWith('rejected', failCallbacks); 84 | return this; 85 | }; 86 | 87 | Deferred.when = function when() { 88 | var subordinates = Array.prototype.slice.call(arguments, 0), 89 | remaining = subordinates.length, 90 | results = [], 91 | failed = false, 92 | d = new Deferred(); 93 | 94 | if (remaining === 0) { 95 | d.resolve(); 96 | } 97 | 98 | subordinates.forEach(function waitForSubordinate(subordinate, i) { 99 | subordinate.done(function success() { 100 | results[i] = Array.prototype.slice.call(arguments, 0); 101 | remaining--; 102 | if (remaining === 0 && !failed) { 103 | d.resolve.apply(d, results); 104 | } 105 | }); 106 | subordinate.fail(function failure() { 107 | remaining--; 108 | if (!failed) { 109 | failed = true; 110 | d.reject.apply(d, arguments); 111 | } 112 | }); 113 | }); 114 | 115 | return d.promise(); 116 | }; 117 | 118 | var execute = function execute(context, callbacks, args) { 119 | for (var i = 0; i < callbacks.length; i++) { 120 | callbacks[i].apply(context, args); 121 | } 122 | }; 123 | 124 | // We allow duck-typing to interop with conformant Deferred implementations 125 | var isPromise = function isPromise(o) { 126 | return o && o.state && o.done && o.fail && o.always && o.pipe && o.then; 127 | }; 128 | 129 | return Deferred; 130 | }); 131 | -------------------------------------------------------------------------------- /src/js/lib/events.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 'use strict'; 3 | 4 | var Events = function Events() { 5 | this.listeners = {}; 6 | }; 7 | 8 | Events.prototype.on = function on(event, listener) { 9 | if (!this.listeners[event]) { 10 | this.listeners[event] = []; 11 | } 12 | this.listeners[event].push(listener); 13 | return listener; 14 | }; 15 | 16 | Events.prototype.off = function off(event, listener) { 17 | if (this.listeners[event]) { 18 | this.listeners[event] = this.listeners[event].filter(function (element) { 19 | return element !== listener; 20 | }); 21 | } 22 | return listener; 23 | }; 24 | 25 | Events.prototype.trigger = function trigger() { 26 | var event = arguments[0], 27 | args = Array.prototype.slice.call(arguments, 1), 28 | listeners = this.listeners[event] || []; 29 | listeners.forEach(function (listener) { 30 | listener.apply(null, args); 31 | }); 32 | return listeners.length; 33 | }; 34 | 35 | return Events; 36 | }); 37 | -------------------------------------------------------------------------------- /src/js/lib/extend.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 'use strict'; 3 | 4 | return function extend(properties) { 5 | var parent = this; 6 | var child = function () { 7 | return parent.apply(this, arguments); 8 | }; 9 | 10 | Object.keys(parent).forEach(function (key) { 11 | child[key] = parent[key]; 12 | }); 13 | 14 | var Surrogate = function () { this.constructor = child; }; 15 | Surrogate.prototype = parent.prototype; 16 | child.prototype = new Surrogate(); 17 | 18 | Object.keys(properties).forEach(function (key) { 19 | child.prototype[key] = properties[key]; 20 | }); 21 | 22 | child['__super__'] = parent.prototype; 23 | 24 | return child; 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/js/lib/lru.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /** 5 | * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most 6 | * recently used items while discarding least recently used items when its limit 7 | * is reached. 8 | * 9 | * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson 10 | * See README.md for details. 11 | * 12 | * Illustration of the design: 13 | * 14 | * entry entry entry entry 15 | * ______ ______ ______ ______ 16 | * | head |.newer => | |.newer => | |.newer => | tail | 17 | * | A | | B | | C | | D | 18 | * |______| <= older.|______| <= older.|______| <= older.|______| 19 | * 20 | * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added 21 | */ 22 | var LRUCache = function LRUCache(limit) { 23 | // Current size of the cache. (Read-only). 24 | this.size = 0; 25 | // Maximum number of items this cache can hold. 26 | this.limit = limit; 27 | this._keymap = {}; 28 | }; 29 | 30 | /** 31 | * Put into the cache associated with . Returns the entry which was 32 | * removed to make room for the new entry. Otherwise undefined is returned 33 | * (i.e. if there was enough room already). 34 | */ 35 | LRUCache.prototype.put = function(key, value) { 36 | var entry = {key:key, value:value}; 37 | // Note: No protection agains replacing, and thus orphan entries. By design. 38 | this._keymap[key] = entry; 39 | if (this.tail) { 40 | // link previous tail to the new tail (entry) 41 | this.tail.newer = entry; 42 | entry.older = this.tail; 43 | } else { 44 | // we're first in -- yay 45 | this.head = entry; 46 | } 47 | // add new entry to the end of the linked list -- it's now the freshest entry. 48 | this.tail = entry; 49 | if (this.size === this.limit) { 50 | // we hit the limit -- remove the head 51 | return this.shift(); 52 | } else { 53 | // increase the size counter 54 | this.size++; 55 | } 56 | }; 57 | 58 | /** 59 | * Purge the least recently used (oldest) entry from the cache. Returns the 60 | * removed entry or undefined if the cache was empty. 61 | * 62 | * If you need to perform any form of finalization of purged items, this is a 63 | * good place to do it. Simply override/replace this function: 64 | * 65 | * var c = new LRUCache(123); 66 | * c.shift = function() { 67 | * var entry = LRUCache.prototype.shift.call(this); 68 | * doSomethingWith(entry); 69 | * return entry; 70 | * } 71 | */ 72 | LRUCache.prototype.shift = function() { 73 | // todo: handle special case when limit == 1 74 | var entry = this.head; 75 | if (entry) { 76 | if (this.head.newer) { 77 | this.head = this.head.newer; 78 | this.head.older = undefined; 79 | } else { 80 | this.head = undefined; 81 | } 82 | // Remove last strong reference to and remove links from the purged 83 | // entry being returned: 84 | entry.newer = entry.older = undefined; 85 | // delete is slow, but we need to do this to avoid uncontrollable growth: 86 | delete this._keymap[entry.key]; 87 | } 88 | return entry; 89 | }; 90 | 91 | /** 92 | * Get and register recent use of . Returns the value associated with 93 | * or undefined if not in cache. 94 | */ 95 | LRUCache.prototype.get = function(key, returnEntry) { 96 | // First, find our cache entry 97 | var entry = this._keymap[key]; 98 | if (entry === undefined) { 99 | return; // Not cached. Sorry. 100 | } 101 | // As was found in the cache, register it as being requested recently 102 | if (entry === this.tail) { 103 | // Already the most recenlty used entry, so no need to update the list 104 | return returnEntry ? entry : entry.value; 105 | } 106 | // HEAD--------------TAIL 107 | // <.older .newer> 108 | // <--- add direction -- 109 | // A B C E 110 | if (entry.newer) { 111 | if (entry === this.head) { 112 | this.head = entry.newer; 113 | } 114 | entry.newer.older = entry.older; // C <-- E. 115 | } 116 | if (entry.older) { 117 | entry.older.newer = entry.newer; // C. --> E 118 | } 119 | entry.newer = undefined; // D --x 120 | entry.older = this.tail; // D. --> E 121 | if (this.tail) { 122 | this.tail.newer = entry; // E. <-- D 123 | } 124 | this.tail = entry; 125 | return returnEntry ? entry : entry.value; 126 | }; 127 | 128 | // ---------------------------------------------------------------------------- 129 | // Following code is optional and can be removed without breaking the core 130 | // functionality. 131 | 132 | /** 133 | * Check if is in the cache without registering recent use. Feasible if 134 | * you do not want to chage the state of the cache, but only "peek" at it. 135 | * Returns the entry associated with if found, or undefined if not found. 136 | */ 137 | LRUCache.prototype.find = function(key) { 138 | return this._keymap[key]; 139 | }; 140 | 141 | /** 142 | * Update the value of entry with . Returns the old value, or undefined if 143 | * entry was not in the cache. 144 | */ 145 | LRUCache.prototype.set = function(key, value) { 146 | var oldvalue, entry = this.get(key, true); 147 | if (entry) { 148 | oldvalue = entry.value; 149 | entry.value = value; 150 | } else { 151 | oldvalue = this.put(key, value); 152 | if (oldvalue) { 153 | oldvalue = oldvalue.value; 154 | } 155 | } 156 | return oldvalue; 157 | }; 158 | 159 | /** 160 | * Remove entry from cache and return its value. Returns undefined if not 161 | * found. 162 | */ 163 | LRUCache.prototype.remove = function(key) { 164 | var entry = this._keymap[key]; 165 | if (!entry) { 166 | return; 167 | } 168 | delete this._keymap[entry.key]; // need to do delete unfortunately 169 | if (entry.newer && entry.older) { 170 | // relink the older entry with the newer entry 171 | entry.older.newer = entry.newer; 172 | entry.newer.older = entry.older; 173 | } else if (entry.newer) { 174 | // remove the link to us 175 | entry.newer.older = undefined; 176 | // link the newer entry to head 177 | this.head = entry.newer; 178 | } else if (entry.older) { 179 | // remove the link to us 180 | entry.older.newer = undefined; 181 | // link the newer entry to head 182 | this.tail = entry.older; 183 | } else {// if(entry.older === undefined && entry.newer === undefined) { 184 | this.head = this.tail = undefined; 185 | } 186 | 187 | this.size--; 188 | return entry.value; 189 | }; 190 | 191 | /** Removes all entries */ 192 | LRUCache.prototype.removeAll = function() { 193 | // This should be safe, as we never expose strong refrences to the outside 194 | this.head = this.tail = undefined; 195 | this.size = 0; 196 | this._keymap = {}; 197 | }; 198 | 199 | /** 200 | * Return an array containing all keys of entries stored in the cache object, in 201 | * arbitrary order. 202 | */ 203 | LRUCache.prototype.keys = function() { return Object.keys(this._keymap); }; 204 | 205 | /** 206 | * Call `fun` for each entry. Starting with the newest entry if `desc` is a true 207 | * value, otherwise starts with the oldest (head) enrty and moves towards the 208 | * tail. 209 | * 210 | * `fun` is called with 3 arguments in the context `context`: 211 | * `fun.call(context, Object key, Object value, LRUCache self)` 212 | */ 213 | LRUCache.prototype.forEach = function(fun, context, desc) { 214 | var entry; 215 | if (context === true) { 216 | desc = true; 217 | context = undefined; 218 | } else if (typeof context !== 'object') { 219 | context = this; 220 | } 221 | if (desc) { 222 | entry = this.tail; 223 | while (entry) { 224 | fun.call(context, entry.key, entry.value, this); 225 | entry = entry.older; 226 | } 227 | } else { 228 | entry = this.head; 229 | while (entry) { 230 | fun.call(context, entry.key, entry.value, this); 231 | entry = entry.newer; 232 | } 233 | } 234 | }; 235 | 236 | /** Returns a JSON (array) representation */ 237 | LRUCache.prototype.toJSON = function() { 238 | var s = [], entry = this.head; 239 | while (entry) { 240 | s.push({key:entry.key.toJSON(), value:entry.value.toJSON()}); 241 | entry = entry.newer; 242 | } 243 | return s; 244 | }; 245 | 246 | /** Returns a String representation */ 247 | LRUCache.prototype.toString = function() { 248 | var s = '', entry = this.head; 249 | while (entry) { 250 | s += String(entry.key)+':'+entry.value; 251 | entry = entry.newer; 252 | if (entry) { 253 | s += ' < '; 254 | } 255 | } 256 | return s; 257 | }; 258 | 259 | return LRUCache; 260 | }); 261 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | //>>excludeStart('productionExclude', pragmas.productionExclude); 2 | require.config({ 3 | baseUrl: "/", 4 | urlArgs: "_=" + (new Date()).getTime(), 5 | paths: { 6 | 'less': "vendor/less", 7 | 'lcss': "vendor/lcss", 8 | 'text': "vendor/text", 9 | 'deferred': "js/lib/deferred", 10 | 'events': "js/lib/events", 11 | 'extend': "js/lib/extend", 12 | 'lru': "js/lib/lru", 13 | 'app': "js/app", 14 | 'css': "css" 15 | }, 16 | shim: { 17 | 'lcss': { deps: ['less'] } 18 | } 19 | }); 20 | //>>excludeEnd('productionExclude'); 21 | require(['app/views/app', 'app/presenters/app', 'app/services/messagebus', 'app/services/storage', 'app/services/settings', 'app/services/navigation', 'app/services/ospy', 'app/services/frida', 'lcss!css/app'], function onLoad(AppView, AppPresenter, MessageBus, Storage, Settings, Navigation, OSpy, Frida) { 22 | 'use strict'; 23 | 24 | var view, 25 | presenter, 26 | services = {}; 27 | 28 | services.bus = new MessageBus(services); 29 | services.storage = new Storage(services); 30 | services.settings = new Settings(services, ['ospy.host'], window.AppConfig); 31 | services.navigation = new Navigation(services); 32 | services.ospy = new OSpy(services); 33 | services.frida = new Frida(services); 34 | 35 | view = new AppView(document.getElementById('main')); 36 | presenter = new AppPresenter(view, services); 37 | 38 | window.app = presenter; 39 | window.addEventListener('unload', function onUnload() { 40 | delete window.app; 41 | presenter.dispose(); 42 | view.dispose(); 43 | for (var name in services) { 44 | if (services.hasOwnProperty(name)) { 45 | services[name].dispose(); 46 | } 47 | } 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/templates/project.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
Plugin is not installed
8 |
9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
Total:
20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
IndexTypeTimestampEventDetails
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /src/templates/session.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Detached () 4 | Attached 5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/vendor/lcss.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license cs 0.4.1 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/jrburke/require-cs for details 5 | */ 6 | 7 | /*jslint */ 8 | /*global define, window, XMLHttpRequest, importScripts, Packages, java, 9 | ActiveXObject, process, require */ 10 | 11 | define([], function () { 12 | 'use strict'; 13 | var less, fs, getXhr, 14 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 15 | fetchText = function () { 16 | throw new Error('Environment unsupported.'); 17 | }, 18 | buildMap = {}; 19 | 20 | if (typeof process !== "undefined" && 21 | process.versions && 22 | !!process.versions.node) { 23 | //Using special require.nodeRequire, something added by r.js. 24 | less = require.nodeRequire('less'); 25 | fs = require.nodeRequire('fs'); 26 | fetchText = function (path, callback) { 27 | callback(fs.readFileSync(path, 'utf8')); 28 | }; 29 | } else if ((typeof window !== "undefined" && window.navigator && window.document) || typeof importScripts !== "undefined") { 30 | // Browser action 31 | less = require("less"); 32 | getXhr = function () { 33 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 34 | var xhr, i, progId; 35 | if (typeof XMLHttpRequest !== "undefined") { 36 | return new XMLHttpRequest(); 37 | } else { 38 | for (i = 0; i < 3; i++) { 39 | progId = progIds[i]; 40 | try { 41 | xhr = new ActiveXObject(progId); 42 | } catch (e) {} 43 | 44 | if (xhr) { 45 | progIds = [progId]; // so faster next time 46 | break; 47 | } 48 | } 49 | } 50 | 51 | if (!xhr) { 52 | throw new Error("getXhr(): XMLHttpRequest not available"); 53 | } 54 | 55 | return xhr; 56 | }; 57 | 58 | fetchText = function (url, callback) { 59 | var xhr = getXhr(); 60 | xhr.open('GET', url, true); 61 | xhr.onreadystatechange = function (evt) { 62 | if (xhr.readyState === 4) { 63 | var response = xhr.responseText; 64 | if (xhr.status !== 200) 65 | response = null; 66 | callback(response); 67 | } 68 | }; 69 | xhr.send(null); 70 | }; 71 | // end browser.js adapters 72 | } else if (typeof Packages !== 'undefined') { 73 | //Why Java, why is this so awkward? 74 | less = require("less"); 75 | fetchText = function (path, callback) { 76 | var encoding = "utf-8", 77 | file = new java.io.File(path), 78 | lineSeparator = java.lang.System.getProperty("line.separator"), 79 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 80 | stringBuffer, line, 81 | content = ''; 82 | try { 83 | stringBuffer = new java.lang.StringBuffer(); 84 | line = input.readLine(); 85 | 86 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 87 | // http://www.unicode.org/faq/utf_bom.html 88 | 89 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 90 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 91 | if (line && line.length() && line.charAt(0) === 0xfeff) { 92 | // Eat the BOM, since we've already found the encoding on this file, 93 | // and we plan to concatenating this buffer with others; the BOM should 94 | // only appear at the top of a file. 95 | line = line.substring(1); 96 | } 97 | 98 | stringBuffer.append(line); 99 | 100 | while ((line = input.readLine()) !== null) { 101 | stringBuffer.append(lineSeparator); 102 | stringBuffer.append(line); 103 | } 104 | //Make sure we return a JavaScript string and not a Java string. 105 | content = String(stringBuffer.toString()); //String 106 | } finally { 107 | input.close(); 108 | } 109 | callback(content); 110 | }; 111 | } 112 | 113 | function jsEscape (content) { 114 | return content.replace(/(['\\])/g, '\\$1') 115 | .replace(/[\f]/g, "\\f") 116 | .replace(/[\b]/g, "\\b") 117 | .replace(/[\n]/g, "\\n") 118 | .replace(/[\t]/g, "\\t") 119 | .replace(/[\r]/g, "\\r"); 120 | } 121 | 122 | var lcss = { 123 | get: function () { 124 | return less; 125 | }, 126 | 127 | write: function (pluginName, name, write) { 128 | if (buildMap.hasOwnProperty(name)) { 129 | write.asModule(pluginName + "!" + name, "define(function () { return undefined; });"); 130 | } 131 | }, 132 | 133 | writeFile: function (pluginName, name, parentRequire, write, config) { 134 | var outputUrl = parentRequire.toUrl(name + ".css"); 135 | lcss.load(name, parentRequire, function (css) { 136 | write(outputUrl, css); 137 | }, config); 138 | }, 139 | 140 | version: '0.1.0', 141 | 142 | load: function (name, parentRequire, load, config) { 143 | var sourceUrl, pos, sourceDir; 144 | sourceUrl = parentRequire.toUrl(name + ".less"); 145 | pos = sourceUrl.lastIndexOf('/'); 146 | if (pos === -1) 147 | sourceDir = "/"; 148 | else 149 | sourceDir = sourceUrl.substr(0, pos + 1); 150 | fetchText(sourceUrl, function (source) { 151 | if (!source) { 152 | load.error("Error: Failed to fetch '" + sourceUrl + "'. Is 'grunt server' running?"); 153 | return; 154 | } 155 | 156 | var optimization, compress; 157 | if (config.isBuild) { 158 | optimization = 3; 159 | compress = true; 160 | } else { 161 | optimization = 0; 162 | compress = false; 163 | } 164 | 165 | var parser = new less.Parser({ 166 | optimization: optimization, 167 | paths: [ sourceDir ] 168 | }); 169 | parser.parse(source, function (err, tree) { 170 | if (err) { 171 | load.error(err); 172 | return; 173 | } 174 | 175 | var css; 176 | try { 177 | css = tree.toCSS({ 178 | compress: compress, 179 | yuicompress: compress 180 | }); 181 | } catch (err) { 182 | load.error("Error: " + err.filename + ":" + err.line + ": error: " + err.message); 183 | return; 184 | } 185 | 186 | if (config.isBuild) { 187 | buildMap[name] = css; 188 | } else if (typeof window !== 'undefined') { 189 | var nextElement = null; 190 | var elements = document.head.getElementsByTagName("style"); 191 | if (elements.length > 0) 192 | nextElement = elements[0]; 193 | var style = document.createElement("style"); 194 | style.type = "text/css"; 195 | if (style.styleSheet) 196 | style.styleSheet.cssText = css; 197 | else 198 | style.appendChild(document.createTextNode(css)); 199 | if (nextElement === null) 200 | document.head.appendChild(style); 201 | else 202 | document.head.insertBefore(style, nextElement); 203 | } 204 | 205 | load(css); 206 | }); 207 | }); 208 | } 209 | }; 210 | return lcss; 211 | }); 212 | -------------------------------------------------------------------------------- /src/vendor/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.5 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require: false, XMLHttpRequest: false, ActiveXObject: false, 8 | define: false, window: false, process: false, Packages: false, 9 | java: false, location: false */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = [], 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.5', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var modName, ext, temp, 87 | strip = false, 88 | index = name.indexOf("."), 89 | isRelative = name.indexOf('./') === 0 || 90 | name.indexOf('../') === 0; 91 | 92 | if (index !== -1 && (!isRelative || index > 1)) { 93 | modName = name.substring(0, index); 94 | ext = name.substring(index + 1, name.length); 95 | } else { 96 | modName = name; 97 | } 98 | 99 | temp = ext || modName; 100 | index = temp.indexOf("!"); 101 | if (index !== -1) { 102 | //Pull off the strip arg. 103 | strip = temp.substring(index + 1) === "strip"; 104 | temp = temp.substring(0, index); 105 | if (ext) { 106 | ext = temp; 107 | } else { 108 | modName = temp; 109 | } 110 | } 111 | 112 | return { 113 | moduleName: modName, 114 | ext: ext, 115 | strip: strip 116 | }; 117 | }, 118 | 119 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 120 | 121 | /** 122 | * Is an URL on another domain. Only works for browser use, returns 123 | * false in non-browser environments. Only used to know if an 124 | * optimized .js version of a text resource should be loaded 125 | * instead. 126 | * @param {String} url 127 | * @returns Boolean 128 | */ 129 | useXhr: function (url, protocol, hostname, port) { 130 | var uProtocol, uHostName, uPort, 131 | match = text.xdRegExp.exec(url); 132 | if (!match) { 133 | return true; 134 | } 135 | uProtocol = match[2]; 136 | uHostName = match[3]; 137 | 138 | uHostName = uHostName.split(':'); 139 | uPort = uHostName[1]; 140 | uHostName = uHostName[0]; 141 | 142 | return (!uProtocol || uProtocol === protocol) && 143 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 144 | ((!uPort && !uHostName) || uPort === port); 145 | }, 146 | 147 | finishLoad: function (name, strip, content, onLoad) { 148 | content = strip ? text.strip(content) : content; 149 | if (masterConfig.isBuild) { 150 | buildMap[name] = content; 151 | } 152 | onLoad(content); 153 | }, 154 | 155 | load: function (name, req, onLoad, config) { 156 | //Name has format: some.module.filext!strip 157 | //The strip part is optional. 158 | //if strip is present, then that means only get the string contents 159 | //inside a body tag in an HTML string. For XML/SVG content it means 160 | //removing the declarations so the content can be inserted 161 | //into the current doc without problems. 162 | 163 | // Do not bother with the work if a build and text will 164 | // not be inlined. 165 | if (config.isBuild && !config.inlineText) { 166 | onLoad(); 167 | return; 168 | } 169 | 170 | masterConfig.isBuild = config.isBuild; 171 | 172 | var parsed = text.parseName(name), 173 | nonStripName = parsed.moduleName + 174 | (parsed.ext ? '.' + parsed.ext : ''), 175 | url = req.toUrl(nonStripName), 176 | useXhr = (masterConfig.useXhr) || 177 | text.useXhr; 178 | 179 | //Load the text. Use XHR if possible and in a browser. 180 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 181 | text.get(url, function (content) { 182 | text.finishLoad(name, parsed.strip, content, onLoad); 183 | }, function (err) { 184 | if (onLoad.error) { 185 | onLoad.error(err); 186 | } 187 | }); 188 | } else { 189 | //Need to fetch the resource across domains. Assume 190 | //the resource has been optimized into a JS module. Fetch 191 | //by the module name + extension, but do not include the 192 | //!strip part to avoid file system issues. 193 | req([nonStripName], function (content) { 194 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 195 | parsed.strip, content, onLoad); 196 | }); 197 | } 198 | }, 199 | 200 | write: function (pluginName, moduleName, write, config) { 201 | if (buildMap.hasOwnProperty(moduleName)) { 202 | var content = text.jsEscape(buildMap[moduleName]); 203 | write.asModule(pluginName + "!" + moduleName, 204 | "define(function () { return '" + 205 | content + 206 | "';});\n"); 207 | } 208 | }, 209 | 210 | writeFile: function (pluginName, moduleName, req, write, config) { 211 | var parsed = text.parseName(moduleName), 212 | extPart = parsed.ext ? '.' + parsed.ext : '', 213 | nonStripName = parsed.moduleName + extPart, 214 | //Use a '.js' file name so that it indicates it is a 215 | //script that can be loaded across domains. 216 | fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; 217 | 218 | //Leverage own load() method to load plugin value, but only 219 | //write out values that do not have the strip argument, 220 | //to avoid any potential issues with ! in file names. 221 | text.load(nonStripName, req, function (value) { 222 | //Use own write() method to construct full module value. 223 | //But need to create shell that translates writeFile's 224 | //write() to the right interface. 225 | var textWrite = function (contents) { 226 | return write(fileName, contents); 227 | }; 228 | textWrite.asModule = function (moduleName, contents) { 229 | return write.asModule(moduleName, fileName, contents); 230 | }; 231 | 232 | text.write(pluginName, nonStripName, textWrite, config); 233 | }, config); 234 | } 235 | }; 236 | 237 | if (masterConfig.env === 'node' || (!masterConfig.env && 238 | typeof process !== "undefined" && 239 | process.versions && 240 | !!process.versions.node)) { 241 | //Using special require.nodeRequire, something added by r.js. 242 | fs = require.nodeRequire('fs'); 243 | 244 | text.get = function (url, callback) { 245 | var file = fs.readFileSync(url, 'utf8'); 246 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 247 | if (file.indexOf('\uFEFF') === 0) { 248 | file = file.substring(1); 249 | } 250 | callback(file); 251 | }; 252 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 253 | text.createXhr())) { 254 | text.get = function (url, callback, errback, headers) { 255 | var xhr = text.createXhr(), header; 256 | xhr.open('GET', url, true); 257 | 258 | //Allow plugins direct access to xhr headers 259 | if (headers) { 260 | for (header in headers) { 261 | if (headers.hasOwnProperty(header)) { 262 | xhr.setRequestHeader(header.toLowerCase(), headers[header]); 263 | } 264 | } 265 | } 266 | 267 | //Allow overrides specified in config 268 | if (masterConfig.onXhr) { 269 | masterConfig.onXhr(xhr, url); 270 | } 271 | 272 | xhr.onreadystatechange = function (evt) { 273 | var status, err; 274 | //Do not explicitly handle errors, those should be 275 | //visible via console output in the browser. 276 | if (xhr.readyState === 4) { 277 | status = xhr.status; 278 | if (status > 399 && status < 600) { 279 | //An http 4xx or 5xx error. Signal an error. 280 | err = new Error(url + ' HTTP status: ' + status); 281 | err.xhr = xhr; 282 | errback(err); 283 | } else { 284 | callback(xhr.responseText); 285 | } 286 | } 287 | }; 288 | xhr.send(null); 289 | }; 290 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 291 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 292 | //Why Java, why is this so awkward? 293 | text.get = function (url, callback) { 294 | var stringBuffer, line, 295 | encoding = "utf-8", 296 | file = new java.io.File(url), 297 | lineSeparator = java.lang.System.getProperty("line.separator"), 298 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 299 | content = ''; 300 | try { 301 | stringBuffer = new java.lang.StringBuffer(); 302 | line = input.readLine(); 303 | 304 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 305 | // http://www.unicode.org/faq/utf_bom.html 306 | 307 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 308 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 309 | if (line && line.length() && line.charAt(0) === 0xfeff) { 310 | // Eat the BOM, since we've already found the encoding on this file, 311 | // and we plan to concatenating this buffer with others; the BOM should 312 | // only appear at the top of a file. 313 | line = line.substring(1); 314 | } 315 | 316 | stringBuffer.append(line); 317 | 318 | while ((line = input.readLine()) !== null) { 319 | stringBuffer.append(lineSeparator); 320 | stringBuffer.append(line); 321 | } 322 | //Make sure we return a JavaScript string and not a Java string. 323 | content = String(stringBuffer.toString()); //String 324 | } finally { 325 | input.close(); 326 | } 327 | callback(content); 328 | }; 329 | } 330 | 331 | return text; 332 | }); 333 | --------------------------------------------------------------------------------