├── .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 |
10 |
Loading...
11 |
12 |
13 |
14 |
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 |
18 |
19 |
Total:
20 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------