From 150c06a1171be1ad965afc37beeec9863818b508 Mon Sep 17 00:00:00 2001
From: Matt Strapp
Date: Mon, 14 Feb 2022 15:39:22 -0600
Subject: maybe achieve feature parity
Signed-off-by: Matt Strapp
---
package.json | 8 ++--
src/public/css/style.css | 36 ++++-----------
src/public/example.py | 42 +++++++++++++++++
src/public/js/form.js | 45 ++++++++++++++++--
src/public/lib/prism.css | 3 ++
src/public/lib/prism.js | 4 ++
src/routes/api.ts | 112 +++++++++++++++++++++++++++++++++++----------
src/views/pages/about.ejs | 6 +--
src/views/pages/index.ejs | 35 +++++++++-----
src/views/partials/nav.ejs | 2 +-
yarn.lock | 33 +++----------
11 files changed, 224 insertions(+), 102 deletions(-)
create mode 100644 src/public/example.py
create mode 100644 src/public/lib/prism.css
create mode 100644 src/public/lib/prism.js
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(/=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&&(El.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),1l.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"+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 @@
Remotely Accessible Inverted Pendulum
-
-
-
+
+
Created by
Abrar Nair Islam, Brendan Lake, Cory Ohnsted, Matt Strapp, Kiflu Woldegiorgis (2022)
Sam Hansen, Rezkath Awal, Donovan Peterson, Joseph Jewett (2021)
@@ -35,7 +34,6 @@
A vertical pendulum that is balanced via the base moving.
Controlled movements are determined by measuring the position and speed of the pendulum and base.
-
<%- include('../partials/nav.ejs') %>
-
- Please upload a Python file (.py file extension) to run on the Inverted Pendulum.
-
-
+
+
Please enable JavaScript to use the inverted pendulum.
+
+
+
+
Please upload a Python file (.py file extension) to run on the Inverted Pendulum.
+
A heavily documented example can be found here.
+
+
+
+
+
+
+
+
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 @@