Compare commits

...

3 Commits

Author SHA1 Message Date
octospacc e965dfa862 Update CI 2024-02-24 20:35:31 +01:00
octospacc 8fcbedcf15 Update pkglock 2024-02-24 20:34:07 +01:00
octospacc 1a66d02cb0 Update WuppiMini 2024-02-24 20:27:20 +01:00
5 changed files with 318 additions and 473 deletions

View File

@ -2,16 +2,17 @@ image: alpine:latest
before_script: |
apk update
apk add nodejs npm
apk add git nodejs npm
pages:
stage: deploy
script: |
mkdir ./public/WuppiMini
cd ./src/WuppiMini
npm update
npm install
node ./index.js html
cp ./index.js ./index.html ../../public/WuppiMini/
cp ./index.js ./index.html ./node_modules/SpaccDotWeb/SpaccDotWeb.Server.js ../../public/WuppiMini/
artifacts:
paths:
- public

View File

@ -1 +1,2 @@
/index.html
/SpaccDotWeb.Server.js

View File

@ -1,5 +1,4 @@
// NodeJS requirements, for server deployment:
// `npm install mime-types parse-multipart-data`
// search "Copyright" in this file for licensing info
// configuration
const appName = 'WuppìMini';
@ -7,7 +6,7 @@ const serverPort = 8135;
const detailedLogging = true;
const serverLanUpstreams = false;
const serverPlaintextUpstreams = false;
let resFiles = ['package.json', 'package-lock.json'];
let resFiles = [ 'package.json', 'package-lock.json' ];
const appTerms = `
<p >(These terms apply to the server-hosted version of the app only.)
<br/>This service is offered for free, in the hope that it can be useful, but without any warranty.
@ -18,18 +17,21 @@ const appTerms = `
<br/>By continuing with the usage of this site, you declare to understand and agree to these terms.
<br/>If you don't agree with these terms, discontinue usage of this site immediately, and instead <a href="/info#h-floss">get the source code</a> to host it yourself, find another instance, or use the <a href="/info#h-versions">local, client-side version</a>.
</p>`;
const suggestedTags = ['fromWuppiMini'];
const corsProxies = ['corsproxy.io', 'corsproxy.org'];
const suggestedTags = [ 'fromWuppiMini' ];
const corsProxies = [ 'corsproxy.io', 'corsproxy.org' ];
let fs, path, mime, multipart, crypto;
let isEnvServer = (typeof(window) === 'undefined');
let isEnvBrowser = !isEnvServer;
const SpaccDotWebServer = require('SpaccDotWeb/SpaccDotWeb.Server.js');
let crypto;
let isEnvServer = SpaccDotWebServer.envIsNode;
let isEnvBrowser = SpaccDotWebServer.envIsBrowser;
const httpCodes = { success: [200,201] };
const strings = {
csrfErrorHtml: `<p class="notice error">Authorization token mismatch. Please try resubmitting.</p>`,
upstreamDisallowedHtml: `<p class="notice error">Upstream destination is not allowed from backend.</p>`,
};
const appPager = (content, title) => `${title ? `<h2>${title}</h2>` : ''}${content}`;
const newHtmlPage = (content, title) => `<!DOCTYPE html><html><head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@ -85,7 +87,7 @@ div#app {
margin-right: auto;
padding: 0px 8px;
}
div#transitioner {
div#transition {
width: 100vw;
height: 100vh;
position: absolute;
@ -108,15 +110,15 @@ p.notice.error {
border-color: #de0000;
}
</style>
</head><body>
${isEnvBrowser ? `<div id="transitioner"></div>` : ''}
<div id="header">
<h1><a href="/">${appName}</a></h1><!--
</head><body><!--
-->${isEnvBrowser ? `<div id="transition"></div>` : ''}<!--
--><div id="header"><!--
--><h1><a href="/">${appName}</a></h1><!--
--><a href="/info">Info</a><!--
--><a href="/settings">Settings</a>
</div>
<div id="app">${content}</div>
</body></html>`;
--><a href="/settings">Settings</a><!--
--></div><!--
--><div id="app">${appPager(content, title)}</div><!--
--></body></html>`;
const A = (href) => `<a href="${href}">${href}</a>`
@ -135,62 +137,6 @@ const checkUpstreamAllowed = (url) => {
return true;
}
const redirectTo = (url, res) => {
if (isEnvServer) {
res.statusCode = 302;
res.setHeader('Location', url);
res.end();
} else if (isEnvBrowser) {
location.hash = url;
}
};
const setPageContent = (res, content, title) => {
const titleHtml = (title ? `<h2>${title}</h2>` : '');
if (isEnvServer) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(newHtmlPage((titleHtml + content), title));
} else if (isEnvBrowser) {
if (title) {
document.title = title;
}
document.querySelector('div#app').innerHTML = (titleHtml + content);
//for (const srcElem of document.querySelectorAll('[src^="/res/"]')) {
// srcElem.src = resFilesData[srcElem.getAttribute('src')];
//}
//for (const linkElem of document.querySelectorAll('link[rel="stylesheet"][href^="/res/"]')) {
// linkElem.href = resFilesData[linkElem.getAttribute('href')];
//}
for (const aElem of document.querySelectorAll('a[href^="/"]')) {
aElem.href = `#${aElem.getAttribute('href')}`;
}
for (const formElem of document.querySelectorAll('form')) {
const submitElem = formElem.querySelector('input[type="submit"]');
formElem.onsubmit = (event) => {
event.preventDefault();
const formData = (new FormData(formElem));
formData.append(submitElem.getAttribute('name'), (submitElem.value || 'Submit'));
handleRequest({
method: (formElem.getAttribute('method') || 'GET'),
url: (formElem.getAttribute('action') || location.hash.slice(1)),// + '?' + (new URLSearchParams(new FormData(formElem))),
headers: {
"content-type": (formElem.getAttribute('enctype') || "application/x-www-form-urlencoded"),
},
body: formData,
//body: `${(new URLSearchParams(new FormData(formElem))).toString()}&${submitElem.name}=${submitElem.value || 'Submit'}`,
})
};
//submitElem.type = 'button';
//submitElem.value = (submitElem.getAttribute('value') || 'Submit');
//submitElem.onclick = () => handleRequest({
// method: (formElem.getAttribute('method') || 'GET'),
// url: (formElem.getAttribute('action') || location.hash.slice(1)) + '?' + (new URLSearchParams(new FormData(formElem))),
//});
}
document.querySelector('div#transitioner').style.display = 'none';
}
};
// the below anti-CSRF routines do useful work only on the server, that kind of attack is not possible with the client app
const makeFormCsrf = (accountString) => {
@ -208,72 +154,10 @@ const genCsrfToken = (accountString, time) => (isEnvServer && time && crypto.scr
const matchCsrfToken = (bodyParams, accountString) => (isEnvServer ? bodyParams.formToken === genCsrfToken(accountString, bodyParams.formTime) : true);
// try to use the built-in cookie API, fallback to a Storage-based wrapper in case it fails (for example on file:///)
const clientCookieApi = (isEnvBrowser && (document.cookie || (!document.cookie && (document.cookie = '_=_') && document.cookie) ? (set) => (set ? (document.cookie = set) : document.cookie) : (set) => {
if (set) {
let api = sessionStorage;
const tokens = set.split(';');
const [key, ...rest] = tokens[0].split('=');
for (let token of tokens) {
token = token.trim();
if (['expires', 'max-age'].includes(token.split('=')[0].toLowerCase())) {
api = localStorage;
break;
}
}
api.setItem(`${appName}/${key}`, rest.join('='));
} else /*(get)*/ {
let items = '';
for (const item of Object.entries({ ...localStorage, ...sessionStorage })) {
items += (item.join('=') + ';').slice(appName.length + 1);
}
return items.slice(0, -1);
}
}));
const getCookie = (req, keyQuery) => {
if (isEnvServer) {
if (keyQuery) {
// get a specific cookie
for (const cookie of (req.headers?.cookie?.split(';') || [])) {
const [key, ...rest] = cookie.split('=');
if (key === keyQuery) {
return rest.join('=');
}
}
} else {
// get all cookies
return req.headers?.cookie?.join(';');
}
} else if (isEnvBrowser) {
const cookies = clientCookieApi();
if (keyQuery) {
for (const cookie of cookies.split(';')) {
const [key, ...rest] = cookie.split('=');
if (key === keyQuery) {
return rest.join('=');
}
}
} else {
return cookies;
}
}
}
// TODO warn if the browser has cookies disabled when running on server side
const setCookies = (res, list) => {
if (isEnvServer) {
res.setHeader('Set-Cookie', list);
} else if (isEnvBrowser) {
for (const cookie of list) {
clientCookieApi(cookie);
}
}
}
const corsProxyIfNeed = (need) => (isEnvBrowser /*&& need*/ ? `https://${corsProxies[~~(Math.random() * corsProxies.length)]}?` : '');
const handleRequest = async (req, res={}) => {
/*const handleRequest = async (req, res={}) => {
// TODO warn if the browser has cookies disabled when running on server side
// to check if we can save cookies:
// first check if any cookie is saved, if it is then we assume to be good
// if none is present, redirect to another endpoint that should set a "flag cookie" and redirect to a second one that checks if the flag is present
@ -283,59 +167,59 @@ const handleRequest = async (req, res={}) => {
//if (!getCookie(req)) {
// return redirectTo('/thecookieflagthingy', res);
//}
if (isEnvBrowser && document.querySelector('div#transitioner')) {
document.querySelector('div#transitioner').style.display = 'block';
}
const section = req.url.split('/')[1].split('?')[0].toLowerCase();
const urlParams = new URLSearchParams(req.url.split('?')[1]);
const bodyParams = {};
if (req.method === 'HEAD') {
req.method = 'GET';
} else if (req.method === 'POST') {
try {
if (isEnvServer) {
req.body = Buffer.alloc(0);
req.on('data', (data) => {
req.body = Buffer.concat([req.body, data]);
if (req.body.length > 4e6) { // 4 MB upload limit is (fair enough?)
req.connection.destroy();
}
})
await new Promise((resolve) => req.on('end', () => resolve()));
}
const contentMime = req.headers['content-type'].split(';')[0];
if (isEnvServer && contentMime === 'application/x-www-form-urlencoded') {
for (const [key, value] of (new URLSearchParams(req.body.toString())).entries()) {
bodyParams[key] = value;
}
} else if (isEnvServer && contentMime === 'multipart/form-data') {
for (const param of multipart.parse(req.body, req.headers['content-type'].split(';')[1].split('boundary=')[1])) {
bodyParams[param.name] = (param.type && param.filename !== undefined ? param : param.data.toString());
}
} else if (isEnvBrowser && ['application/x-www-form-urlencoded', 'multipart/form-data'].includes(contentMime)) {
for (const [key, value] of req.body) {
bodyParams[key] = value;
bodyParams[key].filename = bodyParams[key].name;
}
}
} catch(err) {
console.log(err);
req.connection?.destroy();
}
}
if (!section)
{
if (getCookie(req, 'account')) {
return redirectTo('/compose', res);
};
};*/
// todo handle optional options field(s)
const accountDataFromString = (accountString) => {
const tokens = accountString.split(',');
return { instance: tokens[0], username: tokens[1], password: tokens.slice(2).join(',') };
}
const makeFragmentLoggedIn = (accountString) => {
const accountData = accountDataFromString(accountString);
return `<p>Logged in as <i>${accountData.username} @ ${A(accountData.instance)}</i>.</p>`;
}
const main = () => {
if (SpaccDotWebServer.envIsNode && process.argv[2] !== 'html') {
resFiles = [__filename.split(require('path').sep).slice(-1)[0], ...resFiles];
};
const server = SpaccDotWebServer.setup({
appName: appName,
staticPrefix: '/res/',
staticFiles: resFiles,
appPager: appPager,
htmlPager: newHtmlPage,
});
if (SpaccDotWebServer.envIsNode && process.argv[2] === 'html') {
server.writeStaticHtml();
} else {
return redirectTo('/info', res);
}
}
else if (section === 'info' && req.method === 'GET')
{
res.statusCode = 200;
return setPageContent(res, `
${!getCookie(req, 'account') ? `<p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p>` : ''}
if (SpaccDotWebServer.envIsNode) {
crypto = require('crypto');
console.log('Running Server...');
};
server.initServer({
port: serverPort,
address: '0.0.0.0',
maxBodyUploadSize: 4e6, // 4 MB
endpoints: [ endpointRoot, endpointInfo, endpointCompose, endpointSettings, endpointCatch ],
});
};
};
const endpointRoot = [ (ctx) => (!ctx.urlSections[0]), (ctx) => ctx.redirectTo(ctx.getCookie('account') ? '/compose' : '/info') ];
const endpointCatch = [ (ctx) => true, (ctx) => ctx.renderPage('', (ctx.response.statusCode = 404)) ];
const endpointInfo = [ (ctx) => (ctx.urlSections[0] === 'info' && ctx.request.method === 'GET'), (ctx) => {
ctx.response.statusCode = 200;
ctx.renderPage(`
${!ctx.getCookie('account') ? `<p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p>` : ''}
<h3>About</h3>
<p>
${appName} (temporary name?) is a minimalist, basic HTML-based frontend, designed for quickly and efficiently publishing to social media and content management services (note that only WordPress is currently supported).
@ -378,6 +262,11 @@ const handleRequest = async (req, res={}) => {
</p>
${isEnvServer ? `<h3>Terms of Use and Privacy Policy</h3>${appTerms}` : ''}
<h3>Changelog</h3>
<h4>2024-02-24</h3>
<ul>
<li>Allow uploading posts as published or draft, via 2 distinct buttons.</li>
<li>Migrated fancy portable-server-codebase to <a href="https://gitlab.com/SpaccInc/SpaccDotWeb">SpaccDotWeb</a> for code reuse and slimming down of the application core.</li>
</ul>
<h4>2024-02-12</h3>
<ul>
<li>First working client-side version of the current app, without backend server (still a bit buggy).</li>
@ -386,7 +275,7 @@ const handleRequest = async (req, res={}) => {
<h4>2024-02-10</h4>
<ul>
<li>Add "remember me" login option.</li>
<!--<li>Add "suggested tags" publishing option, will automatically add this list of tags to the post: [${suggestedTags}].</li>-->
<li>Add "suggested tags" publishing option, will automatically add this list of tags to the post: [${suggestedTags}].</li>
</ul>
<h4>2024-02-09</h4>
<ul>
@ -396,47 +285,48 @@ const handleRequest = async (req, res={}) => {
<li>Tested on New and Old 3DS.</li>
</ul>
`);
}
else if (section === 'compose' && ['GET', 'POST'].includes(req.method))
{
} ];
const endpointCompose = [ (ctx) => (ctx.urlSections[0] === 'compose' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
let noticeHtml = '';
const accountString = getCookie(req, 'account');
const accountString = ctx.getCookie('account');
if (!accountString) {
return redirectTo('/', res);
return ctx.redirectTo('/');
}
res.statusCode = 200;
if (req.method === 'POST' && bodyParams.publish) {
if (!matchCsrfToken(bodyParams, accountString)) {
res.statusCode = 401;
ctx.response.statusCode = 200;
const postUploadStatus = ((ctx.bodyParameters?.publish && 'publish') || (ctx.bodyParameters?.draft && 'draft'));
if (ctx.request.method === 'POST' && postUploadStatus) {
if (!matchCsrfToken(ctx.bodyParameters, accountString)) {
ctx.response.statusCode = 401;
noticeHtml = strings.csrfErrorHtml;
}
const isThereAnyFile = ((bodyParams.file?.data?.length || bodyParams.file?.size) > 0);
if (!bodyParams.text?.trim() && !isThereAnyFile) {
res.statusCode = 500;
const isThereAnyFile = ((ctx.bodyParameters.file?.data?.length || ctx.bodyParameters.file?.size) > 0);
if (!ctx.bodyParameters.text?.trim() && !isThereAnyFile) {
ctx.response.statusCode = 500;
noticeHtml = `<p class="notice error">Post content is empty. Please write some text or upload a media.</p>`;
}
const account = accountDataFromString(accountString);
if (!checkUpstreamAllowed(account.instance)) {
res.statusCode = 500;
ctx.response.statusCode = 500;
noticeHtml = strings.upstreamDisallowedHtml;
}
let mediaData;
try {
// there is a media to upload first
if (httpCodes.success.includes(res.statusCode) && isThereAnyFile) {
if (httpCodes.success.includes(ctx.response.statusCode) && isThereAnyFile) {
const mediaReq = await fetch(`${corsProxyIfNeed(account.cors)}${account.instance}/wp-json/wp/v2/media`, { headers: {
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
"Content-Type": bodyParams.file.type,
"Content-Disposition": `attachment; filename=${bodyParams.file.filename}`,
}, method: "POST", body: (bodyParams.file.data || bodyParams.file) });
"Content-Type": ctx.bodyParameters.file.type,
"Content-Disposition": `attachment; filename=${ctx.bodyParameters.file.filename}`,
}, method: "POST", body: (ctx.bodyParameters.file.data || ctx.bodyParameters.file) });
mediaData = await mediaReq.json();
if (!httpCodes.success.includes(mediaReq.status)) {
noticeHtml = `<p class="notice error">Upstream server responded with error ${res.statusCode = mediaReq.status}: ${JSON.stringify(mediaData)}</p>`;
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = mediaReq.status}: ${JSON.stringify(mediaData)}</p>`;
}
}
// upload actual post if nothing has errored before
if (httpCodes.success.includes(res.statusCode)) {
const tagsHtml = (bodyParams.tags === 'on' ? `
if (httpCodes.success.includes(ctx.response.statusCode)) {
const tagsHtml = (ctx.bodyParameters.tags === 'on' ? `
<!-- wp:paragraph -->
<p> #${suggestedTags.join(' #')} </p>
<!-- /wp:paragraph -->
@ -450,17 +340,17 @@ const handleRequest = async (req, res={}) => {
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
"Content-Type": "application/json",
}, method: "POST", body: JSON.stringify({
status: "publish",
status: postUploadStatus,
featured_media: mediaData?.id,
title: bodyParams.title,
content: (bodyParams.html === 'on' ? `${bodyParams.text}${tagsHtml}${figureHtml}` : `
${bodyParams.text?.trim() ? `
title: ctx.bodyParameters.title,
content: (ctx.bodyParameters.html === 'on' ? `${ctx.bodyParameters.text}${tagsHtml}${figureHtml}` : `
${ctx.bodyParameters.text?.trim() ? `
<!-- wp:paragraph -->
<p>${bodyParams.text.replaceAll('\r\n', '<br/>')}</p>
<p>${ctx.bodyParameters.text.replaceAll('\r\n', '<br/>')}</p>
<!-- /wp:paragraph -->
` : ''}
${tagsHtml}
${bodyParams.text?.trim() && mediaData?.id && mediaData?.source_url ? `
${ctx.bodyParameters.text?.trim() && mediaData?.id && mediaData?.source_url ? `
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->
@ -469,75 +359,74 @@ ${figureHtml}`.trim()),
}) });
const postData = await postReq.json();
if (httpCodes.success.includes(postReq.status)) {
noticeHtml = `<p class="notice success">Post uploaded! ${A(postData.link)}</p>`;
noticeHtml = `<p class="notice success">${postUploadStatus === 'publish' ? 'Post published' : ''}${postUploadStatus === 'draft' ? 'Draft uploaded' : ''}! ${A(postData.link)}</p>`;
} else {
noticeHtml = `<p class="notice error">Upstream server responded with error ${res.statusCode = postReq.status}: ${JSON.stringify(postData)}</p>`;
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = postReq.status}: ${JSON.stringify(postData)}</p>`;
}
}
} catch (err) {
console.log(err);
res.statusCode = 500;
ctx.response.statusCode = 500;
// display only generic error from server-side, for security
noticeHtml = `<p class="notice error">${isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err}</p>`;
}
// TODO handle media upload success but post fail, either delete the remote media or find a way to reuse it when the user probably retries posting
}
return setPageContent(res, `${noticeHtml}
ctx.renderPage(`${noticeHtml}
${makeFragmentLoggedIn(accountString)}
<form method="POST" enctype="multipart/form-data">${makeFormCsrf(accountString)}
<input type="text" name="title" placeholder="Post Title" value="${bodyParams.title && res.statusCode !== 200 ? bodyParams.title : ''}"/>
<input type="text" name="title" placeholder="Post Title" value="${ctx.bodyParameters.title && ctx.response.statusCode !== 200 ? ctx.bodyParameters.title : ''}"/>
<input type="file" accept="image/jpeg,image/gif,image/png,image/webp,image/bmp" name="file"/>
<textarea name="text" rows="10" placeholder="What's on your mind?">${bodyParams.text && res.statusCode !== 200 ? bodyParams.text : ''}</textarea>
<label><input type="checkbox" name="html" ${bodyParams.html === 'on' && res.statusCode !== 200 ? 'checked="true"' : ''}/> Raw HTML mode</label>
<label><input type="checkbox" name="tags" ${req.method === 'GET' || (bodyParams.tags === 'on' && res.statusCode !== 200) ? 'checked="true"' : ''}/> Include suggested tags</label>
<!--<input type="submit" name="draft" value="Save Draft"/>-->
<textarea name="text" rows="10" placeholder="What's on your mind?">${ctx.bodyParameters.text && ctx.response.statusCode !== 200 ? ctx.bodyParameters.text : ''}</textarea>
<!-- TODO: fix the turn off-on on submit of these checkboxes... -->
<label><input type="checkbox" name="html" ${ctx.bodyParameters.html === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''}/> Raw HTML mode</label>
<label><input type="checkbox" name="tags" ${ctx.request.method === 'GET' || (ctx.bodyParameters.tags === 'on' && ctx.response.statusCode !== 200) ? 'checked="true"' : ''}/> Include suggested tags</label>
<input type="submit" name="draft" value="Upload Draft"/>
<input type="submit" name="publish" value="Publish!"/>
</form>
`, 'Compose Post');
}
else if (section === 'settings' && ['GET', 'POST'].includes(req.method))
{
} ];
const endpointSettings = [ (ctx) => (ctx.urlSections[0] === 'settings' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
let noticeHtml = '';
const accountString = getCookie(req, 'account');
res.statusCode = 200;
if (req.method === 'POST') {
if (accountString && !matchCsrfToken(bodyParams, accountString)) {
res.statusCode = 401;
const accountString = ctx.getCookie('account');
ctx.response.statusCode = 200;
if (ctx.request.method === 'POST') {
if (accountString && !matchCsrfToken(ctx.bodyParameters, accountString)) {
ctx.response.statusCode = 401;
noticeHtml = strings.csrfErrorHtml;
}
if (res.statusCode === 200 && bodyParams.login) {
bodyParams.instance = bodyParams.instance.trim();
if (!checkUpstreamAllowed(bodyParams.instance)) {
res.statusCode = 500;
if (ctx.response.statusCode === 200 && ctx.bodyParameters.login) {
ctx.bodyParameters.instance = ctx.bodyParameters.instance.trim();
if (!checkUpstreamAllowed(ctx.bodyParameters.instance)) {
ctx.response.statusCode = 500;
noticeHtml = strings.upstreamDisallowedHtml;
}
try {
const upstreamReq = await fetch(`${corsProxyIfNeed(bodyParams.cors === 'on')}${bodyParams.instance}/wp-json/wp/v2/users?context=edit`, { headers: {
Authorization: `Basic ${btoa(bodyParams.username + ':' + bodyParams.password)}`,
const upstreamReq = await fetch(`${corsProxyIfNeed(ctx.bodyParameters.cors === 'on')}${ctx.bodyParameters.instance}/wp-json/wp/v2/users?context=edit`, { headers: {
Authorization: `Basic ${btoa(ctx.bodyParameters.username + ':' + ctx.bodyParameters.password)}`,
} });
const upstreamData = await upstreamReq.json();
if (upstreamReq.status === 200) {
let cookieFlags = (bodyParams.remember === 'on' ? `; max-age=${365*24*60*60}` : '');
setCookies(res, [
`account=${bodyParams.instance},${bodyParams.username},${bodyParams.password}${cookieFlags}`
]); // TODO: add cookie renewal procedure
return redirectTo('/', res);
let cookieFlags = (ctx.bodyParameters.remember === 'on' ? `; max-age=${365*24*60*60}` : '');
ctx.setCookie(`account=${ctx.bodyParameters.instance},${ctx.bodyParameters.username},${ctx.bodyParameters.password}${cookieFlags}`); // TODO: add cookie renewal procedure
return ctx.redirectTo('/');
} else {
res.statusCode = upstreamReq.status;
ctx.response.statusCode = upstreamReq.status;
noticeHtml = `<p class="notice error">Upstream server responded with error ${upstreamReq.status}: ${JSON.stringify(upstreamData)}</p>`;
}
} catch (err) {
console.log(err);
res.statusCode = 500;
ctx.response.statusCode = 500;
// display only generic error from server-side, for security
noticeHtml = `<p class="notice error">${isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err}</p>`;
}
} else if (res.statusCode === 200 && bodyParams.logout) {
setCookies(res, [`account=`]);
return redirectTo('/', res);
} else if (ctx.response.statusCode === 200 && ctx.bodyParameters.logout) {
ctx.setCookie(`account=`);
return ctx.redirectTo('/');
}
}
return setPageContent(res, `${noticeHtml}
ctx.renderPage(`${noticeHtml}
${accountString ? `
<h3>Current Account</h3>
${makeFragmentLoggedIn(accountString)}
@ -553,11 +442,11 @@ ${figureHtml}`.trim()),
</option>
</select>
<label><i>Note: For WordPress.org you must use an "application password" (<code>/wp-admin/profile.php#application-passwords-section</code>)</i></label>
<input type="url" name="instance" placeholder="Site/Instance URL" value="${bodyParams.instance || ''}" required="true"/>
<input type="text" name="username" placeholder="Username" value="${bodyParams.username || ''}" required="true"/>
<input type="password" name="password" placeholder="Password" value="${bodyParams.password || ''}" required="true"/>
<!--${isEnvBrowser ? `<label><input type="checkbox" name="cors" ${bodyParams.cors === 'on' && res.statusCode !== 200 ? 'checked="true"' : ''}/> Site disallows CORS, use proxy</label>` : ''}-->
<label><input type="checkbox" name="remember" ${req.method === 'POST' && bodyParams.remember !== 'on' ? '' : 'checked="true"'}/> Remember me</label>
<input type="url" name="instance" placeholder="Site/Instance URL" value="${ctx.bodyParameters.instance || ''}" required="true"/>
<input type="text" name="username" placeholder="Username" value="${ctx.bodyParameters.username || ''}" required="true"/>
<input type="password" name="password" placeholder="Password" value="${ctx.bodyParameters.password || ''}" required="true"/>
<!--${isEnvBrowser ? `<label><input type="checkbox" name="cors" ${ctx.bodyParameters.cors === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''}/> Site disallows CORS, use proxy</label>` : ''}-->
<label><input type="checkbox" name="remember" ${ctx.request.method === 'POST' && ctx.bodyParameters.remember !== 'on' ? '' : 'checked="true"'}/> Remember me</label>
<input type="submit" name="login" value="Login and Save"/>
</form>` : ''}
<!--${true ? `
@ -571,62 +460,6 @@ ${figureHtml}`.trim()),
</form>
` : ''}-->
`, 'Settings');
}
else if (section === 'res' && req.method === 'GET' && isEnvServer)
{
// serve static files if it exists and is allowed
const resPath = req.url.split('/res/').slice(1).join('/res/');
const filePath = (__dirname + path.sep + resPath);
if (resFiles.includes(resPath) && fs.existsSync(filePath)) {
res.setHeader('Content-Type', mime.lookup(filePath));
return res.end(fs.readFileSync(filePath));
} else {
return setPageContent(res, '', (res.statusCode = 404));
}
}
else
{
return setPageContent(res, '', (res.statusCode = 404));
}
return setPageContent(res, '', (res.statusCode = 500));
};
} ];
// todo handle optional options field(s)
const accountDataFromString = (accountString) => {
const tokens = accountString.split(',');
return { instance: tokens[0], username: tokens[1], password: tokens.slice(2).join(',') };
}
const makeFragmentLoggedIn = (accountString) => {
const accountData = accountDataFromString(accountString);
return `<p>Logged in as <i>${accountData.username} @ ${A(accountData.instance)}</i>.</p>`;
}
if (isEnvServer) {
fs = require('fs');
path = require('path');
mime = require('mime-types');
multipart = require('parse-multipart-data');
crypto = require("crypto");
const scriptName = __filename.split(path.sep).slice(-1)[0];
if (process.argv[2] === 'html') {
// dump the base html for static usage
isEnvBrowser = true;
fs.writeFileSync(`${__dirname}${path.sep}${scriptName.split('.').slice(0, -1).join('.')}.html`, newHtmlPage(`
<!-- <script> window.resFilesData = { ${resFiles.map((file) => `"${file}": "${''}"`)} }; </script> -->
<script src="${scriptName}"></script>
`));
} else {
console.log('Running Server...')
resFiles = [scriptName, ...resFiles];
require('http').createServer(handleRequest).listen(serverPort, '0.0.0.0');
}
} else if (isEnvBrowser) {
location.hash ||= '/';
const navigatePage = () => handleRequest({ url: location.hash.slice(1), method: "GET" });
window.onhashchange = () => {
location.hash ||= '/';
navigatePage();
};
navigatePage();
}
main();

View File

@ -6,7 +6,8 @@
"": {
"dependencies": {
"mime-types": "^2.1.35",
"parse-multipart-data": "^1.5.0"
"parse-multipart-data": "^1.5.0",
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
}
},
"node_modules/mime-db": {
@ -32,6 +33,14 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/parse-multipart-data/-/parse-multipart-data-1.5.0.tgz",
"integrity": "sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw=="
},
"node_modules/SpaccDotWeb": {
"version": "indev",
"resolved": "git+ssh://git@gitlab.com/SpaccInc/SpaccDotWeb.git#63dc2e648f93c7917cced6b08305255f8e4ce330",
"dependencies": {
"mime-types": "^2.1.35",
"parse-multipart-data": "^1.5.0"
}
}
}
}

View File

@ -1,6 +1,7 @@
{
"dependencies": {
"mime-types": "^2.1.35",
"parse-multipart-data": "^1.5.0"
"parse-multipart-data": "^1.5.0",
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
}
}