Skip to content

Commit

Permalink
[WO-592] implement webmail using socket.io
Browse files Browse the repository at this point in the history
* Relax CSP to allow iframe assets to load
* Integrate socket.io proxy
* go to /# when controllers not initiated
* Add offline caching using AppCache manifest
  • Loading branch information
Tankred Hase committed Sep 16, 2014
1 parent e29f083 commit 487bb31
Show file tree
Hide file tree
Showing 20 changed files with 296 additions and 234 deletions.
17 changes: 16 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ module.exports = function(grunt) {
}
},

manifest: {
generate: {
options: {
basePath: 'dist/',
timestamp: true,
hash: true,
cache: ['socket.io/socket.io.js'],
master: ['index.html']
},
src: ['**/*.*'],
dest: 'dist/appcache.manifest'
}
},

nodewebkit: {
options: {
version: '0.9.2', // node-webkit version
Expand All @@ -238,12 +252,13 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-compress');
grunt.loadNpmTasks('grunt-node-webkit-builder');
grunt.loadNpmTasks('grunt-manifest');

// Build tasks
grunt.registerTask('dist-npm', ['copy:npm', 'copy:npmDev', 'copy:cryptoLib']);
grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']);
grunt.registerTask('dist-copy', ['copy']);
grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-copy']);
grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-copy', 'manifest']);

// Test/Dev tasks
grunt.registerTask('dev', ['connect:dev']);
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ Then visit [http://localhost:8580/dist/#/desktop?dev=true](http://localhost:8580

grunt watch

## Releasing
## Releasing Chrome App

grunt release-test --release=0.0.0.x
grunt release-stable --release=0.x.0

## Deploying Web App

First build and generate the `dist/` directory:

grunt

Then deploy that directoy by adding it to a local git branch. Push that branch to your node.js server and then start the server:

npm start

## License

Copyright © 2014, Whiteout Networks GmbH. All rights reserved.
Expand Down
12 changes: 12 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

module.exports = {
server: {
port: process.env.PORT || 8889,
host: "0.0.0.0"
},
log: {
level: "silly",
http: ':remote-addr [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer"'
}
};
10 changes: 10 additions & 0 deletions config/integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

module.exports = {
server: {
port: 8889
},
log: {
level: "error"
}
};
28 changes: 16 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"node": ">=0.10"
},
"scripts": {
"postinstall": "grunt",
"test": "grunt && grunt test",
"start": "node server.js"
},
Expand All @@ -39,26 +38,31 @@
"ng-infinite-scroll": "~1.1.2",
"pgpbuilder": "~0.4.0",
"pgpmailer": "~0.4.0",
"requirejs": "~2.1.14"
"requirejs": "~2.1.14",
"config": "^1.0.2",
"morgan": "^1.2.3",
"npmlog": "^0.1.1",
"socket.io": "^1.0.6"
},
"devDependencies": {
"angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version",
"browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master",
"browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master",
"grunt": "~0.4.1",
"mocha": "~1.13.0",
"chai": "~1.7.2",
"sinon": "~1.7.3",
"grunt": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-manifest": "^0.4.0",
"grunt-autoprefixer": "~0.7.2",
"grunt-contrib-compress": "~0.5.2",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-jshint": "~0.6.4",
"grunt-mocha": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-csso": "~0.6.1",
"grunt-contrib-sass": "~0.7.3",
"grunt-autoprefixer": "~0.7.2",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-compress": "~0.5.2",
"grunt-node-webkit-builder": "~0.1.17"
"grunt-csso": "~0.6.1",
"grunt-mocha": "~0.4.1",
"grunt-node-webkit-builder": "~0.1.17",
"mocha": "~1.13.0",
"sinon": "~1.7.3"
}
}
146 changes: 141 additions & 5 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,70 @@
'use strict';

var express = require('express'),
compression = require('compression'),
app = express();
process.chdir(__dirname);

var cluster = require('cluster');
var config = require('config');
var log = require('npmlog');

log.level = config.log.level;

// Handle error conditions
process.on('SIGTERM', function() {
log.warn('exit', 'Exited on SIGTERM');
process.exit(0);
});

process.on('SIGINT', function() {
log.warn('exit', 'Exited on SIGINT');
process.exit(0);
});

process.on('uncaughtException', function(err) {
log.error('uncaughtException ', err);
process.exit(1);
});

if (cluster.isMaster) {
// MASTER process

cluster.on('fork', function(worker) {
log.info('cluster', 'Forked worker #%s [pid:%s]', worker.id, worker.process.pid);
});

cluster.on('exit', function(worker) {
log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid);
setTimeout(function() {
cluster.fork();
}, 1000);
});

// Fork a single worker
cluster.fork();
return;
}

// WORKER process

var express = require('express');
var compression = require('compression');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);
var net = require('net');

// Setup logger. Stream all http logs to general logger
app.use(require('morgan')(config.log.http, {
'stream': {
'write': function(line) {
if ((line = (line || '').trim())) {
log.http('express', line);
}
}
}
}));

// Do not advertise Express
app.disable('x-powered-by');

//
// web server config
Expand All @@ -17,7 +79,8 @@ app.use(function(req, res, next) {
// HSTS
res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains');
// CSP
res.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data:");
var iframe = development ? "http://" + req.hostname + ":" + port : "https://" + req.hostname; // allow iframe to load assets
res.set('Content-Security-Policy', "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src 'self' data:");
return next();
});

Expand All @@ -38,11 +101,84 @@ app.use(express.static(__dirname + '/dist', {
maxAge: oneDay
}));

//
// Socket.io proxy
//

io.on('connection', function(socket) {

log.info('io', 'New connection [%s]', socket.conn.id);

var idCounter = 0;

socket.on('open', function(data, fn) {
var socketId = ++idCounter;
var tcp;

log.verbose('io', 'Open request to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);

tcp = net.connect(data.port, data.host, function() {
log.verbose('io', 'Opened tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);

tcp.on('data', function(chunk) {
log.silly('io', 'Received %s bytes from %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId);
socket.emit('data-' + socketId, chunk);
});

tcp.on('error', function(err) {
log.verbose('io', 'Error for %s:%s [%s:%s]: %s', data.host, data.port, socket.conn.id, socketId, err.message);
socket.emit('error-' + socketId, err.message);
});

tcp.on('end', function() {
socket.emit('end-' + socketId);
});

tcp.on('close', function() {
log.verbose('io', 'Closed tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
socket.emit('close-' + socketId);

socket.removeAllListeners('data-' + socketId);
socket.removeAllListeners('end-' + socketId);
});

socket.on('data-' + socketId, function(chunk, fn) {
if (!chunk || !chunk.length) {
if (typeof fn === 'function') {
fn();
}
return;
}
log.silly('io', 'Sending %s bytes to %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId);
tcp.write(chunk, function() {
if (typeof fn === 'function') {
fn();
}
});
});

socket.on('end-' + socketId, function() {
log.verbose('io', 'Received request to close connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
tcp.end();
});

if (typeof fn === 'function') {
fn(socketId);
}
});
});

socket.on('disconnect', function() {
log.info('io', 'Closed connection [%s]', socket.conn.id);
socket.removeAllListeners();
});
});

//
// start server
//

app.listen(port);
server.listen(port);
if (development) {
console.log(' > starting in development mode');
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html ng-app="mail" ng-csp>
<html ng-app="mail" ng-csp manifest="appcache.manifest">
<head>
<meta charset="utf-8">
<title>Whiteout Mail</title>
Expand Down Expand Up @@ -29,6 +29,7 @@
<link rel="stylesheet" media="all" href="css/all.min.css" type="text/css">

<!-- The Scripts -->
<script src="socket.io/socket.io.js"></script>
<script src="lib/require.js"></script>
<script src="require-config.js"></script>
<script src="js/app.js"></script>
Expand Down
7 changes: 6 additions & 1 deletion src/js/controller/add-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ define(function(require) {

var appCtrl = require('js/app-controller');

var AddAccountCtrl = function($scope, $location) {
var AddAccountCtrl = function($scope, $location, $routeParams) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}

$scope.connectToGoogle = function() {
// test for oauth support
if (appCtrl._auth._oauth.isSupported()) {
Expand Down
7 changes: 6 additions & 1 deletion src/js/controller/login-existing.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ define(function(require) {

var appController = require('js/app-controller');

var LoginExistingCtrl = function($scope, $location) {
var LoginExistingCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}

var emailDao = appController._emailDao;

$scope.buttonEnabled = true;
Expand Down
7 changes: 6 additions & 1 deletion src/js/controller/login-initial.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ define(function(require) {

var appController = require('js/app-controller');

var LoginInitialCtrl = function($scope, $location) {
var LoginInitialCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}

var emailDao = appController._emailDao,
states, termsMsg = 'You must accept the Terms of Service to continue.';

Expand Down
7 changes: 6 additions & 1 deletion src/js/controller/login-new-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ define(function(require) {
var angular = require('angular'),
appController = require('js/app-controller');

var LoginExistingCtrl = function($scope, $location) {
var LoginExistingCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}

var emailDao = appController._emailDao,
pgp = appController._pgp;

Expand Down
7 changes: 6 additions & 1 deletion src/js/controller/login-privatekey-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ define(function(require) {

var appController = require('js/app-controller');

var LoginPrivateKeyDownloadCtrl = function($scope, $location) {
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}

var keychain = appController._keychain,
emailDao = appController._emailDao,
userId = emailDao._account.emailAddress;
Expand Down
Loading

0 comments on commit 487bb31

Please sign in to comment.