@jamesmddoyle/phantom 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard-ui/_app/version.json +1 -1
- package/dist/dashboard-ui/index.html +3 -3
- package/dist/index.js +44 -38
- package/dist/package.json +2 -2
- package/dist/router.js +24 -1
- package/dist/static.js +29 -7
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1793230346425"}
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
<div style="display: contents">
|
|
23
23
|
<script>
|
|
24
24
|
{
|
|
25
|
-
|
|
25
|
+
__sveltekit_1qwiifu = {
|
|
26
26
|
base: "/_dashboard",
|
|
27
|
-
version: "
|
|
27
|
+
version: "1793230346425",
|
|
28
28
|
assets: "/_dashboard"
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const element = document.currentScript.parentElement;
|
|
32
32
|
|
|
33
33
|
import("/_dashboard/_app/immutable/entry/start.DRnrjJWJ.js").then(async (kit) => {
|
|
34
|
-
kit.init(
|
|
34
|
+
kit.init(__sveltekit_1qwiifu);
|
|
35
35
|
const app = await import("/_dashboard/_app/immutable/entry/app.xWq8izru.js");
|
|
36
36
|
kit.start(app, element);
|
|
37
37
|
});
|
package/dist/index.js
CHANGED
|
@@ -63,9 +63,8 @@ server.on("connection", socket => {
|
|
|
63
63
|
socketManager.add(socket);
|
|
64
64
|
const socketID = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
65
65
|
let requestBuffer = Buffer.alloc(0);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
socket.on("data", async (chunk) => {
|
|
66
|
+
let currentRequest = "";
|
|
67
|
+
async function handleData(chunk) {
|
|
69
68
|
// if its a websocket connection, ignore the http parser
|
|
70
69
|
if (wsClients.has(socketID))
|
|
71
70
|
return;
|
|
@@ -73,6 +72,7 @@ server.on("connection", socket => {
|
|
|
73
72
|
if (isRequestComplete(requestBuffer)) {
|
|
74
73
|
const request = parseRequest(requestBuffer);
|
|
75
74
|
if (request) {
|
|
75
|
+
currentRequest = `${request.method} ${request.path}`;
|
|
76
76
|
if (request.method === "OPTIONS") {
|
|
77
77
|
logger.info(`CORS Preflight ${request.path}`);
|
|
78
78
|
sendCorsPreflightResponse(socket);
|
|
@@ -113,51 +113,57 @@ server.on("connection", socket => {
|
|
|
113
113
|
const response = handleRouting(request);
|
|
114
114
|
request.params = response.params;
|
|
115
115
|
const isAdmin = request.path.startsWith("/_admin/");
|
|
116
|
-
let logEntry = null;
|
|
117
116
|
if (isAdmin && request.path === "/_admin/clear" && request.method === "POST") {
|
|
118
117
|
requestHistory.length = 0;
|
|
119
118
|
logger.admin("Request history cleared");
|
|
120
119
|
}
|
|
121
120
|
const delay = options.delay ?? response.delay ?? 0;
|
|
121
|
+
requestBuffer = Buffer.alloc(0);
|
|
122
122
|
if (delay > 0) {
|
|
123
123
|
logger.info(`Delaying response by ${delay}ms`);
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
sendResponse(socket, response.status, response.message, response.body, response.headers);
|
|
157
|
-
}, delay);
|
|
158
|
-
requestBuffer = Buffer.alloc(0);
|
|
126
|
+
if (socket.destroyed)
|
|
127
|
+
return;
|
|
128
|
+
const errorRate = options.errorRate ?? response.errorRate ?? 0;
|
|
129
|
+
const shouldError = Math.random() < errorRate;
|
|
130
|
+
if (!isAdmin) {
|
|
131
|
+
const logEntry = {
|
|
132
|
+
id: crypto.randomUUID(),
|
|
133
|
+
method: request.method,
|
|
134
|
+
path: request.path,
|
|
135
|
+
timestamp: start,
|
|
136
|
+
headers: request.headers,
|
|
137
|
+
body: request.body.toString("utf-8"),
|
|
138
|
+
status: shouldError ? 500 : response.status,
|
|
139
|
+
duration: Date.now() - start,
|
|
140
|
+
responseBody: shouldError ? { error: "Artificial failure hit!" } : response.body,
|
|
141
|
+
};
|
|
142
|
+
requestHistory.push(logEntry);
|
|
143
|
+
if (requestHistory.length > 100)
|
|
144
|
+
requestHistory.shift();
|
|
145
|
+
broadcast(wsClients, logEntry);
|
|
146
|
+
logger.request(request.method, logEntry.status, request.path, logEntry.duration);
|
|
147
|
+
}
|
|
148
|
+
if (shouldError) {
|
|
149
|
+
logger.error("Artificial failure hit!");
|
|
150
|
+
sendResponse(socket, 500, "Internal Server Error", {
|
|
151
|
+
error: "Artificial failure hit!",
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
sendResponse(socket, response.status, response.message, response.body, response.headers);
|
|
159
156
|
}
|
|
160
157
|
}
|
|
158
|
+
}
|
|
159
|
+
socket.on("data", (chunk) => {
|
|
160
|
+
handleData(chunk).catch((error) => {
|
|
161
|
+
requestBuffer = Buffer.alloc(0);
|
|
162
|
+
logger.error(`Failed to handle ${currentRequest || "a request"}`, error);
|
|
163
|
+
if (socket.writableEnded || socket.destroyed)
|
|
164
|
+
return;
|
|
165
|
+
sendResponse(socket, 500, "Internal Server Error", { error: "Internal server error" });
|
|
166
|
+
});
|
|
161
167
|
});
|
|
162
168
|
socket.on("close", () => {
|
|
163
169
|
socketManager.remove(socket);
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamesmddoyle/phantom",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "API mocker and traffic inspector with a built-in dashboard.",
|
|
5
5
|
"author": "James Doyle",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"directory": "server"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"phantom": "
|
|
13
|
+
"phantom": "dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
package/dist/router.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { getActiveRoutes, writeRoutes } from "./config.js";
|
|
2
2
|
import { VERSION } from "./index.js";
|
|
3
3
|
export function handleRouting(request, customRoutes) {
|
|
4
|
+
if (!hasValidEncoding(request.path)) {
|
|
5
|
+
return { status: 400, message: "Bad Request", body: { error: `Path ${request.path} has invalid percent-encoding` }, params: {} };
|
|
6
|
+
}
|
|
4
7
|
if (request.path === "/_admin/version" && request.method === "GET") {
|
|
5
8
|
return { status: 200, message: "OK", body: { version: VERSION }, params: {} };
|
|
6
9
|
}
|
|
@@ -39,12 +42,32 @@ export function handleRouting(request, customRoutes) {
|
|
|
39
42
|
params: {},
|
|
40
43
|
};
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks that the path part of a request path can be percent-decoded.
|
|
47
|
+
*/
|
|
48
|
+
function hasValidEncoding(requestPath) {
|
|
49
|
+
try {
|
|
50
|
+
decodeURIComponent(requestPath.split("?")[0] ?? "");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Escapes the characters that have a special meaning in a regular expression.
|
|
59
|
+
*/
|
|
60
|
+
function escapeRegExp(text) {
|
|
61
|
+
// "$&" puts back the character that matched, after the backslash.
|
|
62
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
}
|
|
42
64
|
/**
|
|
43
65
|
* Matches a request path against a pattern like /users/:id.
|
|
66
|
+
* Everything except the :params matches literally.
|
|
44
67
|
*/
|
|
45
68
|
function matchPath(pattern, actualPath) {
|
|
46
69
|
const paramNames = [];
|
|
47
|
-
const regexPath = pattern.replace(/:([a-zA-Z0-9_]+)/g, (_, name) => {
|
|
70
|
+
const regexPath = escapeRegExp(pattern).replace(/:([a-zA-Z0-9_]+)/g, (_, name) => {
|
|
48
71
|
paramNames.push(name);
|
|
49
72
|
return "([^/]+)";
|
|
50
73
|
});
|
package/dist/static.js
CHANGED
|
@@ -11,7 +11,29 @@ const MIME_TYPES = {
|
|
|
11
11
|
".json": "application/json",
|
|
12
12
|
};
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
14
|
+
/** The published build first, then the repo build during development. */
|
|
15
|
+
const DASHBOARD_FOLDERS = [path.join(__dirname, "dashboard-ui"), path.join(__dirname, "../dashboard/build")];
|
|
16
|
+
/**
|
|
17
|
+
* Decodes a request path and joins it onto the folder.
|
|
18
|
+
* Returns null when the path can't be decoded or ends up outside the folder.
|
|
19
|
+
*/
|
|
20
|
+
function resolveInside(folder, requestPath) {
|
|
21
|
+
let decodedPath;
|
|
22
|
+
try {
|
|
23
|
+
decodedPath = decodeURIComponent(requestPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const filePath = path.join(folder, decodedPath);
|
|
29
|
+
const isInside = filePath === folder || filePath.startsWith(folder + path.sep);
|
|
30
|
+
return isInside ? filePath : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Finds the dashboard file for a request. Returns null when the request isn't for the dashboard,
|
|
34
|
+
* or when its path leaves the dashboard folder, so the request ends in a 404.
|
|
35
|
+
*/
|
|
36
|
+
export async function handleStaticFile(requestPath, folders = DASHBOARD_FOLDERS) {
|
|
15
37
|
const isDashboardBase = requestPath.startsWith("/_dashboard");
|
|
16
38
|
const isAppAsset = requestPath.startsWith("/_app");
|
|
17
39
|
const isPhantomSVG = requestPath === "/phantom.svg";
|
|
@@ -23,9 +45,10 @@ export async function handleStaticFile(requestPath) {
|
|
|
23
45
|
}
|
|
24
46
|
if (cleanPath === "" || cleanPath === "/")
|
|
25
47
|
cleanPath = "/index.html";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
for (const folder of folders) {
|
|
49
|
+
const filePath = resolveInside(folder, cleanPath);
|
|
50
|
+
if (!filePath)
|
|
51
|
+
return null;
|
|
29
52
|
try {
|
|
30
53
|
const content = await fs.readFile(filePath);
|
|
31
54
|
const ext = path.extname(filePath);
|
|
@@ -41,10 +64,9 @@ export async function handleStaticFile(requestPath) {
|
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
66
|
// SPA fallback for /_dashboard sub-routes
|
|
44
|
-
const
|
|
45
|
-
for (const fallback of fallbackPaths) {
|
|
67
|
+
for (const folder of folders) {
|
|
46
68
|
try {
|
|
47
|
-
const indexContent = await fs.readFile(
|
|
69
|
+
const indexContent = await fs.readFile(path.join(folder, "index.html"));
|
|
48
70
|
return { body: indexContent, headers: { "Content-Type": "text/html" } };
|
|
49
71
|
}
|
|
50
72
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamesmddoyle/phantom",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "API mocker and traffic inspector with a built-in dashboard.",
|
|
5
5
|
"author": "James Doyle",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"directory": "server"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"phantom": "
|
|
13
|
+
"phantom": "dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|