@jamesmddoyle/phantom 0.2.5 → 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.
@@ -1 +1 @@
1
- {"version":"1793228583037"}
1
+ {"version":"1793230346425"}
@@ -22,16 +22,16 @@
22
22
  <div style="display: contents">
23
23
  <script>
24
24
  {
25
- __sveltekit_15nbta3 = {
25
+ __sveltekit_1qwiifu = {
26
26
  base: "/_dashboard",
27
- version: "1793228583037",
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(__sveltekit_15nbta3);
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
- // listen to raw data
67
- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- Nothing catches errors in this handler, so one bad request can stop the server.
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
- setTimeout(() => {
126
- if (socket.destroyed)
127
- return;
128
- const errorRate = options.errorRate ?? response.errorRate ?? 0;
129
- const shouldError = Math.random() < errorRate;
130
- if (!isAdmin) {
131
- 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
- // log the request
147
- logger.request(request.method, logEntry.status, request.path, logEntry.duration);
148
- }
149
- if (shouldError) {
150
- logger.error("Artificial failure hit!");
151
- sendResponse(socket, 500, "Internal Server Error", {
152
- error: "Artificial failure hit!",
153
- });
154
- return;
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.5",
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": "./dist/index.js"
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesmddoyle/phantom",
3
- "version": "0.2.5",
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": "./dist/index.js"
13
+ "phantom": "dist/index.js"
14
14
  },
15
15
  "files": [
16
16
  "dist",