├── web ├── scoreboard.html ├── img │ ├── favicon.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── stuff.html ├── js │ ├── routes.js │ ├── model.js │ ├── jquery.cookie.js │ ├── etherpad.js │ ├── helpers.js │ ├── jquery.timeago.js │ ├── ctfpad.coffee │ ├── jquery.iframe-transport.js │ ├── ctfpad.js │ ├── jquery.ui.widget.js │ └── bootstrap.min.js ├── css │ ├── jquery.fileupload.css │ └── github-markdown.css ├── files.html ├── login.html ├── doc │ └── API.html └── index.html ├── main.js ├── .gitignore ├── new_certs.sh ├── render.rb ├── config.json.example ├── package.json ├── ctfpad.sql ├── LICENSE.md ├── README.md ├── API.md ├── database.coffee ├── api.coffee └── main.coffee /web/scoreboard.html: -------------------------------------------------------------------------------- 1 | {{#.}}{{.}}{{/.}} 2 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./main.coffee'); 3 | -------------------------------------------------------------------------------- /web/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StratumAuhuur/CTFPad/HEAD/web/img/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ctfpad.sqlite 3 | cert.pem 4 | key.pem 5 | config.json 6 | *.swp 7 | uploads 8 | -------------------------------------------------------------------------------- /web/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StratumAuhuur/CTFPad/HEAD/web/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /web/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StratumAuhuur/CTFPad/HEAD/web/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /new_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | openssl genrsa -out key.pem 3 | openssl req -new -key key.pem -out csr.pem 4 | openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem 5 | rm csr.pem 6 | -------------------------------------------------------------------------------- /render.rb: -------------------------------------------------------------------------------- 1 | require 'github/markup' 2 | puts '' 3 | puts '
' 4 | puts GitHub::Markup.render('API.md', File.read('API.md')) 5 | puts '
' 6 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "port": 1234, 3 | "etherpad_port": 1235, 4 | "etherpad_internal_port": 9001, 5 | "keyfile": "key.pem", 6 | "certfile": "cert.pem", 7 | "authkey": "CHANGE_THIS", 8 | "useHTTPS": false, 9 | "proxyUseHTTPS": false, 10 | "team_name": "Foo" 11 | } 12 | -------------------------------------------------------------------------------- /web/stuff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | StratumAuhuur 5 | 6 | 7 | 8 | 9 | 10 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CTFPad", 3 | "version": "0.1.0", 4 | "description": "CTF Challenge Management with Etherpad", 5 | "author": "Kasalehlia ", 6 | "dependencies": { 7 | "bcrypt-nodejs": "*", 8 | "consolidate": "*", 9 | "express": "3.x", 10 | "http-proxy": "1.x", 11 | "mustache": "*", 12 | "mv": "^2.0.3", 13 | "sqlite3": "*", 14 | "ws": "*", 15 | "coffee-script": "1.x" 16 | }, 17 | "main": "main.js", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/StratumAuhuur/CTFPad" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/StratumAuhuur/CTFPad/issues" 24 | }, 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /web/js/routes.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var route = function (mode) { 3 | return function (ctf, challenge) { 4 | if (ctf === undefined) ctf = null; 5 | if (challenge === undefined) challenge = null; 6 | if (mode === undefined) mode = null; 7 | model.currentCtf(ctf); 8 | model.currentChallenge(challenge); 9 | model.mode(mode); 10 | } 11 | }; 12 | 13 | var routes = { 14 | '/global': route('pad'), 15 | '/new': route('new'), 16 | '/:ctf': { 17 | on: route('pad'), 18 | '/files': route('files'), 19 | '/edit': route('edit'), 20 | '/:challenge': { 21 | on: route('pad'), 22 | '/files': route('files') 23 | } 24 | } 25 | }; 26 | 27 | var router = Router(routes); 28 | router.configure({strict: false}); 29 | window.dataReady = function () { 30 | router.init(); 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /web/css/jquery.fileupload.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin CSS 1.3.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button { 14 | position: relative; 15 | overflow: hidden; 16 | } 17 | .fileinput-button input { 18 | position: absolute; 19 | top: 0; 20 | right: 0; 21 | margin: 0; 22 | opacity: 0; 23 | -ms-filter: 'alpha(opacity=0)'; 24 | font-size: 200px; 25 | direction: ltr; 26 | cursor: pointer; 27 | } 28 | 29 | /* Fixes for IE < 8 */ 30 | @media screen\9 { 31 | .fileinput-button input { 32 | filter: alpha(opacity=0); 33 | font-size: 100%; 34 | height: 100%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ctfpad.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "ctf" ( 2 | "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | "name" TEXT NOT NULL 4 | ); 5 | CREATE TABLE "challenge" ( 6 | "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 7 | "title" TEXT NOT NULL, 8 | "category" TEXT NOT NULL, 9 | "points" INTEGER NOT NULL, 10 | "done" INTEGER NOT NULL DEFAULT (0), 11 | "ctf" INTEGER NOT NULL 12 | ); 13 | CREATE TABLE "assigned" ( 14 | "user" TEXT NOT NULL, 15 | "challenge" INTEGER NOT NULL 16 | ); 17 | CREATE TABLE user ( 18 | "name" TEXT PRIMARY KEY NOT NULL, 19 | "pwhash" TEXT NOT NULL, 20 | "sessid" TEXT, 21 | "scope" INTEGER NOT NULL DEFAULT (0), 22 | "apikey" TEXT 23 | ); 24 | CREATE TABLE file ( 25 | "id" TEXT PRIMARY KEY NOT NULL, 26 | "name" TEXT NOT NULL, 27 | "user" INTEGER NOT NULL, 28 | "ctf" INTEGER, 29 | "challenge" INTEGER, 30 | "uploaded" INTEGER NOT NULL DEFAULT (0), 31 | "mimetype" TEXT DEFAULT ('application/octet-stream; charset=binary') 32 | ); 33 | -------------------------------------------------------------------------------- /web/files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{#files}} 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | {{/files}} 22 | 23 |
NameUserMime TypeUploadedActions
{{name}}{{user}}{{mimetype}}{{uploaded}} 17 | 18 | 19 |
24 | 27 | 28 | 29 | Upload 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kasalehlia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /web/js/model.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | model = {values: {}}; 3 | 4 | var getCtf = function (data) { 5 | for (i in model.values.ctfs) { 6 | if (""+model.values.ctfs[i].id == data) { 7 | var ctf = model.values.ctfs[i]; 8 | if (ctf.chals === undefined) { 9 | ctf.chals = {}; 10 | for (j in model.values.challenges) { 11 | var chal = model.values.challenges[j]; 12 | if (ctf.challenges.indexOf(chal.id) > -1) { 13 | if (ctf.chals[chal.category] === undefined) { 14 | ctf.chals[chal.category] = [chal]; 15 | } else { 16 | ctf.chals[chal.category].push(chal); 17 | } 18 | } 19 | } 20 | } 21 | return ctf; 22 | } 23 | } 24 | return {name: 'no CTF chosen'}; 25 | } 26 | var getChallenge = function (data) { 27 | var res = model.challenges.search({id: data}); 28 | if (res.length > 0) { 29 | return res[0]; 30 | } else { 31 | return null 32 | } 33 | } 34 | 35 | 36 | var ctfname = H.view('ctfname', 'ctfname', getCtf); 37 | ctfname.postReplaceHook = function (ctf) { 38 | $(this).attr('href', '#/'+ctf.id+'/edit'); 39 | }; 40 | model.currentCtf = H.savedValue('currentCtf', [ 41 | H.view('ctf', 'root', getCtf), 42 | ctfname 43 | ]); 44 | model.currentChallenge = H.savedValue('currentChallenge', H.view('challenge', 'challenge', getChallenge)); 45 | 46 | model.mode = H.savedValue('mode', H.view('mode', 'mode')); 47 | model.user = H.savedValue('user', H.inlineView('{{.}}', 'user')); 48 | 49 | model.ctfs = H.savedArray('ctfs', H.view('ctflist', 'ctflist')); 50 | model.challenges = H.savedArray('challenges', function () {}); 51 | }); 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CTFPad 2 | ====== 3 | 4 | A web UI and server for task based competitions employing Etherpad Lite. It features CTF and Task managemeent, CTF&Task based file uploads and user assignment. May be used for other purposes. 5 | 6 | Installing 7 | ----- 8 | 9 | - acquire Node.JS v10.x and npm, sqlite3 and openssl 10 | - check if `node` is in PATH, create a link to `nodejs` if necessary (usually necessary on Debian/Ubuntu) 11 | - execute`npm install` 12 | - install etherpad-lite into `etherpad-lite` (see `https://github.com/ether/etherpad-lite`) and run it once to install all the dependencies 13 | - create ctfpad.sqlite with ctfpad.sql (on Debian use `sqlite3 ctfpad.sqlite < ctfpad.sql`) 14 | - if necessary, create a keypair with `new_certs.sh` (or use any other certificate pair) 15 | - copy `config.json.example` to `config.json` and tweak to your needs (see **Configuration**) 16 | - create a directory named `uploads` or soft link another location 17 | 18 | Configuration 19 | ----- 20 | 21 | ### CTFPad 22 | - port: TCP port the CTFPad will listen on 23 | - etherpad\_port: TCP port where Etherpad Lite will be reachable from the outside 24 | - etherpad\_internal\_port: TCP port where Etherpad Lite is reachable locally 25 | - keyfile: location of the SSL key file 26 | - certfile: location of the SSL cert file 27 | - authkey: a *secret* string needed to register 28 | 29 | ### Etherpad Lite 30 | Follow the instructions on the project pages. **It is recommended to configure Etherpad Lite to only listen locally.** The CTFPad will provide an authenticating proxy for accessing Etherpad Lite. 31 | 32 | Running 33 | ----- 34 | Start the server by running `node main.js` or `coffee main.coffee` if Coffeescript ist installed globally. 35 | 36 | Using 37 | ----- 38 | If you are using a self-signed certificate (which is the case for certificates generated with `new_certs.sh`) it may be necessary to access `https://$host:$etherpad_port` directly to add an certificate exception since most browsers do not allow adding exceptions for iframes. 39 | 40 | -------------------------------------------------------------------------------- /web/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.0 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function encode(s) { 21 | return config.raw ? s : encodeURIComponent(s); 22 | } 23 | 24 | function decode(s) { 25 | return config.raw ? s : decodeURIComponent(s); 26 | } 27 | 28 | function stringifyCookieValue(value) { 29 | return encode(config.json ? JSON.stringify(value) : String(value)); 30 | } 31 | 32 | function parseCookieValue(s) { 33 | if (s.indexOf('"') === 0) { 34 | // This is a quoted cookie as according to RFC2068, unescape... 35 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 36 | } 37 | 38 | try { 39 | // Replace server-side written pluses with spaces. 40 | // If we can't decode the cookie, ignore it, it's unusable. 41 | s = decodeURIComponent(s.replace(pluses, ' ')); 42 | } catch(e) { 43 | return; 44 | } 45 | 46 | try { 47 | // If we can't parse the cookie, ignore it, it's unusable. 48 | return config.json ? JSON.parse(s) : s; 49 | } catch(e) {} 50 | } 51 | 52 | function read(s, converter) { 53 | var value = config.raw ? s : parseCookieValue(s); 54 | return $.isFunction(converter) ? converter(value) : value; 55 | } 56 | 57 | var config = $.cookie = function (key, value, options) { 58 | 59 | // Write 60 | if (value !== undefined && !$.isFunction(value)) { 61 | options = $.extend({}, config.defaults, options); 62 | 63 | if (typeof options.expires === 'number') { 64 | var days = options.expires, t = options.expires = new Date(); 65 | t.setDate(t.getDate() + days); 66 | } 67 | 68 | return (document.cookie = [ 69 | encode(key), '=', stringifyCookieValue(value), 70 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 71 | options.path ? '; path=' + options.path : '', 72 | options.domain ? '; domain=' + options.domain : '', 73 | options.secure ? '; secure' : '' 74 | ].join('')); 75 | } 76 | 77 | // Read 78 | 79 | var result = key ? undefined : {}; 80 | 81 | // To prevent the for loop in the first place assign an empty array 82 | // in case there are no cookies at all. Also prevents odd result when 83 | // calling $.cookie(). 84 | var cookies = document.cookie ? document.cookie.split('; ') : []; 85 | 86 | for (var i = 0, l = cookies.length; i < l; i++) { 87 | var parts = cookies[i].split('='); 88 | var name = decode(parts.shift()); 89 | var cookie = parts.join('='); 90 | 91 | if (key && key === name) { 92 | // If second argument (value) is a function it's a converter... 93 | result = read(cookie, value); 94 | break; 95 | } 96 | 97 | // Prevent storing a cookie that we couldn't decode. 98 | if (!key && (cookie = read(cookie)) !== undefined) { 99 | result[name] = cookie; 100 | } 101 | } 102 | 103 | return result; 104 | }; 105 | 106 | config.defaults = {}; 107 | 108 | $.removeCookie = function (key, options) { 109 | if ($.cookie(key) !== undefined) { 110 | // Must not alter options, thus extending a fresh object... 111 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 112 | return true; 113 | } 114 | return false; 115 | }; 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /web/js/etherpad.js: -------------------------------------------------------------------------------- 1 | (function( $ ){ 2 | $.fn.pad = function( options ) { 3 | var hostUrl = new URL(location.origin); 4 | var settings = { 5 | 'host' : hostUrl.protocol + '//' + hostUrl.hostname + ":" + window.etherpad_port + "/", 6 | 'baseUrl' : 'p/', 7 | 'showControls' : true, 8 | 'showChat' : false, 9 | 'showLineNumbers' : true, 10 | 'useMonospaceFont' : true, 11 | 'noColors' : false, 12 | 'userColor' : false, 13 | 'hideQRCode' : true, 14 | 'alwaysShowChat' : false, 15 | 'height' : 600, 16 | 'border' : 0, 17 | 'borderStyle' : 'solid', 18 | 'toggleTextOn' : 'Disable Rich-text', 19 | 'toggleTextOff' : 'Enable Rich-text' 20 | }; 21 | 22 | var $self = this; 23 | if (!$self.length) return; 24 | if (!$self.attr('id')) throw new Error('No "id" attribute'); 25 | 26 | var useValue = $self[0].tagName.toLowerCase() == 'textarea'; 27 | var selfId = $self.attr('id'); 28 | var epframeId = 'epframe'+ selfId; 29 | // This writes a new frame if required 30 | if ( !options.getContents ) { 31 | if ( options ) { 32 | $.extend( settings, options ); 33 | } 34 | 35 | var iFrameLink = ''; 51 | 52 | 53 | var $iFrameLink = $(iFrameLink); 54 | 55 | if (useValue) { 56 | var $toggleLink = $(''+ settings.toggleTextOn +'').click(function(){ 57 | var $this = $(this); 58 | $this.toggleClass('active'); 59 | if ($this.hasClass('active')) $this.text(settings.toggleTextOff); 60 | $self.pad({getContents: true}); 61 | return false; 62 | }); 63 | $self 64 | .hide() 65 | .after($toggleLink) 66 | .after($iFrameLink) 67 | ; 68 | } 69 | else { 70 | $self.html(iFrameLink); 71 | } 72 | } 73 | 74 | // This reads the etherpad contents if required 75 | else { 76 | var frameUrl = $('#'+ epframeId).attr('src').split('?')[0]; 77 | var contentsUrl = frameUrl + "/export/html"; 78 | var target = $('#'+ options.getContents); 79 | 80 | // perform an ajax call on contentsUrl and write it to the parent 81 | $.get(contentsUrl, function(data) { 82 | 83 | if (target.is(':input')) { 84 | target.val(data).show(); 85 | } 86 | else { 87 | target.html(data); 88 | } 89 | 90 | $('#'+ epframeId).remove(); 91 | }); 92 | } 93 | return $self; 94 | }; 95 | })( jQuery ); 96 | -------------------------------------------------------------------------------- /web/js/helpers.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | H = {hooks: {}}; 3 | H.savedValue = function (parameter, listener) { 4 | var notify = function(value) { 5 | if (typeof listener === 'function') { 6 | listener.call(model.values[parameter], parameter, value); 7 | } else if (Array.isArray(listener)) { 8 | for (var i = 0; i < listener.length; i++) { 9 | listener[i].call(model.values[parameter], parameter, value); 10 | } 11 | } 12 | } 13 | var val = function(value) { 14 | if (value === undefined) { 15 | return model.values[parameter]; 16 | } else if (model.values[parameter] !== value) { 17 | model.values[parameter] = value; 18 | console.log(parameter+' has been changed to '+value); 19 | notify(value); 20 | } 21 | } 22 | val.rerender = function () { notify(model.values[parameter]); }; 23 | val.listener = listener; 24 | return val 25 | }; 26 | 27 | H.savedArray = function (parameter, listener) { 28 | var array = H.savedValue(parameter, listener); 29 | array.push = function () { 30 | model.values[parameter].push.apply(model.values[parameter], arguments); 31 | } 32 | array.search = function (obj) { 33 | return model.values[parameter].filter(function (e) { 34 | for (key in obj) { 35 | if (e[key] != obj[key]) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | }); 41 | } 42 | return array; 43 | }; 44 | 45 | H.inlineView = function (template, outletName, acquireData) { 46 | var fun = function () { 47 | "use strict"; // null context has to be null, not window 48 | if (acquireData === undefined) { 49 | acquireData = function (value) { return value; } 50 | } 51 | var outlet = $('[data-outlet="'+outletName+'"]'); 52 | if (outlet.length === 0) { 53 | console.log("ERROR: outlet "+outletName+" not found"); 54 | return; 55 | } else if (outlet.length > 1) { 56 | console.log("WARNING: multiple outlets for "+outletName+" found"); 57 | } 58 | // detect old outlets to copy 59 | var innerOutlets = outlet.find('[data-outlet]'); 60 | // fill current outlet with new content 61 | var data = acquireData(this); 62 | fun.lastData = data; 63 | var content = Handlebars.compile(template)(data); 64 | if (fun.postRenderHook.call(outlet, content) === false) return 65 | outlet.html(content); 66 | fun.postReplaceHook.call(outlet, data); 67 | // detect newly inserted outlets that are already rendered outside 68 | outlet.find('[data-outlet]').each(function () { 69 | // if not one of the old outlets 70 | if (!innerOutlets.is($(this))) { 71 | var self = $(this); 72 | var name = self.attr('data-outlet'); 73 | $('[data-outlet="'+name+'"]').each(function () { 74 | // outlet must not be inside the current outlet 75 | if ($(this).parent().closest('[data-outlet]').get(0) != outlet.get(0)) { 76 | // copy content of these outlets 77 | self.html($(this).html()); 78 | } 79 | }); 80 | } 81 | }); 82 | // copy content of old inner outlets into newly created outlets 83 | innerOutlets.each(function () { 84 | var parentOutlet = $(this).parent().closest('[data-outlet]'); 85 | if (parentOutlet.length === 0) { 86 | var innerOutletName = $(this).attr('data-outlet'); 87 | var newOutlet = outlet.find('[data-outlet="'+innerOutletName+'"]'); 88 | newOutlet.html($(this).html()); 89 | } 90 | }); 91 | } 92 | fun.postRenderHook = function () {}; 93 | fun.postReplaceHook = function () {}; 94 | return fun; 95 | }; 96 | H.view = function (templateName, outletName, acquireData) { 97 | var template = $('script[data-template="'+templateName+'"]').html(); 98 | if (template === undefined) { 99 | console.log("ERROR: template "+templateName+" not found!"); 100 | return; 101 | } 102 | return H.inlineView(template, outletName, acquireData); 103 | }; 104 | }); 105 | -------------------------------------------------------------------------------- /web/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{team_name}} 5 | 6 | 7 | 8 | 9 | 22 | 23 | 24 | 36 |
37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # CTFPad REST API 2 | 3 | Each request has to contain an apikey generated by CTFPad as HTTP header 4 | field `X-Apikey` 5 | 6 | ## User endpoints 7 | 8 | ### *GET* user/whoami 9 | Returns the username to the provided apikey 10 | 11 | Response body: 12 | 13 | { 14 | "username": "username" 15 | } 16 | 17 | ### *GET* user/scope 18 | Returns the scope of the current user 19 | 20 | Response body: 21 | 22 | { 23 | "scope": 1 24 | } 25 | 26 | ### *PUT* user/scope 27 | Sets the scope of the current user 28 | 29 | Request body: 30 | 31 | { 32 | "scope": 2 33 | } 34 | 35 | Response body: 36 | 37 | { 38 | "scope": 2 39 | } 40 | 41 | ### *PUT* user/scope/latest 42 | Sets the scope of the current user to the newest CTF 43 | 44 | Response body: 45 | 46 | { 47 | "scope": 4 48 | } 49 | 50 | ## CTF endpoints 51 | 52 | ### *GET* ctfs 53 | Lists all available CTFs 54 | 55 | Response body: 56 | 57 | { 58 | "ctfs": [ { 59 | "id": 1, 60 | "name": "some ctf" 61 | }, { 62 | "id": 2, 63 | "name": "anyCTF" 64 | } ] 65 | } 66 | 67 | ### *POST* ctfs 68 | Creates a new CTF with the given name 69 | 70 | Request body: 71 | 72 | { 73 | "name": "new CTF" 74 | } 75 | 76 | Response body: 77 | 78 | { 79 | "ctf": { 80 | "id": 3, 81 | "name": "new CTF" 82 | } 83 | } 84 | 85 | ### *GET* ctfs/<$CTF> 86 | Returns details about a specific CTF by id 87 | 88 | Response body: 89 | 90 | { 91 | "ctf": { 92 | "id": 2, 93 | "name": "anyCTF" 94 | } 95 | } 96 | 97 | ### *GET* ctfs/<$CTF>/challenges 98 | Lists challenges for a specific CTF 99 | 100 | Response body: 101 | 102 | { 103 | "challenges": [ { 104 | "id": 1, 105 | "title": "random web", 106 | "category": "web", 107 | "points": 100, 108 | "done": false 109 | }, { 110 | "id": 2, 111 | "title": "crypto awesome" 112 | "category": "crypto", 113 | "points": 250, 114 | "done": true 115 | } ] 116 | } 117 | 118 | ### *POST* ctfs/<$CTF>/challenges 119 | Creates a new challenge for a CTF 120 | 121 | Request body: 122 | 123 | { 124 | "challenge": { 125 | "title": "new chal", 126 | "category": "reversing", 127 | "points": 300 128 | } 129 | } 130 | 131 | Response body: 132 | 133 | { 134 | "challenge": { 135 | "id": 3, 136 | "title": "new chal", 137 | "category": "reversing", 138 | "points": 300, 139 | "done": false 140 | } 141 | } 142 | 143 | ### *GET* ctfs/<$CTF>/files 144 | Lists all available files for a ctf 145 | 146 | Response body: 147 | 148 | { 149 | "files": [ { 150 | "id": "4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb", 151 | "name": "test.py", 152 | "user": "stratumauhuur", 153 | "path": "/files/4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb/test.py" 154 | } ] 155 | } 156 | 157 | ### *POST* ctfs/<$CTF>/files 158 | Upload a file for a ctf 159 | 160 | Request body: 161 | 162 | { 163 | "files": @file 164 | } 165 | 166 | Response body: 167 | 168 | { 169 | "success": true, 170 | "id": "4a1da276d9cd0d245d6d186dc28148e2bc8c10b8aa19bdfeaf2e5d9dcc0ecd22" 171 | } 172 | 173 | ## Challenge endpoints 174 | 175 | ### *GET* challenges/<$CHALLENGE> 176 | Returns details about a specific challenge 177 | 178 | Response body: 179 | 180 | { 181 | "challenge": { 182 | "id": 3, 183 | "title": "new chal", 184 | "category": "reversing", 185 | "points": 300, 186 | "done": false, 187 | "ctf": 1 188 | "filecount": 2, 189 | "assigned": [ 190 | "stratumauhuur" 191 | ] 192 | } 193 | } 194 | 195 | ### *PUT* challenges/<$CHALLENGE>/assign 196 | Assigns the current user to the specified challenge 197 | 198 | Response body: 199 | 200 | { 201 | "assigned": [ 202 | "stratumauhuur", 203 | "username" 204 | ] 205 | } 206 | 207 | ### *DELETE* challenges/<$CHALLENGE>/assign 208 | Unassigns the current user from the specified challenge 209 | 210 | Response body: 211 | 212 | { 213 | "assgined": [ 214 | "stratumauhuur" 215 | ] 216 | } 217 | 218 | ### *PUT* challenges/<$CHALLENGE>/done 219 | Marks the challenge as done 220 | 221 | ### *DELETE* challenges/<$CHALLENGE>/done 222 | Marks the challenge as not done 223 | 224 | ### *GET* challenges/<$CHALLENGE>/files 225 | Lists the files for a specific challenge 226 | 227 | Response body: 228 | 229 | { 230 | "files": [ { 231 | "id": "4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb", 232 | "name": "test.py", 233 | "user": "stratumauhuur", 234 | "path": "/files/4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb/test.py" 235 | } ] 236 | } 237 | 238 | ### *POST* challenges/<$CHALLENGE>/files 239 | Upload a file for a challenge 240 | 241 | Request body: 242 | 243 | { 244 | "files": @file 245 | } 246 | 247 | Response body: 248 | 249 | { 250 | "success": true, 251 | "id": "4a1da276d9cd0d245d6d186dc28148e2bc8c10b8aa19bdfeaf2e5d9dcc0ecd22" 252 | } 253 | 254 | -------------------------------------------------------------------------------- /web/doc/API.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

CTFPad REST API

4 | 5 |

Each request has to contain an apikey generated by CTFPad as HTTP header 6 | field X-Apikey

7 | 8 |

User endpoints

9 | 10 |

GET user/whoami

11 | 12 |

Returns the username to the provided apikey

13 | 14 |

Response body:

15 | 16 |
{
 17 |     "username": "username"
 18 | }
 19 | 
20 | 21 |

GET user/scope

22 | 23 |

Returns the scope of the current user

24 | 25 |

Response body:

26 | 27 |
{
 28 |     "scope": 1
 29 | }
 30 | 
31 | 32 |

PUT user/scope

33 | 34 |

Sets the scope of the current user

35 | 36 |

Request body:

37 | 38 |
{
 39 |     "scope": 2
 40 | }
 41 | 
42 | 43 |

Response body:

44 | 45 |
{
 46 |     "scope": 2
 47 | }
 48 | 
49 | 50 |

PUT user/scope/latest

51 | 52 |

Sets the scope of the current user to the newest CTF

53 | 54 |

Response body:

55 | 56 |
{
 57 |     "scope": 4
 58 | }
 59 | 
60 | 61 |

CTF endpoints

62 | 63 |

GET ctfs

64 | 65 |

Lists all available CTFs

66 | 67 |

Response body:

68 | 69 |
{
 70 |     "ctfs": [ {
 71 |         "id": 1,
 72 |         "name": "some ctf"
 73 |     }, {
 74 |         "id": 2,
 75 |         "name": "anyCTF"
 76 |     } ]
 77 | }
 78 | 
79 | 80 |

POST ctfs

81 | 82 |

Creates a new CTF with the given name

83 | 84 |

Request body:

85 | 86 |
{
 87 |     "name": "new CTF"
 88 | }
 89 | 
90 | 91 |

Response body:

92 | 93 |
{
 94 |     "ctf": {
 95 |          "id": 3,
 96 |          "name": "new CTF"
 97 |     }
 98 | }
 99 | 
100 | 101 |

GET ctfs/<$CTF>

102 | 103 |

Returns details about a specific CTF by id

104 | 105 |

Response body:

106 | 107 |
{
108 |     "ctf": {
109 |          "id": 2,
110 |          "name": "anyCTF"
111 |     }
112 | }
113 | 
114 | 115 |

GET ctfs/<$CTF>/challenges

116 | 117 |

Lists challenges for a specific CTF

118 | 119 |

Response body:

120 | 121 |
{
122 |     "challenges": [ {
123 |         "id": 1,
124 |         "title": "random web",
125 |         "category": "web",
126 |         "points": 100,
127 |         "done": false
128 |     }, {
129 |         "id": 2,
130 |         "title": "crypto awesome"
131 |         "category": "crypto",
132 |         "points": 250,
133 |         "done": true
134 |     } ]
135 | }
136 | 
137 | 138 |

POST ctfs/<$CTF>/challenges

139 | 140 |

Creates a new challenge for a CTF

141 | 142 |

Request body:

143 | 144 |
{
145 |     "challenge": {
146 |         "title": "new chal",
147 |         "category": "reversing",
148 |         "points": 300
149 |     }
150 | }
151 | 
152 | 153 |

Response body:

154 | 155 |
{
156 |     "challenge": {
157 |         "id": 3,
158 |         "title": "new chal",
159 |         "category": "reversing",
160 |         "points": 300,
161 |         "done": false
162 |     }
163 | }
164 | 
165 | 166 |

GET ctfs/<$CTF>/files

167 | 168 |

Lists all available files for a ctf

169 | 170 |

Response body:

171 | 172 |
{
173 |     "files": [ {
174 |         "id": "4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb",
175 |         "name": "test.py",
176 |         "user": "stratumauhuur",
177 |         "path": "/files/4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb/test.py"
178 |     } ]
179 | }
180 | 
181 | 182 |

POST ctfs/<$CTF>/files

183 | 184 |

Upload a file for a ctf

185 | 186 |

Request body:

187 | 188 |
{
189 |     "files": @file
190 | }
191 | 
192 | 193 |

Response body:

194 | 195 |
{
196 |     "success": true,
197 |     "id": "4a1da276d9cd0d245d6d186dc28148e2bc8c10b8aa19bdfeaf2e5d9dcc0ecd22"
198 | }
199 | 
200 | 201 |

Challenge endpoints

202 | 203 |

GET challenges/<$CHALLENGE>

204 | 205 |

Returns details about a specific challenge

206 | 207 |

Response body:

208 | 209 |
{
210 |     "challenge": {
211 |         "id": 3,
212 |         "title": "new chal",
213 |         "category": "reversing",
214 |         "points": 300,
215 |         "done": false,
216 |         "ctf": 1
217 |         "filecount": 2,
218 |         "assigned": [
219 |             "stratumauhuur"
220 |         ]
221 |     }
222 | }
223 | 
224 | 225 |

PUT challenges/<$CHALLENGE>/assign

226 | 227 |

Assigns the current user to the specified challenge

228 | 229 |

Response body:

230 | 231 |
{
232 |     "assigned": [
233 |         "stratumauhuur",
234 |         "username"
235 |     ]
236 | }
237 | 
238 | 239 |

DELETE challenges/<$CHALLENGE>/assign

240 | 241 |

Unassigns the current user from the specified challenge

242 | 243 |

Response body:

244 | 245 |
{
246 |     "assgined": [
247 |         "stratumauhuur"
248 |     ]
249 | }
250 | 
251 | 252 |

PUT challenges/<$CHALLENGE>/done

253 | 254 |

Marks the challenge as done

255 | 256 |

DELETE challenges/<$CHALLENGE>/done

257 | 258 |

Marks the challenge as not done

259 | 260 |

GET challenges/<$CHALLENGE>/files

261 | 262 |

Lists the files for a specific challenge

263 | 264 |

Response body:

265 | 266 |
{
267 |     "files": [ {
268 |         "id": "4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb",
269 |         "name": "test.py",
270 |         "user": "stratumauhuur",
271 |         "path": "/files/4fb15f2bac64cccd6470753b9333534f2065ed14aca439043399a267cba7c6fb/test.py"
272 |     } ]
273 | }
274 | 
275 | 276 |

POST challenges/<$CHALLENGE>/files

277 | 278 |

Upload a file for a challenge

279 | 280 |

Request body:

281 | 282 |
{
283 |     "files": @file
284 | }
285 | 
286 | 287 |

Response body:

288 | 289 |
{
290 |     "success": true,
291 |     "id": "4a1da276d9cd0d245d6d186dc28148e2bc8c10b8aa19bdfeaf2e5d9dcc0ecd22"
292 | }
293 | 
294 |
295 | -------------------------------------------------------------------------------- /database.coffee: -------------------------------------------------------------------------------- 1 | #database.coffee 2 | bcrypt = require 'bcrypt-nodejs' 3 | sqlite3 = require 'sqlite3' 4 | fs = require 'fs' 5 | 6 | # SQLITE DB 7 | stmts = {} 8 | sql = new sqlite3.Database 'ctfpad.sqlite', -> 9 | stmts.getUser = sql.prepare 'SELECT name,scope,apikey FROM user WHERE sessid = ?' 10 | stmts.getUserByApiKey = sql.prepare 'SELECT name,scope FROM user WHERE apikey = ? AND apikey NOT NULL' 11 | stmts.addUser = sql.prepare 'INSERT INTO user (name,pwhash) VALUES (?,?)' 12 | stmts.getUserPW = sql.prepare 'SELECT pwhash FROM user WHERE name = ?' 13 | stmts.insertSession = sql.prepare 'UPDATE user SET sessid = ? WHERE name = ?' 14 | stmts.voidSession = sql.prepare 'UPDATE user SET sessid = NULL WHERE sessid = ?' 15 | stmts.getChallenges = sql.prepare 'SELECT id,title,category,points,done FROM challenge WHERE ctf = ? ORDER BY category,points,id' 16 | stmts.getChallenge = sql.prepare 'SELECT * FROM challenge WHERE id = ?' 17 | stmts.addChallenge = sql.prepare 'INSERT INTO challenge (ctf, title, category, points) VALUES (?,?,?,?)' 18 | stmts.modifyChallenge = sql.prepare 'UPDATE challenge SET title = ?, category = ?, points = ? WHERE id = ?' 19 | stmts.setDone = sql.prepare 'UPDATE challenge SET done = ? WHERE id = ?' 20 | stmts.getCTFs = sql.prepare 'SELECT id,name FROM ctf ORDER BY id DESC' 21 | stmts.addCTF = sql.prepare 'INSERT INTO ctf (name) VALUES (?)' 22 | stmts.changeScope = sql.prepare 'UPDATE user SET scope = ? WHERE name = ?' 23 | stmts.isAssigned = sql.prepare 'SELECT COUNT(*) AS assigned FROM assigned WHERE user = ? AND challenge = ?' 24 | stmts.assign = sql.prepare 'INSERT INTO assigned VALUES (?,?)' 25 | stmts.unassign = sql.prepare 'DELETE FROM assigned WHERE user = ? AND challenge = ?' 26 | stmts.changePassword = sql.prepare 'UPDATE user SET pwhash = ? WHERE sessid = ?' 27 | stmts.getApiKeyFor = sql.prepare 'SELECT apikey FROM user WHERE sessid = ?' 28 | stmts.setApiKeyFor = sql.prepare 'UPDATE user SET apikey = ? WHERE sessid = ?' 29 | stmts.listAssignments = sql.prepare 'SELECT assigned.challenge,assigned.user FROM assigned JOIN challenge ON assigned.challenge = challenge.id JOIN user ON assigned.user = user.name WHERE challenge.ctf = ?' 30 | stmts.listAssignmentsForChallenge = sql.prepare 'SELECT user FROM assigned WHERE challenge = ?' 31 | stmts.getFiles = sql.prepare 'SELECT id,name,user,uploaded,mimetype FROM file WHERE CASE ? WHEN 1 THEN ctf WHEN 2 THEN challenge END = ?' 32 | stmts.addFile = sql.prepare 'INSERT INTO file (id, name, user, ctf, challenge, uploaded, mimetype) VALUES (?,?,?,?,?,?,?)' 33 | stmts.findFile = sql.prepare 'SELECT ctf,challenge FROM file WHERE id = ?' 34 | stmts.fileMimetype = sql.prepare 'SELECT mimetype FROM file WHERE id = ?' 35 | stmts.deleteFile = sql.prepare 'DELETE FROM file WHERE id = ?' 36 | stmts.getLatestCtfId = sql.prepare 'SELECT id FROM ctf ORDER BY id DESC LIMIT 1' 37 | 38 | # 39 | # EXPORTS 40 | # 41 | exports.validateSession = (sess, cb = ->) -> 42 | stmts.getUser.get [sess], H cb 43 | 44 | exports.checkPassword = (name, pw, cb = ->) -> 45 | stmts.getUserPW.get [name], H (row) -> 46 | unless row then cb false 47 | else bcrypt.compare pw, row.pwhash, (err, res) -> 48 | if err or not res then cb false 49 | else 50 | sess = newRandomId() 51 | cb sess 52 | stmts.insertSession.run [sess, name] 53 | 54 | exports.validateApiKey = (apikey, cb) -> 55 | stmts.getUserByApiKey.get [apikey], H cb 56 | stmts.getUserByApiKey.reset() 57 | 58 | exports.voidSession = (sessionId) -> stmts.voidSession.run [sessionId] 59 | 60 | exports.setChallengeDone = (chalId, done) -> 61 | stmts.setDone.run [(if done then 1 else 0), chalId] 62 | 63 | exports.getChallenges = (ctfId, cb = ->) -> 64 | stmts.getChallenges.all [ctfId], H cb 65 | 66 | exports.getChallenge = (challengeId, cb = ->) -> 67 | stmts.getChallenge.get [challengeId], H cb 68 | stmts.getChallenge.reset() 69 | 70 | exports.addChallenge = (ctfId, title, category, points, cb = ->) -> 71 | stmts.addChallenge.run [ctfId, title, category, points], (err) -> 72 | cb(this.lastID) 73 | 74 | exports.modifyChallenge = (chalId, title, category, points) -> 75 | stmts.modifyChallenge.run [title, category, points, chalId] 76 | 77 | exports.getCTFs = (cb = ->) -> 78 | stmts.getCTFs.all [], H cb 79 | 80 | exports.addCTF = (title, cb = ->) -> 81 | stmts.addCTF.run [title], (err) -> 82 | cb(this.lastID) 83 | 84 | exports.changeScope = (user, ctfid) -> 85 | stmts.changeScope.run [ctfid, user] 86 | 87 | exports.toggleAssign = (user, chalid, cb = ->) -> 88 | stmts.isAssigned.get [user, chalid], H (ans) -> 89 | if ans.assigned 90 | exports.unassign user, chalid 91 | cb false 92 | else 93 | exports.assign user, chalid 94 | cb true 95 | stmts.isAssigned.reset() 96 | 97 | exports.assign = (user, chalid, cb = ->) -> 98 | stmts.assign.run [user,chalid], cb 99 | 100 | exports.unassign = (user, chalid, cb = ->) -> 101 | stmts.unassign.run [user,chalid], cb 102 | 103 | exports.listAssignments = (ctfid, cb = ->) -> 104 | stmts.listAssignments.all [ctfid], H cb 105 | stmts.listAssignments.reset() 106 | 107 | exports.listAssignmentsForChallenge = (chalId, cb = ->) -> 108 | stmts.listAssignmentsForChallenge.all [chalId], H cb 109 | stmts.listAssignmentsForChallenge.reset() 110 | 111 | exports.changePassword = (sessid, newpw, cb = ->) -> 112 | bcrypt.hash newpw, bcrypt.genSaltSync(), null, (err, hash) -> 113 | if err then cb err 114 | else 115 | stmts.changePassword.run [hash, sessid] 116 | cb false 117 | 118 | exports.getApiKeyFor = (sessid, cb = ->) -> 119 | stmts.getApiKeyFor.get [sessid], H (row) -> 120 | cb if row then row.apikey else '' 121 | stmts.getApiKeyFor.reset() 122 | 123 | exports.newApiKeyFor = (sessid, cb = ->) -> 124 | apikey = newRandomId 32 125 | stmts.setApiKeyFor.run [apikey, sessid] 126 | setImmediate cb, apikey 127 | 128 | exports.addUser = (name, pw, cb = ->) -> 129 | bcrypt.hash pw, bcrypt.genSaltSync(), null, (err, hash) -> 130 | if err then cb err 131 | else 132 | stmts.addUser.run [name, hash], (err, ans) -> 133 | if err 134 | cb err 135 | else 136 | cb false 137 | 138 | exports.getCTFFiles = (id, cb = ->) -> 139 | stmts.getFiles.all [1, id], H cb 140 | stmts.getFiles.reset() 141 | 142 | exports.getChallengeFiles = (id, cb = ->) -> 143 | stmts.getFiles.all [2, id], H cb 144 | stmts.getFiles.reset() 145 | 146 | exports.addChallengeFile = (chal, name, user, mimetype, cb = ->) -> 147 | id = newRandomId(32) 148 | stmts.addFile.run [id, name, user, null, chal, new Date().getTime()/1000, mimetype], (err, ans) -> 149 | cb err, id 150 | 151 | exports.addCTFFile = (ctf, name, user, mimetype, cb = ->) -> 152 | id = newRandomId(32) 153 | stmts.addFile.run [id, name, user, ctf, null, new Date().getTime()/1000, mimetype], (err, ans) -> 154 | cb err, id 155 | 156 | exports.mimetypeForFile = (id, cb = ->) -> 157 | stmts.fileMimetype.get [id], H ({mimetype: mimetype}) -> cb(mimetype) 158 | 159 | exports.deleteFile = (fileid, cb = ->) -> 160 | stmts.findFile.get [fileid], H ({ctf:ctf, challenge:challenge}) -> 161 | stmts.deleteFile.run [fileid], (err) -> 162 | cb err, (if ctf then 0 else 1), (if ctf then ctf else challenge) 163 | 164 | exports.getLatestCtfId = (cb = ->) -> 165 | stmts.getLatestCtfId.get H (row) -> 166 | stmts.getLatestCtfId.reset -> 167 | cb(if row isnt undefined then row.id else -1) 168 | 169 | # 170 | # UTIL 171 | # 172 | H = (cb=->) -> 173 | return (err, ans) -> 174 | if err then console.log err 175 | else cb ans 176 | 177 | newRandomId = (length = 16) -> 178 | buf = new Buffer length 179 | fd = fs.openSync '/dev/urandom', 'r' 180 | fs.readSync fd, buf, 0, length, null 181 | buf.toString 'hex' 182 | 183 | -------------------------------------------------------------------------------- /api.coffee: -------------------------------------------------------------------------------- 1 | exports.init = (app, db, upload, prefix) -> 2 | # UTIL 3 | validateApiKey = (req, res, cb = ->) -> 4 | db.validateApiKey req.header('X-Apikey'), (user) -> 5 | if user 6 | cb user 7 | else 8 | res.send 401 9 | 10 | recursiveTypeCheck = (proto, obj) -> 11 | unless obj 12 | return false 13 | for k,v of proto 14 | if typeof(v) is 'string' 15 | unless typeof(obj[k]) is v 16 | return false 17 | else if typeof(v) is 'object' 18 | unless recursiveTypeCheck(v, obj[k]) 19 | return false 20 | return true 21 | 22 | validateArguments = (req, res, args) -> 23 | if recursiveTypeCheck args, req.body 24 | return true 25 | else 26 | res.send 400 27 | return false 28 | 29 | # USER Endpoints 30 | app.get "#{prefix}/user/whoami", (req, res) -> 31 | validateApiKey req, res, (user) -> 32 | res.json {username: user.name} 33 | 34 | app.get "#{prefix}/user/scope", (req, res) -> 35 | validateApiKey req, res, (user) -> 36 | res.json {scope: user.scope} 37 | 38 | app.put "#{prefix}/user/scope", (req, res) -> 39 | validateApiKey req, res, (user) -> 40 | if validateArguments req, res, {scope: 'number'} 41 | db.changeScope user.name, req.body.scope 42 | res.json {scope: req.body.scope} 43 | #TODO change ws.authenticated.scope 44 | 45 | app.put "#{prefix}/user/scope/latest", (req, res) -> 46 | validateApiKey req, res, (user) -> 47 | db.getLatestCtfId (id) -> 48 | db.changeScope user.name, id 49 | res.json {scope: id} 50 | #TODO change ws.authenticated.scope 51 | 52 | # CTF Endpoints 53 | 54 | app.get "#{prefix}/ctfs", (req, res) -> 55 | validateApiKey req, res, (user) -> 56 | db.getCTFs (ctfs) -> 57 | res.json {ctfs: ctfs} 58 | 59 | app.post "#{prefix}/ctfs", (req, res) -> 60 | validateApiKey req, res, (user) -> 61 | if validateArguments req, res, {name: 'string'} 62 | db.addCTF req.body.name, (id) -> 63 | res.json {ctf: {id: id, name: req.body.name}} 64 | 65 | app.get "#{prefix}/ctfs/:ctf", (req, res) -> 66 | try 67 | req.params.ctf = parseInt req.params.ctf 68 | catch e 69 | res.send 400 70 | return 71 | validateApiKey req, res, (user) -> 72 | db.getCTFs (ctfs) -> 73 | candidates = (ctf for ctf in ctfs when ctf.id is req.params.ctf) 74 | if candidates.length > 0 75 | res.json {ctf: candidates[0]} 76 | else 77 | res.send 404 78 | 79 | app.get "#{prefix}/ctfs/:ctf/challenges", (req, res) -> 80 | try 81 | req.params.ctf = parseInt req.params.ctf 82 | catch e 83 | res.send 400 84 | return 85 | validateApiKey req, res, (user) -> 86 | db.getChallenges req.params.ctf, (challenges) -> 87 | challenge.done = new Boolean(challenge.done) for challenge in challenges 88 | res.json {challenges: challenges} 89 | 90 | app.post "#{prefix}/ctfs/:ctf/challenges", (req, res) -> 91 | try 92 | req.params.ctf = parseInt req.params.ctf 93 | catch e 94 | res.send 400 95 | return 96 | validateApiKey req, res, (user) -> 97 | validArg = {challenge: {title: 'string', category: 'string', points: 'number'}} 98 | if validateArguments req, res, validArg 99 | db.addChallenge req.params.ctf, req.body.challenge.title, 100 | req.body.challenge.category, req.body.challenge.points, (id) -> 101 | res.json {challenge: 102 | id: id 103 | title: req.body.challenge.title 104 | category: req.body.challenge.category 105 | points: req.body.challenge.points 106 | done: false 107 | } 108 | exports.broadcast {type: 'ctfmodification'}, req.params.ctf 109 | 110 | app.get "#{prefix}/ctfs/:ctf/files", (req, res) -> 111 | try 112 | req.params.ctf = parseInt req.params.ctf 113 | catch e 114 | res.send 400 115 | return 116 | validateApiKey req, res, (user) -> 117 | db.getCTFFiles req.params.ctf, (files) -> 118 | file.path = "/file/#{file.id}/#{file.name}" for file in files 119 | res.json {files: files} 120 | 121 | app.post "#{prefix}/ctfs/:ctf/files", (req, res) -> 122 | validateApiKey req, res, (user) -> 123 | upload user, 'ctf', req.params.ctf, req, res 124 | 125 | #CHALLENGE Endpoints 126 | app.get "#{prefix}/challenges/:challenge", (req, res) -> 127 | try 128 | req.params.challenge = parseInt req.params.challenge 129 | catch e 130 | res.send 400 131 | return 132 | validateApiKey req, res, (user) -> 133 | db.getChallenge req.params.challenge, (challenge) -> 134 | unless challenge 135 | res.send 404 136 | return 137 | challenge.done = new Boolean challenge.done 138 | done = -> 139 | if challenge.filecount isnt undefined and challenge.assigned isnt undefined 140 | res.json {challenge: challenge} 141 | db.getChallengeFiles challenge.id, (files) -> 142 | challenge.filecount = files.length 143 | done() 144 | db.listAssignmentsForChallenge challenge.id, (assignments) -> 145 | challenge.assigned = (a.user for a in assignments) 146 | done() 147 | 148 | app.put "#{prefix}/challenges/:challenge/assign", (req, res) -> 149 | try 150 | req.params.challenge = parseInt req.params.challenge 151 | catch e 152 | res.send 400 153 | return 154 | validateApiKey req, res, (user) -> 155 | db.assign user.name, req.params.challenge, -> 156 | db.listAssignmentsForChallenge req.params.challenge, (assignments) -> 157 | res.json {assigned: (a.user for a in assignments)} 158 | exports.broadcast {type: 'assign', subject: req.params.challenge, data: [{name: user.name}, true]} 159 | 160 | app.delete "#{prefix}/challenges/:challenge/assign", (req, res) -> 161 | try 162 | req.params.challenge = parseInt req.params.challenge 163 | catch e 164 | res.send 400 165 | return 166 | validateApiKey req, res, (user) -> 167 | db.unassign user.name, req.params.challenge, -> 168 | db.listAssignmentsForChallenge req.params.challenge, (assignments) -> 169 | res.json {assigned: (a.user for a in assignments)} 170 | exports.broadcast {type: 'assign', subject: req.params.challenge, data: [{name: user.name}, false]} 171 | 172 | app.put "#{prefix}/challenges/:challenge/done", (req, res) -> 173 | try 174 | req.params.challenge = parseInt req.params.challenge 175 | catch e 176 | res.send 400 177 | return 178 | validateApiKey req, res, (user) -> 179 | db.setChallengeDone req.params.challenge, true 180 | exports.broadcast {type: 'done', subject: req.params.challenge, data: true} 181 | res.send 204 182 | 183 | app.delete "#{prefix}/challenges/:challenge/done", (req, res) -> 184 | try 185 | req.params.challenge = parseInt req.params.challenge 186 | catch e 187 | res.send 400 188 | return 189 | validateApiKey req, res, (user) -> 190 | db.setChallengeDone req.params.challenge, false 191 | exports.broadcast {type: 'done', subject: req.params.challenge, data: false} 192 | res.send 204 193 | 194 | app.get "#{prefix}/challenges/:challenge/files", (req, res) -> 195 | try 196 | req.params.challenge = parseInt req.params.challenge 197 | catch e 198 | res.send 400 199 | return 200 | validateApiKey req, res, (user) -> 201 | db.getChallengeFiles req.params.challenge, (files) -> 202 | file.path = "/file/#{file.id}/#{file.name}" for file in files 203 | res.json {files: files} 204 | 205 | app.post "#{prefix}/challenges/:challenge/files", (req, res) -> 206 | validateApiKey req, res, (user) -> 207 | upload user, 'challenge', req.params.challenge, req, res 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /web/js/jquery.timeago.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Timeago is a jQuery plugin that makes it easy to support automatically 3 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). 4 | * 5 | * @name timeago 6 | * @version 1.4.1 7 | * @requires jQuery v1.2.3+ 8 | * @author Ryan McGeary 9 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * For usage and examples, visit: 12 | * http://timeago.yarp.com/ 13 | * 14 | * Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) 15 | */ 16 | 17 | (function (factory) { 18 | if (typeof define === 'function' && define.amd) { 19 | // AMD. Register as an anonymous module. 20 | define(['jquery'], factory); 21 | } else { 22 | // Browser globals 23 | factory(jQuery); 24 | } 25 | }(function ($) { 26 | $.timeago = function(timestamp) { 27 | if (timestamp instanceof Date) { 28 | return inWords(timestamp); 29 | } else if (typeof timestamp === "string") { 30 | return inWords($.timeago.parse(timestamp)); 31 | } else if (typeof timestamp === "number") { 32 | return inWords(new Date(timestamp)); 33 | } else { 34 | return inWords($.timeago.datetime(timestamp)); 35 | } 36 | }; 37 | var $t = $.timeago; 38 | 39 | $.extend($.timeago, { 40 | settings: { 41 | refreshMillis: 60000, 42 | allowPast: true, 43 | allowFuture: false, 44 | localeTitle: false, 45 | cutoff: 0, 46 | strings: { 47 | prefixAgo: null, 48 | prefixFromNow: null, 49 | suffixAgo: "ago", 50 | suffixFromNow: "from now", 51 | inPast: 'any moment now', 52 | seconds: "less than a minute", 53 | minute: "about a minute", 54 | minutes: "%d minutes", 55 | hour: "about an hour", 56 | hours: "about %d hours", 57 | day: "a day", 58 | days: "%d days", 59 | month: "about a month", 60 | months: "%d months", 61 | year: "about a year", 62 | years: "%d years", 63 | wordSeparator: " ", 64 | numbers: [] 65 | } 66 | }, 67 | 68 | inWords: function(distanceMillis) { 69 | if(!this.settings.allowPast && ! this.settings.allowFuture) { 70 | throw 'timeago allowPast and allowFuture settings can not both be set to false.'; 71 | } 72 | 73 | var $l = this.settings.strings; 74 | var prefix = $l.prefixAgo; 75 | var suffix = $l.suffixAgo; 76 | if (this.settings.allowFuture) { 77 | if (distanceMillis < 0) { 78 | prefix = $l.prefixFromNow; 79 | suffix = $l.suffixFromNow; 80 | } 81 | } 82 | 83 | if(!this.settings.allowPast && distanceMillis >= 0) { 84 | return this.settings.strings.inPast; 85 | } 86 | 87 | var seconds = Math.abs(distanceMillis) / 1000; 88 | var minutes = seconds / 60; 89 | var hours = minutes / 60; 90 | var days = hours / 24; 91 | var years = days / 365; 92 | 93 | function substitute(stringOrFunction, number) { 94 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; 95 | var value = ($l.numbers && $l.numbers[number]) || number; 96 | return string.replace(/%d/i, value); 97 | } 98 | 99 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || 100 | seconds < 90 && substitute($l.minute, 1) || 101 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) || 102 | minutes < 90 && substitute($l.hour, 1) || 103 | hours < 24 && substitute($l.hours, Math.round(hours)) || 104 | hours < 42 && substitute($l.day, 1) || 105 | days < 30 && substitute($l.days, Math.round(days)) || 106 | days < 45 && substitute($l.month, 1) || 107 | days < 365 && substitute($l.months, Math.round(days / 30)) || 108 | years < 1.5 && substitute($l.year, 1) || 109 | substitute($l.years, Math.round(years)); 110 | 111 | var separator = $l.wordSeparator || ""; 112 | if ($l.wordSeparator === undefined) { separator = " "; } 113 | return $.trim([prefix, words, suffix].join(separator)); 114 | }, 115 | 116 | parse: function(iso8601) { 117 | var s = $.trim(iso8601); 118 | s = s.replace(/\.\d+/,""); // remove milliseconds 119 | s = s.replace(/-/,"/").replace(/-/,"/"); 120 | s = s.replace(/T/," ").replace(/Z/," UTC"); 121 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 122 | s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 123 | return new Date(s); 124 | }, 125 | datetime: function(elem) { 126 | var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); 127 | return $t.parse(iso8601); 128 | }, 129 | isTime: function(elem) { 130 | // jQuery's `is()` doesn't play well with HTML5 in IE 131 | return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); 132 | } 133 | }); 134 | 135 | // functions that can be called via $(el).timeago('action') 136 | // init is default when no action is given 137 | // functions are called with context of a single element 138 | var functions = { 139 | init: function(){ 140 | var refresh_el = $.proxy(refresh, this); 141 | refresh_el(); 142 | var $s = $t.settings; 143 | if ($s.refreshMillis > 0) { 144 | this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); 145 | } 146 | }, 147 | update: function(time){ 148 | var parsedTime = $t.parse(time); 149 | $(this).data('timeago', { datetime: parsedTime }); 150 | if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString()); 151 | refresh.apply(this); 152 | }, 153 | updateFromDOM: function(){ 154 | $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); 155 | refresh.apply(this); 156 | }, 157 | dispose: function () { 158 | if (this._timeagoInterval) { 159 | window.clearInterval(this._timeagoInterval); 160 | this._timeagoInterval = null; 161 | } 162 | } 163 | }; 164 | 165 | $.fn.timeago = function(action, options) { 166 | var fn = action ? functions[action] : functions.init; 167 | if(!fn){ 168 | throw new Error("Unknown function name '"+ action +"' for timeago"); 169 | } 170 | // each over objects here and call the requested function 171 | this.each(function(){ 172 | fn.call(this, options); 173 | }); 174 | return this; 175 | }; 176 | 177 | function refresh() { 178 | //check if it's still visible 179 | if(!$.contains(document.documentElement,this)){ 180 | //stop if it has been removed 181 | $(this).timeago("dispose"); 182 | return this; 183 | } 184 | 185 | var data = prepareData(this); 186 | var $s = $t.settings; 187 | 188 | if (!isNaN(data.datetime)) { 189 | if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) { 190 | $(this).text(inWords(data.datetime)); 191 | } 192 | } 193 | return this; 194 | } 195 | 196 | function prepareData(element) { 197 | element = $(element); 198 | if (!element.data("timeago")) { 199 | element.data("timeago", { datetime: $t.datetime(element) }); 200 | var text = $.trim(element.text()); 201 | if ($t.settings.localeTitle) { 202 | element.attr("title", element.data('timeago').datetime.toLocaleString()); 203 | } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { 204 | element.attr("title", text); 205 | } 206 | } 207 | return element.data("timeago"); 208 | } 209 | 210 | function inWords(date) { 211 | return $t.inWords(distance(date)); 212 | } 213 | 214 | function distance(date) { 215 | return (new Date().getTime() - date.getTime()); 216 | } 217 | 218 | // fix for IE6 suckage 219 | document.createElement("abbr"); 220 | document.createElement("time"); 221 | })); 222 | -------------------------------------------------------------------------------- /web/js/ctfpad.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | proto = if location.protocol is 'http:' then 'ws' else 'wss' 3 | sock = new WebSocket "#{proto}#{location.href.substring location.protocol.length-1, location.href.lastIndexOf '/'}" 4 | sock.onopen = -> 5 | sock.send "\"#{sessid}\"" 6 | sock.onclose = -> 7 | unless window.preventSocketAlert 8 | alert 'the websocket has been disconnected, reloading the page' 9 | document.location.reload() 10 | sock.onmessage = (event) -> 11 | msg = JSON.parse event.data 12 | console.log msg 13 | if msg.type is 'done' 14 | self = $("input[data-chalid='#{msg.subject}']") 15 | self.prop 'checked', msg.data 16 | self.parent().next().css 'text-decoration', if msg.data then 'line-through' else 'none' 17 | if msg.data 18 | self.parent().parent().addClass 'done' 19 | else 20 | self.parent().parent().removeClass 'done' 21 | updateProgress() 22 | else if msg.type is 'assign' 23 | self = $(".labels[data-chalid='#{msg.subject}']") 24 | if msg.data[1] 25 | self.append $("
  • ").append($("").addClass("label").attr("data-name", msg.data[0].name).text(msg.data[0].name)) 26 | else 27 | self.find(".label[data-name='#{msg.data[0].name}']").parent().remove() 28 | $(".assignment-count[data-chalid='#{msg.subject}']").text self.first().find('.label').length 29 | else if msg.type is 'ctfmodification' 30 | $('#ctfmodification').fadeIn 500 31 | else if msg.type is 'login' 32 | $('#userlist').append $("
  • ").text(msg.data) 33 | $('#usercount').text $('#userlist').children('li').length 34 | else if msg.type is 'logout' 35 | $("#userlist li:contains('#{msg.data}')").remove() 36 | $('#usercount').text $('#userlist').children('li').length 37 | else if msg.type is 'fileupload' or msg.type is 'filedeletion' 38 | if "#{msg.data}files" is window.currentPage 39 | current = window.currentPage 40 | window.currentPage = null 41 | $(".contentlink[href='##{current}']").click() 42 | subject = $(".contentlink[href='##{msg.data}files']") 43 | if msg.filecount > 0 44 | subject.children('i').removeClass('icon-folder-close').addClass('icon-folder-open') 45 | else 46 | subject.children('i').removeClass('icon-folder-open').addClass('icon-folder-close') 47 | subject.nextAll('sup').text msg.filecount 48 | else 49 | alert event.data 50 | #TODO handle events 51 | 52 | window.onbeforeunload = -> 53 | window.preventSocketAlert = true 54 | return 55 | 56 | sessid = $.cookie 'ctfpad' 57 | if $.cookie('ctfpad_hide') is undefined then $.cookie 'ctfpad_hide', 'false' 58 | 59 | updateProgress = -> 60 | #challenge progress 61 | d = $('.challenge.done').length / $('.challenge').length 62 | $('#progress').css 'width', "#{d*100}%" 63 | $('#progress').siblings('span').text "#{$('.challenge.done').length} / #{$('.challenge').length}" 64 | #score progress 65 | totalScore = 0 66 | score = 0 67 | $('.challenge').each -> 68 | totalScore += parseInt($(this).attr 'data-chalpoints') 69 | if $(this).hasClass 'done' then score += parseInt($(this).attr 'data-chalpoints') 70 | $('#scoreprogress').css 'width', "#{(score/totalScore)*100}%" 71 | $('#scoreprogress').siblings('span').text "#{score} / #{totalScore}" 72 | #categories progress 73 | $('.category').each -> 74 | cat = $(this).attr 'data-category' 75 | done = $(this).siblings(".done[data-category='#{cat}']").length 76 | $(this).find('.done-count').text done 77 | 78 | updateProgress() 79 | 80 | window.uploads = [] 81 | 82 | window.upload_refresh = (remove) -> 83 | if remove 84 | window.uploads.splice(window.uploads.indexOf(remove), 1) 85 | if window.uploads.length == 0 86 | $('#uploadbutton').hide() 87 | return 88 | total_size = total_prog = 0 89 | for upload in window.uploads 90 | total_size += upload.file.size 91 | total_prog += upload.progress 92 | progress = parseInt(total_prog / total_size * 100, 10) 93 | $('#uploadprogress').text "#{progress}% / #{window.uploads.length} files" 94 | 95 | window.upload_handler_send = (e, data) -> 96 | if window.uploads.length == 0 97 | $('#uploadbutton').show() 98 | data.context = 99 | file: data.files[0] 100 | progress: 0 101 | window.uploads.push data.context 102 | window.upload_refresh() 103 | 104 | window.upload_handler_done = (e, data) -> 105 | window.upload_refresh(data.context) 106 | 107 | window.upload_handler_fail = (e, data) -> 108 | window.upload_refresh(data.context) 109 | alert "Upload failed: #{data.errorThrown}" 110 | 111 | window.upload_handler_progress = (e, data) -> 112 | data.context.progress = data.loaded 113 | window.upload_refresh() 114 | 115 | 116 | $('.contentlink').click -> 117 | page = $(this).attr('href').replace '#', '' 118 | unless window.currentPage is page 119 | if m = /^(ctf|challenge)(.+)files$/.exec(page) 120 | $('#content').html "" 121 | $.get "/files/#{m[1]}/#{m[2]}", (data) -> 122 | $('#content').html data 123 | url = "/upload/#{m[1]}/#{m[2]}" 124 | $('#fileupload').fileupload({ 125 | url: url, 126 | dataType: 'json', 127 | send: window.upload_handler_send, 128 | done: window.upload_handler_done, 129 | fail: window.upload_handler_fail, 130 | progress: window.upload_handler_progress 131 | }).prop('disabled', !$.support.fileInput).parent().addClass $.support.fileInput ? undefined : 'disabled' 132 | else 133 | $('#content').pad {'padId':page} 134 | $(".highlighted").removeClass("highlighted") 135 | $(this).parents(".highlightable").addClass("highlighted") 136 | window.currentPage = page 137 | 138 | $(".contentlink[href='#{location.hash}']").click() 139 | 140 | $("input[type='checkbox']").change -> 141 | $(this).parent().next().css 'text-decoration',if this.checked then 'line-through' else 'none' 142 | sock.send JSON.stringify {type:'done', subject:parseInt($(this).attr('data-chalid')), data:this.checked} 143 | 144 | $('.assignments').popover({html:true, content: -> $(this).parent().find('.popover-content').html()}).click (e)-> 145 | $('.assignments').not(this).popover('hide') 146 | $(this).popover 'toggle' 147 | e.stopPropagation() 148 | $('html').click -> 149 | $('.assignments').popover('hide') 150 | 151 | 152 | $('.scoreboard-toggle').popover {html: true, content: -> 153 | $.get '/scoreboard', (ans) -> #FIXME function gets executed twice? 154 | $('#scoreboard').html(ans) 155 | , 'html' 156 | return 'loading...' 157 | } 158 | 159 | $('body').delegate '.btn-assign', 'click', -> 160 | sock.send JSON.stringify {type:'assign', subject:parseInt($(this).attr('data-chalid'))} 161 | 162 | $('body').delegate '.add-challenge', 'click', -> 163 | a = $(this).parent().clone() 164 | a.find('input').val('').removeClass 'hide' 165 | $(this).parent().after a 166 | if a.hasClass 'dummy' 167 | a.removeClass('dummy') 168 | $(this).parent().remove() 169 | 170 | $('body').delegate '.remove-challenge', 'click', -> 171 | if $('.category-formgroup').length > 1 then $(this).parent().remove() 172 | 173 | $('body').delegate '.deletefile', 'click', -> 174 | fileid = $(this).attr('data-id') 175 | filename = $(this).attr('data-name') 176 | $('#deletefilemodal .alert').removeClass('alert-success alert-error').hide() 177 | $('#deletefilename').text filename 178 | $('#deletefilebtnno').text 'no' 179 | $('#deletefilebtnyes').show() 180 | $('#deletefilemodal').data('fileid', fileid).modal 'show' 181 | return false 182 | 183 | $('#hidefinished').click -> 184 | unless $(this).hasClass 'active' 185 | $('head').append '' 186 | $.cookie 'ctfpad_hide', 'true' 187 | else 188 | $('#hidefinishedcss').remove() 189 | $.cookie 'ctfpad_hide', 'false' 190 | if $.cookie('ctfpad_hide') is 'true' then $('#hidefinished').click() 191 | 192 | window.newctf = -> 193 | l = $('#ctfform').serializeArray() 194 | newctf = {title: l.shift().value, challenges:[]} 195 | until l.length is 0 196 | newctf.challenges.push {'title':l.shift().value, 'category':l.shift().value, 'points':parseInt(l.shift().value)} 197 | sock.send JSON.stringify {type:'newctf', data: newctf} 198 | $('#ctfmodal').modal 'hide' 199 | $('#ctfform').find('input').val '' 200 | document.location = '/scope/latest' 201 | 202 | window.ajaxPost = (url, data = null, cb) -> $.ajax 203 | 204 | window.changepw = -> 205 | $.ajax { 206 | url: '/changepassword' 207 | type: 'post' 208 | data: $('#passwordform').serialize() 209 | dataType: 'json' 210 | headers: 211 | 'x-session-id': $.cookie('ctfpad') 212 | success: (ans) -> 213 | $('#passwordmodal .alert').removeClass('alert-success alert-error') 214 | if ans.success 215 | $('#passwordmodal .alert').addClass('alert-success').text 'your password has been changed' 216 | else 217 | $('#passwordmodal .alert').addClass('alert-error').text ans.error 218 | $('#passwordmodal .alert').show() 219 | } 220 | 221 | window.newapikey = -> 222 | $.ajax { 223 | url: '/newapikey' 224 | type: 'post' 225 | dataType: 'text' 226 | headers: 227 | 'x-session-id': $.cookie('ctfpad') 228 | success: (apikey) -> 229 | if apikey then $('#apikey').text apikey 230 | } 231 | 232 | window.modifyctf = -> 233 | l = $('#ctfmodifyform').serializeArray() 234 | ctf = {ctf: window.current.id, challenges:[]} 235 | until l.length is 0 236 | ctf.challenges.push {'id':parseInt(l.shift().value), 'title':l.shift().value, 'category':l.shift().value, 'points':parseInt(l.shift().value)} 237 | sock.send JSON.stringify {type:'modifyctf', data: ctf} 238 | $('#ctfmodifymodal').modal 'hide' 239 | setTimeout -> 240 | document.location.reload() 241 | ,500 242 | 243 | window.delete_file_confirmed = () -> 244 | $.get '/delete_file/' + $('#deletefilemodal').data('fileid'), (ans) -> 245 | $('#deletefilemodal .alert').removeClass('alert-success alert-error') 246 | if ans.success 247 | $('#deletefilemodal .alert').addClass('alert-success').text 'file has been deleted' 248 | else 249 | $('#deletefilemodal .alert').addClass('alert-error').text ans.error 250 | $('#deletefilemodal .alert').show() 251 | $('#deletefilebtnno').text('close') 252 | $('#deletefilebtnyes').hide() 253 | ,'json' 254 | 255 | -------------------------------------------------------------------------------- /web/js/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 1.8.2 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, window, document */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else { 20 | // Browser globals: 21 | factory(window.jQuery); 22 | } 23 | }(function ($) { 24 | 'use strict'; 25 | 26 | // Helper variable to create unique names for the transport iframes: 27 | var counter = 0; 28 | 29 | // The iframe transport accepts four additional options: 30 | // options.fileInput: a jQuery collection of file input fields 31 | // options.paramName: the parameter name for the file form data, 32 | // overrides the name property of the file input field(s), 33 | // can be a string or an array of strings. 34 | // options.formData: an array of objects with name and value properties, 35 | // equivalent to the return data of .serializeArray(), e.g.: 36 | // [{name: 'a', value: 1}, {name: 'b', value: 2}] 37 | // options.initialIframeSrc: the URL of the initial iframe src, 38 | // by default set to "javascript:false;" 39 | $.ajaxTransport('iframe', function (options) { 40 | if (options.async) { 41 | // javascript:false as initial iframe src 42 | // prevents warning popups on HTTPS in IE6: 43 | /*jshint scripturl: true */ 44 | var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', 45 | /*jshint scripturl: false */ 46 | form, 47 | iframe, 48 | addParamChar; 49 | return { 50 | send: function (_, completeCallback) { 51 | form = $('
    '); 52 | form.attr('accept-charset', options.formAcceptCharset); 53 | addParamChar = /\?/.test(options.url) ? '&' : '?'; 54 | // XDomainRequest only supports GET and POST: 55 | if (options.type === 'DELETE') { 56 | options.url = options.url + addParamChar + '_method=DELETE'; 57 | options.type = 'POST'; 58 | } else if (options.type === 'PUT') { 59 | options.url = options.url + addParamChar + '_method=PUT'; 60 | options.type = 'POST'; 61 | } else if (options.type === 'PATCH') { 62 | options.url = options.url + addParamChar + '_method=PATCH'; 63 | options.type = 'POST'; 64 | } 65 | // IE versions below IE8 cannot set the name property of 66 | // elements that have already been added to the DOM, 67 | // so we set the name along with the iframe HTML markup: 68 | counter += 1; 69 | iframe = $( 70 | '' 72 | ).bind('load', function () { 73 | var fileInputClones, 74 | paramNames = $.isArray(options.paramName) ? 75 | options.paramName : [options.paramName]; 76 | iframe 77 | .unbind('load') 78 | .bind('load', function () { 79 | var response; 80 | // Wrap in a try/catch block to catch exceptions thrown 81 | // when trying to access cross-domain iframe contents: 82 | try { 83 | response = iframe.contents(); 84 | // Google Chrome and Firefox do not throw an 85 | // exception when calling iframe.contents() on 86 | // cross-domain requests, so we unify the response: 87 | if (!response.length || !response[0].firstChild) { 88 | throw new Error(); 89 | } 90 | } catch (e) { 91 | response = undefined; 92 | } 93 | // The complete callback returns the 94 | // iframe content document as response object: 95 | completeCallback( 96 | 200, 97 | 'success', 98 | {'iframe': response} 99 | ); 100 | // Fix for IE endless progress bar activity bug 101 | // (happens on form submits to iframe targets): 102 | $('') 103 | .appendTo(form); 104 | window.setTimeout(function () { 105 | // Removing the form in a setTimeout call 106 | // allows Chrome's developer tools to display 107 | // the response result 108 | form.remove(); 109 | }, 0); 110 | }); 111 | form 112 | .prop('target', iframe.prop('name')) 113 | .prop('action', options.url) 114 | .prop('method', options.type); 115 | if (options.formData) { 116 | $.each(options.formData, function (index, field) { 117 | $('') 118 | .prop('name', field.name) 119 | .val(field.value) 120 | .appendTo(form); 121 | }); 122 | } 123 | if (options.fileInput && options.fileInput.length && 124 | options.type === 'POST') { 125 | fileInputClones = options.fileInput.clone(); 126 | // Insert a clone for each file input field: 127 | options.fileInput.after(function (index) { 128 | return fileInputClones[index]; 129 | }); 130 | if (options.paramName) { 131 | options.fileInput.each(function (index) { 132 | $(this).prop( 133 | 'name', 134 | paramNames[index] || options.paramName 135 | ); 136 | }); 137 | } 138 | // Appending the file input fields to the hidden form 139 | // removes them from their original location: 140 | form 141 | .append(options.fileInput) 142 | .prop('enctype', 'multipart/form-data') 143 | // enctype must be set as encoding for IE: 144 | .prop('encoding', 'multipart/form-data'); 145 | // Remove the HTML5 form attribute from the input(s): 146 | options.fileInput.removeAttr('form'); 147 | } 148 | form.submit(); 149 | // Insert the file input fields at their original location 150 | // by replacing the clones with the originals: 151 | if (fileInputClones && fileInputClones.length) { 152 | options.fileInput.each(function (index, input) { 153 | var clone = $(fileInputClones[index]); 154 | // Restore the original name and form properties: 155 | $(input) 156 | .prop('name', clone.prop('name')) 157 | .attr('form', clone.attr('form')); 158 | clone.replaceWith(input); 159 | }); 160 | } 161 | }); 162 | form.append(iframe).appendTo(document.body); 163 | }, 164 | abort: function () { 165 | if (iframe) { 166 | // javascript:false as iframe src aborts the request 167 | // and prevents warning popups on HTTPS in IE6. 168 | // concat is used to avoid the "Script URL" JSLint error: 169 | iframe 170 | .unbind('load') 171 | .prop('src', initialIframeSrc); 172 | } 173 | if (form) { 174 | form.remove(); 175 | } 176 | } 177 | }; 178 | } 179 | }); 180 | 181 | // The iframe transport returns the iframe content document as response. 182 | // The following adds converters from iframe to text, json, html, xml 183 | // and script. 184 | // Please note that the Content-Type for JSON responses has to be text/plain 185 | // or text/html, if the browser doesn't include application/json in the 186 | // Accept header, else IE will show a download dialog. 187 | // The Content-Type for XML responses on the other hand has to be always 188 | // application/xml or text/xml, so IE properly parses the XML response. 189 | // See also 190 | // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation 191 | $.ajaxSetup({ 192 | converters: { 193 | 'iframe text': function (iframe) { 194 | return iframe && $(iframe[0].body).text(); 195 | }, 196 | 'iframe json': function (iframe) { 197 | return iframe && $.parseJSON($(iframe[0].body).text()); 198 | }, 199 | 'iframe html': function (iframe) { 200 | return iframe && $(iframe[0].body).html(); 201 | }, 202 | 'iframe xml': function (iframe) { 203 | var xmlDoc = iframe && iframe[0]; 204 | return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : 205 | $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || 206 | $(xmlDoc.body).html()); 207 | }, 208 | 'iframe script': function (iframe) { 209 | return iframe && $.globalEval($(iframe[0].body).text()); 210 | } 211 | } 212 | }); 213 | 214 | })); 215 | -------------------------------------------------------------------------------- /web/js/ctfpad.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | (function() { 3 | $(function() { 4 | var sessid, sock, updateProgress; 5 | proto = location.protocol == "http:" ? "ws" : "wss" 6 | sock = new WebSocket(proto + (location.href.substring(location.protocol.length - 1, location.href.lastIndexOf('/')))); 7 | sock.onopen = function() { 8 | sock.send("\"" + sessid + "\""); 9 | sock.onclose = function() { 10 | if (!window.preventSocketAlert) { 11 | alert('the websocket has been disconnected, reloading the page'); 12 | return document.location.reload(); 13 | } 14 | }; 15 | return sock.onmessage = function(event) { 16 | var current, msg, self, subject; 17 | msg = JSON.parse(event.data); 18 | console.log(msg); 19 | if (msg.type === 'done') { 20 | self = $("input[data-chalid='" + msg.subject + "']"); 21 | self.prop('checked', msg.data); 22 | self.parent().next().css('text-decoration', msg.data ? 'line-through' : 'none'); 23 | if (msg.data) { 24 | self.parent().parent().addClass('done'); 25 | } else { 26 | self.parent().parent().removeClass('done'); 27 | } 28 | return updateProgress(); 29 | } else if (msg.type === 'assign') { 30 | self = $(".labels[data-chalid='" + msg.subject + "']"); 31 | if (msg.data[1]) { 32 | self.append($("
  • ").append($("").addClass("label").attr("data-name", msg.data[0].name).text(msg.data[0].name))); 33 | } else { 34 | self.find(".label[data-name='" + msg.data[0].name + "']").parent().remove(); 35 | } 36 | return $(".assignment-count[data-chalid='" + msg.subject + "']").text(self.first().find('.label').length); 37 | } else if (msg.type === 'ctfmodification') { 38 | return $('#ctfmodification').fadeIn(500); 39 | } else if (msg.type === 'login') { 40 | $('#userlist').append($("
  • ").text(msg.data)); 41 | return $('#usercount').text($('#userlist').children('li').length); 42 | } else if (msg.type === 'logout') { 43 | $("#userlist li:contains('" + msg.data + "')").remove(); 44 | return $('#usercount').text($('#userlist').children('li').length); 45 | } else if (msg.type === 'fileupload' || msg.type === 'filedeletion') { 46 | if (("" + msg.data + "files") === window.currentPage) { 47 | current = window.currentPage; 48 | window.currentPage = null; 49 | $(".contentlink[href='#" + current + "']").click(); 50 | } 51 | subject = $(".contentlink[href='#" + msg.data + "files']"); 52 | if (msg.filecount > 0) { 53 | subject.children('i').removeClass('icon-folder-close').addClass('icon-folder-open'); 54 | } else { 55 | subject.children('i').removeClass('icon-folder-open').addClass('icon-folder-close'); 56 | } 57 | return subject.nextAll('sup').text(msg.filecount); 58 | } else { 59 | return alert(event.data); 60 | } 61 | }; 62 | }; 63 | window.onbeforeunload = function() { 64 | window.preventSocketAlert = true; 65 | }; 66 | sessid = $.cookie('ctfpad'); 67 | if ($.cookie('ctfpad_hide') === void 0) { 68 | $.cookie('ctfpad_hide', 'false'); 69 | } 70 | updateProgress = function() { 71 | var d, score, totalScore; 72 | d = $('.challenge.done').length / $('.challenge').length; 73 | $('#progress').css('width', "" + (d * 100) + "%"); 74 | $('#progress').siblings('span').text("" + ($('.challenge.done').length) + " / " + ($('.challenge').length)); 75 | totalScore = 0; 76 | score = 0; 77 | $('.challenge').each(function() { 78 | totalScore += parseInt($(this).attr('data-chalpoints')); 79 | if ($(this).hasClass('done')) { 80 | return score += parseInt($(this).attr('data-chalpoints')); 81 | } 82 | }); 83 | $('#scoreprogress').css('width', "" + ((score / totalScore) * 100) + "%"); 84 | $('#scoreprogress').siblings('span').text("" + score + " / " + totalScore); 85 | return $('.category').each(function() { 86 | var cat, done; 87 | cat = $(this).attr('data-category'); 88 | done = $(this).siblings(".done[data-category='" + cat + "']").length; 89 | return $(this).find('.done-count').text(done); 90 | }); 91 | }; 92 | updateProgress(); 93 | window.uploads = []; 94 | window.upload_refresh = function(remove) { 95 | var progress, total_prog, total_size, upload, _i, _len, _ref; 96 | if (remove) { 97 | window.uploads.splice(window.uploads.indexOf(remove), 1); 98 | if (window.uploads.length === 0) { 99 | $('#uploadbutton').hide(); 100 | return; 101 | } 102 | } 103 | total_size = total_prog = 0; 104 | _ref = window.uploads; 105 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 106 | upload = _ref[_i]; 107 | total_size += upload.file.size; 108 | total_prog += upload.progress; 109 | } 110 | progress = parseInt(total_prog / total_size * 100, 10); 111 | return $('#uploadprogress').text("" + progress + "% / " + window.uploads.length + " files"); 112 | }; 113 | window.upload_handler_send = function(e, data) { 114 | if (window.uploads.length === 0) { 115 | $('#uploadbutton').show(); 116 | } 117 | data.context = { 118 | file: data.files[0], 119 | progress: 0 120 | }; 121 | window.uploads.push(data.context); 122 | return window.upload_refresh(); 123 | }; 124 | window.upload_handler_done = function(e, data) { 125 | return window.upload_refresh(data.context); 126 | }; 127 | window.upload_handler_fail = function(e, data) { 128 | window.upload_refresh(data.context); 129 | return alert("Upload failed: " + data.errorThrown); 130 | }; 131 | window.upload_handler_progress = function(e, data) { 132 | data.context.progress = data.loaded; 133 | return window.upload_refresh(); 134 | }; 135 | $('.contentlink').click(function() { 136 | var m, page; 137 | page = $(this).attr('href').replace('#', ''); 138 | if (window.currentPage !== page) { 139 | if (m = /^(ctf|challenge)(.+)files$/.exec(page)) { 140 | $('#content').html(""); 141 | $.get("/files/" + m[1] + "/" + m[2], function(data) { 142 | var url, _ref; 143 | $('#content').html(data); 144 | url = "/upload/" + m[1] + "/" + m[2]; 145 | return $('#fileupload').fileupload({ 146 | url: url, 147 | dataType: 'json', 148 | send: window.upload_handler_send, 149 | done: window.upload_handler_done, 150 | fail: window.upload_handler_fail, 151 | progress: window.upload_handler_progress 152 | }).prop('disabled', !$.support.fileInput).parent().addClass((_ref = $.support.fileInput) != null ? _ref : { 153 | undefined: 'disabled' 154 | }); 155 | }); 156 | } else { 157 | $('#content').pad({ 158 | 'padId': page 159 | }); 160 | } 161 | $(".highlighted").removeClass("highlighted"); 162 | $(this).parents(".highlightable").addClass("highlighted"); 163 | return window.currentPage = page; 164 | } 165 | }); 166 | $(".contentlink[href='" + location.hash + "']").click(); 167 | $("input[type='checkbox']").change(function() { 168 | $(this).parent().next().css('text-decoration', this.checked ? 'line-through' : 'none'); 169 | return sock.send(JSON.stringify({ 170 | type: 'done', 171 | subject: parseInt($(this).attr('data-chalid')), 172 | data: this.checked 173 | })); 174 | }); 175 | $('.assignments').popover({ 176 | html: true, 177 | content: function() { 178 | return $(this).parent().find('.popover-content').html(); 179 | } 180 | }).click(function(e) { 181 | $('.assignments').not(this).popover('hide'); 182 | $(this).popover('toggle'); 183 | return e.stopPropagation(); 184 | }); 185 | $('html').click(function() { 186 | return $('.assignments').popover('hide'); 187 | }); 188 | $('.scoreboard-toggle').popover({ 189 | html: true, 190 | content: function() { 191 | $.get('/scoreboard', function(ans) { 192 | return $('#scoreboard').html(ans); 193 | }, 'html'); 194 | return 'loading...'; 195 | } 196 | }); 197 | $('body').delegate('.btn-assign', 'click', function() { 198 | return sock.send(JSON.stringify({ 199 | type: 'assign', 200 | subject: parseInt($(this).attr('data-chalid')) 201 | })); 202 | }); 203 | $('body').delegate('.add-challenge', 'click', function() { 204 | var a; 205 | a = $(this).parent().clone(); 206 | a.find('input').val('').removeClass('hide'); 207 | $(this).parent().after(a); 208 | if (a.hasClass('dummy')) { 209 | a.removeClass('dummy'); 210 | return $(this).parent().remove(); 211 | } 212 | }); 213 | $('body').delegate('.remove-challenge', 'click', function() { 214 | if ($('.category-formgroup').length > 1) { 215 | return $(this).parent().remove(); 216 | } 217 | }); 218 | $('body').delegate('.deletefile', 'click', function() { 219 | var fileid, filename; 220 | fileid = $(this).attr('data-id'); 221 | filename = $(this).attr('data-name'); 222 | $('#deletefilemodal .alert').removeClass('alert-success alert-error').hide(); 223 | $('#deletefilename').text(filename); 224 | $('#deletefilebtnno').text('no'); 225 | $('#deletefilebtnyes').show(); 226 | $('#deletefilemodal').data('fileid', fileid).modal('show'); 227 | return false; 228 | }); 229 | $('#hidefinished').click(function() { 230 | if (!$(this).hasClass('active')) { 231 | $('head').append(''); 232 | return $.cookie('ctfpad_hide', 'true'); 233 | } else { 234 | $('#hidefinishedcss').remove(); 235 | return $.cookie('ctfpad_hide', 'false'); 236 | } 237 | }); 238 | if ($.cookie('ctfpad_hide') === 'true') { 239 | $('#hidefinished').click(); 240 | } 241 | window.newctf = function() { 242 | var l, newctf; 243 | l = $('#ctfform').serializeArray(); 244 | newctf = { 245 | title: l.shift().value, 246 | challenges: [] 247 | }; 248 | while (l.length !== 0) { 249 | newctf.challenges.push({ 250 | 'title': l.shift().value, 251 | 'category': l.shift().value, 252 | 'points': parseInt(l.shift().value) 253 | }); 254 | } 255 | sock.send(JSON.stringify({ 256 | type: 'newctf', 257 | data: newctf 258 | })); 259 | $('#ctfmodal').modal('hide'); 260 | $('#ctfform').find('input').val(''); 261 | return document.location = '/scope/latest'; 262 | }; 263 | window.ajaxPost = function(url, data, cb) { 264 | if (data == null) { 265 | data = null; 266 | } 267 | return $.ajax; 268 | }; 269 | window.changepw = function() { 270 | return $.ajax({ 271 | url: '/changepassword', 272 | type: 'post', 273 | data: $('#passwordform').serialize(), 274 | dataType: 'json', 275 | headers: { 276 | 'x-session-id': $.cookie('ctfpad') 277 | }, 278 | success: function(ans) { 279 | $('#passwordmodal .alert').removeClass('alert-success alert-error'); 280 | if (ans.success) { 281 | $('#passwordmodal .alert').addClass('alert-success').text('your password has been changed'); 282 | } else { 283 | $('#passwordmodal .alert').addClass('alert-error').text(ans.error); 284 | } 285 | return $('#passwordmodal .alert').show(); 286 | } 287 | }); 288 | }; 289 | window.newapikey = function() { 290 | return $.ajax({ 291 | url: '/newapikey', 292 | type: 'post', 293 | dataType: 'text', 294 | headers: { 295 | 'x-session-id': $.cookie('ctfpad') 296 | }, 297 | success: function(apikey) { 298 | if (apikey) { 299 | return $('#apikey').text(apikey); 300 | } 301 | } 302 | }); 303 | }; 304 | window.modifyctf = function() { 305 | var ctf, l; 306 | l = $('#ctfmodifyform').serializeArray(); 307 | ctf = { 308 | ctf: window.current.id, 309 | challenges: [] 310 | }; 311 | while (l.length !== 0) { 312 | ctf.challenges.push({ 313 | 'id': parseInt(l.shift().value), 314 | 'title': l.shift().value, 315 | 'category': l.shift().value, 316 | 'points': parseInt(l.shift().value) 317 | }); 318 | } 319 | sock.send(JSON.stringify({ 320 | type: 'modifyctf', 321 | data: ctf 322 | })); 323 | $('#ctfmodifymodal').modal('hide'); 324 | return setTimeout(function() { 325 | return document.location.reload(); 326 | }, 500); 327 | }; 328 | return window.delete_file_confirmed = function() { 329 | return $.get('/delete_file/' + $('#deletefilemodal').data('fileid'), function(ans) { 330 | $('#deletefilemodal .alert').removeClass('alert-success alert-error'); 331 | if (ans.success) { 332 | $('#deletefilemodal .alert').addClass('alert-success').text('file has been deleted'); 333 | } else { 334 | $('#deletefilemodal .alert').addClass('alert-error').text(ans.error); 335 | } 336 | $('#deletefilemodal .alert').show(); 337 | $('#deletefilebtnno').text('close'); 338 | return $('#deletefilebtnyes').hide(); 339 | }, 'json'); 340 | }; 341 | }); 342 | 343 | }).call(this); 344 | -------------------------------------------------------------------------------- /main.coffee: -------------------------------------------------------------------------------- 1 | express = require 'express' 2 | http = require 'http' 3 | https = require 'https' 4 | httpProxy = require 'http-proxy' 5 | process = require 'child_process' 6 | fs = require 'fs' 7 | mv = require 'mv' 8 | cons = require 'consolidate' 9 | WebSocketServer = require('ws').Server 10 | db = require './database.coffee' 11 | 12 | # parse config file 13 | config = null 14 | console.log 'checking for config file' 15 | if fs.existsSync 'config.json' 16 | console.log 'config file found, parsing...' 17 | try 18 | config = JSON.parse fs.readFileSync 'config.json' 19 | catch err 20 | console.log "error parsing config file: #{err}" 21 | return 22 | console.log "config loaded" 23 | else 24 | console.log "config file not found" 25 | return 26 | 27 | app = express() 28 | app.engine 'html', cons.mustache 29 | app.set 'view engine', 'html' 30 | app.set 'views', 'web' 31 | app.use express.bodyParser() 32 | app.use express.cookieParser() 33 | 34 | app.use '/js/', express.static 'web/js/' 35 | app.use '/css/', express.static 'web/css/' 36 | app.use '/img/', express.static 'web/img/' 37 | app.use '/doc/', express.static 'web/doc/' 38 | 39 | options = 40 | key: fs.readFileSync config.keyfile 41 | cert: fs.readFileSync config.certfile 42 | scoreboards = {2: ['test','test2']} 43 | 44 | if config.useHTTPS 45 | server = https.createServer options, app 46 | else 47 | server = http.createServer app 48 | 49 | 50 | validateLogin = (user, pass, cb) -> 51 | if user and pass then db.checkPassword user, pass, cb 52 | else setImmediate cb, false 53 | 54 | validateSession = (session, cb=->) -> 55 | if session is undefined then setImmediate cb, false 56 | else db.validateSession session, cb 57 | 58 | app.get '/', (req, res) -> 59 | validateSession req.cookies.ctfpad, (user) -> 60 | unless user then res.render 'login.html', { "team_name": config.team_name } 61 | else 62 | user.etherpad_port = config.etherpad_port 63 | user.team_name = config.team_name 64 | db.getCTFs (ctfs) -> 65 | user.all_ctfs = ctfs 66 | n = 0 67 | user.ctfs = [] 68 | for i in ctfs 69 | if i.id is user.scope then user.current = i 70 | if n < 5 or i.id is user.scope 71 | user.ctfs.push(i) 72 | n++ 73 | if user.current 74 | done = -> #have it prepared 75 | db.getChallenges user.current.id, (challenges) -> 76 | buf = {} 77 | for challenge in challenges 78 | do (challenge) -> 79 | db.getChallengeFiles challenge.id, (files) -> 80 | challenge.filecount = files.length 81 | done() 82 | if buf[challenge.category] is undefined then buf[challenge.category] = [] 83 | buf[challenge.category].push challenge 84 | user.categories = [] 85 | for k,v of buf 86 | user.categories.push {name:k, challenges:v} 87 | doneCount = 0 88 | done = -> 89 | doneCount++ 90 | if doneCount is challenges.length+1 # +1 for ctf filecount 91 | res.render 'index.html', user 92 | db.getCTFFiles user.current.id, (files) -> 93 | user.current.filecount = files.length 94 | done() 95 | else res.render 'index.html', user 96 | 97 | app.post '/login', (req, res) -> 98 | validateSession req.cookies.ctfpad, (ans) -> 99 | validateLogin req.body.name, req.body.password, (session) -> 100 | if session then res.cookie 'ctfpad', session 101 | res.redirect 303, '/' 102 | 103 | app.get '/login', (req, res) -> res.redirect 303, '/' 104 | 105 | app.post '/register', (req, res) -> 106 | if req.body.name and req.body.password1 and req.body.password2 and req.body.authkey 107 | if req.body.password1 == req.body.password2 108 | if req.body.authkey == config.authkey 109 | db.addUser req.body.name, req.body.password1, (err) -> 110 | if err then res.json {success: false, error: "#{err}"} 111 | else res.json {success: true} 112 | else res.json {success: false, error: 'incorrect authkey'} 113 | else res.json {success: false, error: 'passwords do not match'} 114 | else res.json {success: false, error: 'incomplete request'} 115 | 116 | app.get '/logout', (req, res) -> 117 | res.clearCookie 'ctfpad' 118 | res.redirect 303, '/' 119 | 120 | app.post '/changepassword', (req, res) -> 121 | validateSession req.header('x-session-id'), (ans) -> 122 | if ans 123 | if req.body.newpw and req.body.newpw2 124 | if req.body.newpw == req.body.newpw2 125 | db.changePassword req.header('x-session-id'), req.body.newpw, (err) -> 126 | if err then res.json {success: false, error: "#{err}"} 127 | else res.json {success: true} 128 | else res.json {success: false, error: 'inputs do not match'} 129 | else res.json {success: false, error: 'incomplete request'} 130 | else res.json {success: false, error: 'invalid session'} 131 | 132 | app.post '/newapikey', (req, res) -> 133 | validateSession req.header('x-session-id'), (ans) -> 134 | if ans 135 | db.newApiKeyFor req.header('x-session-id'), (apikey) -> 136 | res.send apikey 137 | else res.send 403 138 | 139 | app.get '/scope/latest', (req, res) -> 140 | validateSession req.cookies.ctfpad, (ans) -> 141 | if ans 142 | db.getLatestCtfId (id) -> 143 | db.changeScope ans.name, id 144 | res.redirect 303, '/' 145 | 146 | app.get '/scope/:ctfid', (req, res) -> 147 | validateSession req.cookies.ctfpad, (ans) -> 148 | if ans then db.changeScope ans.name, req.params.ctfid 149 | res.redirect 303, '/' 150 | 151 | app.get '/scoreboard', (req, res) -> 152 | validateSession req.cookies.ctfpad, (ans) -> 153 | if ans and scoreboards[ans.scope] 154 | res.render 'scoreboard', scoreboards[ans.scope] 155 | else res.send '' 156 | 157 | app.get '/files/:objtype/:objid', (req, res) -> 158 | validateSession req.cookies.ctfpad, (ans) -> 159 | if ans 160 | objtype = ["ctf", "challenge"].indexOf(req.params.objtype) 161 | if objtype != -1 162 | objid = parseInt(req.params.objid) 163 | if isNaN objid 164 | res.send 400 165 | return 166 | files = db[["getCTFFiles", "getChallengeFiles"][objtype]] objid, (files) -> 167 | for file in files 168 | file.uploaded = new Date(file.uploaded*1000).toISOString() 169 | if file.mimetype 170 | file.mimetype = file.mimetype.substr 0, file.mimetype.indexOf ';' 171 | res.render 'files.html', {files: files, objtype: req.params.objtype, objid: req.params.objid} 172 | else res.send 404 173 | else res.send 403 174 | 175 | app.get '/file/:fileid/:filename', (req, res) -> 176 | file = "#{__dirname}/uploads/#{req.params.fileid}" 177 | if /^[a-f0-9A-F]+$/.test(req.params.fileid) and fs.existsSync(file) 178 | db.mimetypeForFile req.params.fileid, (mimetype) -> 179 | res.set 'Content-Type', mimetype.trim() 180 | res.sendfile file 181 | else res.send 404 182 | 183 | app.get '/delete_file/:fileid', (req, res) -> 184 | validateSession req.cookies.ctfpad, (ans) -> 185 | if ans 186 | file = "#{__dirname}/uploads/#{req.params.fileid}" 187 | if /^[a-f0-9A-F]+$/.test(req.params.fileid) and fs.existsSync(file) 188 | db.deleteFile req.params.fileid, (err, type, typeId) -> 189 | unless err 190 | fs.unlink file, (fserr) -> 191 | unless fserr 192 | res.json {success: true} 193 | fun = db[["getCTFFiles", "getChallengeFiles"][type]] 194 | fun typeId, (files) -> 195 | wss.broadcast JSON.stringify {type: 'filedeletion', data: "#{["ctf", "challenge"][type]}#{typeId}", filecount: files.length} 196 | else res.json {success: false, error: fserr} 197 | else res.json {success: false, error: err} 198 | else res.json {success: false, error: "file not found"} 199 | else res.send 403 200 | 201 | upload = (user, objtype, objid, req, res) -> 202 | type = ["ctf", "challenge"].indexOf(objtype) 203 | if type != -1 and req.files.files 204 | mimetype = null 205 | process.execFile '/usr/bin/file', ['-bi', req.files.files.path], (err, stdout) -> 206 | mimetype = unless err then stdout.toString().trim() 207 | db[["addCTFFile", "addChallengeFile"][type]] objid, req.files.files.name, user.name, mimetype, (err, id) -> 208 | if err then res.json {success: false, error: err} 209 | else 210 | mv req.files.files.path, "#{__dirname}/uploads/#{id}", (err) -> 211 | if err then res.json {success: false, error: err} 212 | else 213 | res.json {success: true, id: id} 214 | fun = db[["getCTFFiles", "getChallengeFiles"][type]] 215 | fun parseInt(objid), (files) -> 216 | wss.broadcast JSON.stringify {type: 'fileupload', data: "#{objtype}#{objid}", filecount: files.length} 217 | else res.send 400 218 | 219 | app.post '/upload/:objtype/:objid', (req, res) -> 220 | validateSession req.cookies.ctfpad, (user) -> 221 | if user 222 | upload user, req.params.objtype, req.params.objid, req, res 223 | else res.send 403 224 | 225 | api = require './api.coffee' 226 | api.init app, db, upload, '' 227 | 228 | ## PROXY INIT 229 | proxyTarget = {host: 'localhost', port: config.etherpad_internal_port} 230 | proxy = httpProxy.createProxyServer {target: proxyTarget} 231 | proxy.on 'error', (err, req, res) -> 232 | if err then console.log err 233 | try 234 | res.send 500 235 | catch e then return 236 | 237 | proxyServer = null 238 | if config.proxyUseHTTPS 239 | proxyServer = https.createServer options, (req, res) -> 240 | if req.headers.cookie 241 | sessid = req.headers.cookie.substr req.headers.cookie.indexOf('ctfpad=')+7, 32 242 | validateSession sessid, (ans) -> 243 | if ans 244 | proxy.web req, res 245 | else 246 | res.writeHead 403 247 | res.end() 248 | else 249 | res.writeHead 403 250 | res.end() 251 | else 252 | proxyServer = http.createServer (req, res) -> 253 | if req.headers.cookie 254 | sessid = req.headers.cookie.substr req.headers.cookie.indexOf('ctfpad=')+7, 32 255 | validateSession sessid, (ans) -> 256 | if ans 257 | proxy.web req, res 258 | else 259 | res.writeHead 403 260 | res.end() 261 | else 262 | res.writeHead 403 263 | res.end() 264 | 265 | ###proxyServer.on 'upgrade', (req, socket, head) -> ## USELESS SOMEHOW??? 266 | console.log "UPGRADE UPGRADE UPGRADE" 267 | sessid = req.headers.cookie.substr req.headers.cookie.indexOf('ctfpad=')+7, 32 268 | validateSession sessid, (ans) -> 269 | if ans then proxy.ws req, socket, head else res.send 403### 270 | 271 | ## START ETHERPAD 272 | etherpad = process.spawn 'etherpad-lite/bin/run.sh' 273 | etherpad.stdout.on 'data', (line) -> 274 | console.log "[etherpad] #{line.toString 'utf8', 0, line.length-1}" 275 | etherpad.stderr.on 'data', (line) -> 276 | console.log "[etherpad] #{line.toString 'utf8', 0, line.length-1}" 277 | 278 | wss = new WebSocketServer {server:server} 279 | wss.broadcast = (msg, exclude, scope=null) -> 280 | this.clients.forEach (c) -> 281 | unless c.authenticated then return 282 | if c isnt exclude and (scope is null or scope is c.authenticated.scope) 283 | try 284 | c.send msg 285 | catch e 286 | console.log e 287 | api.broadcast = (obj, scope) -> wss.broadcast JSON.stringify(obj), null, scope 288 | wss.getClients = -> this.clients 289 | wss.on 'connection', (sock) -> 290 | sock.on 'close', -> 291 | if sock.authenticated 292 | wss.broadcast JSON.stringify {type: 'logout', data: sock.authenticated.name} 293 | sock.on 'message', (message) -> 294 | msg = null 295 | try msg = JSON.parse(message) catch e then return 296 | unless sock.authenticated 297 | if typeof msg is 'string' 298 | validateSession msg, (ans) -> 299 | if ans 300 | sock.authenticated = ans 301 | # send assignments on auth 302 | if ans.scope then db.listAssignments ans.scope, (list) -> 303 | for i in list 304 | sock.send JSON.stringify {type: 'assign', subject: i.challenge, data: [{name: i.user}, true]} 305 | # notify all users about new authentication and notify new socket about other users 306 | wss.broadcast JSON.stringify {type: 'login', data: ans.name} 307 | wss.getClients().forEach (s) -> 308 | if s.authenticated and s.authenticated.name isnt ans.name 309 | sock.send JSON.stringify {type: 'login', data: s.authenticated.name} 310 | else 311 | if msg.type and msg.type is 'done' 312 | clean = {data: Boolean(msg.data), subject: msg.subject, type: 'done'} 313 | wss.broadcast JSON.stringify(clean), null 314 | db.setChallengeDone clean.subject, clean.data 315 | else if msg.type and msg.type is 'assign' 316 | db.toggleAssign sock.authenticated.name, msg.subject, (hasBeenAssigned) -> 317 | data = [{name:sock.authenticated.name},hasBeenAssigned] 318 | wss.broadcast JSON.stringify({type: 'assign', data: data, subject: msg.subject}), null 319 | else if msg.type and msg.type is 'newctf' 320 | db.addCTF msg.data.title, (ctfid) -> 321 | for c in msg.data.challenges 322 | db.addChallenge ctfid, c.title, c.category, c.points 323 | else if msg.type and msg.type is 'modifyctf' 324 | for c in msg.data.challenges 325 | 326 | if !c.points 327 | c.points = 0 328 | if c.id 329 | db.modifyChallenge c.id, c.title, c.category, c.points 330 | else 331 | db.addChallenge msg.data.ctf, c.title, c.category, c.points 332 | wss.clients.forEach (s) -> 333 | if s.authenticated and s.authenticated.scope is msg.data.ctf 334 | s.send JSON.stringify {type: 'ctfmodification'} 335 | else console.log msg 336 | 337 | server.listen config.port 338 | proxyServer.listen config.etherpad_port 339 | console.log "listening on port #{config.port} and #{config.etherpad_port}" 340 | 341 | filetype = (path,cb = ->) -> 342 | p = process.spawn 'file', ['-b', path] 343 | p.stdout.on 'data', (output) -> 344 | cb output.toString().substr 0,output.length-1 345 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CTFPad :: {{team_name}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 41 | 46 | 47 | 48 | 114 |
    115 |
    116 |
    117 | {{#current}} 118 |
    119 |
    120 | {{current.name}} 121 | 122 | 123 | {{filecount}} 124 | 125 |
    126 |
    127 |
    128 | 129 |
    130 |
    131 |
    132 | 133 |
    134 | 137 |
    138 |
    139 | This CTF has been modified. You may want to reload the page. 140 |
    141 |
    142 | 143 | 144 | 147 | 148 | {{#categories}} 149 | 150 | 151 | 152 | {{#challenges}} 153 | 154 | 155 | 158 | 159 | 162 | 172 | 173 | {{/challenges}} 174 | {{/categories}} 175 |
    145 | 146 |
    {{name}} [0/{{challenges.length}}]
    156 | [{{points}}] {{title}} 157 | 160 | 161 | {{filecount}} 163 | 164 | 0 165 | 171 |
    176 |
    177 |
    178 |
    179 | {{/current}} 180 | {{^current}}no CTF chosen{{/current}} 181 |
    182 |
    183 | 196 | 221 | 248 | 261 | {{#current}} 262 | 298 | {{/current}} 299 | 309 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /web/js/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Widget 1.10.4+amd 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * http://api.jqueryui.com/jQuery.widget/ 10 | */ 11 | 12 | (function (factory) { 13 | if (typeof define === "function" && define.amd) { 14 | // Register as an anonymous AMD module: 15 | define(["jquery"], factory); 16 | } else { 17 | // Browser globals: 18 | factory(jQuery); 19 | } 20 | }(function( $, undefined ) { 21 | 22 | var uuid = 0, 23 | slice = Array.prototype.slice, 24 | _cleanData = $.cleanData; 25 | $.cleanData = function( elems ) { 26 | for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 27 | try { 28 | $( elem ).triggerHandler( "remove" ); 29 | // http://bugs.jquery.com/ticket/8235 30 | } catch( e ) {} 31 | } 32 | _cleanData( elems ); 33 | }; 34 | 35 | $.widget = function( name, base, prototype ) { 36 | var fullName, existingConstructor, constructor, basePrototype, 37 | // proxiedPrototype allows the provided prototype to remain unmodified 38 | // so that it can be used as a mixin for multiple widgets (#8876) 39 | proxiedPrototype = {}, 40 | namespace = name.split( "." )[ 0 ]; 41 | 42 | name = name.split( "." )[ 1 ]; 43 | fullName = namespace + "-" + name; 44 | 45 | if ( !prototype ) { 46 | prototype = base; 47 | base = $.Widget; 48 | } 49 | 50 | // create selector for plugin 51 | $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { 52 | return !!$.data( elem, fullName ); 53 | }; 54 | 55 | $[ namespace ] = $[ namespace ] || {}; 56 | existingConstructor = $[ namespace ][ name ]; 57 | constructor = $[ namespace ][ name ] = function( options, element ) { 58 | // allow instantiation without "new" keyword 59 | if ( !this._createWidget ) { 60 | return new constructor( options, element ); 61 | } 62 | 63 | // allow instantiation without initializing for simple inheritance 64 | // must use "new" keyword (the code above always passes args) 65 | if ( arguments.length ) { 66 | this._createWidget( options, element ); 67 | } 68 | }; 69 | // extend with the existing constructor to carry over any static properties 70 | $.extend( constructor, existingConstructor, { 71 | version: prototype.version, 72 | // copy the object used to create the prototype in case we need to 73 | // redefine the widget later 74 | _proto: $.extend( {}, prototype ), 75 | // track widgets that inherit from this widget in case this widget is 76 | // redefined after a widget inherits from it 77 | _childConstructors: [] 78 | }); 79 | 80 | basePrototype = new base(); 81 | // we need to make the options hash a property directly on the new instance 82 | // otherwise we'll modify the options hash on the prototype that we're 83 | // inheriting from 84 | basePrototype.options = $.widget.extend( {}, basePrototype.options ); 85 | $.each( prototype, function( prop, value ) { 86 | if ( !$.isFunction( value ) ) { 87 | proxiedPrototype[ prop ] = value; 88 | return; 89 | } 90 | proxiedPrototype[ prop ] = (function() { 91 | var _super = function() { 92 | return base.prototype[ prop ].apply( this, arguments ); 93 | }, 94 | _superApply = function( args ) { 95 | return base.prototype[ prop ].apply( this, args ); 96 | }; 97 | return function() { 98 | var __super = this._super, 99 | __superApply = this._superApply, 100 | returnValue; 101 | 102 | this._super = _super; 103 | this._superApply = _superApply; 104 | 105 | returnValue = value.apply( this, arguments ); 106 | 107 | this._super = __super; 108 | this._superApply = __superApply; 109 | 110 | return returnValue; 111 | }; 112 | })(); 113 | }); 114 | constructor.prototype = $.widget.extend( basePrototype, { 115 | // TODO: remove support for widgetEventPrefix 116 | // always use the name + a colon as the prefix, e.g., draggable:start 117 | // don't prefix for widgets that aren't DOM-based 118 | widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 119 | }, proxiedPrototype, { 120 | constructor: constructor, 121 | namespace: namespace, 122 | widgetName: name, 123 | widgetFullName: fullName 124 | }); 125 | 126 | // If this widget is being redefined then we need to find all widgets that 127 | // are inheriting from it and redefine all of them so that they inherit from 128 | // the new version of this widget. We're essentially trying to replace one 129 | // level in the prototype chain. 130 | if ( existingConstructor ) { 131 | $.each( existingConstructor._childConstructors, function( i, child ) { 132 | var childPrototype = child.prototype; 133 | 134 | // redefine the child widget using the same prototype that was 135 | // originally used, but inherit from the new version of the base 136 | $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); 137 | }); 138 | // remove the list of existing child constructors from the old constructor 139 | // so the old child constructors can be garbage collected 140 | delete existingConstructor._childConstructors; 141 | } else { 142 | base._childConstructors.push( constructor ); 143 | } 144 | 145 | $.widget.bridge( name, constructor ); 146 | }; 147 | 148 | $.widget.extend = function( target ) { 149 | var input = slice.call( arguments, 1 ), 150 | inputIndex = 0, 151 | inputLength = input.length, 152 | key, 153 | value; 154 | for ( ; inputIndex < inputLength; inputIndex++ ) { 155 | for ( key in input[ inputIndex ] ) { 156 | value = input[ inputIndex ][ key ]; 157 | if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { 158 | // Clone objects 159 | if ( $.isPlainObject( value ) ) { 160 | target[ key ] = $.isPlainObject( target[ key ] ) ? 161 | $.widget.extend( {}, target[ key ], value ) : 162 | // Don't extend strings, arrays, etc. with objects 163 | $.widget.extend( {}, value ); 164 | // Copy everything else by reference 165 | } else { 166 | target[ key ] = value; 167 | } 168 | } 169 | } 170 | } 171 | return target; 172 | }; 173 | 174 | $.widget.bridge = function( name, object ) { 175 | var fullName = object.prototype.widgetFullName || name; 176 | $.fn[ name ] = function( options ) { 177 | var isMethodCall = typeof options === "string", 178 | args = slice.call( arguments, 1 ), 179 | returnValue = this; 180 | 181 | // allow multiple hashes to be passed on init 182 | options = !isMethodCall && args.length ? 183 | $.widget.extend.apply( null, [ options ].concat(args) ) : 184 | options; 185 | 186 | if ( isMethodCall ) { 187 | this.each(function() { 188 | var methodValue, 189 | instance = $.data( this, fullName ); 190 | if ( !instance ) { 191 | return $.error( "cannot call methods on " + name + " prior to initialization; " + 192 | "attempted to call method '" + options + "'" ); 193 | } 194 | if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { 195 | return $.error( "no such method '" + options + "' for " + name + " widget instance" ); 196 | } 197 | methodValue = instance[ options ].apply( instance, args ); 198 | if ( methodValue !== instance && methodValue !== undefined ) { 199 | returnValue = methodValue && methodValue.jquery ? 200 | returnValue.pushStack( methodValue.get() ) : 201 | methodValue; 202 | return false; 203 | } 204 | }); 205 | } else { 206 | this.each(function() { 207 | var instance = $.data( this, fullName ); 208 | if ( instance ) { 209 | instance.option( options || {} )._init(); 210 | } else { 211 | $.data( this, fullName, new object( options, this ) ); 212 | } 213 | }); 214 | } 215 | 216 | return returnValue; 217 | }; 218 | }; 219 | 220 | $.Widget = function( /* options, element */ ) {}; 221 | $.Widget._childConstructors = []; 222 | 223 | $.Widget.prototype = { 224 | widgetName: "widget", 225 | widgetEventPrefix: "", 226 | defaultElement: "
    ", 227 | options: { 228 | disabled: false, 229 | 230 | // callbacks 231 | create: null 232 | }, 233 | _createWidget: function( options, element ) { 234 | element = $( element || this.defaultElement || this )[ 0 ]; 235 | this.element = $( element ); 236 | this.uuid = uuid++; 237 | this.eventNamespace = "." + this.widgetName + this.uuid; 238 | this.options = $.widget.extend( {}, 239 | this.options, 240 | this._getCreateOptions(), 241 | options ); 242 | 243 | this.bindings = $(); 244 | this.hoverable = $(); 245 | this.focusable = $(); 246 | 247 | if ( element !== this ) { 248 | $.data( element, this.widgetFullName, this ); 249 | this._on( true, this.element, { 250 | remove: function( event ) { 251 | if ( event.target === element ) { 252 | this.destroy(); 253 | } 254 | } 255 | }); 256 | this.document = $( element.style ? 257 | // element within the document 258 | element.ownerDocument : 259 | // element is window or document 260 | element.document || element ); 261 | this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); 262 | } 263 | 264 | this._create(); 265 | this._trigger( "create", null, this._getCreateEventData() ); 266 | this._init(); 267 | }, 268 | _getCreateOptions: $.noop, 269 | _getCreateEventData: $.noop, 270 | _create: $.noop, 271 | _init: $.noop, 272 | 273 | destroy: function() { 274 | this._destroy(); 275 | // we can probably remove the unbind calls in 2.0 276 | // all event bindings should go through this._on() 277 | this.element 278 | .unbind( this.eventNamespace ) 279 | // 1.9 BC for #7810 280 | // TODO remove dual storage 281 | .removeData( this.widgetName ) 282 | .removeData( this.widgetFullName ) 283 | // support: jquery <1.6.3 284 | // http://bugs.jquery.com/ticket/9413 285 | .removeData( $.camelCase( this.widgetFullName ) ); 286 | this.widget() 287 | .unbind( this.eventNamespace ) 288 | .removeAttr( "aria-disabled" ) 289 | .removeClass( 290 | this.widgetFullName + "-disabled " + 291 | "ui-state-disabled" ); 292 | 293 | // clean up events and states 294 | this.bindings.unbind( this.eventNamespace ); 295 | this.hoverable.removeClass( "ui-state-hover" ); 296 | this.focusable.removeClass( "ui-state-focus" ); 297 | }, 298 | _destroy: $.noop, 299 | 300 | widget: function() { 301 | return this.element; 302 | }, 303 | 304 | option: function( key, value ) { 305 | var options = key, 306 | parts, 307 | curOption, 308 | i; 309 | 310 | if ( arguments.length === 0 ) { 311 | // don't return a reference to the internal hash 312 | return $.widget.extend( {}, this.options ); 313 | } 314 | 315 | if ( typeof key === "string" ) { 316 | // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 317 | options = {}; 318 | parts = key.split( "." ); 319 | key = parts.shift(); 320 | if ( parts.length ) { 321 | curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); 322 | for ( i = 0; i < parts.length - 1; i++ ) { 323 | curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; 324 | curOption = curOption[ parts[ i ] ]; 325 | } 326 | key = parts.pop(); 327 | if ( arguments.length === 1 ) { 328 | return curOption[ key ] === undefined ? null : curOption[ key ]; 329 | } 330 | curOption[ key ] = value; 331 | } else { 332 | if ( arguments.length === 1 ) { 333 | return this.options[ key ] === undefined ? null : this.options[ key ]; 334 | } 335 | options[ key ] = value; 336 | } 337 | } 338 | 339 | this._setOptions( options ); 340 | 341 | return this; 342 | }, 343 | _setOptions: function( options ) { 344 | var key; 345 | 346 | for ( key in options ) { 347 | this._setOption( key, options[ key ] ); 348 | } 349 | 350 | return this; 351 | }, 352 | _setOption: function( key, value ) { 353 | this.options[ key ] = value; 354 | 355 | if ( key === "disabled" ) { 356 | this.widget() 357 | .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) 358 | .attr( "aria-disabled", value ); 359 | this.hoverable.removeClass( "ui-state-hover" ); 360 | this.focusable.removeClass( "ui-state-focus" ); 361 | } 362 | 363 | return this; 364 | }, 365 | 366 | enable: function() { 367 | return this._setOption( "disabled", false ); 368 | }, 369 | disable: function() { 370 | return this._setOption( "disabled", true ); 371 | }, 372 | 373 | _on: function( suppressDisabledCheck, element, handlers ) { 374 | var delegateElement, 375 | instance = this; 376 | 377 | // no suppressDisabledCheck flag, shuffle arguments 378 | if ( typeof suppressDisabledCheck !== "boolean" ) { 379 | handlers = element; 380 | element = suppressDisabledCheck; 381 | suppressDisabledCheck = false; 382 | } 383 | 384 | // no element argument, shuffle and use this.element 385 | if ( !handlers ) { 386 | handlers = element; 387 | element = this.element; 388 | delegateElement = this.widget(); 389 | } else { 390 | // accept selectors, DOM elements 391 | element = delegateElement = $( element ); 392 | this.bindings = this.bindings.add( element ); 393 | } 394 | 395 | $.each( handlers, function( event, handler ) { 396 | function handlerProxy() { 397 | // allow widgets to customize the disabled handling 398 | // - disabled as an array instead of boolean 399 | // - disabled class as method for disabling individual parts 400 | if ( !suppressDisabledCheck && 401 | ( instance.options.disabled === true || 402 | $( this ).hasClass( "ui-state-disabled" ) ) ) { 403 | return; 404 | } 405 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 406 | .apply( instance, arguments ); 407 | } 408 | 409 | // copy the guid so direct unbinding works 410 | if ( typeof handler !== "string" ) { 411 | handlerProxy.guid = handler.guid = 412 | handler.guid || handlerProxy.guid || $.guid++; 413 | } 414 | 415 | var match = event.match( /^(\w+)\s*(.*)$/ ), 416 | eventName = match[1] + instance.eventNamespace, 417 | selector = match[2]; 418 | if ( selector ) { 419 | delegateElement.delegate( selector, eventName, handlerProxy ); 420 | } else { 421 | element.bind( eventName, handlerProxy ); 422 | } 423 | }); 424 | }, 425 | 426 | _off: function( element, eventName ) { 427 | eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; 428 | element.unbind( eventName ).undelegate( eventName ); 429 | }, 430 | 431 | _delay: function( handler, delay ) { 432 | function handlerProxy() { 433 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 434 | .apply( instance, arguments ); 435 | } 436 | var instance = this; 437 | return setTimeout( handlerProxy, delay || 0 ); 438 | }, 439 | 440 | _hoverable: function( element ) { 441 | this.hoverable = this.hoverable.add( element ); 442 | this._on( element, { 443 | mouseenter: function( event ) { 444 | $( event.currentTarget ).addClass( "ui-state-hover" ); 445 | }, 446 | mouseleave: function( event ) { 447 | $( event.currentTarget ).removeClass( "ui-state-hover" ); 448 | } 449 | }); 450 | }, 451 | 452 | _focusable: function( element ) { 453 | this.focusable = this.focusable.add( element ); 454 | this._on( element, { 455 | focusin: function( event ) { 456 | $( event.currentTarget ).addClass( "ui-state-focus" ); 457 | }, 458 | focusout: function( event ) { 459 | $( event.currentTarget ).removeClass( "ui-state-focus" ); 460 | } 461 | }); 462 | }, 463 | 464 | _trigger: function( type, event, data ) { 465 | var prop, orig, 466 | callback = this.options[ type ]; 467 | 468 | data = data || {}; 469 | event = $.Event( event ); 470 | event.type = ( type === this.widgetEventPrefix ? 471 | type : 472 | this.widgetEventPrefix + type ).toLowerCase(); 473 | // the original event may come from any element 474 | // so we need to reset the target on the new event 475 | event.target = this.element[ 0 ]; 476 | 477 | // copy original event properties over to the new event 478 | orig = event.originalEvent; 479 | if ( orig ) { 480 | for ( prop in orig ) { 481 | if ( !( prop in event ) ) { 482 | event[ prop ] = orig[ prop ]; 483 | } 484 | } 485 | } 486 | 487 | this.element.trigger( event, data ); 488 | return !( $.isFunction( callback ) && 489 | callback.apply( this.element[0], [ event ].concat( data ) ) === false || 490 | event.isDefaultPrevented() ); 491 | } 492 | }; 493 | 494 | $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { 495 | $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { 496 | if ( typeof options === "string" ) { 497 | options = { effect: options }; 498 | } 499 | var hasOptions, 500 | effectName = !options ? 501 | method : 502 | options === true || typeof options === "number" ? 503 | defaultEffect : 504 | options.effect || defaultEffect; 505 | options = options || {}; 506 | if ( typeof options === "number" ) { 507 | options = { duration: options }; 508 | } 509 | hasOptions = !$.isEmptyObject( options ); 510 | options.complete = callback; 511 | if ( options.delay ) { 512 | element.delay( options.delay ); 513 | } 514 | if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { 515 | element[ method ]( options ); 516 | } else if ( effectName !== method && element[ effectName ] ) { 517 | element[ effectName ]( options.duration, options.easing, callback ); 518 | } else { 519 | element.queue(function( next ) { 520 | $( this )[ method ](); 521 | if ( callback ) { 522 | callback.call( element[ 0 ] ); 523 | } 524 | next(); 525 | }); 526 | } 527 | }; 528 | }); 529 | 530 | })); 531 | -------------------------------------------------------------------------------- /web/css/github-markdown.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-anchor; 3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) format('woff'); 4 | } 5 | 6 | html { 7 | font-family: sans-serif; 8 | -ms-text-size-adjust: 100%; 9 | -webkit-text-size-adjust: 100% 10 | } 11 | 12 | body { 13 | min-width: 1020px; 14 | font: 13px/1.4 Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 15 | /* color: #333; */ 16 | background-color: #fff; 17 | } 18 | 19 | table { 20 | border-collapse: collapse; 21 | border-spacing: 0 22 | } 23 | 24 | td,th { 25 | padding: 0 26 | } 27 | 28 | * { 29 | -moz-box-sizing: border-box; 30 | box-sizing: border-box; 31 | } 32 | 33 | .container { 34 | width: 980px; 35 | margin-right: auto; 36 | margin-left: auto 37 | } 38 | 39 | .container:before { 40 | display: table; 41 | content: "" 42 | } 43 | 44 | .container:after { 45 | display: table; 46 | clear: both; 47 | content: "" 48 | } 49 | 50 | .boxed-group { 51 | position: relative; 52 | border-radius: 3px; 53 | margin-bottom: 30px 54 | } 55 | 56 | #readme .markdown-body, #readme .plain { 57 | background-color: #fff; 58 | border: 1px solid #ddd; 59 | border-bottom-left-radius: 3px; 60 | border-bottom-right-radius: 3px; 61 | padding: 30px; 62 | word-wrap: break-word; 63 | } 64 | 65 | #readme .plain pre { 66 | font-size: 15px; 67 | white-space: pre-wrap 68 | } 69 | 70 | .repository-with-sidebar:before { 71 | display: table; 72 | content: "" 73 | } 74 | 75 | .repository-with-sidebar:after { 76 | display: table; 77 | clear: both; 78 | content: "" 79 | } 80 | 81 | .repository-with-sidebar .repository-sidebar { 82 | float: right; 83 | width: 38px 84 | } 85 | 86 | .repository-with-sidebar .repository-sidebar .sidebar-button { 87 | width: 100%; 88 | margin: 0 0 10px; 89 | text-align: center 90 | } 91 | 92 | .repository-with-sidebar .repository-sidebar h3 { 93 | margin-bottom: 5px; 94 | font-size: 11px; 95 | font-weight: normal; 96 | color: #999 97 | } 98 | 99 | .repository-with-sidebar .repository-sidebar .clone-url { 100 | display: none; 101 | margin-top: -5px 102 | } 103 | 104 | .repository-with-sidebar .repository-sidebar .clone-url.open { 105 | display: block 106 | } 107 | 108 | .repository-with-sidebar .repository-sidebar .clone-options { 109 | margin: 8px 0 15px; 110 | font-size: 11px; 111 | color: #666 112 | } 113 | 114 | .repository-with-sidebar .repository-sidebar .clone-options .octicon-question { 115 | position: relative; 116 | bottom: 1px; 117 | font-size: 11px; 118 | color: #000; 119 | cursor: pointer 120 | } 121 | 122 | .repository-with-sidebar .repository-content { 123 | /* float: left; */ 124 | width: 920px; 125 | } 126 | 127 | .repository-with-sidebar.with-full-navigation .repository-content { 128 | width: 790px; 129 | } 130 | 131 | .repository-with-sidebar.with-full-navigation .repository-sidebar { 132 | width: 170px 133 | } 134 | 135 | .repository-with-sidebar.with-full-navigation .sunken-menu-group .tooltipped:before, .repository-with-sidebar.with-full-navigation .sunken-menu-group .tooltipped:after { 136 | display: none 137 | } 138 | 139 | .markdown-body { 140 | -ms-text-size-adjust: 100%; 141 | -webkit-text-size-adjust: 100%; 142 | color: #333; 143 | overflow: hidden; 144 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif; 145 | font-size: 16px; 146 | line-height: 1.6; 147 | word-wrap: break-word; 148 | } 149 | 150 | .markdown-body a { 151 | background: transparent; 152 | } 153 | 154 | .markdown-body a:active, 155 | .markdown-body a:hover { 156 | outline: 0; 157 | } 158 | 159 | .markdown-body strong { 160 | font-weight: bold; 161 | } 162 | 163 | .markdown-body h1 { 164 | font-size: 2em; 165 | margin: 0.67em 0; 166 | } 167 | 168 | .markdown-body img { 169 | border: 0; 170 | } 171 | 172 | .markdown-body hr { 173 | -moz-box-sizing: content-box; 174 | box-sizing: content-box; 175 | height: 0; 176 | } 177 | 178 | .markdown-body pre { 179 | overflow: auto; 180 | } 181 | 182 | .markdown-body code, 183 | .markdown-body kbd, 184 | .markdown-body pre { 185 | font-family: monospace, monospace; 186 | font-size: 1em; 187 | } 188 | 189 | .markdown-body input { 190 | color: inherit; 191 | font: inherit; 192 | margin: 0; 193 | } 194 | 195 | .markdown-body html input[disabled] { 196 | cursor: default; 197 | } 198 | 199 | .markdown-body input { 200 | line-height: normal; 201 | } 202 | 203 | .markdown-body input[type="checkbox"] { 204 | -moz-box-sizing: border-box; 205 | box-sizing: border-box; 206 | padding: 0; 207 | } 208 | 209 | .markdown-body table { 210 | border-collapse: collapse; 211 | border-spacing: 0; 212 | } 213 | 214 | .markdown-body td, 215 | .markdown-body th { 216 | padding: 0; 217 | } 218 | 219 | .markdown-body * { 220 | -moz-box-sizing: border-box; 221 | box-sizing: border-box; 222 | } 223 | 224 | .markdown-body input { 225 | font: 13px/1.4 Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 226 | } 227 | 228 | .markdown-body a { 229 | color: #4183c4; 230 | text-decoration: none; 231 | } 232 | 233 | .markdown-body a:hover, 234 | .markdown-body a:focus, 235 | .markdown-body a:active { 236 | text-decoration: underline; 237 | } 238 | 239 | .markdown-body hr { 240 | height: 0; 241 | margin: 15px 0; 242 | overflow: hidden; 243 | background: transparent; 244 | border: 0; 245 | border-bottom: 1px solid #ddd; 246 | } 247 | 248 | .markdown-body hr:before { 249 | display: table; 250 | content: ""; 251 | } 252 | 253 | .markdown-body hr:after { 254 | display: table; 255 | clear: both; 256 | content: ""; 257 | } 258 | 259 | .markdown-body h1, 260 | .markdown-body h2, 261 | .markdown-body h3, 262 | .markdown-body h4, 263 | .markdown-body h5, 264 | .markdown-body h6 { 265 | margin-top: 15px; 266 | margin-bottom: 15px; 267 | line-height: 1.1; 268 | } 269 | 270 | .markdown-body h1 { 271 | font-size: 30px; 272 | } 273 | 274 | .markdown-body h2 { 275 | font-size: 21px; 276 | } 277 | 278 | .markdown-body h3 { 279 | font-size: 16px; 280 | } 281 | 282 | .markdown-body h4 { 283 | font-size: 14px; 284 | } 285 | 286 | .markdown-body h5 { 287 | font-size: 12px; 288 | } 289 | 290 | .markdown-body h6 { 291 | font-size: 11px; 292 | } 293 | 294 | .markdown-body blockquote { 295 | margin: 0; 296 | } 297 | 298 | .markdown-body ul, 299 | .markdown-body ol { 300 | padding: 0; 301 | margin-top: 0; 302 | margin-bottom: 0; 303 | } 304 | 305 | .markdown-body ol ol, 306 | .markdown-body ul ol { 307 | list-style-type: lower-roman; 308 | } 309 | 310 | .markdown-body ul ul ol, 311 | .markdown-body ul ol ol, 312 | .markdown-body ol ul ol, 313 | .markdown-body ol ol ol { 314 | list-style-type: lower-alpha; 315 | } 316 | 317 | .markdown-body dd { 318 | margin-left: 0; 319 | } 320 | 321 | .markdown-body code { 322 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 323 | } 324 | 325 | .markdown-body pre { 326 | margin-top: 0; 327 | margin-bottom: 0; 328 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 329 | } 330 | 331 | .markdown-body .octicon { 332 | font: normal normal 16px octicons-anchor; 333 | line-height: 1; 334 | display: inline-block; 335 | text-decoration: none; 336 | -webkit-font-smoothing: antialiased; 337 | -moz-osx-font-smoothing: grayscale; 338 | -webkit-user-select: none; 339 | -moz-user-select: none; 340 | -ms-user-select: none; 341 | user-select: none; 342 | } 343 | 344 | .markdown-body .octicon-link:before { 345 | content: '\f05c'; 346 | } 347 | 348 | .markdown-body>*:first-child { 349 | margin-top: 0 !important; 350 | } 351 | 352 | .markdown-body>*:last-child { 353 | margin-bottom: 0 !important; 354 | } 355 | 356 | .markdown-body .anchor { 357 | position: absolute; 358 | top: 0; 359 | bottom: 0; 360 | left: 0; 361 | display: block; 362 | padding-right: 6px; 363 | padding-left: 30px; 364 | margin-left: -30px; 365 | } 366 | 367 | .markdown-body .anchor:focus { 368 | outline: none; 369 | } 370 | 371 | .markdown-body h1, 372 | .markdown-body h2, 373 | .markdown-body h3, 374 | .markdown-body h4, 375 | .markdown-body h5, 376 | .markdown-body h6 { 377 | position: relative; 378 | margin-top: 1em; 379 | margin-bottom: 16px; 380 | font-weight: bold; 381 | line-height: 1.4; 382 | } 383 | 384 | .markdown-body h1 .octicon-link, 385 | .markdown-body h2 .octicon-link, 386 | .markdown-body h3 .octicon-link, 387 | .markdown-body h4 .octicon-link, 388 | .markdown-body h5 .octicon-link, 389 | .markdown-body h6 .octicon-link { 390 | display: none; 391 | color: #000; 392 | vertical-align: middle; 393 | } 394 | 395 | .markdown-body h1:hover .anchor, 396 | .markdown-body h2:hover .anchor, 397 | .markdown-body h3:hover .anchor, 398 | .markdown-body h4:hover .anchor, 399 | .markdown-body h5:hover .anchor, 400 | .markdown-body h6:hover .anchor { 401 | padding-left: 8px; 402 | margin-left: -30px; 403 | line-height: 1; 404 | text-decoration: none; 405 | } 406 | 407 | .markdown-body h1:hover .anchor .octicon-link, 408 | .markdown-body h2:hover .anchor .octicon-link, 409 | .markdown-body h3:hover .anchor .octicon-link, 410 | .markdown-body h4:hover .anchor .octicon-link, 411 | .markdown-body h5:hover .anchor .octicon-link, 412 | .markdown-body h6:hover .anchor .octicon-link { 413 | display: inline-block; 414 | } 415 | 416 | .markdown-body h1 { 417 | padding-bottom: 0.3em; 418 | font-size: 2.25em; 419 | line-height: 1.2; 420 | border-bottom: 1px solid #eee; 421 | } 422 | 423 | .markdown-body h2 { 424 | padding-bottom: 0.3em; 425 | font-size: 1.75em; 426 | line-height: 1.225; 427 | border-bottom: 1px solid #eee; 428 | } 429 | 430 | .markdown-body h3 { 431 | font-size: 1.5em; 432 | line-height: 1.43; 433 | } 434 | 435 | .markdown-body h4 { 436 | font-size: 1.25em; 437 | } 438 | 439 | .markdown-body h5 { 440 | font-size: 1em; 441 | } 442 | 443 | .markdown-body h6 { 444 | font-size: 1em; 445 | color: #777; 446 | } 447 | 448 | .markdown-body p, 449 | .markdown-body blockquote, 450 | .markdown-body ul, 451 | .markdown-body ol, 452 | .markdown-body dl, 453 | .markdown-body table, 454 | .markdown-body pre { 455 | margin-top: 0; 456 | margin-bottom: 16px; 457 | } 458 | 459 | .markdown-body hr { 460 | height: 4px; 461 | padding: 0; 462 | margin: 16px 0; 463 | background-color: #e7e7e7; 464 | border: 0 none; 465 | } 466 | 467 | .markdown-body ul, 468 | .markdown-body ol { 469 | padding-left: 2em; 470 | } 471 | 472 | .markdown-body ul ul, 473 | .markdown-body ul ol, 474 | .markdown-body ol ol, 475 | .markdown-body ol ul { 476 | margin-top: 0; 477 | margin-bottom: 0; 478 | } 479 | 480 | .markdown-body li>p { 481 | margin-top: 16px; 482 | } 483 | 484 | .markdown-body dl { 485 | padding: 0; 486 | } 487 | 488 | .markdown-body dl dt { 489 | padding: 0; 490 | margin-top: 16px; 491 | font-size: 1em; 492 | font-style: italic; 493 | font-weight: bold; 494 | } 495 | 496 | .markdown-body dl dd { 497 | padding: 0 16px; 498 | margin-bottom: 16px; 499 | } 500 | 501 | .markdown-body blockquote { 502 | padding: 0 15px; 503 | color: #777; 504 | border-left: 4px solid #ddd; 505 | } 506 | 507 | .markdown-body blockquote>:first-child { 508 | margin-top: 0; 509 | } 510 | 511 | .markdown-body blockquote>:last-child { 512 | margin-bottom: 0; 513 | } 514 | 515 | .markdown-body table { 516 | display: block; 517 | width: 100%; 518 | overflow: auto; 519 | word-break: normal; 520 | word-break: keep-all; 521 | } 522 | 523 | .markdown-body table th { 524 | font-weight: bold; 525 | } 526 | 527 | .markdown-body table th, 528 | .markdown-body table td { 529 | padding: 6px 13px; 530 | border: 1px solid #ddd; 531 | } 532 | 533 | .markdown-body table tr { 534 | background-color: #fff; 535 | border-top: 1px solid #ccc; 536 | } 537 | 538 | .markdown-body table tr:nth-child(2n) { 539 | background-color: #f8f8f8; 540 | } 541 | 542 | .markdown-body img { 543 | max-width: 100%; 544 | -moz-box-sizing: border-box; 545 | box-sizing: border-box; 546 | } 547 | 548 | .markdown-body code { 549 | padding: 0; 550 | padding-top: 0.2em; 551 | padding-bottom: 0.2em; 552 | margin: 0; 553 | font-size: 85%; 554 | background-color: rgba(0,0,0,0.04); 555 | border-radius: 3px; 556 | } 557 | 558 | .markdown-body code:before, 559 | .markdown-body code:after { 560 | letter-spacing: -0.2em; 561 | content: "\00a0"; 562 | } 563 | 564 | .markdown-body pre>code { 565 | padding: 0; 566 | margin: 0; 567 | font-size: 100%; 568 | word-break: normal; 569 | white-space: pre; 570 | background: transparent; 571 | border: 0; 572 | } 573 | 574 | .markdown-body .highlight { 575 | margin-bottom: 16px; 576 | } 577 | 578 | .markdown-body .highlight pre, 579 | .markdown-body pre { 580 | padding: 16px; 581 | overflow: auto; 582 | font-size: 85%; 583 | line-height: 1.45; 584 | background-color: #f7f7f7; 585 | border-radius: 3px; 586 | } 587 | 588 | .markdown-body .highlight pre { 589 | margin-bottom: 0; 590 | word-break: normal; 591 | } 592 | 593 | .markdown-body pre { 594 | word-wrap: normal; 595 | } 596 | 597 | .markdown-body pre code { 598 | display: inline; 599 | max-width: initial; 600 | padding: 0; 601 | margin: 0; 602 | overflow: initial; 603 | line-height: inherit; 604 | word-wrap: normal; 605 | background-color: transparent; 606 | border: 0; 607 | } 608 | 609 | .markdown-body pre code:before, 610 | .markdown-body pre code:after { 611 | content: normal; 612 | } 613 | 614 | .markdown-body .pl-c { 615 | color: #969896; 616 | } 617 | 618 | .markdown-body .pl-c1, 619 | .markdown-body .pl-mdh, 620 | .markdown-body .pl-mm, 621 | .markdown-body .pl-mp, 622 | .markdown-body .pl-mr, 623 | .markdown-body .pl-s1 .pl-v, 624 | .markdown-body .pl-s3, 625 | .markdown-body .pl-sc, 626 | .markdown-body .pl-sv { 627 | color: #0086b3; 628 | } 629 | 630 | .markdown-body .pl-e, 631 | .markdown-body .pl-en { 632 | color: #795da3; 633 | } 634 | 635 | .markdown-body .pl-s1 .pl-s2, 636 | .markdown-body .pl-smi, 637 | .markdown-body .pl-smp, 638 | .markdown-body .pl-stj, 639 | .markdown-body .pl-vo, 640 | .markdown-body .pl-vpf { 641 | color: #333; 642 | } 643 | 644 | .markdown-body .pl-ent { 645 | color: #63a35c; 646 | } 647 | 648 | .markdown-body .pl-k, 649 | .markdown-body .pl-s, 650 | .markdown-body .pl-st { 651 | color: #a71d5d; 652 | } 653 | 654 | .markdown-body .pl-pds, 655 | .markdown-body .pl-s1, 656 | .markdown-body .pl-s1 .pl-pse .pl-s2, 657 | .markdown-body .pl-sr, 658 | .markdown-body .pl-sr .pl-cce, 659 | .markdown-body .pl-sr .pl-sra, 660 | .markdown-body .pl-sr .pl-sre, 661 | .markdown-body .pl-src, 662 | .markdown-body .pl-v { 663 | color: #df5000; 664 | } 665 | 666 | .markdown-body .pl-id { 667 | color: #b52a1d; 668 | } 669 | 670 | .markdown-body .pl-ii { 671 | background-color: #b52a1d; 672 | color: #f8f8f8; 673 | } 674 | 675 | .markdown-body .pl-sr .pl-cce { 676 | color: #63a35c; 677 | font-weight: bold; 678 | } 679 | 680 | .markdown-body .pl-ml { 681 | color: #693a17; 682 | } 683 | 684 | .markdown-body .pl-mh, 685 | .markdown-body .pl-mh .pl-en, 686 | .markdown-body .pl-ms { 687 | color: #1d3e81; 688 | font-weight: bold; 689 | } 690 | 691 | .markdown-body .pl-mq { 692 | color: #008080; 693 | } 694 | 695 | .markdown-body .pl-mi { 696 | color: #333; 697 | font-style: italic; 698 | } 699 | 700 | .markdown-body .pl-mb { 701 | color: #333; 702 | font-weight: bold; 703 | } 704 | 705 | .markdown-body .pl-md, 706 | .markdown-body .pl-mdhf { 707 | background-color: #ffecec; 708 | color: #bd2c00; 709 | } 710 | 711 | .markdown-body .pl-mdht, 712 | .markdown-body .pl-mi1 { 713 | background-color: #eaffea; 714 | color: #55a532; 715 | } 716 | 717 | .markdown-body .pl-mdr { 718 | color: #795da3; 719 | font-weight: bold; 720 | } 721 | 722 | .markdown-body .pl-mo { 723 | color: #1d3e81; 724 | } 725 | 726 | .markdown-body kbd { 727 | background-color: #e7e7e7; 728 | background-image: -webkit-linear-gradient(#fefefe, #e7e7e7); 729 | background-image: linear-gradient(#fefefe, #e7e7e7); 730 | background-repeat: repeat-x; 731 | display: inline-block; 732 | padding: 3px 5px; 733 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 734 | line-height: 10px; 735 | color: #000; 736 | border: 1px solid #cfcfcf; 737 | border-radius: 2px; 738 | } 739 | 740 | .markdown-body .task-list-item { 741 | list-style-type: none; 742 | } 743 | 744 | .markdown-body .task-list-item+.task-list-item { 745 | margin-top: 3px; 746 | } 747 | 748 | .markdown-body .task-list-item input { 749 | float: left; 750 | margin: 0.3em 0 0.25em -1.6em; 751 | vertical-align: middle; 752 | } 753 | 754 | .markdown-body :checked+.radio-label { 755 | z-index: 1; 756 | position: relative; 757 | border-color: #4183c4; 758 | } 759 | -------------------------------------------------------------------------------- /web/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js 4 | * Copyright 2013 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.focus().trigger("shown")}):b.$element.focus().trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('
  • ',minLength:1},a.fn.typeahead.Constructor=b,a.fn.typeahead.noConflict=function(){return a.fn.typeahead=c,this},a(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;c.typeahead(c.data())})}(window.jQuery) --------------------------------------------------------------------------------