diff options
-rw-r--r-- | package.json | 8 | ||||
-rw-r--r-- | src/public/css/style.css | 36 | ||||
-rw-r--r-- | src/public/example.py | 42 | ||||
-rw-r--r-- | src/public/js/form.js | 45 | ||||
-rw-r--r-- | src/public/lib/prism.css | 3 | ||||
-rw-r--r-- | src/public/lib/prism.js | 4 | ||||
-rw-r--r-- | src/routes/api.ts | 112 | ||||
-rw-r--r-- | src/views/pages/about.ejs | 6 | ||||
-rw-r--r-- | src/views/pages/index.ejs | 35 | ||||
-rw-r--r-- | src/views/partials/nav.ejs | 2 | ||||
-rw-r--r-- | yarn.lock | 33 |
11 files changed, 224 insertions, 102 deletions
diff --git a/package.json b/package.json index 7116af3..57b1c43 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,16 @@ "express": "^4.17.2", "express-fileupload": "^1.3.1", "express-rate-limit": "^6.2.1", - "express-slow-down": "^1.4.0", - "helmet": "^5.0.2" + "helmet": "^5.0.2", + "shell-quote": "^1.7.3" }, "devDependencies": { "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.11.2", "@types/express": "^4.17.13", "@types/express-fileupload": "^1.2.2", - "@types/express-slow-down": "^1.3.2", "@types/node": "^17.0.17", + "@types/shell-quote": "^1.7.1", "@typescript-eslint/eslint-plugin": "^5.11.0", "@typescript-eslint/parser": "^5.11.0", "eslint": "^8.9.0", @@ -45,4 +45,4 @@ "repository": "https: //github.com/RosstheRoss/4951w-pendulum", "license": "MIT", "private": true -}
\ No newline at end of file +} diff --git a/src/public/css/style.css b/src/public/css/style.css index 0e7892d..d48e8b9 100644 --- a/src/public/css/style.css +++ b/src/public/css/style.css @@ -7,8 +7,6 @@ body { } .header { - - /* top: 30px; */ margin: 5px; font-family: "Times New Roman", Times, serif; } @@ -55,44 +53,21 @@ p { a { - color: white; + color: blue; text-decoration: none; } -/* Used for id=main */ +/* Used for the about page*/ -#main { +.about { color: white; - /* border-style: solid; */ - /* border: 1px solid maroon; */ text-align: center; margin: 5px; -} - -#about { font-size: large; } - -/* Used for id=title2 */ - -#title2 { - font-size: xx-large; - font-weight: bold; - margin-top: 20px; -} - - -/* Used for id=title3 */ - -#title3 { - font-size: xx-large; - font-weight: bold; - margin-top: 15px; -} - #Background { color: #ffbb00; } @@ -140,4 +115,9 @@ a { color: #000000; /* Remove underlines from links */ text-decoration: none; +} + +.error { + color: red; + font-size: larger; }
\ No newline at end of file diff --git a/src/public/example.py b/src/public/example.py new file mode 100644 index 0000000..27f5835 --- /dev/null +++ b/src/public/example.py @@ -0,0 +1,42 @@ +import sys +sys.path.insert(0, '/home/pi/pendulum/System') +from System.system import System +import time +from sys import exit +sys.path.insert(0, '/home/pi/pendulum/System') +from encoder import Encoder +import RPi.GPIO as GPIO + +clk_pin = 3 +cs_pin = 23 +data_pin = 2 + +e = Encoder(clk_pin, cs_pin, data_pin) +e.set_zero() +sys = System(angular_units = 'Radians') + +for x in range(4,20): + linear = 0 + + print("beginning of test with speed " + str(x)) + + while linear > -7: + sys.adjust(-5) + angle, linear = sys.measure() + print("Angle: " + str(angle) + ", Linear: " + str(linear)) + time.sleep(0.1) + sys.adjust(0) + time.sleep(3) + sys.add_log("this is a test with speed " + str(x)) + + while linear < 7: + sys.adjust(x) + angle, linear = sys.measure() + print("Angle: " + str(angle) + ", Linear: " + str(linear)) + sys.add_results(e.read_position('Degrees'), linear, x) + time.sleep(0.1) + sys.adjust(0) + print("end of test with speed " + str(x)) + time.sleep(3) +deinitialize() +exit() diff --git a/src/public/js/form.js b/src/public/js/form.js index 7085422..f58a27f 100644 --- a/src/public/js/form.js +++ b/src/public/js/form.js @@ -1,5 +1,14 @@ +window.onload = function () { + document.getElementById('nojs').hidden = true; + document.getElementById('block').hidden = false; +}; + // File submit AJAX request document.getElementById('upload').onsubmit = function () { + // Reset error message + document.getElementById('upload-err').innerText = ''; + document.getElementById('actuate-err').innerText = ''; + // Make AJAX request let xhr = new XMLHttpRequest(); xhr.open('POST', '/api/v1/upload'); let formData = new FormData(this); @@ -8,11 +17,41 @@ document.getElementById('upload').onsubmit = function () { if (xhr.readyState === 4) { let response = JSON.parse(xhr.responseText); if (xhr.status === 200) { - console.log(response); + actuate(response); } else { - console.log(response.error); + // Display upload error message to the user + document.getElementById('upload-err').innerText = response.error; + // DEBUG: Print full error if unknown error occurs + if (xhr.status === 500) + console.error(response.error_msg); } } }; return false; -};
\ No newline at end of file +}; + +function actuate(file) { + let xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/v1/actuate'); + let data = { + name: file.name, + path: file.path, + }; + xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); + xhr.setRequestHeader('X-CSRF-TOKEN', file.csrf); + xhr.send(JSON.stringify(data)); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + let response = JSON.parse(xhr.responseText); + if (xhr.status === 200) { + console.log(response); + } else { + // Display upload error message to the user + document.getElementById('actuate-err').innerText = response.error; + // DEBUG: Print full error if unknown error occurs + if (xhr.status === 500) + console.error(response.error_msg); + } + } + }; +}
\ No newline at end of file diff --git a/src/public/lib/prism.css b/src/public/lib/prism.css new file mode 100644 index 0000000..4fb0a7c --- /dev/null +++ b/src/public/lib/prism.css @@ -0,0 +1,3 @@ +/* PrismJS 1.26.0 +https://prismjs.com/download.html#themes=prism-okaidia&languages=python */ +code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} diff --git a/src/public/lib/prism.js b/src/public/lib/prism.js new file mode 100644 index 0000000..958c4c6 --- /dev/null +++ b/src/public/lib/prism.js @@ -0,0 +1,4 @@ +/* PrismJS 1.26.0 +https://prismjs.com/download.html#themes=prism-okaidia&languages=python */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,e={},M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++n}),e.__id},clone:function t(e,r){var a,n;switch(r=r||{},M.util.type(e)){case"Object":if(n=M.util.objId(e),r[n])return r[n];for(var i in a={},r[n]=a,e)e.hasOwnProperty(i)&&(a[i]=t(e[i],r));return a;case"Array":return n=M.util.objId(e),r[n]?r[n]:(a=[],r[n]=a,e.forEach(function(e,n){a[n]=t(e,r)}),a);default:return e}},getLanguage:function(e){for(;e;){var n=t.exec(e.className);if(n)return n[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,n){e.className=e.className.replace(RegExp(t,"gi"),""),e.classList.add("language-"+n)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(e){var n=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(e.stack)||[])[1];if(n){var t=document.getElementsByTagName("script");for(var r in t)if(t[r].src==n)return t[r]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:e,plaintext:e,text:e,txt:e,extend:function(e,n){var t=M.util.clone(M.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(t,e,n,r){var a=(r=r||M.languages)[t],i={};for(var l in a)if(a.hasOwnProperty(l)){if(l==e)for(var o in n)n.hasOwnProperty(o)&&(i[o]=n[o]);n.hasOwnProperty(l)||(i[l]=a[l])}var s=r[t];return r[t]=i,M.languages.DFS(M.languages,function(e,n){n===s&&e!=t&&(this[e]=i)}),i},DFS:function e(n,t,r,a){a=a||{};var i=M.util.objId;for(var l in n)if(n.hasOwnProperty(l)){t.call(n,l,n[l],r||l);var o=n[l],s=M.util.type(o);"Object"!==s||a[i(o)]?"Array"!==s||a[i(o)]||(a[i(o)]=!0,e(o,t,l,a)):(a[i(o)]=!0,e(o,t,null,a))}}},plugins:{},highlightAll:function(e,n){M.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};M.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),M.hooks.run("before-all-elements-highlight",r);for(var a,i=0;a=r.elements[i++];)M.highlightElement(a,!0===n,r.callback)},highlightElement:function(e,n,t){var r=M.util.getLanguage(e),a=M.languages[r];M.util.setLanguage(e,r);var i=e.parentElement;i&&"pre"===i.nodeName.toLowerCase()&&M.util.setLanguage(i,r);var l={element:e,language:r,grammar:a,code:e.textContent};function o(e){l.highlightedCode=e,M.hooks.run("before-insert",l),l.element.innerHTML=l.highlightedCode,M.hooks.run("after-highlight",l),M.hooks.run("complete",l),t&&t.call(l.element)}if(M.hooks.run("before-sanity-check",l),(i=l.element.parentElement)&&"pre"===i.nodeName.toLowerCase()&&!i.hasAttribute("tabindex")&&i.setAttribute("tabindex","0"),!l.code)return M.hooks.run("complete",l),void(t&&t.call(l.element));if(M.hooks.run("before-highlight",l),l.grammar)if(n&&u.Worker){var s=new Worker(M.filename);s.onmessage=function(e){o(e.data)},s.postMessage(JSON.stringify({language:l.language,code:l.code,immediateClose:!0}))}else o(M.highlight(l.code,l.grammar,l.language));else o(M.util.encode(l.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(M.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=M.tokenize(r.code,r.grammar),M.hooks.run("after-tokenize",r),W.stringify(M.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new i;return I(a,a.head,e),function e(n,t,r,a,i,l){for(var o in r)if(r.hasOwnProperty(o)&&r[o]){var s=r[o];s=Array.isArray(s)?s:[s];for(var u=0;u<s.length;++u){if(l&&l.cause==o+","+u)return;var c=s[u],g=c.inside,f=!!c.lookbehind,h=!!c.greedy,d=c.alias;if(h&&!c.pattern.global){var v=c.pattern.toString().match(/[imsuy]*$/)[0];c.pattern=RegExp(c.pattern.source,v+"g")}for(var p=c.pattern||c,m=a.next,y=i;m!==t.tail&&!(l&&y>=l.reach);y+=m.value.length,m=m.next){var k=m.value;if(t.length>n.length)return;if(!(k instanceof W)){var x,b=1;if(h){if(!(x=z(p,y,n,f))||x.index>=n.length)break;var w=x.index,A=x.index+x[0].length,E=y;for(E+=m.value.length;E<=w;)m=m.next,E+=m.value.length;if(E-=m.value.length,y=E,m.value instanceof W)continue;for(var P=m;P!==t.tail&&(E<A||"string"==typeof P.value);P=P.next)b++,E+=P.value.length;b--,k=n.slice(y,E),x.index-=y}else if(!(x=z(p,0,k,f)))continue;var w=x.index,L=x[0],S=k.slice(0,w),O=k.slice(w+L.length),j=y+k.length;l&&j>l.reach&&(l.reach=j);var C=m.prev;S&&(C=I(t,C,S),y+=S.length),T(t,C,b);var N=new W(o,g?M.tokenize(L,g):L,d,L);if(m=I(t,C,N),O&&I(t,m,O),1<b){var _={cause:o+","+u,reach:j};e(n,t,r,m.prev,y,_),l&&_.reach>l.reach&&(l.reach=_.reach)}}}}}}(e,a,n,a.head,0),function(e){var n=[],t=e.head.next;for(;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=M.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=M.hooks.all[e];if(t&&t.length)for(var r,a=0;r=t[a++];)r(n)}},Token:W};function W(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function z(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function i(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function I(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function T(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;(n.next=r).prev=n,e.length-=a}if(u.Prism=M,W.stringify=function n(e,t){if("string"==typeof e)return e;if(Array.isArray(e)){var r="";return e.forEach(function(e){r+=n(e,t)}),r}var a={type:e.type,content:n(e.content,t),tag:"span",classes:["token",e.type],attributes:{},language:t},i=e.alias;i&&(Array.isArray(i)?Array.prototype.push.apply(a.classes,i):a.classes.push(i)),M.hooks.run("wrap",a);var l="";for(var o in a.attributes)l+=" "+o+'="'+(a.attributes[o]||"").replace(/"/g,""")+'"';return"<"+a.tag+' class="'+a.classes.join(" ")+'"'+l+">"+a.content+"</"+a.tag+">"},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var r=M.util.currentScript();function a(){M.manual||M.highlightAll()}if(r&&(M.filename=r.src,r.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var l=document.readyState;"loading"===l||"interactive"===l&&r&&r.defer?document.addEventListener("DOMContentLoaded",a):window.requestAnimationFrame?window.requestAnimationFrame(a):window.setTimeout(a,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; diff --git a/src/routes/api.ts b/src/routes/api.ts index e360709..1e9cd49 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -2,38 +2,104 @@ import express, { Request, Response } from 'express'; import csurf from 'csurf'; import cookieParser from 'cookie-parser'; import fileUpload, { UploadedFile } from 'express-fileupload'; -import slowDown from 'express-slow-down'; +import rateLimit from 'express-rate-limit'; +import { access, stat } from 'fs/promises'; +import { quote } from 'shell-quote'; +import { exec } from 'child_process'; const api = express.Router(); // For file uploads -api.use(fileUpload()); - -// Slow down everything to prevent DoS attacks -const speedLimiter = slowDown({ - windowMs: 5 * 60 * 1000, // 5 minutes - delayAfter: 50, // allow 50 requests per 5 minutes, then... - delayMs: 500 // begin adding 500ms of delay per request above 100: - // request # 101 is delayed by 500ms - // request # 102 is delayed by 1000ms - // request # 103 is delayed by 1500ms - // etc. +api.use(fileUpload({ + preserveExtension: true, // Preserve file extension on upload + safeFileNames: true, // Only allow alphanumeric characters in file names + limits: { fileSize: 1 * 1024 * 1024 }, // Limit file size to 1MB + useTempFiles: true, // Store files in temp instead of memory + tempFileDir: '/tmp/', // Store files in /tmp/ + debug: false, // Log debug information +})); + +// Slow down frequent requests to prevent DoS attacks +const rateLimiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 10, // Limit each IP to 10 requests per `window` (here, per 1 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); -api.use(speedLimiter); +api.use(rateLimiter); // CSRF protection api.use(cookieParser()); const csrf = csurf({ cookie: true }); -api.post('/upload', csrf, (req: Request, res: Response) => { - // Check if there is a file - if (!req.files || Object.keys(req.files).length === 0) - return res.status(400).json({ error: 'No file uploaded' }); - const file: UploadedFile = req.files.file as UploadedFile; // Kludge to prevent a compiler error - // Check if the file is a python file - if (file.mimetype !== 'text/x-python') - return res.status(400).json({ error: 'Not a Python file' }); - res.status(200).json({ file: file.name }); -}); +api.use(express.json()); +api.route('/upload') + .post(csrf, (req: Request, res: Response) => { + try { + // Check if anything was actually uploaded + if (!req.files || Object.keys(req.files).length === 0) + return res.status(400).json({ error: 'No file uploaded.' }); + + const file: UploadedFile = req.files.file as UploadedFile; // Kludge to prevent a compiler error, only one file gets uploaded so this should be fine + + // Check if the file is too large (see fileUpload.limits for the limit) + if (file.truncated) + return res.status(413).json({ error: 'File uploaded was too large.' }); + + // Check if the file is a python file + if (file.mimetype !== 'text/x-python') + return res.status(415).json({ error: 'File uploaded was not a Python file.' }); + + res.status(200).json({ file: file.name, path: file.tempFilePath, csrf: req.csrfToken() }); + } catch (err) { + // Generic error handler + res.status(500).json({ error: 'An unknown error occurred while uploading the file.', error_msg: err }); + } + }) + // Fallback + .all(csrf, (req: Request, res: Response) => { + res.set('Allow', 'POST'); + res.status(405).json({ error: 'Method not allowed.' }); + }); + +/* + This route is probably a complete security hole. It allows anyone with a login cookie access to run arbitrary Python code on the server. + + Minimizing PE vectors like running this as a low privilege user is a must. +*/ +api.route('/actuate') + .post(csrf, async (req: Request, res: Response) => { + // Make sure the file being requested to run exists + try { + await access(req.body.path); + } catch (err) { + return res.status(403).json({ error: 'File is not accessible.' }); + } + + + const stats = await stat(req.body.path); + // Make sure the file being requested to run is a regular file + if (!stats.isFile()) + return res.status(403).json({ error: 'File is not a regular file.' }); + // Make sure the file being requested to run is not a directory + if (stats.isDirectory()) + return res.status(403).json({ error: 'File is a directory.' }); + + const escaped = quote([ 'python', req.body.path]); + // Run the code + exec(escaped, (err, stdout, stderr) => { + if (err) + return res.status(500).json({ error: 'An unknown error occurred while executing the file.', error_msg: stderr }); + + // Return the output + res.status(200).json({ output: stdout }); + }); + }) + // Fallback + .all(csrf, (req: Request, res: Response) => { + res.set('Allow', 'POST'); + res.status(405).json({ error: 'Method not allowed.' }); + }); + export default api;
\ No newline at end of file diff --git a/src/views/pages/about.ejs b/src/views/pages/about.ejs index 2feede5..26e5687 100644 --- a/src/views/pages/about.ejs +++ b/src/views/pages/about.ejs @@ -15,9 +15,8 @@ <h1>Remotely Accessible Inverted Pendulum</h1> </div> - <div id="main"> - <div id="about"> - <h3 id="subtitle"> + <div class="about"> + <h3> Created by <br /> Abrar Nair Islam, Brendan Lake, Cory Ohnsted, Matt Strapp, Kiflu Woldegiorgis (2022) <br /> Sam Hansen, Rezkath Awal, Donovan Peterson, Joseph Jewett (2021) <br /> @@ -35,7 +34,6 @@ <p> A vertical pendulum that is balanced via the base moving. <br /> Controlled movements are determined by measuring the position and speed of the pendulum and base. </p> - </div> </div> </body> diff --git a/src/views/pages/index.ejs b/src/views/pages/index.ejs index 8b5f044..81624f6 100644 --- a/src/views/pages/index.ejs +++ b/src/views/pages/index.ejs @@ -2,25 +2,36 @@ <html lang="en"> <head> - <!-- HTML headers information --> + <!-- HTML headers --> <%- include('../partials/head.ejs') %> + <title>Upload your code here!</title> <script type="module" src="public/js/form.js" defer></script> </head> <body> <!-- Get the navbar --> <%- include('../partials/nav.ejs') %> - <br /> - <h2>Please upload a Python file (.py file extension) to run on the Inverted Pendulum.</h2> - <br /> <br /> - <form id="upload" enctype="multipart/form-data"> - <input type="hidden" name="_csrf" value="<%= csrfToken %>"> - <input type="file" name="file" accept=".py" /> - <br /> <br /> - <br /> <br /> - <label id="Start">Start pendulum: </label> - <input type="submit" id="actuate_but" value="Actuate!" /> - </form> + <div id="nojs"> + <h1>Please enable JavaScript to use the inverted pendulum.</h1> + </div> + <div id="block" hidden="true"> + <br /> + <h1>Please upload a Python file (.py file extension) to run on the Inverted Pendulum.</h1> + <h3>A heavily documented example can be <a href="public/example.py">found here</a>.</h3> + <h2> + <span class="error" id="upload-err"></span> + <span class="error" id="actuate-err"></span> + </h2> + + <form id="upload" enctype="multipart/form-data"> + <input type="hidden" name="_csrf" value="<%= csrfToken %>"> + <input type="file" name="file" accept=".py" /> + <br /> <br /> + <label id="Start">Start pendulum: </label> + <input type="submit" id="actuate_but" value="Actuate!" /> + </form> + </span> + </body> </html>
\ No newline at end of file diff --git a/src/views/partials/nav.ejs b/src/views/partials/nav.ejs index 1ee6c00..b36e72e 100644 --- a/src/views/partials/nav.ejs +++ b/src/views/partials/nav.ejs @@ -2,6 +2,6 @@ <ul class="navbar"> <li><a href="/">Home</a></li> <li><a href="/about">About the Inverted Pendulum</a></li> - <li><a href="https://github.com/RosstheRoss/4951w-pendulum">GitHub Repo</a></li> + <li><a href="https://github.com/RosstheRoss/4951w-pendulum" style="right: 0px">GitHub Repo</a></li> </ul> </nav>
\ No newline at end of file @@ -117,13 +117,6 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express-slow-down@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/express-slow-down/-/express-slow-down-1.3.2.tgz#99a5513b3f465efb1cb4f2db6375d36d7ab72fa4" - integrity sha512-Jw/orNMX+htFMYMugjhLotS5eRkdId4V5/89vqNXoUfxaHBU8VVIshItDh5YuWeyGeKuPli2fh94ORjYz/dkxA== - dependencies: - "@types/express" "*" - "@types/express@*", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" @@ -172,6 +165,11 @@ "@types/mime" "^1" "@types/node" "*" +"@types/shell-quote@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.7.1.tgz#2d059091214a02c29f003f591032172b2aff77e8" + integrity sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw== + "@typescript-eslint/eslint-plugin@^5.11.0": version "5.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.11.0.tgz#3b866371d8d75c70f9b81535e7f7d3aa26527c7a" @@ -506,11 +504,6 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -674,13 +667,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -1008,13 +994,6 @@ express-rate-limit@^6.2.1: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.2.1.tgz#4a7619634fb24417ae723ad2ac3707b38e2e1c64" integrity sha512-22ovnpEiKR5iAMXDOQ7A6aOvb078JLvoHGlyrrWBl3PeJ34coyakaviPelj4Nc8d+yDoVIWYmaUNP5aYT4ICDQ== -express-slow-down@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/express-slow-down/-/express-slow-down-1.4.0.tgz#89e0aef6c3bb3602b70f06e0824889bd2362cc21" - integrity sha512-Tw5aa0plPj2STiuc2SyMw2VSjMvBgLGQHHoPhkIL4iPQcFZDueWBaiLxFZ3SrwrJhiu3b3sHNcsP6lXeWnbwAw== - dependencies: - defaults "^1.0.3" - express@^4.17.2: version "4.17.2" resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" @@ -2293,7 +2272,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1: +shell-quote@^1.6.1, shell-quote@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== |