aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Strapp <matt@mattstrapp.net>2022-02-11 00:11:52 -0600
committerMatt Strapp <matt@mattstrapp.net>2022-02-11 00:11:52 -0600
commit0fbc317e926b5d80363979ee51a4e3c930014efd (patch)
treebfd8ce0fc829d80a46a2182b759057c25e4894fa
parentGet rid of express-session and use a cookie instead (diff)
downloadee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar.gz
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar.bz2
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar.lz
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar.xz
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.tar.zst
ee4511w-web-0fbc317e926b5d80363979ee51a4e3c930014efd.zip
Do a bunch of random things (still no feature parity)
sadge Signed-off-by: Matt Strapp <matt@mattstrapp.net>
-rw-r--r--.eslintignore3
-rw-r--r--.gitignore2
-rw-r--r--.vscode/settings.json5
-rw-r--r--package.json4
-rw-r--r--src/index.ts56
-rw-r--r--src/public/css/style.css101
-rw-r--r--src/public/js/form.js17
-rw-r--r--src/routes/api.ts40
-rw-r--r--src/routes/api/actuate.ts0
-rw-r--r--src/routes/api/login.ts0
-rw-r--r--src/views/pages/about.ejs6
-rw-r--r--src/views/pages/index.ejs76
-rw-r--r--src/views/pages/login.ejs11
-rw-r--r--src/views/partials/head.ejs10
-rw-r--r--src/views/partials/nav.ejs14
-rw-r--r--yarn.lock41
16 files changed, 184 insertions, 202 deletions
diff --git a/.eslintignore b/.eslintignore
index 7773828..8a4274e 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
-dist/ \ No newline at end of file
+dist/
+src/public/lib/
diff --git a/.gitignore b/.gitignore
index 6a7d6d8..26ebeff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -122,6 +122,8 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
+.dccache
+
# yarn v2
.yarn/cache
.yarn/unplugged
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a3606ef..3f638b5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,8 @@
"**/**.js": {
"when": "$(basename).tsx"
}
- }
+ },
+ "cSpell.words": [
+ "fileupload"
+ ]
} \ No newline at end of file
diff --git a/package.json b/package.json
index 45ff92a..e0a656c 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"csurf": "^1.11.0",
"ejs": "^3.1.6",
"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"
@@ -12,6 +13,7 @@
"@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",
"@typescript-eslint/eslint-plugin": "^5.11.0",
@@ -43,4 +45,4 @@
"repository": "https: //github.com/RosstheRoss/4951w-pendulum",
"license": "MIT",
"private": true
-} \ No newline at end of file
+}
diff --git a/src/index.ts b/src/index.ts
index 9e7d082..bd2c7d5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,56 +1,54 @@
import express, { Request, Response } from 'express';
-import rateLimit from 'express-rate-limit';
-import slowDown from 'express-slow-down';
+
import path from 'path';
import { env } from 'process';
import helmet from 'helmet';
import csurf from 'csurf';
import cookieParser from 'cookie-parser';
+import rateLimit from 'express-rate-limit';
+import api from './routes/api';
const app = express();
-// Middleware
-const port: string = env.PORT || '2000';
+/* MIDDLEWARE */
-app.use(cookieParser());
-const csrf = csurf({ cookie: true });
+// Rate limiting
const rateLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
- max: 30, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
+ max: 40, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
-const speedLimiter = slowDown({
- windowMs: 15 * 60 * 1000, // 15 minutes
- delayAfter: 100, // allow 100 requests per 15 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.
-});
-// This will be run behind an nginx proxy
-app.enable('trust proxy');
-// apply to all requests
-app.use(speedLimiter);
-app.use('/api', rateLimiter);
+app.use(rateLimiter);
+
+// CSRF protection
+app.use(cookieParser());
+const csrf = csurf({ cookie: true });
+
+
+// Hide the software being used (helps security)
app.use(helmet());
-// Add ejs as view engine
-app.set('view engine', 'ejs');
-app.set('views', path.join(__dirname, 'views/pages'));
-app.use('/public', express.static(path.join(__dirname, 'public')));
+// The API
+app.use('/api', api);
+
+/* RENDERING */
+
+app.set('view engine', 'ejs'); // Add ejs as view engine
+app.set('views', path.join(__dirname, 'views/pages')); // Set views directory (where the ejs lies)
+app.use('/public', express.static(path.join(__dirname, 'public'))); // Set static directory (where the static CSS/JS/images lie)
+
+/* ROUTING */
app.get('/', csrf, (req: Request, res: Response) => {
- res.render('index', {
- errors: [],
- });
+ res.render('index', { csrfToken: req.csrfToken() });
});
-
app.get('/about', csrf, (req: Request, res: Response) => {
res.render('about');
});
+// Start the server
+const port = env.PORT || 2000;
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
}); \ No newline at end of file
diff --git a/src/public/css/style.css b/src/public/css/style.css
index cdcad8c..0e7892d 100644
--- a/src/public/css/style.css
+++ b/src/public/css/style.css
@@ -1,12 +1,13 @@
body {
background-color: black;
-}
-
-.header {
color: white;
text-align: center;
vertical-align: middle;
position: relative;
+}
+
+.header {
+
/* top: 30px; */
margin: 5px;
font-family: "Times New Roman", Times, serif;
@@ -24,17 +25,12 @@ p {
}
-/* Customize the positioning of the logo */
-
-
-
-
/*-------------------------Buttons-----------------------------------------------------*/
/* Used for id=button1 */
-#button1 {
+#actuate_but {
background-color: #0a3f73;
border: solid;
border-color: #d5d6d2;
@@ -51,68 +47,13 @@ p {
/* Used for id=button1 - What the button looks like when a user puts one's cursor on it */
-#button1:hover {
+#actuate_but:hover {
background-color: white;
color: maroon;
cursor: pointer;
}
-/* Used for id=button2 */
-
-#button2 {
- /* #ebcba9 is similar to a tan color */
- background-color: #ebcba9;
- border: solid;
- border-color: #d5d6d2;
- /* #693100 is similar to a brown color */
- color: #693100;
- padding: 10px 20px;
- text-align: center;
- text-decoration: none;
- font-family: "Constantia", sans-serif;
- border-radius: 10px;
- display: inline-block;
- font-size: 20px;
-}
-
-
-/* Used for id=button2 - What the button looks like when a user puts one's cursor on it */
-
-#button2:hover {
- background-color: white;
- color: #0a3f73;
- cursor: pointer;
-}
-
-
-/* Used for id=button2 */
-
-#button3 {
- /* #cf7d4e is similar to a light brown color */
- background-color: #cf7d4e;
- border: solid;
- border-color: #d5d6d2;
- /* #094f02 is similar to a dark green color */
- color: #094f02;
- padding: 10px 20px;
- text-align: center;
- text-decoration: none;
- font-family: "Constantia", sans-serif;
- border-radius: 10px;
- display: inline-block;
- font-size: 20px;
-}
-
-
-/* Used for id=button2 - What the button looks like when a user puts one's cursor on it */
-
-#button3:hover {
- background-color: white;
- color: maroon;
- cursor: pointer;
-}
-
a {
color: white;
text-decoration: none;
@@ -134,12 +75,6 @@ a {
}
-/* Used for id=title1 */
-
-#title1 {
- font-size: xx-large;
-}
-
/* Used for id=title2 */
@@ -158,14 +93,6 @@ a {
margin-top: 15px;
}
-#subtitle {
- font-size: large;
-}
-
-#subtitle1 {
- font-size: 23px;
-}
-
#Background {
color: #ffbb00;
}
@@ -175,14 +102,6 @@ a {
color: #4294cf;
}
-#choose_file {
- border: none;
- text-decoration: none;
- display: inline-block;
-}
-
-#submit {}
-
/* Used for id=Start */
@@ -203,11 +122,11 @@ a {
background-color: #7a0019;
}
-li {
+.navbar li {
float: left;
}
-li a {
+.navbar li a {
display: block;
color: white;
text-align: center;
@@ -216,9 +135,7 @@ li a {
text-decoration: none;
}
-
-
-li a:hover {
+.navbar li a:hover {
background-color: #ffcc33;
color: #000000;
/* Remove underlines from links */
diff --git a/src/public/js/form.js b/src/public/js/form.js
new file mode 100644
index 0000000..cfa80fd
--- /dev/null
+++ b/src/public/js/form.js
@@ -0,0 +1,17 @@
+document.getElementById('upload').onsubmit = function () {
+ var data = new FormData(document.getElementById('upload'));
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/api/upload');
+ xhr.send(data);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState == 4 && xhr.status == 200) {
+ var response = JSON.parse(xhr.responseText);
+ if (response.success) {
+ document.getElementById('success').style.display = 'block';
+ } else {
+ document.getElementById('error').style.display = 'block';
+ }
+ }
+ };
+ return false;
+};
diff --git a/src/routes/api.ts b/src/routes/api.ts
new file mode 100644
index 0000000..4612c16
--- /dev/null
+++ b/src/routes/api.ts
@@ -0,0 +1,40 @@
+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';
+
+
+// Slow down everything to prevent DoS attacks
+const speedLimiter = slowDown({
+ windowMs: 5 * 60 * 1000, // 15 minutes
+ delayAfter: 50, // allow 100 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.
+});
+
+
+const api = express.Router();
+
+api.use(fileUpload());
+api.use(speedLimiter);
+
+// CSRF protection
+api.use(cookieParser());
+const csrf = csurf({ cookie: true });
+
+api.post('/upload', csrf, (req: Request, res: Response) => {
+ if (!req.files || Object.keys(req.files).length === 0)
+ return res.status(400).json({ err: 'ENOENT' });
+ // Kludge to prevent a compiler error
+ const file: UploadedFile = req.files.file as UploadedFile;
+ console.log(file.mimetype);
+ if (file.mimetype !== 'text/x-python')
+ return res.status(400).json({ err: 'EINVAL' });
+ res.status(200).json({ err: null });
+});
+
+export default api; \ No newline at end of file
diff --git a/src/routes/api/actuate.ts b/src/routes/api/actuate.ts
deleted file mode 100644
index e69de29..0000000
--- a/src/routes/api/actuate.ts
+++ /dev/null
diff --git a/src/routes/api/login.ts b/src/routes/api/login.ts
deleted file mode 100644
index e69de29..0000000
--- a/src/routes/api/login.ts
+++ /dev/null
diff --git a/src/views/pages/about.ejs b/src/views/pages/about.ejs
index 3b376d5..022354c 100644
--- a/src/views/pages/about.ejs
+++ b/src/views/pages/about.ejs
@@ -12,19 +12,19 @@
<title>About the Remotely Accessible Inverted Pendulum</title>
<div class="header">
<img src="public/img/site_logo.png" alt="Pendulum logo" width="160" height="130" />
- <h1 id="title1">Remotely Accessible Inverted Pendulum</h1>
+ <h1>Remotely Accessible Inverted Pendulum</h1>
</div>
<div id="main">
<div id="about">
- <h2 id="subtitle">
+ <h3 id="subtitle">
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 />
Alin Butoi, Paul D’Amico, Dat Nguyen, Ross Olson, Rachel Schow (2019) <br /> <br />
Advisor: Professor Andrew Lamperski <br />
Sponsored by the University of Minnesota -Twin Cities <br />
- </h2>
+ </h3>
<br />
<h1 id="Background">Background</h1>
<p>
diff --git a/src/views/pages/index.ejs b/src/views/pages/index.ejs
index 6087732..0e6fe15 100644
--- a/src/views/pages/index.ejs
+++ b/src/views/pages/index.ejs
@@ -4,73 +4,23 @@
<head>
<!-- HTML headers information -->
<%- include('../partials/head.ejs') %>
- <script type="text/javascript" src="/static/js/app.js"></script>
+ <script type="module" src="public/js/form.js" defer></script>
</head>
<body>
+ <!-- Get the navbar -->
<%- include('../partials/nav.ejs') %>
- <div class="header">
- <!-- Get the navbar -->
-
- <div class="row">
- <div class="col">
-
- <h1 id="title2">Homepage</h1>
- <hr>
-
-
- <h3 id="subtitle1">Please upload a Python file (.py file extension) onto the Inverted Pendulum</h3>
- <!--**************************Send form data to web server**************************-->
- <form method="POST" action="/index" enctype="multipart/form-data">
- <p><input type="file" name="file" accept=".py" /></p>
- <p><input type="submit" value="Upload File" onclick="update()"></p>
- </form>
-
- For file upload progress bar
- <div id="myProgress">
- <div id="myBar"></div>
- </div>
-
- <!--The following lines are for the warning flash() messages-->
- <% if (errors) { %>
- <% for (const message in errors) { %>
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
- <span>
- <% message %>
- </span>
- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- </div>
- <% } %>
- <% } %>
-
- <!--The following lines are for the danger flash() messages-->
- <% if (errors) { %>
- <% for (const message in errors) { %>
- <div class="alert alert-danger alert-dismissible fade show" role="alert">
- <span>
- <% message %>
- </span>
- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- </div>
- <% } %>
- <% } %>
-
-
- <div class="mb-3">
- <div class="form-group">
- <label id="Start">Start pendulum: </label>
- <!-- <button class="btn btn-primary" id="onbutton">Actuate!</button> -->
- <button type="button" id=button1 onclick="window.location.href='{{ url_for( 'run_test_file') }}';">Actuate!</button>
- </div>
- </div>
- <hr>
- </div>
- </div>
- </div>
+ <br />
+ <h2>Please upload a Python file (.py file extension) onto 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>
</body>
</html> \ No newline at end of file
diff --git a/src/views/pages/login.ejs b/src/views/pages/login.ejs
index e69de29..7e77ee2 100644
--- a/src/views/pages/login.ejs
+++ b/src/views/pages/login.ejs
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <!-- HTML headers information -->
+ <%- include('../partials/head.ejs') %>
+</head>
+
+<body>
+ <%- include('../partials/nav.ejs') %>
+</body> \ No newline at end of file
diff --git a/src/views/partials/head.ejs b/src/views/partials/head.ejs
index 02786be..320f0e6 100644
--- a/src/views/partials/head.ejs
+++ b/src/views/partials/head.ejs
@@ -1,6 +1,6 @@
-<meta charset="utf-8" />
-<meta http-equiv="X-UA-Compatible" content="IE=edge" />
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="public/css/style.css" type="text/css">
-<link rel="icon" href="public/img/site_logo.png" type="image/x-icon"> \ No newline at end of file
+ <link rel="stylesheet" href="public/css/style.css" type="text/css">
+ <link rel="icon" href="public/img/site_logo.png" type="image/x-icon"> \ No newline at end of file
diff --git a/src/views/partials/nav.ejs b/src/views/partials/nav.ejs
index ab2626b..1ee6c00 100644
--- a/src/views/partials/nav.ejs
+++ b/src/views/partials/nav.ejs
@@ -1,7 +1,7 @@
-<nav>
- <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>
- </ul>
-</nav> \ No newline at end of file
+ <nav>
+ <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>
+ </ul>
+ </nav> \ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index b510ea3..444b300 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -72,6 +72,13 @@
"@types/connect" "*"
"@types/node" "*"
+"@types/busboy@^0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-0.3.2.tgz#2f29b017513415399c42632ae6a7cfcb1409b79c"
+ integrity sha512-iEvdm9Z9KdSs/ozuh1Z7ZsXrOl8F4M/CLMXPZHr3QuJ4d6Bjn+HBMC5EMKpwpAo8oi8iK9GZfFoHaIMrrZgwVw==
+ dependencies:
+ "@types/node" "*"
+
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
@@ -93,6 +100,14 @@
dependencies:
"@types/express-serve-static-core" "*"
+"@types/express-fileupload@^1.2.2":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@types/express-fileupload/-/express-fileupload-1.2.2.tgz#98c10e900c222744bba16c848505a1fa95ab3ff0"
+ integrity sha512-sWU1EVFfLsdAginKVrkwTRbRPnbn7dawxEFEBgaRDcpNFCUuksZtASaAKEhqwEIg6fSdeTyI6dIUGl3thhrypg==
+ dependencies:
+ "@types/busboy" "^0"
+ "@types/express" "*"
+
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
version "4.17.28"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
@@ -399,6 +414,13 @@ braces@^3.0.1, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
+busboy@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
+ integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
+ dependencies:
+ dicer "0.3.0"
+
bytes@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a"
@@ -681,6 +703,13 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+dicer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
+ integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
+ dependencies:
+ streamsearch "0.1.2"
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -967,6 +996,13 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+express-fileupload@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.3.1.tgz#3238472def305b8cb4cc5936a953761d0c442011"
+ integrity sha512-LD1yabD3exmWIFujKGDnT1rmxSomaqQSlUvzIsrA1ZgwCJ6ci7lg2YHFGM3Q6DfK+Yk0gAVU7GWLE7qDMwZLkw==
+ dependencies:
+ busboy "^0.3.1"
+
express-rate-limit@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.2.1.tgz#4a7619634fb24417ae723ad2ac3707b38e2e1c64"
@@ -2312,6 +2348,11 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+streamsearch@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
+ integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
+
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"