Browse Source

bgpvis

master
Milan Pässler 3 months ago
parent
commit
23a90cf5a4
17 changed files with 169 additions and 654 deletions
  1. +3
    -5
      client/index.html
  2. +1
    -1
      client/package.json
  3. +1
    -1
      client/src/canvas.js
  4. +7
    -1
      client/src/force.js
  5. +44
    -62
      client/src/index.js
  6. +2
    -2
      client/src/state.js
  7. +0
    -77
      server/alpine.js
  8. +0
    -70
      server/archlinux.js
  9. +0
    -67
      server/debian.js
  10. +22
    -95
      server/index.js
  11. +0
    -85
      server/nixpkgs.js
  12. +0
    -56
      server/openwrt.js
  13. +8
    -9
      server/package.json
  14. +0
    -31
      server/util.js
  15. +0
    -59
      server/void.js
  16. +72
    -0
      server/webserver.js
  17. +9
    -33
      server/yarn.lock

+ 3
- 5
client/index.html View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>package dependency viewer</title>
<title>bgp route viewer</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="theme-color" content="#ffffff">
@@ -17,20 +17,18 @@
<div id="loading"><div id="spinner"></div></div>
<div id="menu">
<form id="inputs">
<select id="backend">
</select>
<input type="text" id="name">
<input type="submit" id="go" value="Go">
</form>
<div id="details"></div>
<div class="pusher"></div>
<a id="source-link" href="https://git.pbb.lc/petabyteboy/pkgvis/">
<a id="source-link" href="https://git.pbb.lc/petabyteboy/bgpvis/">
<h2>Fork me on Gitea</h2>
<img src="gitea.svg">
</a>
</div>
<canvas id="canvas"></canvas>
<noscript>JavaScript is required to use package dependency viewer</noscript>
<noscript>JavaScript is required to use bgp route viewer</noscript>
</body>
<script type="module" src="main.js"></script>
</html>

+ 1
- 1
client/package.json View File

@@ -1,5 +1,5 @@
{
"name": "pkgvis-client",
"name": "bgpvis-client",
"version": "1.1.0",
"license": "AGPL-3.0",
"author": "Milan Pässler",


+ 1
- 1
client/src/canvas.js View File

@@ -50,7 +50,7 @@ const drawGraph = () => {
ctx.fillRect(x(node.x), y(node.y), node.weight * view.scale * dpr, node.weight * view.scale * dpr);
ctx.fillStyle = "white";
ctx.font = `${node.weight * Math.sqrt(view.scale) / 10 * (1 + dpr) / 2}em sans-serif`;
ctx.fillText(node.name, x(node.x), y(node.y));
ctx.fillText(node.owner, x(node.x), y(node.y));
}
window.requestAnimationFrame(drawGraph);
};


+ 7
- 1
client/src/force.js View File

@@ -2,7 +2,13 @@ import { graph, nodesAsArray } from "./state";
import * as d3Force from "d3-force";

let simulation = d3Force.forceSimulation();
let linkForce = d3Force.forceLink([]).id(d => String(d.id));
let linkForce = d3Force.forceLink([]).id(d => {
if (!d || !d.as_number) {
console.log(d);
return null;
}
return d.as_number;
});

export const updateGraph = () => {
simulation.nodes(nodesAsArray());


+ 44
- 62
client/src/index.js View File

@@ -2,22 +2,47 @@ import { graph, selectedNode, setGraph, setSelectedNode, nodesAsArray } from "./
import { updateGraph } from "./force";
import "./canvas";

const apiUri = "/api";
const backendSelect = document.getElementById("backend");
const apiUri = "http://192.168.42.136:8080";
let currentRequestNumber = 0;
let loadedGraph;

const go = async (backend, name, selected) => {
const go = async (name, selected) => {
document.getElementById("name").blur();
document.getElementById("name").value = name;
document.getElementById("name").value = decodeURIComponent(name);

document.getElementById("loading").classList.remove("hidden");
currentRequestNumber++;
const thisRequest = currentRequestNumber;
let newGraph;
try {
newGraph = await fetch(`${apiUri}/${backend}/${name}`)
const data = await fetch(`/api/data.json?resource=${name}&unix_timestamps=false`)
.then(resp => resp.json());

newGraph = {
nodes: {
"-1": {
as_number: -1,
owner: "No AS",
},
},
links: [],
};
for (let node of data.data.nodes) {
newGraph.nodes[node.as_number] = node;
}
for (let link of data.data.initial_state) {
let last_n = null;
for (let n of link.path) {
if (last_n) {
newGraph.links.push({
source: last_n,
target: n,
});
}
last_n = n;
}
}
console.log(newGraph);
} catch(err) {
console.log(err)
newGraph = {
@@ -28,7 +53,7 @@ const go = async (backend, name, selected) => {
if (currentRequestNumber !== thisRequest) return;

for (let id of Object.keys(newGraph.nodes)) {
newGraph.nodes[id].weight = Math.min(50, 5 + (newGraph.nodes[id].size / 5000000) || 0);
newGraph.nodes[id].weight = 5 + newGraph.links.filter(l => l.source == id || l.target == id).length / 50; //Math.min(50, 5 + (newGraph.nodes[id].size / 5000000) || 0);
}
setGraph(newGraph);
document.getElementById("loading").classList.add("hidden");
@@ -42,91 +67,48 @@ const go = async (backend, name, selected) => {
updateDetails();
};

const displaySize = (size) => `${Math.round(size / 1024 / 1024 * 10) / 10}MiB`;

const updateDetails = () => {
const details = document.getElementById("details");

details.innerHTML = `
${selectedNode ? `
<p><b>Package ${selectedNode.name}</b></p>
<p>Installed size: ${displaySize(selectedNode.size)}</p>
<br/>
` : ``}
${graph.details ? `
<p><b>Graph for package ${nodesAsArray()[0].name}</b></p>
<p>Total nodes: ${graph.details.count}</p>
<p>Total installed size: ${displaySize(graph.details.size)}</p>
AS${selectedNode.as_number}
` : ``}
`;
};

const handleSubmit = (evt) => {
evt.preventDefault();
const backend = backendSelect[backendSelect.selectedIndex].value;
const name = document.getElementById("name").value;
window.location.hash = `#${backend}:${name}`;
window.location.hash = `#${encodeURIComponent(name)}`;
return true;
};

const handleHashChange = () => {
const hash = window.location.hash.substr(1).split(":");
if (hash.length < 1) return;
const backend = hash[0];
document.querySelector('#backend [value="' + backend + '"]').selected = true;
if (hash.length < 2) return;
const name = hash[1];
const selected = hash[2];
const graphId = `${backend}:${name}`;
console.log(hash);
const name = hash[0];
const selected = hash[1];
const graphId = `${name}`;
if (loadedGraph === graphId) {
updateDetails();
return;
}
loadedGraph = graphId;
go(backend, name, selected);
};

const fetchBackendList = async () => {
const backends = await fetch(`${apiUri}/backends`)
.then(resp => resp.json());

for (let backend of backends) {
const option = document.createElement("option");
option.value = backend;
option.appendChild(document.createTextNode(backend));
backendSelect.appendChild(option);
}

document.querySelector("option:first-child").selected = true;
};

const handleBackendChange = () => {
const hash = window.location.hash.substr(1).split(":");
const backend = backendSelect[backendSelect.selectedIndex].value;
const name = hash[1];
const selected = hash[2];
if (selected) {
window.location.hash = `#${backend}:${name}:${selected}`;
} else if (name) {
window.location.hash = `#${backend}:${name}`;
} else {
window.location.hash = `#${backend}`;
}
go(name, selected);
};

/* initialization */

fetchBackendList().then(() => {
document.getElementById("loading").classList.add("hidden");
document.getElementById("loading").classList.add("hidden");

if (window.location.hash.length) {
handleHashChange();
} else {
document.getElementById("name").focus();
}
});
if (window.location.hash.length) {
handleHashChange();
} else {
document.getElementById("name").focus();
}

backendSelect.addEventListener("change", handleBackendChange);
document.getElementById("inputs").addEventListener("submit", handleSubmit);
window.addEventListener("hashchange", handleHashChange);


+ 2
- 2
client/src/state.js View File

@@ -13,9 +13,9 @@ export const setSelectedNode = (val) => {
selectedNode = val;
const hash = window.location.hash.substr(1).split(":");
if (val) {
window.location.hash = `#${hash[0]}:${hash[1]}:${val.id}`;
window.location.hash = `#${hash[0]}:${val.as_number}`;
} else {
window.location.hash = `#${hash[0]}:${hash[1]}`;
window.location.hash = `#${hash[0]}`;
}
};



+ 0
- 77
server/alpine.js View File

@@ -1,77 +0,0 @@
"use strict";

const { spawn } = require("child_process");
const fetch = require("node-fetch");
const { HTTPError, Collector, stripVersion } = require("./util");
const { promisify } = require("util");
const pipeline = promisify(require("stream").pipeline);

const urls = [
"http://dl-cdn.alpinelinux.org/alpine/edge/community/x86_64/APKINDEX.tar.gz",
"http://dl-cdn.alpinelinux.org/alpine/edge/main/x86_64/APKINDEX.tar.gz",
"http://dl-cdn.alpinelinux.org/alpine/edge/releases/x86_64/APKINDEX.tar.gz",
"http://dl-cdn.alpinelinux.org/alpine/edge/testing/x86_64/APKINDEX.tar.gz",
];

let packages = {};

const getKey = (text, key) => {
let res = "";
text.split("\n").forEach(line => {
if (line.startsWith(key)) res += line.slice(2) + " ";
});
return res.slice(0, -1); // cut last space
};

const getPackageLists = async () => {
const newPackages = {};

for (let url of urls) {
console.log(`fetching ${url}`);

const { body } = await fetch(url);
const child = spawn("tar", ["-xOzf-", "APKINDEX"]);
const collector = new Collector();

await Promise.all([
pipeline(
body,
child.stdin,
),
pipeline(
child.stdout,
collector,
),
]).catch(console.error);

const packageList = collector.result;
packageList.split("\n\n").forEach(pkg => {
const p = {
deps: getKey(pkg, "D").split(" ").map(dep => stripVersion(dep)),
name: getKey(pkg, "P") + getKey(pkg, "V"),
id: getKey(pkg, "P"),
size: getKey(pkg, "I"),
};
newPackages[getKey(pkg, "P")] = p;
for (let name of getKey(pkg, "p").split(" ")) {
newPackages[stripVersion(name)] = p;
}
});
}

packages = newPackages;
console.log("Alpine packages loaded");
}

setInterval(getPackageLists, 24 * 60 * 60 * 1000); // 24 hours
getPackageLists();

const getNode = async (type, name) => {
if (!packages[name]) {
throw new HTTPError(404, "Failed to get package info");
}
return packages[name];
};

module.exports = getNode;

+ 0
- 70
server/archlinux.js View File

@@ -1,70 +0,0 @@
"use strict";

const fetch = require("node-fetch");
const { HTTPError } = require("./util");
const { stripVersion } = require("./util");
let packages = {};

const urls = [
"http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/core/os/x86_64/core.db",
"http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/community/os/x86_64/community.db",
"http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/extra/os/x86_64/extra.db",
"http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/multilib/os/x86_64/multilib.db",
];

const getPackageLists = async () => {
const newPackages = {};

for (let url of urls) {
console.log(`fetching ${url}`);

const packageList = await fetch(url)
.then(res => res.text());

/*
* skip this if you don't like very bad, dirty code
*/

const pkgs = packageList.split("%FILENAME%").map(p => "%FILENAME%" + p);
for (let pkg of pkgs) {
const attrs = {};
const re = /^%[^%]*%[^%]*\n$/gsm;
const matches = pkg.match(re);
if (!matches) continue;
for (let m of matches) {
const s = m.slice(0, -1).split("%\n");
const key = s.shift().slice(1);
const val = s.join("");
attrs[key] = val;
}
const p = {
id: attrs["NAME"],
name: `${attrs["NAME"]}-${attrs["VERSION"]}`,
deps: attrs["DEPENDS"] ? attrs["DEPENDS"].split("\n").map(stripVersion) : [],
size: attrs["ISIZE"],
};

newPackages[attrs["NAME"]] = p;
if (attrs["PROVIDES"]) {
for (let name of attrs["PROVIDES"].split("\n").map(stripVersion)) {
newPackages[name] = p;
}
}
}
}

packages = newPackages;
console.log("ArchLinux packages loaded");
}

setInterval(getPackageLists, 24 * 60 * 60 * 1000); // 24 hours
getPackageLists();

const getNode = async (type, name) => {
if (!packages[name]) {
throw new HTTPError(404, "Failed to get package info");
}
return packages[name];
};

module.exports = getNode;

+ 0
- 67
server/debian.js View File

@@ -1,67 +0,0 @@
"use strict";

const fetch = require("node-fetch");
const InternetMessage = require("internet-message");
const { HTTPError, Collector } = require("./util");
const { createGunzip } = require("zlib");
const { promisify } = require("util");
const pipeline = promisify(require("stream").pipeline);

let packages = {};

const urls = [
"http://ftp.de.debian.org/debian/dists/testing/main/binary-amd64/Packages.gz",
"http://ftp.de.debian.org/debian/dists/testing/contrib/binary-amd64/Packages.gz",
// "http://ftp.de.debian.org/debian/dists/testing/nonfree/binary-amd64/Packages.gz",
];

const getPackageLists = async () => {
const newPackages = {};

for (let url of urls) {
console.log(`fetching ${url}`);

const { body } = await fetch(url);
const collector = new Collector();
await pipeline(
body,
createGunzip(),
collector,
);
const packageList = collector.result;

packageList.split("\n\n")
.forEach(pkg => {
let attrs = InternetMessage.parse(pkg + "\n")
if (attrs.package) {
attrs.depends = (attrs.depends || "").split(", ").map(dep => dep.split(" (")[0]);
attrs.provides = (attrs.provides || "").split(", ").map(dep => dep.split(" (")[0]);
const p = {
deps: attrs.depends,
name: `${attrs.package}-${attrs.version}`,
id: attrs.package,
size: Number(attrs["installed-size"]) * 1000,
};
newPackages[attrs.package] = p;
for (let name of attrs.provides) {
newPackages[name] = p;
}
}
});
}

packages = newPackages;
console.log("Debian packages loaded");
}

setInterval(getPackageLists, 24 * 60 * 60 * 1000); // 24 hours
getPackageLists();

const getNode = async (type, name) => {
if (!packages[name]) {
throw new HTTPError(404, "Failed to get package info");
}
return packages[name];
};

module.exports = getNode;

+ 22
- 95
server/index.js View File

@@ -1,96 +1,23 @@
"use strict";
#!/usr/bin/env node
'use strict';

const url = require('url');
const WebServer = require('./webserver');
const fetch = require('node-fetch');

const server = new WebServer('../client/', 8085, (req, resp) => {
const requrl = url.parse(req.url);
if (requrl.pathname === '/api/data.json') {
fetch(`https://stat.ripe.net/data/bgplay/data.json${requrl.search}`).then(resp => {
return resp.text();
}).then(body => {
resp.writeHead(200, {'Content-Type': 'application/json; charset=UTF-8'});
resp.end(body);
}).catch(err => {
resp.writeHead(502, {'Content-Type': 'text/plain; charset=UTF-8'});
resp.end("failed to fetch");
});
return true;
}
});

const http = require("http");
const url = require("url");
const fetch = require("node-fetch");
const { HTTPError } = require("./util");

const backends = {
NixOS: require("./nixpkgs"),
OpenWRT: require("./openwrt"),
ArchLinux: require("./archlinux"),
Alpine: require("./alpine"),
Void: require("./void"),
Debian: require("./debian"),
};

const addNode = async (graph, backend, node) => {
if (!graph.nodes[node.id]) {
graph.nodes[node.id] = {
...node,
//deps: undefined,
}

graph.details.count++;
graph.details.size += Number(node.size);

const pendingRequests = [];
for (let link of node.deps || []) {
let neighbour;
try {
neighbour = await backend("id", link);
} catch(err) {
continue;
}
graph.links.push({
source: node.id,
target: neighbour.id
});
pendingRequests.push(addNode(graph, backend, neighbour));
}
await Promise.all(pendingRequests);
}
};

const buildGraph = async (backend, name) => {
const graph = {
nodes: {},
links: [],
details: {
count: 0,
size: 0,
},
};
await addNode(graph, backend, await backend("name", name));
return graph;
};

http.createServer(async (req, stream) => {
stream.setHeader("Access-Control-Allow-Origin", "*");

const { pathname } = url.parse(req.url);
let parameters = pathname.split("/");

let res;
try {
if (parameters.length < 3) {
if (parameters.length === 2 && parameters[1] === "backends") {
res = Object.keys(backends);
} else {
throw new HTTPError(404, "Invalid URI");
}
} else {
const backend = backends[parameters[1]];
if (!backend) {
throw new HTTPError(404, "Invalid Backend");
}
const name = parameters[2];

res = await buildGraph(backend, name);
}
} catch(err) {
if (err instanceof HTTPError) {
stream.writeHead(err.code, { "Content-Type": "text/plain" });
stream.end(err.msg);
} else {
console.error(`Unexpected Error: ${err.msg || err}\n${err.stack}`);
stream.writeHead(500, { "Content-Type": "text/plain" });
stream.end("Unexpected Error");
}

return;
}

stream.writeHead(200, { "Content-Type": "application/json" });
stream.end(JSON.stringify(res));
}).listen(process.env.PORT || 8080);

+ 0
- 85
server/nixpkgs.js View File

@@ -1,85 +0,0 @@
"use strict";

const { promisify } = require("util");
const execFile = promisify(require("child_process").execFile);
const fetch = require("node-fetch");
const { HTTPError } = require("./util");

let narinfoCache = {};

const getNarinfo = async (hash) => {
if (!narinfoCache[hash]) {
narinfoCache[hash] = await fetch(`https://cache.nixos.org/${hash}.narinfo`)
.then(res => res.text());
}
return narinfoCache[hash];
};

const getNarinfoKey = async (hash, key) => {
const narinfo = await getNarinfo(hash);
return narinfo.split(`${key}: `)[1].split("\n")[0];
};

const parseDerivationPath = (derivationPath) => {
return {
id: derivationPath.split('-')[0],
name: derivationPath.split('-').slice(1).join('-')
}
};

const getStorePathFromName = async (name) => {
const { stdout } = await execFile("nix-instantiate", ["--strict", "--eval", "<nixpkgs>", "-A", `${name}.outPath`]);
return stdout.split('"')[1];
};

const getStorePathFromHash = (hash) => getNarinfoKey(hash, "StorePath");

const getReferences = async (hash) => {
const key = await getNarinfoKey(hash, "References");
if (!key.length) return [];
return key.split(" ").map(parseDerivationPath);
};

const getNode = async (type, name) => {
let getStorePathFunc;
if (type === "name") {
getStorePathFunc = getStorePathFromName;
} else if (type === "id") {
getStorePathFunc = getStorePathFromHash;
} else {
throw new HTTPError(400, "Invalid request type");
}

let storePath;
try {
storePath = await getStorePathFunc(name);
} catch(err) {
// omit original error for security reasons
throw new HTTPError(404, "Failed to get store path");
}

let res;
try {
const derivationPath = storePath.split("/")[3];
res = parseDerivationPath(derivationPath);
} catch(err) {
throw new HTTPError(500, `Failed to parse store path: ${err}`);
}

try {
const references = await getReferences(res.id);
res.deps = references.map(ref => ref.id);
} catch(err) {
throw new HTTPError(500, `Failed to get references: ${err}`);
}

try {
res.size = Number(await getNarinfoKey(res.id, "NarSize"));
} catch (err) {
throw new HTTPError(500, `Failed to get size: ${err}`);
}

return res;
};

module.exports = getNode;

+ 0
- 56
server/openwrt.js View File

@@ -1,56 +0,0 @@
"use strict";

const fetch = require("node-fetch");
const InternetMessage = require("internet-message");
const { HTTPError } = require("./util");

let packages = {};

const urls = [
"https://downloads.openwrt.org/snapshots/packages/mips_24kc/base/Packages",
"https://downloads.openwrt.org/snapshots/packages/mips_24kc/packages/Packages",
"https://downloads.openwrt.org/snapshots/packages/mips_24kc/routing/Packages",
"https://downloads.openwrt.org/snapshots/packages/mips_24kc/luci/Packages",
"https://downloads.openwrt.org/snapshots/packages/mips_24kc/telephony/Packages",
"https://downloads.openwrt.org/snapshots/targets/ar71xx/generic/packages/Packages"
];

const getPackageLists = async () => {
const newPackages = {};

for (let url of urls) {
console.log(`fetching ${url}`);

const packageList = await fetch(url)
.then(res => res.text());

packageList.split("\n\n")
.forEach(pkg => {
let attrs = InternetMessage.parse(pkg + "\n")
if (attrs.package && attrs.depends) {
attrs.depends = attrs.depends.split(", ").map(dep => dep.split(" (")[0]);
newPackages[attrs.package] = {
deps: attrs.depends,
name: attrs.package,
id: attrs.package,
size: Number(attrs["installed-size"]),
};
}
});
}

packages = newPackages;
console.log("OpenWRT packages loaded");
}

setInterval(getPackageLists, 24 * 60 * 60 * 1000); // 24 hours
getPackageLists();

const getNode = async (type, name) => {
if (!packages[name]) {
throw new HTTPError(404, "Failed to get package info");
}
return packages[name];
};

module.exports = getNode;

+ 8
- 9
server/package.json View File

@@ -1,12 +1,11 @@
{
"name": "pkgvis-server",
"version": "1.1.0",
"license": "AGPL-3.0",
"author": "Milan Pässler",
"bin": "index.js",
"dependencies": {
"internet-message": "^1.0.0",
"node-fetch": "^2.3.0",
"plist": "^3.0.1"
}
"mime": "^2.4.4",
"node-fetch": "^2.6.0"
},
"name": "bgpvis-server",
"version": "1.0.0",
"repository": "https://git.pbb.lc/petabyteboy/bgpvis/",
"author": "Milan Pässler",
"license": "AGPL-3.0"
}

+ 0
- 31
server/util.js View File

@@ -1,31 +0,0 @@
"use strict";

const { Writable } = require("stream");

const stripVersion = (s) => s.split(/[<>]?=.*/)[0];

class Collector extends Writable {
constructor() {
super();
this.result = "";
}

_write(chunk, enc, next) {
this.result += String(chunk);
next();
}
}

class HTTPError extends Error {
constructor(code, msg) {
super();
this.code = code;
this.msg = msg;
}
};

module.exports = {
stripVersion,
Collector,
HTTPError,
};

+ 0
- 59
server/void.js View File

@@ -1,59 +0,0 @@
"use strict";

const { createGunzip } = require("zlib");
const { HTTPError, Collector, stripVersion } = require("./util");
const fetch = require("node-fetch");
const plist = require('plist');
const { promisify } = require("util");
const pipeline = promisify(require("stream").pipeline);

const urls = [
"https://alpha.de.repo.voidlinux.org/current/x86_64-repodata",
"https://alpha.de.repo.voidlinux.org/current/nonfree/x86_64-repodata",
"https://alpha.de.repo.voidlinux.org/current/debug/x86_64-repodata",
];

let packages = {};

const getPackageLists = async () => {
const newPackages = {};

for (let url of urls) {
console.log(`fetching ${url}`);
const { body } = await fetch(url);
const collector = new Collector();
await pipeline(
body,
createGunzip(),
collector,
);
const packageList = plist.parse(collector.result);
Object.keys(packageList).forEach(name => {
const pkg = packageList[name];
const p = {
name: pkg["pkgver"],
deps: (pkg["run_depends"] || []).map(stripVersion),
id: name,
size: pkg["installed_size"],
};
newPackages[name] = p;
});
}

packages = newPackages;
console.log("Void packages loaded");
}

setInterval(getPackageLists, 24 * 60 * 60 * 1000); // 24 hours
getPackageLists();

const getNode = async (type, name) => {
if (!packages[name]) {
throw new HTTPError(404, "Failed to get package info");
}
return packages[name];
};

module.exports = getNode;

+ 72
- 0
server/webserver.js View File

@@ -0,0 +1,72 @@
'use strict';

// adapted from https://gist.github.com/kentbrew/764238

const fs = require('fs');
const http = require('http');
const mime = require('mime');
const path = require('path');
const url = require('url');

class WebServer {
constructor(dir, port, hook) {
// the main thing
const server = http.createServer((req, resp) => {

// extract the pathname from the request URL
const pathname = url.parse(req.url).pathname;

if (hook) {
if (hook(req, resp)) {
return;
}
}

// add the home directory, /public or whatever
let filename = path.join(process.cwd(), dir, pathname);

// if the requested path has no file extension, assume it's a directory
// caution: if you are shipping an API, this is the wrong thing to do
if (!path.extname(filename)) {
filename = filename + '/index.html';
}

// does this path exist?
fs.exists(filename, (gotPath) => {

// no, bail out
if (!gotPath) {
resp.writeHead(404, {'Content-Type': 'text/plain; charset=UTF-8'});
resp.write('404 Not Found');
resp.end();
return;
}

// still here? filename is good
// look up the mime type by file extension
resp.writeHead(200, {'Content-Type': mime.getType(filename) + '; charset=UTF-8'});

// read and pass the file as a stream. Not really sure if this is better,
// but it feels less block-ish than reading the whole file
// and we get to do awesome things with listeners
fs.createReadStream(filename, {
'flags': 'r',
'encoding': 'binary',
'mode': parseInt('0666', 8),
'bufferSize': 4 * 1024,
}).addListener('data', (chunk) => {
resp.write(chunk, 'binary');
}).addListener('close', () => {
resp.end();
});
});
});

// fire it up
server.listen(port);

return server;
}
}

module.exports = WebServer;

+ 9
- 33
server/yarn.lock View File

@@ -2,36 +2,12 @@
# yarn lockfile v1


base64-js@^1.2.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==

internet-message@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/internet-message/-/internet-message-1.0.0.tgz#28f65368a22a6207994c6e21f0e3bffbf1278d2b"
integrity sha1-KPZTaKIqYgeZTG4h8OO/+/EnjSs=

node-fetch@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==

plist@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c"
integrity sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ==
dependencies:
base64-js "^1.2.3"
xmlbuilder "^9.0.7"
xmldom "0.1.x"

xmlbuilder@^9.0.7:
version "9.0.7"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=

xmldom@0.1.x:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
mime@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==

node-fetch@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==

Loading…
Cancel
Save