mirror of https://github.com/aredn/aredn.git
668 lines
22 KiB
Plaintext
668 lines
22 KiB
Plaintext
{%
|
|
/*
|
|
* Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks
|
|
* Copyright (C) 2024 Tim Wilkinson
|
|
* See Contributors file for additional contributors
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Additional Terms:
|
|
*
|
|
* Additional use restrictions exist on the AREDN® trademark and logo.
|
|
* See AREDNLicense.txt for more info.
|
|
*
|
|
* Attributions to the AREDN® Project must be retained in the source code.
|
|
* If importing this code into a new or existing project attribution
|
|
* to the AREDN® project must be added to the source code.
|
|
*
|
|
* You must not misrepresent the origin of the material contained within.
|
|
*
|
|
* Modified versions must be modified to attribute to the original source
|
|
* and be marked in reasonable ways as differentiate it from the original
|
|
* version
|
|
*/
|
|
%}
|
|
{%
|
|
import * as config from "./config.uc";
|
|
import * as fs from "fs";
|
|
import * as math from "math";
|
|
import * as uci from "uci";
|
|
import * as ubus from "ubus";
|
|
import * as log from "log";
|
|
import * as zlib from "zlib";
|
|
import * as lucihttp from "lucihttp";
|
|
import * as configuration from "aredn.configuration";
|
|
import * as hardware from "aredn.hardware";
|
|
import * as lqm from "aredn.lqm";
|
|
import * as network from "aredn.network";
|
|
import * as olsr from "aredn.olsr";
|
|
import * as units from "aredn.units";
|
|
import * as radios from "aredn.radios";
|
|
import * as messages from "aredn.messages";
|
|
import * as mesh from "aredn.mesh";
|
|
import * as constants from "./constants.uc";
|
|
|
|
const pageCache = {};
|
|
const resourceVersions = {};
|
|
const lockfile = "/tmp/ui.lock";
|
|
|
|
log.openlog("uhttpd.aredn", log.LOG_PID, log.LOG_USER);
|
|
|
|
fs.writefile(`${config.application}/resource/css/themes/default.css`, `${fs.readfile(`${config.application}/resource/css/themes/light.css`)}@media (prefers-color-scheme: dark) {\n${fs.readfile(`${config.application}/resource/css/themes/dark.css`)}\n}`);
|
|
if (!fs.access(`${config.application}/resource/css/theme.css`)) {
|
|
fs.symlink("themes/default.css", `${config.application}/resource/css/theme.css`);
|
|
}
|
|
|
|
if (config.preload) {
|
|
function cp(path) {
|
|
const dir = fs.opendir(`${config.application}${path}`);
|
|
for (;;) {
|
|
const entry = dir.read();
|
|
if (!entry) {
|
|
break;
|
|
}
|
|
if (match(entry, /\.ut$/)) {
|
|
const tpath = `${config.application}${path}${entry}`;
|
|
pageCache[tpath] = loadfile(tpath, { raw_mode: false });
|
|
}
|
|
}
|
|
}
|
|
cp("/main/");
|
|
cp("/partial/");
|
|
}
|
|
|
|
if (config.resourcehash) {
|
|
function prepareResource(id, resource)
|
|
{
|
|
const path = `${config.application}/resource/${resource}`;
|
|
const pathgz = `${config.application}/resource/${resource}.gz`;
|
|
if (fs.access(path)) {
|
|
fs.unlink(pathgz);
|
|
system(`/bin/gzip -k ${path}`);
|
|
}
|
|
const md = fs.popen(`/usr/bin/md5sum ${pathgz}`);
|
|
resourceVersions[id] = match(md.read("all"), /^([0-9a-f]+)/)[1];
|
|
md.close();
|
|
const m = match(path, /^(.+)\.([a-z]+)$/);
|
|
fs.symlink(pathgz, `${m[1]}.${resourceVersions[id]}.${m[2]}.gz`);
|
|
}
|
|
prepareResource("usercss", "css/user.css");
|
|
prepareResource("admincss", "css/admin.css");
|
|
prepareResource("mobilecss", "css/mobile.css");
|
|
prepareResource("themecss", "css/theme.css");
|
|
prepareResource("htmx", "js/htmx.min.js");
|
|
prepareResource("meshpage", "js/meshpage.js");
|
|
let cthemeversion = null;
|
|
const ctheme = fs.readlink(`${config.application}/resource/css/theme.css`);
|
|
const themes = fs.lsdir(`${config.application}/resource/css/themes`);
|
|
for (let i = 0; i < length(themes); i++) {
|
|
const theme = themes[i];
|
|
if (match(theme, /^.*\.css$/)) {
|
|
prepareResource("themecss", `css/themes/${theme}`);
|
|
if (ctheme === `themes/${theme}`) {
|
|
cthemeversion = resourceVersions.themecss;
|
|
}
|
|
fs.unlink(`${config.application}/resource/css/theme.${resourceVersions.themecss}.css.gz`);
|
|
fs.symlink(`themes/${replace(theme, /\.css$/, "")}.${resourceVersions.themecss}.css.gz`, `${config.application}/resource/css/theme.${resourceVersions.themecss}.css.gz`);
|
|
}
|
|
}
|
|
resourceVersions.themecss = cthemeversion;
|
|
fs.unlink(`${config.application}/resource/css/theme.version`);
|
|
fs.symlink(cthemeversion, `${config.application}/resource/css/theme.version`);
|
|
}
|
|
else {
|
|
let dir = fs.lsdir(`${config.application}/resource/css`);
|
|
for (let i = 0; i < length(dir); i++) {
|
|
if (match(dir[i], /\.gz$/)) {
|
|
fs.unlink(`${config.application}/resource/css/${dir[i]}`);
|
|
}
|
|
}
|
|
dir = fs.lsdir(`${config.application}/resource/css/themes`);
|
|
for (let i = 0; i < length(dir); i++) {
|
|
if (match(dir[i], /\.gz$/)) {
|
|
fs.unlink(`${config.application}/resource/css/themes/${dir[i]}`);
|
|
}
|
|
}
|
|
dir = fs.lsdir(`${config.application}/resource/js`);
|
|
for (let i = 0; i < length(dir); i++) {
|
|
if (match(dir[i], /\.gz$/) && dir[i] !== "htmx.min.js.gz") {
|
|
fs.unlink(`${config.application}/resource/js/${dir[i]}`);
|
|
}
|
|
}
|
|
fs.unlink(`${config.application}/resource/css/theme.version`);
|
|
}
|
|
|
|
fs.writefile(lockfile, "");
|
|
|
|
global._R = function(path, arg)
|
|
{
|
|
const tpath = `${config.application}/partial/${path}.ut`;
|
|
const fn = pageCache[tpath] || loadfile(tpath, { raw_mode: false });
|
|
let old = inner;
|
|
let r = "";
|
|
try {
|
|
inner = arg;
|
|
r = render(fn);
|
|
}
|
|
catch (_) {
|
|
}
|
|
inner = old;
|
|
return r;
|
|
};
|
|
|
|
global._H = function(str)
|
|
{
|
|
return includeHelp ? `<div class="help">${str}</div>` : "";
|
|
};
|
|
|
|
const uciMethods =
|
|
{
|
|
init: function()
|
|
{
|
|
if (!cursor)
|
|
{
|
|
cursor = uci.cursor();
|
|
}
|
|
},
|
|
|
|
add: function(a, b)
|
|
{
|
|
this.init();
|
|
cursor.load(a);
|
|
return cursor.add(a, b);
|
|
},
|
|
|
|
get: function(a, b, c)
|
|
{
|
|
this.init();
|
|
return cursor.get(a, b, c);
|
|
},
|
|
|
|
set: function(a, b, c, d)
|
|
{
|
|
this.init();
|
|
if (d === undefined) {
|
|
cursor.set(a, b, c);
|
|
}
|
|
else {
|
|
if (type(d) === "array") {
|
|
for (let i = 0; i < length(d); i++) {
|
|
d[i] = replace(replace(`${d[i]}`, /['<>]/g, ""), /[\r\n]/g, " ");
|
|
}
|
|
}
|
|
else {
|
|
d = replace(replace(`${d}`, /['<>]/g, ""), /[\r\n]/g, " ");
|
|
}
|
|
cursor.set(a, b, c, d);
|
|
}
|
|
},
|
|
|
|
foreach: function(a, b, fn)
|
|
{
|
|
this.init();
|
|
cursor.foreach(a, b, fn);
|
|
},
|
|
|
|
commit: function(a)
|
|
{
|
|
if (cursor) {
|
|
cursor.commit(a);
|
|
}
|
|
},
|
|
|
|
"delete": function(a, b)
|
|
{
|
|
this.init();
|
|
cursor.delete(a, b);
|
|
},
|
|
|
|
error: function()
|
|
{
|
|
if (cursor) {
|
|
return cursor.error();
|
|
}
|
|
}
|
|
};
|
|
|
|
const uciMeshMethods =
|
|
{
|
|
init: function()
|
|
{
|
|
if (!cursorm)
|
|
{
|
|
cursorm = uci.cursor("/etc/config.mesh");
|
|
}
|
|
},
|
|
|
|
load: function(a)
|
|
{
|
|
this.init();
|
|
cursorm.load(a);
|
|
},
|
|
|
|
add: function(a, b)
|
|
{
|
|
this.init();
|
|
cursorm.load(a);
|
|
return cursorm.add(a, b);
|
|
},
|
|
|
|
get: function(a, b, c)
|
|
{
|
|
this.init();
|
|
return cursorm.get(a, b, c);
|
|
},
|
|
|
|
set: function(a, b, c, d)
|
|
{
|
|
this.init();
|
|
if (d === undefined) {
|
|
cursorm.set(a, b, c);
|
|
}
|
|
else {
|
|
if (type(d) === "array") {
|
|
for (let i = 0; i < length(d); i++) {
|
|
d[i] = replace(replace(`${d[i]}`, /['<>]/g, ""), /[\r\n]/g, " ");
|
|
}
|
|
}
|
|
else {
|
|
d = replace(replace(`${d}`, /['<>]/g, ""), /[\r\n]/g, " ");
|
|
}
|
|
cursorm.set(a, b, c, d);
|
|
}
|
|
},
|
|
|
|
foreach: function(a, b, fn)
|
|
{
|
|
this.init();
|
|
cursorm.foreach(a, b, fn);
|
|
},
|
|
|
|
commit: function(a)
|
|
{
|
|
if (cursorm) {
|
|
cursorm.commit(a);
|
|
}
|
|
},
|
|
|
|
"delete": function(a, b)
|
|
{
|
|
this.init();
|
|
cursorm.delete(a, b);
|
|
},
|
|
|
|
error: function()
|
|
{
|
|
if (cursorm) {
|
|
return cursorm.error();
|
|
}
|
|
}
|
|
};
|
|
|
|
const auth = {
|
|
isAdmin: false,
|
|
key: null,
|
|
age: 315360000, // 10 years
|
|
|
|
DAYS: [ "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" ],
|
|
MONTHS: [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ],
|
|
|
|
initKey: function()
|
|
{
|
|
if (!this.key) {
|
|
const f = fs.open("/etc/shadow");
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
if (index(l, "root:") === 0) {
|
|
this.key = trim(l);
|
|
break;
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
},
|
|
|
|
runAuthentication: function(env)
|
|
{
|
|
const cookieheader = env.headers?.cookie;
|
|
if (cookieheader) {
|
|
const ca = split(cookieheader, ";");
|
|
for (let i = 0; i < length(ca); i++) {
|
|
const cookie = trim(ca[i]);
|
|
if (index(cookie, "authV1=") === 0) {
|
|
this.initKey();
|
|
if (this.key == b64dec(substr(cookie, 7))) {
|
|
this.isAdmin = true;
|
|
}
|
|
else {
|
|
this.isAdmin = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (config.forceauth) {
|
|
this.isAdmin = true;
|
|
}
|
|
},
|
|
|
|
authenticate: function(password)
|
|
{
|
|
if (!this.isAdmin) {
|
|
this.initKey();
|
|
const s = split(this.key, /[:$]/); // s[3] = salt, s[4] = hashed
|
|
const f = fs.popen(`exec /usr/bin/mkpasswd -S '${s[3]}' '${password}'`);
|
|
if (f) {
|
|
const pwd = rtrim(f.read("all"));
|
|
f.close();
|
|
if (index(this.key, `root:${pwd}:`) === 0) {
|
|
const time = clock();
|
|
const gm = gmtime(time[0] + this.age);
|
|
const tm = `${this.DAYS[gm.wday]}, ${gm.mday} ${this.MONTHS[gm.mon]} ${gm.year} 00:00:00 GMT`;
|
|
response.headers["Set-Cookie"] = `authV1=${b64enc(this.key)}; Path=/; Domain=${replace(request.headers.host, /:\d+$/, "")}; Expires=${tm}; SameSite=Strict`;
|
|
this.isAdmin = true;
|
|
}
|
|
}
|
|
}
|
|
return this.isAdmin;
|
|
},
|
|
|
|
deauthenticate: function()
|
|
{
|
|
if (this.isAdmin) {
|
|
response.headers["Set-Cookie"] = `authV1=; Path=/; Domain=${replace(request.headers.host, /:\d+$/, "")}; Max-Age=0;`;
|
|
this.isAdmin = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const ubusMethods =
|
|
{
|
|
call: function(path, method)
|
|
{
|
|
if (!connection) {
|
|
connection = ubus.connect();
|
|
}
|
|
return connection.call(path, method);
|
|
}
|
|
};
|
|
|
|
const rePath = /^\/([-a-z]*)(.*)$/;
|
|
|
|
global.handle_request = function(env)
|
|
{
|
|
const path = match(env.PATH_INFO, rePath);
|
|
const page = path[1] || "status";
|
|
const secured = index(path[2], "/e/") === 0;
|
|
|
|
if (path[2] == "" || secured) {
|
|
let tpath;
|
|
if (!configuration.isConfigured()) {
|
|
tpath = `${config.application}/main/firstuse-ram.ut`;
|
|
const f = fs.open("/proc/mounts");
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
if (index(l, "overlay") !== -1 || index(l, "ext4") !== -1) {
|
|
tpath = `${config.application}/main/firstuse.ut`;
|
|
const mac2 = uci.cursor("/etc/local/uci").get("hsmmmesh", "settings", "mac2");
|
|
if (!match(mac2, /^\d+\.\d+\.\d+$/)) {
|
|
tpath = `${config.application}/main/firstuse-start.ut`;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
else if (secured) {
|
|
tpath = `${config.application}/main${env.PATH_INFO}.ut`;
|
|
}
|
|
else if ((env.HTTP_HOST === "localnode" || env.HTTP_HOST === "localnode.local.mesh") && env.REQUEST_URI == "/a/status" && !env.headers["hx-boosted"]) {
|
|
uhttpd.send(`Status: 302 Found\r\nLocation: http://${configuration.getName()}.local.mesh\r\n\r\n`);
|
|
return;
|
|
}
|
|
else {
|
|
tpath = `${config.application}/main/${page}.ut`;
|
|
if (!pageCache[tpath] && !fs.access(tpath)) {
|
|
tpath = `${config.application}/main/app.ut`;
|
|
}
|
|
}
|
|
|
|
if (pageCache[tpath] || fs.access(tpath)) {
|
|
auth.runAuthentication(env);
|
|
if (config.authenable && !auth.isAdmin) {
|
|
if (secured) {
|
|
uhttpd.send("Status: 401 Unauthorized\r\n\r\n");
|
|
return;
|
|
}
|
|
if (env.REQUEST_METHOD !== "GET" && configuration.isConfigured()) {
|
|
uhttpd.send("Status: 401 Unauthorized\r\n\r\n");
|
|
return;
|
|
}
|
|
}
|
|
let changelock = null;
|
|
if (env.REQUEST_METHOD !== "GET") {
|
|
changelock = fs.open(lockfile);
|
|
if (!(changelock && changelock.lock("x"))) {
|
|
uhttpd.send("Status: 500 Internal server error\r\n\r\n");
|
|
return;
|
|
}
|
|
}
|
|
const args = {};
|
|
if (env.CONTENT_TYPE === "application/x-www-form-urlencoded") {
|
|
let b = "";
|
|
for (;;) {
|
|
const v = uhttpd.recv(10240);
|
|
if (!length(v)) {
|
|
break;
|
|
}
|
|
b += v;
|
|
}
|
|
const v = split(b, "&");
|
|
for (let i = 0; i < length(v); i++) {
|
|
const kv = split(v[i], "=");
|
|
const k = uhttpd.urldecode(kv[0]);
|
|
if (!(k in args)) {
|
|
args[k] = uhttpd.urldecode(kv[1]);
|
|
}
|
|
}
|
|
}
|
|
if (index(env.CONTENT_TYPE, "multipart/form-data") === 0) {
|
|
let key;
|
|
let val;
|
|
let header;
|
|
let file;
|
|
let parser;
|
|
parser = lucihttp.multipart_parser(env.CONTENT_TYPE, (what, buffer, length) => {
|
|
switch (what) {
|
|
case parser.PART_INIT:
|
|
key = null;
|
|
val = null;
|
|
break;
|
|
case parser.HEADER_NAME:
|
|
header = lc(buffer);
|
|
break;
|
|
case parser.HEADER_VALUE:
|
|
if (header === "content-disposition") {
|
|
const filename = lucihttp.header_attribute(buffer, "filename");
|
|
key = lucihttp.header_attribute(buffer, "name");
|
|
file = {
|
|
name: `/tmp/${key}`,
|
|
filename: filename
|
|
};
|
|
val = filename;
|
|
}
|
|
break;
|
|
case parser.PART_BEGIN:
|
|
if (file) {
|
|
fs.writefile("/proc/sys/vm/drop_caches", "3");
|
|
file.fd = fs.open(file.name, "w");
|
|
return false
|
|
}
|
|
break;
|
|
case parser.PART_DATA:
|
|
if (file) {
|
|
file.fd.write(buffer);
|
|
}
|
|
else {
|
|
val = buffer;
|
|
}
|
|
break;
|
|
case parser.PART_END:
|
|
if (file) {
|
|
file.fd.close();
|
|
file.fd = null;
|
|
args[key] = file.name;
|
|
}
|
|
else if (key) {
|
|
args[key] = val;
|
|
}
|
|
key = null;
|
|
val = null;
|
|
file = null;
|
|
break;
|
|
case parser.ERROR:
|
|
log.syslog(log.LOG_ERR, `multipart error: ${buffer}`);
|
|
break;
|
|
}
|
|
return true;
|
|
});
|
|
for (;;) {
|
|
const v = uhttpd.recv(10240);
|
|
if (!length(v)) {
|
|
parser.parse(null);
|
|
break;
|
|
}
|
|
parser.parse(v);
|
|
}
|
|
}
|
|
const response = { statusCode: 200, headers: { "Content-Type": "text/html", "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*" } };
|
|
const fn = pageCache[tpath] || loadfile(tpath, { raw_mode: false });
|
|
const isMobile = !!(config.forcemobile || match(env.HTTP_USER_AGENT, /iphone/i) || match(env.HTTP_USER_AGENT, /android.*mobile/i));
|
|
let res = "";
|
|
try {
|
|
res = render(call, fn, null, {
|
|
config: config,
|
|
constants: constants,
|
|
versions: resourceVersions,
|
|
request: { env: env, headers: env.headers, args: args, page: page, mobile: isMobile },
|
|
response: response,
|
|
uci: uciMethods,
|
|
uciMesh: uciMeshMethods,
|
|
ubus: ubusMethods,
|
|
auth: auth,
|
|
includeHelp: (env.headers || {})["include-help"] === "1",
|
|
includeAdvanced: (env.headers || {})["include-advanced"] === "1",
|
|
fs: fs,
|
|
configuration: configuration,
|
|
hardware: hardware,
|
|
lqm: lqm,
|
|
network: network,
|
|
olsr: olsr,
|
|
units: units,
|
|
radios: radios,
|
|
math: math,
|
|
log: log,
|
|
messages: messages,
|
|
mesh: mesh
|
|
});
|
|
}
|
|
catch (e) {
|
|
log.syslog(log.LOG_ERR, `${e.message}\n${e.stacktrace[0].context}`);
|
|
res = `<div><b>ERROR: ${e.message}</b><div><pre>${e.stacktrace[0].context}</pre></div>`;
|
|
}
|
|
if (!response.override) {
|
|
let gzip = false;
|
|
if (env.HTTP_ACCEPT_ENCODING && index(env.HTTP_ACCEPT_ENCODING, "gzip") !== -1 && config.compress) {
|
|
gzip = true;
|
|
res = zlib.deflate(res, true);
|
|
}
|
|
response.headers["Content-Length"] = `${length(res)}`;
|
|
uhttpd.send(
|
|
`Status: ${response.statusCode} OK\r\n`,
|
|
(gzip ? `Content-Encoding: gzip\r\n` : ``),
|
|
join("", map(keys(response.headers), k => k + ": " + response.headers[k] + "\r\n")),
|
|
"\r\n",
|
|
res
|
|
);
|
|
}
|
|
if (changelock) {
|
|
changelock.close();
|
|
}
|
|
if (response.reboot) {
|
|
system(`(sleep 2; sync; ${response.reboot})&`);
|
|
}
|
|
return;
|
|
}
|
|
uhttpd.send("Status: 404 Not Found\r\n\r\n");
|
|
return;
|
|
}
|
|
|
|
const rpath = `${config.application}/resource/${env.PATH_INFO}`;
|
|
const gzrpath = `${rpath}.gz`;
|
|
|
|
const localnode = env.HTTP_HOST === "localnode" || env.HTTP_HOST === "localnode.local.mesh";
|
|
const cachecontrol = localnode || !config.resourcehash ? "no-store" : "max-age=604800";
|
|
if (localnode && config.resourcehash && fs.access(gzrpath)) {
|
|
const themecss = fs.readlink(`${config.application}/resource/css/theme.version`);
|
|
if (env.PATH_INFO !== `/css/theme.${themecss}.css` && index(env.PATH_INFO, "/css/theme.") === 0 && uciMethods.get("aredn", "@theme[0]", "portable") === "1") {
|
|
uhttpd.send(`Status: 302 Found\r\nContent-Type: text/css\r\nCache-Control: no-store\r\nLocation: /a/css/theme.${themecss}.css\r\n\r\n`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (fs.access(gzrpath)) {
|
|
uhttpd.send("Status: 200 OK\r\nContent-Encoding: gzip\r\n");
|
|
if (substr(rpath, -3) === ".js") {
|
|
uhttpd.send("Content-Type: application/javascript\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".css") {
|
|
uhttpd.send("Content-Type: text/css\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".svg") {
|
|
uhttpd.send("Content-Type: image/svg+xml\r\n");
|
|
}
|
|
const res = fs.readfile(gzrpath);
|
|
uhttpd.send(`Cache-Control: ${cachecontrol}\r\nContent-Length: ${length(res)}\n`);
|
|
uhttpd.send("\r\n", res);
|
|
return;
|
|
}
|
|
|
|
if (fs.access(rpath)) {
|
|
uhttpd.send("Status: 200 OK\r\n");
|
|
if (substr(rpath, -3) === ".js") {
|
|
uhttpd.send("Content-Type: application/javascript\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".png") {
|
|
uhttpd.send("Content-Type: image/png\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".jpg") {
|
|
uhttpd.send("Content-Type: image/jpeg\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".svg") {
|
|
uhttpd.send("Content-Type: image/svg+xml\r\n");
|
|
}
|
|
else if (substr(rpath, -4) === ".css") {
|
|
uhttpd.send("Content-Type: text/css\r\n");
|
|
}
|
|
const res = fs.readfile(rpath);
|
|
uhttpd.send(`Cache-Control: ${cachecontrol}\r\nContent-Length: ${length(res)}\n`);
|
|
uhttpd.send("\r\n", res);
|
|
return;
|
|
}
|
|
|
|
uhttpd.send(`Status: 404 Not Found\r\nCache-Control: ${cachecontrol}\r\n\r\n`);
|
|
};
|
|
%}
|