Skip to content

Commit a3a0b2b

Browse files
committed
initial commit
0 parents  commit a3a0b2b

20 files changed

+896
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
web/posts/2*

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "my-ramblings",
3+
"main": "server.js",
4+
"dependencies": {
5+
"cookie": "~0.3.1",
6+
"get-stream": "~5.1.0",
7+
"node-static-alias": "~1.1.2",
8+
"random-number-csprng": "~1.0.2"
9+
}
10+
}

server.js

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
"use strict";
2+
3+
var util = require("util");
4+
var fs = require("fs");
5+
var path = require("path");
6+
var http = require("http");
7+
var nodeStaticAlias = require("node-static-alias");
8+
var getStream = require("get-stream");
9+
var cookie = require("cookie");
10+
var rand = require("random-number-csprng");
11+
12+
var fsReadDir = util.promisify(fs.readdir);
13+
var fsReadFile = util.promisify(fs.readFile);
14+
var fsWriteFile = util.promisify(fs.writeFile);
15+
16+
const PORT = 8049;
17+
const WEB_DIR = path.join(__dirname,"web");
18+
19+
var httpServer = http.createServer(handleRequest);
20+
21+
var staticServer = new nodeStaticAlias.Server(WEB_DIR,{
22+
serverInfo: "My Ramblings",
23+
cache: 1,
24+
alias: [
25+
{
26+
// basic static page friendly URL rewrites
27+
match: /^\/(?:index)?(?:[#?]|$)/,
28+
serve: "index.html",
29+
force: true,
30+
},
31+
{
32+
// basic static page friendly URL rewrites
33+
match: /^\/(?:about|contact|login|404|offline)(?:[#?]|$)/,
34+
serve: "<% basename %>.html",
35+
force: true,
36+
},
37+
{
38+
// URL rewrites for individual posts
39+
match: /^\/post\/[\w\d-]+(?:[#?]|$)/,
40+
serve: "posts/<% basename %>.html",
41+
force: true,
42+
},
43+
{
44+
// match (with force) static files
45+
match: /^\/(?:(?:(?:js|css|images)\/.+))$/,
46+
serve: ".<% reqPath %>",
47+
force: true,
48+
},
49+
],
50+
});
51+
52+
53+
httpServer.listen(PORT);
54+
console.log(`Server started on http://localhost:${PORT}...`);
55+
56+
57+
// *******************************
58+
59+
var sessions = [];
60+
61+
async function handleRequest(req,res) {
62+
// parse cookie values?
63+
if (req.headers.cookie) {
64+
req.headers.cookie = cookie.parse(req.headers.cookie);
65+
}
66+
67+
// handle API calls
68+
if (
69+
["GET","POST"].includes(req.method) &&
70+
/^\/api\/.+$/.test(req.url)
71+
) {
72+
if (req.url == "/api/get-posts") {
73+
await getPosts(req,res);
74+
return;
75+
}
76+
else if (req.url == "/api/login") {
77+
let loginData = JSON.parse(await getStream(req));
78+
await doLogin(loginData,req,res);
79+
return;
80+
}
81+
else if (
82+
req.url == "/api/add-post" &&
83+
validateSessionID(req,res)
84+
) {
85+
let newPostData = JSON.parse(await getStream(req));
86+
await addPost(newPostData,req,res);
87+
return;
88+
}
89+
90+
// didn't recognize the API request
91+
res.writeHead(404);
92+
res.end();
93+
}
94+
// handle all other file requests
95+
else if (["GET","HEAD"].includes(req.method)) {
96+
// special handling for empty favicon
97+
if (req.url == "/favicon.ico") {
98+
res.writeHead(204,{
99+
"Content-Type": "image/x-icon",
100+
"Cache-Control": "public, max-age: 604800"
101+
});
102+
res.end();
103+
return;
104+
}
105+
106+
// special handling for service-worker (virtual path)
107+
if (/^\/sw\.js(?:[?#].*)?$/.test(req.url)) {
108+
serveFile("/js/sw.js",200,{ "cache-control": "max-age=0", },req,res)
109+
.catch(console.error);
110+
return;
111+
}
112+
113+
// handle admin pages
114+
if (/^\/(?:add-post)(?:[#?]|$)/.test(req.url)) {
115+
// page not allowed without active session
116+
if (validateSessionID(req,res)) {
117+
await serveFile("/add-post.html",200,{},req,res);
118+
}
119+
// show the login page instead
120+
else {
121+
await serveFile("/login.html",200,{},req,res);
122+
}
123+
return;
124+
}
125+
126+
// login page when already logged in?
127+
if (
128+
/^\/(?:login)(?:[#?]|$)/.test(req.url) &&
129+
validateSessionID(req,res)
130+
) {
131+
res.writeHead(307,{ Location: "/add-post", });
132+
res.end();
133+
return;
134+
}
135+
136+
// handle logout
137+
if (/^\/(?:logout)(?:[#?]|$)/.test(req.url)) {
138+
clearSession(req,res);
139+
res.writeHead(307,{ Location: "/", });
140+
res.end();
141+
return;
142+
}
143+
144+
// handle other static files
145+
staticServer.serve(req,res,function onStaticComplete(err){
146+
if (err) {
147+
if (req.headers["accept"].includes("text/html")) {
148+
serveFile("/404.html",200,{ "X-Not-Found": "1" },req,res)
149+
.catch(console.error);
150+
}
151+
else {
152+
res.writeHead(404);
153+
res.end();
154+
}
155+
}
156+
});
157+
}
158+
// Oops, invalid/unrecognized request
159+
else {
160+
res.writeHead(404);
161+
res.end();
162+
}
163+
}
164+
165+
function serveFile(url,statusCode,headers,req,res) {
166+
var listener = staticServer.serveFile(url,statusCode,headers,req,res);
167+
return new Promise(function c(resolve,reject){
168+
listener.on("success",resolve);
169+
listener.on("error",reject);
170+
});
171+
}
172+
173+
async function getPostIDs() {
174+
var files = await fsReadDir(path.join(WEB_DIR,"posts"));
175+
return (
176+
files
177+
.filter(function onlyPosts(filename){
178+
return /^\d+\.html$/.test(filename);
179+
})
180+
.map(function postID(filename){
181+
let [,postID] = filename.match(/^(\d+)\.html$/);
182+
return Number(postID);
183+
})
184+
.sort(function desc(x,y){
185+
return y - x;
186+
})
187+
);
188+
}
189+
190+
async function getPosts(req,res) {
191+
var postIDs = await getPostIDs();
192+
sendJSONResponse(postIDs,res);
193+
}
194+
195+
async function addPost(newPostData,req,res) {
196+
if (
197+
newPostData.title.length > 0 &&
198+
newPostData.post.length > 0
199+
) {
200+
let postTemplate = await fsReadFile(path.join(WEB_DIR,"posts","post.html"),"utf-8");
201+
let newPost =
202+
postTemplate
203+
.replace(/\{\{TITLE\}\}/g,newPostData.title)
204+
.replace(/\{\{POST\}\}/,newPostData.post);
205+
let postIDs = await getPostIDs();
206+
let newPostCount = 1;
207+
let [,year,month,day] = (new Date()).toISOString().match(/^(\d{4})-(\d{2})-(\d{2})/);
208+
if (postIDs.length > 0) {
209+
let [,latestYear,latestMonth,latestDay,latestCount] = String(postIDs[0]).match(/^(\d{4})(\d{2})(\d{2})(\d+)/);
210+
if (
211+
latestYear == year &&
212+
latestMonth == month &&
213+
latestDay == day
214+
) {
215+
newPostCount = Number(latestCount) + 1;
216+
}
217+
}
218+
let newPostID = `${year}${month}${day}${newPostCount}`;
219+
try {
220+
await fsWriteFile(path.join(WEB_DIR,"posts",`${newPostID}.html`),newPost,"utf8");
221+
sendJSONResponse({ OK: true, postID: newPostID },res);
222+
return;
223+
}
224+
catch (err) {}
225+
}
226+
227+
sendJSONResponse({ failed: true },res);
228+
}
229+
230+
function validateSessionID(req,res) {
231+
if (req.headers.cookie && req.headers.cookie["sessionId"]) {
232+
let isLoggedIn = Number(req.headers.cookie["isLoggedIn"]);
233+
let sessionID = req.headers.cookie["sessionId"];
234+
let session;
235+
236+
if (
237+
isLoggedIn == 1 &&
238+
sessions.includes(sessionID)
239+
) {
240+
req.sessionID = sessionID;
241+
242+
// update cookie headers
243+
res.setHeader(
244+
"Set-Cookie",
245+
getCookieHeaders(sessionID,new Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString())
246+
);
247+
return true;
248+
}
249+
else {
250+
clearSession(req,res);
251+
}
252+
}
253+
254+
return false;
255+
}
256+
257+
async function randomString() {
258+
var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
259+
var str = "";
260+
for (let i = 0; i < 20; i++) {
261+
str += chars[ await rand(0,63) ];
262+
}
263+
return str;
264+
}
265+
266+
async function createSession() {
267+
var sessionID;
268+
do {
269+
sessionID = await randomString();
270+
} while (sessions.includes(sessionID));
271+
sessions.push(sessionID);
272+
return sessionID;
273+
}
274+
275+
function clearSession(req,res) {
276+
var sessionID =
277+
req.sessionID ||
278+
(req.headers.cookie && req.headers.cookie.sessionId);
279+
280+
if (sessionID) {
281+
sessions = sessions.filter(function removeSession(sID){
282+
return sID !== sessionID;
283+
});
284+
}
285+
286+
res.setHeader("Set-Cookie",getCookieHeaders(null,new Date(0).toUTCString()));
287+
}
288+
289+
function getCookieHeaders(sessionID,expires = null) {
290+
var cookieHeaders = [
291+
`sessionId=${sessionID || ""}; HttpOnly; Path=/`,
292+
`isLoggedIn=${sessionID ? "1" : ""}; Path=/`,
293+
];
294+
295+
if (expires != null) {
296+
cookieHeaders = cookieHeaders.map(function addExpires(headerVal){
297+
return `${headerVal}; Expires=${expires}`;
298+
});
299+
}
300+
301+
return cookieHeaders;
302+
}
303+
304+
async function doLogin(loginData,req,res) {
305+
// WARNING: This is absolutely NOT how you should handle logins,
306+
// having credentials hard-coded. Hash all credentials and store
307+
// them in a secure database.
308+
if (loginData.username == "admin" && loginData.password == "changeme") {
309+
let sessionID = await createSession();
310+
sendJSONResponse({ OK: true },res,{
311+
"Set-Cookie": getCookieHeaders(
312+
sessionID,
313+
new Date(Date.now() + /*1 hour in ms*/3.6E5).toUTCString()
314+
)
315+
});
316+
}
317+
else {
318+
sendJSONResponse({ failed: true },res);
319+
}
320+
}
321+
322+
function sendJSONResponse(msg,res,otherHeaders = {}) {
323+
res.writeHead(200,{
324+
"Content-Type": "application/json",
325+
"Cache-Control": "private, no-cache, no-store, must-revalidate, max-age=0",
326+
...otherHeaders
327+
});
328+
res.end(JSON.stringify(msg));
329+
}

web/404.html

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>My Ramblings :: Not Found</title>
6+
<link rel="stylesheet" href="/css/style.css">
7+
</head>
8+
<body>
9+
<header>
10+
<h1>My Ramblings</h1>
11+
<nav>
12+
<ul>
13+
<li><a href="/">Home</a></li>
14+
<li><a href="/about">About</a></li>
15+
<li><a href="/contact">Contact</a></li>
16+
</ul>
17+
</nav>
18+
<div id="connectivity-status" class="hidden"></div>
19+
</header>
20+
21+
<main>
22+
<h1>Not Found</h1>
23+
<p>
24+
Sorry, that couldn't be found. Please try again.
25+
</p>
26+
</main>
27+
28+
<script src="/js/blog.js"></script>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)