├── config.example.json ├── lib ├── proxy.js ├── app.js └── dockerControl.js ├── README.md ├── .gitignore ├── package.json ├── LICENSE ├── server.js └── public └── index.html /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerOptions": { 3 | "socketPath": "/var/run/docker.sock" 4 | }, 5 | "hostName": "generalhenry.com", 6 | "dockerHost": "http://localhost:", 7 | "port": 80, 8 | "imagePrefix": "nodeschool/", 9 | "server": true 10 | } 11 | -------------------------------------------------------------------------------- /lib/proxy.js: -------------------------------------------------------------------------------- 1 | var httpProxy = require('http-proxy'); 2 | 3 | var proxy = module.exports = httpProxy.createProxyServer({}); 4 | 5 | proxy.on('error', handleProxyError); 6 | 7 | function handleProxyError(err) { 8 | console.log('proxy error'); 9 | console.error(err); 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nodeschool-interactive 2 | ====================== 3 | 4 | interactive nodeschool online 5 | 6 | Powered by docker containers 7 | 8 | Connected to the web with pty.js and term.js 9 | 10 | Docker files @ https://github.com/generalhenry/nodeschool-dockerfiles 11 | 12 | Docker images @ https://hub.docker.com/u/nodeschool/ 13 | 14 | Docker garbage collection @ https://github.com/generalhenry/docker-gc 15 | 16 | Connection to web @ https://github.com/generalhenry/expose-bash-over-websockets 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | config.json 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "route", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node --harmony server.js", 9 | "postinstall": "[ ! -f config.json ] && cp config.example.json config.json" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "co-request": "^0.2.0", 15 | "dockerode": "^2.0.5", 16 | "empty-port": "0.0.1", 17 | "http-proxy": "^1.2.0", 18 | "koa": "^0.9.0", 19 | "koa-static": "^1.4.6", 20 | "mkdirp": "^0.5.0", 21 | "rimraf": "^2.2.8", 22 | "thunkify": "^2.1.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Henry Allen-Tilford 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var path = require('path'); 3 | var app = require('./lib/app'); 4 | var proxy = require('./lib/proxy'); 5 | 6 | var portList = [80, 2000, 3000, 4000, 5000, 8000, 8080, 12491, 12492]; 7 | 8 | portList.forEach(attachToPort); 9 | 10 | function attachToPort (port) { 11 | var server = http.createServer(app.callback()); 12 | 13 | server.listen(port); 14 | server.on('error', handleServerError); 15 | server.on('upgrade', handleUpgrade); 16 | 17 | function handleServerError (err) { 18 | console.log('server error', 'port:', port); 19 | console.error(err); 20 | } 21 | } 22 | 23 | function handleUpgrade (req, socket, head) { 24 | var host = req.headers.host; 25 | // if subdomain 26 | if (host.split('.').length > 2) { 27 | var name = host.split('.').shift(); 28 | var socketPath = path.join(__dirname, '../binds', name, 'proxy'); 29 | 30 | proxy.ws(req, socket, head, { 31 | target: { 32 | socketPath: socketPath 33 | } 34 | }, handleRequestErrors); 35 | req.on('error', handleRequestErrors); 36 | 37 | function handleRequestErrors (err) { 38 | console.log('websocket request error', { 39 | name: name, 40 | socketPath: socketPath 41 | }); 42 | console.error(err); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var koa = require('koa'); 3 | var serve = require('koa-static'); 4 | var config = require('../config'); 5 | var proxy = require('./proxy'); 6 | var dockerControl = require('./dockerControl'); 7 | 8 | var app = module.exports = koa(); 9 | 10 | app.use(function *proxyWorkshopContainers(next) { 11 | var host = this.request.headers.host; 12 | // if subdomain 13 | if (host.split('.').length > 2) { 14 | var name = host.split('.').shift(); 15 | yield dockerControl.readyContainer(name); 16 | var socketPath = path.join(__dirname, '../binds', name, 'proxy'); 17 | proxy.web(this.req, this.res, { 18 | target: { 19 | socketPath: socketPath 20 | } 21 | }, function (err) { 22 | console.log('web proxy error'); 23 | console.error(err); 24 | }); 25 | this.respond = false; 26 | } else { 27 | yield next; 28 | } 29 | }); 30 | 31 | app.use(serve('public')); 32 | 33 | app.use(function *createContainers(next) { 34 | var image = path.basename(this.request.url); 35 | try { 36 | var name = yield dockerControl.createContainer(image); 37 | return this.response.redirect('http://' + name + '.' + config.hostName); 38 | } catch (err) { 39 | console.log('container create error', 'image', image); 40 | console.error(err); 41 | return next; 42 | } 43 | }); 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Nodeschool interactive 4 | 5 | 6 | 7 | 16 |
17 |
18 | 23 |
24 |
25 | browserify-adventure 26 | learnyounode 27 | levelmeup 28 | stream-adventure 29 | bytewiser 30 | functional-javascript-workshop 31 | git-it 32 | expressworks 33 | makemehapi 34 | promise-it-wont-hurt 35 | async-you 36 | 37 | goingnative 38 | shader-school 39 | bug-clinic 40 | 41 | count-to-6 42 | npm-tutor 43 | data-plumber 44 | lololodash 45 | planetproto 46 | kick-off-koa 47 | sandbox 48 | repl 49 | -------------------------------------------------------------------------------- /lib/dockerControl.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var path = require('path'); 3 | var Dockerode = require('dockerode'); 4 | var emptyPort = require('empty-port'); 5 | var thunkify = require('thunkify'); 6 | var mkdirp = require('mkdirp'); 7 | var rimraf = require('rimraf'); 8 | var config = require('../config'); 9 | 10 | module.exports = { 11 | readyContainer: readyContainer, 12 | createContainer: createContainer 13 | } 14 | 15 | var docker = new Dockerode(config.dockerOptions); 16 | var emptyPort = thunkify(emptyPort); 17 | docker.createContainer = thunkify(docker.createContainer); 18 | mkdirp = thunkify(mkdirp); 19 | rimraf = thunkify(rimraf); 20 | 21 | function *startContainer(container) { 22 | var port = yield emptyPort({}); 23 | var folder = path.join(__dirname, '../binds', container.id); 24 | yield mkdirp(folder); 25 | yield rimraf(folder + '/*'); 26 | container.start = thunkify(container.start); 27 | yield container.start({ 28 | Binds: [folder + ':/var/run'] 29 | }); 30 | yield waitForPort(folder); 31 | return port; 32 | } 33 | 34 | function *unpauseContainer(container) { 35 | container.unpause = thunkify(container.unpause); 36 | yield container.unpause(); 37 | yield waitForPort(path.join(__dirname, '../binds', container.id)); 38 | } 39 | 40 | function *waitForPort(folder) { 41 | var count = 1000; 42 | while (count-- > 0) { 43 | try { 44 | var result = yield get({ socketPath: path.join(folder, 'proxy') }); 45 | break; 46 | } catch (error) { 47 | ignore(error); 48 | } 49 | } 50 | if (count < 1) { 51 | throw new Error('Could not connect to container'); 52 | } 53 | } 54 | 55 | function get(options) { 56 | return function getThunk(cb) { 57 | setTimeout(function getAttempt () { 58 | var req = http.get(options); 59 | req.on('error', cb); 60 | req.on('response', function handleGetResponse(res) { 61 | res.on('error', cb); 62 | res.on('data', ignore); 63 | res.on('end', cb); 64 | }); 65 | }, 50); 66 | }; 67 | } 68 | 69 | function *readyContainer(name) { 70 | var container = docker.getContainer(name); 71 | container.inspect = thunkify(container.inspect); 72 | var info = yield container.inspect(); 73 | if (!info.State.Running) { 74 | yield startContainer(container); 75 | } 76 | if (info.State.Paused) { 77 | yield unpauseContainer(container); 78 | } 79 | } 80 | 81 | function *createContainer(image) { 82 | var container = yield docker.createContainer({ 83 | Image: config.imagePrefix + image, 84 | Hostname: image, 85 | Tty: true, 86 | Volumes: { 87 | '/var/run': {} 88 | } 89 | }); 90 | container.inspect = thunkify(container.inspect); 91 | var info = yield container.inspect(); 92 | return info.Name; 93 | } 94 | 95 | function ignore() {} 96 | --------------------------------------------------------------------------------