Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b347f3c
Using lowercased usernames (Github is case insensitive here) instead …
Jun 12, 2013
faa4519
Config love
Jun 12, 2013
c9deb93
Readme now reflects config love
Jun 12, 2013
7f697af
Removing console.logs
Jun 13, 2013
5f4cdc9
Improved installation section in README
Jun 13, 2013
d9676b7
Refactored workspaces router + added keepalive
Jun 13, 2013
44dea0e
Port-number wrapping + keep alive in the server
Jun 13, 2013
cb10b3b
Adding keepalive calls in frontend
Jun 13, 2013
caec86c
Improved port number counter middleware
Jun 13, 2013
5f1f9b7
Updated readme
Jun 13, 2013
df25950
Fixed typo on login page
Jun 13, 2013
9f0004b
Adding Topcoat
Jun 13, 2013
dae3d1c
Restyle w/ topcoat and love.
Jun 13, 2013
49e6d18
Extended license info in README
Jun 14, 2013
b775fdb
Update index.html
Nov 4, 2013
fac1a1a
Update style.styl
Nov 4, 2013
603150c
Update README.md
Nov 4, 2013
e43c378
Merge pull request #1 from Karnith/master
AVGP Nov 7, 2013
9466081
Created a quick install script.
AVGP Feb 6, 2014
5b32606
Using config to get baseUrl instead of hardcoding it
AVGP Feb 6, 2014
b983be3
Adding BASE_URL to config.js.example
AVGP Feb 6, 2014
139b3ca
Update README.md
AVGP Feb 6, 2014
99d37f0
Update install.sh
calraiden Feb 18, 2014
ec69b1e
Merge pull request #2 from calraiden/patch-1
AVGP Feb 18, 2014
37b8b79
Fixing duplication of PERMITTED_USERS to fix #3
AVGP Mar 21, 2014
f9e3f17
Use secure https connection if certificates are provided
stealthdave Mar 22, 2014
1727bff
remove unused library (crypto)
stealthdave Mar 22, 2014
fec359f
Create random username and password for each C9 instance
stealthdave Mar 24, 2014
74ed242
Remove sloppy git commit additions
stealthdave Mar 24, 2014
1d0f3a1
remove generated file
stealthdave Mar 24, 2014
5a62f61
Create random username and password for each C9 instance
stealthdave Mar 24, 2014
167dea9
Merge branch 'master' of https://github.com/stealthdave/cloud9hub
stealthdave Mar 24, 2014
f097051
Don't kill the server if an expired workspace process has already died
stealthdave Mar 25, 2014
c9b8bf2
Merge pull request #4 from stealthdave/master
AVGP May 5, 2014
517b08f
Created a quick install script.
AVGP Feb 6, 2014
e05ccfb
Adding BASE_URL to config.js.example
AVGP Feb 6, 2014
7e83ef0
Update install.sh
calraiden Feb 18, 2014
5ed2687
Added .bowerrc
etodanik Mar 23, 2014
3d4237f
.gitignore bower stuff
etodanik Mar 23, 2014
57e51a0
bower config & dependencies
etodanik Mar 23, 2014
240721f
cloud9 is a desktop app, so it would make more sense to use the deskt…
etodanik Mar 23, 2014
9eda6a8
switch to a local bower managed angular.js file
etodanik Mar 23, 2014
0dbf470
install.sh should also take care of bower dependencies
etodanik Mar 23, 2014
6902172
angular-route is now an external module
etodanik Mar 23, 2014
1710522
on second thought, no pushy. let's pursue a model more similar to clo…
etodanik Mar 23, 2014
610fe37
Added some grunt magic
etodanik Mar 23, 2014
8ec1e20
change pack for better view handling
etodanik Mar 23, 2014
058036b
changed to sass + magic
etodanik Mar 23, 2014
e1746e4
visual overhaul
etodanik Mar 23, 2014
66a8bc5
login styling
etodanik Mar 23, 2014
af2930b
Series of fixes
etodanik Mar 24, 2014
5beb4da
styling changes
etodanik Mar 24, 2014
423948d
we don't want compiled css in the repo
etodanik Mar 24, 2014
8acf3a3
don't attempt to create a new workspace if we just want to update
etodanik Mar 24, 2014
208e969
flat-ui is based on bootstrap and seems to be a more flexible base fo…
etodanik Mar 24, 2014
c07c240
Various styling fixes
etodanik Mar 24, 2014
4eadca5
Added port detection. We're not just blindly assigning ports now
etodanik Mar 24, 2014
dfeae83
Bigger header for repositories
etodanik Mar 24, 2014
61adb0a
Get rid of annoying faux bold
etodanik Mar 24, 2014
a714f32
reset current workspace on delete
etodanik Mar 24, 2014
a06d47e
Merge branch 'PR5'
AVGP May 5, 2014
e9e016a
Fixed broken link Readme.md
thoys Sep 3, 2014
bcbd58b
Merge pull request #13 from thoys/patch-1
AVGP Sep 4, 2014
1deb648
Fix broken link for quick install
naufraghi Mar 21, 2015
fa57bf9
Merge pull request #20 from naufraghi/patch-1
AVGP Mar 25, 2015
4f70413
Fix broken Markdown headings
bryant1410 Apr 17, 2017
d67b514
Merge pull request #28 from bryant1410/master
harjot1singh Aug 3, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"directory": "public/lib",
"storage": {
"packages": ".bower-cache",
"registry": ".bower-registry"
},
"tmp": ".bower-tmp"
}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
workspaces/*
node_modules
config.js
.bower-*/
public/lib
.c9revisions
.settings
public/css/*
56 changes: 39 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@
#Cloud9Hub
# Cloud9Hub

##What's this?
## What's this?
It's a simple interface for the Cloud 9 open source edition to easily create, use and manage multiple workspaces.
[The Cloud9 service](https://c9.io) has a shiny and awesome dashboard interface where you can manage multiple workspaces,
however the [open source edition](https://github.com/ajaxorg/cloud9) is a single workspace instance of Cloud9.

As I like the possibility to easily start working on different workspaces, create or delete them, I created Cloud9Hub to do so.

##What's Cloud9?
## What's Cloud9?
A full-blown IDE in your browser. It has a full terminal integration, can run and deploy code of different languages (e.g. Ruby, node.js, PHP)
and [lots more](https://c9.io/site/features/).
and [lots more](http://en.wikipedia.org/wiki/Cloud9_IDE#Features).

##Status Quo of Cloud9Hub
## Status Quo of Cloud9Hub
Right now it can
* Create new workspaces
* Launch multiple workspace instances
* Kill them automatically after 15 minutes
* List available workspaces
* Delete workspaces

It **can't**
* Manage multiple users
* Do authentication/sessions
* Sense, that you're active and **not** kill your workspace after 15 minutes.
* Sense, that you're active and will kill your workspace after 15-20 minutes of inactivity.

right now. These are the next steps for me to build (or you make a Pull Request with the features you want).

##Installation
## Installation
First you will need [node.js](http://nodejs.org/), at least v0.8.

**Note, as of June, 12th 2013:** Cloud9 right now breaks when you try installing it with node > 0.8.x :(
Then you can try the quick install or the manual way:

### Quick install

```shell
curl https://raw.githubusercontent.com/AVGP/cloud9hub/master/install.sh | sh
```

This should install Cloud9 and Cloud9Hub into the current folder. If this succeeded, you can now go to the configuration section.

### Manual installation
1. Install [Cloud9](https://github.com/ajaxorg/cloud9) into some folder, say ``/var/awesomeness/cloud9``.
**Note, the cloud9 is currently hardcoded to c9. when cloning cloud9, clone to c9 dir. If this isn't done, hub will crash.
2. Then install Cloud9Hub into the parent folder above your cloud9 installation, so in my example``/var/awesomeness/cloud9hub` and run ``npm install``.

# Configuration

First things first: You need a Github application to provide the "Login with Github" feature, which is currently the only login mechanism.

Go to [https://github.com/settings/applications/new](https://github.com/settings/applications/new) and create a new application. Note down the client ID and secret, you'll need them later.

Now copy the ``config.js.example`` to ``config.js`` and edit the contents:

- Add your Github client ID and secret
- Change your BASE_URL to your server's address (do not include the port!)

Install [Cloud9](https://github.com/ajaxorg/cloud9) into some folder, say ``/var/awesomeness/cloud9``.
Then install Cloud9hub into the parent folder above your cloud9 installation, so in my example``/var/awesomeness/cloud9hub`.
Start Cloud9 hub with ``node server``.
## Firewall
You will need ports 3000 and 5000 to however many connections will be taking place concurrently (each session is given a different port)

##Running as a daemon
## Running as a daemon
If you wish to, you can run it as a daemon, so that it stays alive.

To do so, I recommend [forever](https://npmjs.org/package/forever).

##License
[MIT License](http://opensource.org/licenses/MIT), baby.
## License
**This project:** [MIT License](http://opensource.org/licenses/MIT), baby.
**Cloud9 itself:** [GPL](http://www.gnu.org/licenses/gpl.html)

##WARNING
## WARNING
This is highly insecure, experimental and it may bite.
11 changes: 11 additions & 0 deletions bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "cloud9hub",
"version": "0.0.1",
"dependencies": {
"angular": "~1.2.15",
"topcoat": "~0.8.0",
"angular-route": "~1.2.15",
"fontawesome": "~4.0.3",
"flat-ui-official": "~2.1.3"
}
}
16 changes: 16 additions & 0 deletions config.js.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
exports.GITHUB_CLIENT_ID = '3909fc1ee9f8ed9e87f2';
exports.GITHUB_CLIENT_SECRET = 'e5b3802ef773d8d32a8b29fe958d04e591be850d';

// This allows everybody to sign up
exports.PERMITTED_USERS = false;
// This would only allow alice and bob to sign up
//exports.PERMITTED_USERS = ['alice', 'bob'];

// Add SSL Certificates to use Cloud9Hub over SSL
// (Cloud9 Workspaces will still be unsecured standard HTTP)
//exports.SSL = {
// key: "/path/to/ssl.key",
// cert: "/path/to/ssl.pem"
//};

exports.BASE_URL = 'http://localhost';
8 changes: 8 additions & 0 deletions controllers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

/*
* GET home page.
*/

exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
8 changes: 8 additions & 0 deletions controllers/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

/*
* GET home page.
*/

exports.login = function(req, res){
res.render('login', {});
};
181 changes: 181 additions & 0 deletions controllers/workspaces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
var fs = require('fs'),
path = require('path'),
rimraf = require('rimraf'),
_ = require('lodash'),
spawn = require('child_process').spawn;

var respondInvalidWorkspace = function(res) {
res.status(400);
res.json({msg: "Invalid workspace name"});
};

var createWorkspace = function(params, req, res) {
var potentiallyBadPathName = params.name.split(path.sep);
var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];

if(workspaceName === '..') {
respondInvalidWorkspace(res);
return;
}

fs.mkdir(__dirname + '/../workspaces/' + req.user + "/" + workspaceName, '0700', function(err) {
if(err) {
respondInvalidWorkspace(res);
return;
}

res.json({msg: "Workspace " + workspaceName + " was created."});
});
}

var createWorkspaceKillTimeout = function(req, workspaceProcess, workspaceName) {
var timeout = setTimeout(function() {
process.kill(-workspaceProcess.pid, 'SIGTERM');
req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = undefined;
console.info("Killed workspace " + workspaceName);
}, 900000); //Workspaces have a lifetime of 15 minutes

return timeout;
};

/*
* POST/GET create a new workspace
*/
exports.create = function(req, res) {
if(req.body.name) {
createWorkspace(req.body, req, res);
} else {
respondInvalidWorkspace(res);
}
}

/*
* GET workspaces listing.
*/
exports.list = function(req, res){
fs.readdir(__dirname + '/../workspaces/' + req.user, function(err, files) {
if(err) {
res.status(500);
res.json({error: err});
} else {
var workspaces = [];
for(var i=0; i< files.length; i++) {
// Skip hidden files
if(files[i][0] === '.') continue;

workspaces.push({name: files[i]})
}
res.json({workspaces: workspaces});
}
});
};

/**
* DELETE destroys a workspace
*/
exports.destroy = function(req, res) {
var potentiallyBadPathName = req.params.name.split(path.sep);
var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];

if(workspaceName === '..') {
respondInvalidWorkspace(res);
return;
}

rimraf(__dirname + "/../workspaces/" + req.user + "/" + workspaceName, function(err) {
if(err) {
res.status("500");
res.json({msg: "Something went wrong :("});
return;
}
res.json({msg: "Successfully deleted " + workspaceName});
})
};

/*
* GET run a workspace
*/
exports.run = function(req, res) {
var potentiallyBadPathName = req.params.name.split(path.sep);
var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];

var isPortTaken = function(port, fn) {
console.log('checking if port', port, 'is taken');
var net = require('net')
var tester = net.createServer()
.once('error', function (err) {
if (err.code != 'EADDRINUSE') return fn(err)
console.log('port', port, 'seems to be taken');
fn(null, true)
})
.once('listening', function() {
tester.once('close', function() {
console.log('port', port, 'seems to be available');
fn(null, false)
})
.close()
})
.listen(port)
};

var getNextAvailablePort = function(callback){
var nextFreeWorkspacePort = req.app.get('nextFreeWorkspacePort');

if(nextFreeWorkspacePort > 10000) {
nextFreeWorkspacePort = 5000;
}

nextFreeWorkspacePort = nextFreeWorkspacePort + 1;
console.log('setting nextFreeWorkspacePort to', nextFreeWorkspacePort);
req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort);

isPortTaken(nextFreeWorkspacePort, function(err, taken){
if(taken){
getNextAvailablePort(callback);
} else {
req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort);
callback(nextFreeWorkspacePort);
}
});
};

if(workspaceName === '..') {
respondInvalidWorkspace(res);
return;
}

if(typeof req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] === 'undefined'){
getNextAvailablePort(function(nextFreePort){
console.log("Starting " + __dirname + '/../../c9/bin/cloud9.sh for workspace ' + workspaceName + " on port " + nextFreePort);

var workspace = spawn(__dirname + '/../../c9/bin/cloud9.sh', ['-w', __dirname + '/../workspaces/' + req.user + '/' + workspaceName, '-l', '0.0.0.0', '-p', nextFreePort], {detached: true});
workspace.stderr.on('data', function (data) {
console.log('stdERR: ' + data);
});

req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = {
killTimeout: createWorkspaceKillTimeout(req, workspace, workspaceName),
process: workspace,
name: workspaceName,
url: req.app.settings.baseUrl + ":" + nextFreePort,
user: req.user
};

res.json({msg: "Attempted to start workspace", user: req.user, url: req.app.settings.baseUrl + ":" + nextFreePort});
});
} else {
console.log("Found running workspace", req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url);
res.json({msg: "Found running workspace", user: req.user, url: req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url});
}

}

/*
* POST to keep the workspace alive
*/
exports.keepAlive = function(req, res) {
var workspace = req.app.get('runningWorkspaces')[req.user + '/' + req.params.name];
clearTimeout(workspace.killTimeout);
workspace.killTimeout = createWorkspaceKillTimeout(req, workspace.process, workspace.name);
res.send();
}
Loading