geonix 1.20.8 → 1.22.1
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/exports.js +3 -1
- package/index.d.ts +58 -12
- package/package.json +3 -3
- package/src/Gateway.js +5 -2
- package/src/Request.js +2 -2
- package/src/Service.js +6 -7
- package/src/Types.js +24 -0
- package/src/Util.js +155 -0
- package/test/gateway.js +20 -2
package/exports.js
CHANGED
|
@@ -11,4 +11,6 @@ export { Request, Subscribe, Publish } from "./src/Request.js";
|
|
|
11
11
|
export { RequestOptions } from "./src/RequestOptions.js";
|
|
12
12
|
export { picoid as randomID } from "./src/Util.js";
|
|
13
13
|
|
|
14
|
-
export { stats as StreamStats } from "./src/Stream.js";
|
|
14
|
+
export { stats as StreamStats } from "./src/Stream.js";
|
|
15
|
+
|
|
16
|
+
export { parseMultipart } from "./src/Util.js";
|
package/index.d.ts
CHANGED
|
@@ -130,7 +130,11 @@ export class RequestOptionsClass {
|
|
|
130
130
|
export function RequestOptions(options: any): RequestOptionsClass;
|
|
131
131
|
export class Service {
|
|
132
132
|
static serviceClasses: any[];
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
*
|
|
135
|
+
* @param {ServiceOptions} options
|
|
136
|
+
*/
|
|
137
|
+
static start(options?: ServiceOptions): void;
|
|
134
138
|
connections: Map<any, any>;
|
|
135
139
|
$createConnection(streamId: any): Promise<boolean>;
|
|
136
140
|
$getEnv(): {
|
|
@@ -148,17 +152,6 @@ export class Service {
|
|
|
148
152
|
$getServiceInfo(): {};
|
|
149
153
|
#private;
|
|
150
154
|
}
|
|
151
|
-
export type ServiceOptions = {
|
|
152
|
-
middleware: {
|
|
153
|
-
json: boolean;
|
|
154
|
-
raw: boolean;
|
|
155
|
-
cookies: boolean;
|
|
156
|
-
};
|
|
157
|
-
/**
|
|
158
|
-
* Enable full beacon
|
|
159
|
-
*/
|
|
160
|
-
fullBeacon: boolean;
|
|
161
|
-
};
|
|
162
155
|
/**
|
|
163
156
|
* Converts data to stream
|
|
164
157
|
*
|
|
@@ -174,6 +167,44 @@ export function streamToString(object: any, encoding: any): Promise<any>;
|
|
|
174
167
|
export function streamToJSON(object: any): Promise<any>;
|
|
175
168
|
export const stats: {};
|
|
176
169
|
export const activeStreams: {};
|
|
170
|
+
type parseMultipartOptions = {
|
|
171
|
+
maxFileSize: number;
|
|
172
|
+
maxFiles: number;
|
|
173
|
+
useMemory: boolean;
|
|
174
|
+
};
|
|
175
|
+
type ParsedMultiPart = {
|
|
176
|
+
/**
|
|
177
|
+
* Field name of the part (extracted from Content-Disposition)
|
|
178
|
+
*/
|
|
179
|
+
"name-": string;
|
|
180
|
+
/**
|
|
181
|
+
* - Filename of the part (extracted from Content-Disposition)
|
|
182
|
+
*/
|
|
183
|
+
filename: string;
|
|
184
|
+
/**
|
|
185
|
+
* - Headers of the part
|
|
186
|
+
*/
|
|
187
|
+
headers: Object;
|
|
188
|
+
/**
|
|
189
|
+
* - Path to the file when useMemory is false
|
|
190
|
+
*/
|
|
191
|
+
bodyFile: string;
|
|
192
|
+
/**
|
|
193
|
+
* - Readable stream of the part
|
|
194
|
+
*/
|
|
195
|
+
body: Readable;
|
|
196
|
+
};
|
|
197
|
+
type ServiceOptions = {
|
|
198
|
+
middleware: {
|
|
199
|
+
json: boolean;
|
|
200
|
+
raw: boolean;
|
|
201
|
+
cookies: boolean;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Enable full beacon
|
|
205
|
+
*/
|
|
206
|
+
fullBeacon: boolean;
|
|
207
|
+
};
|
|
177
208
|
/**
|
|
178
209
|
* Parse nats:// URL
|
|
179
210
|
* @param {string} url
|
|
@@ -182,7 +213,22 @@ export const activeStreams: {};
|
|
|
182
213
|
export function parseURL(url: string): any;
|
|
183
214
|
export function getFirstItemFromAsyncIterable(asyncIterable: any): Promise<any>;
|
|
184
215
|
export function getNetworkAddresses(): any[];
|
|
216
|
+
/**
|
|
217
|
+
*
|
|
218
|
+
*
|
|
219
|
+
* @param {Request} req
|
|
220
|
+
* @param {parseMultipartOptions} options
|
|
221
|
+
* @returns ParsedMultiPart[]
|
|
222
|
+
*/
|
|
223
|
+
export function parseMultipart(req: Request, _options: any): Promise<{
|
|
224
|
+
headers: {};
|
|
225
|
+
bodyFile: any;
|
|
226
|
+
body: any;
|
|
227
|
+
}[]>;
|
|
228
|
+
export function tempFilename(): any;
|
|
229
|
+
export function randomSafeId(size?: number): string;
|
|
185
230
|
export function sleep(delay: number): Promise<any>;
|
|
231
|
+
export function nextTick(): Promise<any>;
|
|
186
232
|
export function picoid(size?: number): any;
|
|
187
233
|
export function hash(data: string | Buffer): any;
|
|
188
234
|
export function createServerAtPort(port: number, pkg: Object, handler: Function): Promise<any>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geonix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"author": "Davor Tarandek <dtarandek@tria.hr>",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"cookie-parser": "1.4.
|
|
20
|
-
"express": "4.21.
|
|
19
|
+
"cookie-parser": "1.4.7",
|
|
20
|
+
"express": "4.21.1",
|
|
21
21
|
"express-async-errors": "3.1.1",
|
|
22
22
|
"express-ws": "5.0.2",
|
|
23
23
|
"nats": "2.28.2",
|
package/src/Gateway.js
CHANGED
|
@@ -102,8 +102,11 @@ export class Gateway {
|
|
|
102
102
|
this.#api.use(raw, (req, res, next) => {
|
|
103
103
|
stats.requests++;
|
|
104
104
|
|
|
105
|
-
if (this.#router) {
|
|
106
|
-
|
|
105
|
+
if (this.#router) {
|
|
106
|
+
this.#router(req, res, next);
|
|
107
|
+
} else {
|
|
108
|
+
next();
|
|
109
|
+
}
|
|
107
110
|
});
|
|
108
111
|
|
|
109
112
|
this.#api.use((req, res, next) => {
|
package/src/Request.js
CHANGED
|
@@ -143,7 +143,7 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
143
143
|
* @param {string|number} payload
|
|
144
144
|
*/
|
|
145
145
|
export async function Publish(subject, payload) {
|
|
146
|
-
connection.publishRaw(
|
|
146
|
+
connection.publishRaw(subject, Buffer.from(payload));
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -158,7 +158,7 @@ export async function Subscribe(subject, callback) {
|
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const subscription = await connection.subscribe(
|
|
161
|
+
const subscription = await connection.subscribe(subject);
|
|
162
162
|
for await (const event of subscription) {
|
|
163
163
|
callback(event.data);
|
|
164
164
|
}
|
package/src/Service.js
CHANGED
|
@@ -20,12 +20,7 @@ const raw = express.raw({ type: "*/*", limit: "100mb" });
|
|
|
20
20
|
const cookies = cookieParser();
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* @
|
|
24
|
-
* @property {Object} middleware
|
|
25
|
-
* @property {boolean} middleware.json Enable JSON middleware
|
|
26
|
-
* @property {boolean} middleware.raw Enable RAW middleware
|
|
27
|
-
* @property {boolean} middleware.cookies Enable cookies middleware
|
|
28
|
-
* @property {boolean} fullBeacon Enable full beacon
|
|
23
|
+
* @type ServiceOptions
|
|
29
24
|
*/
|
|
30
25
|
const defaultServiceOptions = {
|
|
31
26
|
middleware: {
|
|
@@ -40,6 +35,10 @@ export class Service {
|
|
|
40
35
|
|
|
41
36
|
static serviceClasses = [];
|
|
42
37
|
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @param {ServiceOptions} options
|
|
41
|
+
*/
|
|
43
42
|
static start(options = {}) {
|
|
44
43
|
if (!this.serviceClasses.includes(this.prototype.constructor.name)) {
|
|
45
44
|
this.serviceClasses.push(this.prototype.constructor.name);
|
|
@@ -228,7 +227,7 @@ export class Service {
|
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
async #sub(subject, handler) {
|
|
231
|
-
const subscription = await connection.subscribe(
|
|
230
|
+
const subscription = await connection.subscribe(subject);
|
|
232
231
|
const processor = async () => {
|
|
233
232
|
for await (const event of subscription) {
|
|
234
233
|
handler(event.data);
|
package/src/Types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} parseMultipartOptions
|
|
3
|
+
* @property {number} maxFileSize
|
|
4
|
+
* @property {number} maxFiles
|
|
5
|
+
* @property {boolean} useMemory
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} ParsedMultiPart
|
|
10
|
+
* @property {string} name- Field name of the part (extracted from Content-Disposition)
|
|
11
|
+
* @property {string} filename - Filename of the part (extracted from Content-Disposition)
|
|
12
|
+
* @property {Object} headers - Headers of the part
|
|
13
|
+
* @property {string} bodyFile - Path to the file when useMemory is false
|
|
14
|
+
* @property {Readable} body - Readable stream of the part
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ServiceOptions
|
|
19
|
+
* @property {Object} middleware
|
|
20
|
+
* @property {boolean} middleware.json Enable JSON middleware
|
|
21
|
+
* @property {boolean} middleware.raw Enable RAW middleware
|
|
22
|
+
* @property {boolean} middleware.cookies Enable cookies middleware
|
|
23
|
+
* @property {boolean} fullBeacon Enable full beacon
|
|
24
|
+
*/
|
package/src/Util.js
CHANGED
|
@@ -4,9 +4,13 @@ import { readFile } from "fs/promises";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { Transform } from "node:stream";
|
|
6
6
|
import { networkInterfaces } from "os";
|
|
7
|
+
import { Readable } from "stream";
|
|
7
8
|
import * as http from "http";
|
|
8
9
|
import * as https from "https";
|
|
9
10
|
import * as net from "net";
|
|
11
|
+
import { createWriteStream, createReadStream } from "fs";
|
|
12
|
+
import { unlink } from "fs/promises";
|
|
13
|
+
import { tmpdir } from "os";
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Wait for {delay} ms
|
|
@@ -15,6 +19,13 @@ import * as net from "net";
|
|
|
15
19
|
*/
|
|
16
20
|
export const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Wait for next tick
|
|
24
|
+
*
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
export const nextTick = () => sleep(0);
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* Parse nats:// URL
|
|
20
31
|
* @param {string} url
|
|
@@ -228,4 +239,148 @@ export function getNetworkAddresses() {
|
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
return list;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
*
|
|
246
|
+
*
|
|
247
|
+
* @param {Request} req
|
|
248
|
+
* @param {parseMultipartOptions} options
|
|
249
|
+
* @returns ParsedMultiPart[]
|
|
250
|
+
*/
|
|
251
|
+
export async function parseMultipart(req, _options) {
|
|
252
|
+
if (!req.headers["content-type"]?.startsWith("multipart/form-data")) {
|
|
253
|
+
throw new Error("Invalid content type (multipart/form-data expected)");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const BUFFER_SIZE = 1024 * 1024;
|
|
257
|
+
const END_OF_HEADERS = Buffer.from("\r\n\r\n");
|
|
258
|
+
const options = {
|
|
259
|
+
useMemory: false,
|
|
260
|
+
..._options
|
|
261
|
+
};
|
|
262
|
+
const parts = [];
|
|
263
|
+
let stream = req;
|
|
264
|
+
|
|
265
|
+
// if body is present, create a stream from it and use memory
|
|
266
|
+
if (req.body) {
|
|
267
|
+
stream = Readable.from(req.body);
|
|
268
|
+
options.useMemory = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const boundary = Buffer.from("\r\n--" + req.headers["content-type"].split("boundary=")[1]);
|
|
272
|
+
|
|
273
|
+
await nextTick();
|
|
274
|
+
|
|
275
|
+
let lastChunk = Buffer.from("\r\n");
|
|
276
|
+
let activePart;
|
|
277
|
+
let done = false;
|
|
278
|
+
|
|
279
|
+
while (stream.readable) {
|
|
280
|
+
// next next chunk
|
|
281
|
+
let chunk = stream.read(BUFFER_SIZE);
|
|
282
|
+
|
|
283
|
+
if (!chunk) {
|
|
284
|
+
await nextTick();
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let combined = Buffer.concat([lastChunk, chunk]);
|
|
289
|
+
|
|
290
|
+
while (combined.length >= boundary.length + 2) {
|
|
291
|
+
const boundaryIndex = combined.indexOf(boundary);
|
|
292
|
+
const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
|
|
293
|
+
|
|
294
|
+
if (boundaryIndex > -1) {
|
|
295
|
+
if (boundaryIndex > 0) {
|
|
296
|
+
if (options.useMemory) {
|
|
297
|
+
activePart.body.push(combined.slice(0, boundaryIndex));
|
|
298
|
+
} else {
|
|
299
|
+
activePart.body.write(combined.slice(0, boundaryIndex));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (isLastBoundary) {
|
|
304
|
+
combined = combined.slice(boundaryIndex + boundary.length + 2);
|
|
305
|
+
done = true;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// create new part
|
|
310
|
+
const bodyFile = tempFilename();
|
|
311
|
+
activePart = {
|
|
312
|
+
headers: {},
|
|
313
|
+
bodyFile: options.useMemory ? undefined : bodyFile,
|
|
314
|
+
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" })
|
|
315
|
+
};
|
|
316
|
+
parts.push(activePart);
|
|
317
|
+
|
|
318
|
+
const endOfHeaders = combined.indexOf(END_OF_HEADERS, boundaryIndex);
|
|
319
|
+
activePart.headers = combined
|
|
320
|
+
.slice(boundaryIndex + boundary.length + 2, endOfHeaders).toString()
|
|
321
|
+
.split("\r\n")
|
|
322
|
+
.reduce((acc, val) => {
|
|
323
|
+
const [header, value] = val.split(": ");
|
|
324
|
+
acc[header.toLowerCase()] = value;
|
|
325
|
+
return acc;
|
|
326
|
+
}, {});
|
|
327
|
+
|
|
328
|
+
combined = combined.slice(endOfHeaders + END_OF_HEADERS.length);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
lastChunk = combined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// there's no boundary in the chunk, add it to active part
|
|
335
|
+
if (activePart && lastChunk.length > 0 && !done) {
|
|
336
|
+
if (options.useMemory) {
|
|
337
|
+
activePart.body.push(lastChunk);
|
|
338
|
+
} else {
|
|
339
|
+
activePart.body.write(lastChunk);
|
|
340
|
+
}
|
|
341
|
+
lastChunk = Buffer.alloc(0);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const part of parts) {
|
|
346
|
+
// extract name and filename from content-disposition header
|
|
347
|
+
if (part.headers["content-disposition"]) {
|
|
348
|
+
const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/);
|
|
349
|
+
const [, filename] = part.headers["content-disposition"].match(/filename="([^"]+)"/) || [];
|
|
350
|
+
part.name = name ?? null;
|
|
351
|
+
part.filename = filename ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (options.useMemory) {
|
|
355
|
+
part.body = Readable.from(Buffer.concat(part.body));
|
|
356
|
+
delete part.bodyFile;
|
|
357
|
+
} else {
|
|
358
|
+
part.body.end();
|
|
359
|
+
part.body = createReadStream(part.bodyFile);
|
|
360
|
+
part.body.on("close", async () => {
|
|
361
|
+
try {
|
|
362
|
+
await unlink(part.bodyFile);
|
|
363
|
+
} catch {
|
|
364
|
+
// ignore errors
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return parts;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function tempFilename() {
|
|
374
|
+
return join(tmpdir(), `${randomSafeId(12)}.gxtmp`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function randomSafeId(size = 12) {
|
|
378
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
379
|
+
let result = "";
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < size; i++) {
|
|
382
|
+
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return result;
|
|
231
386
|
}
|
package/test/gateway.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
import { Gateway, Service } from "../exports.js";
|
|
1
|
+
import { Gateway, Service, streamToBuffer } from "../exports.js";
|
|
2
|
+
import { parseMultipart } from "../src/Util.js";
|
|
2
3
|
|
|
3
4
|
class TestService extends Service {
|
|
4
5
|
|
|
5
6
|
"GET /"(req, res) {
|
|
6
7
|
res.send("Hello World");
|
|
7
8
|
}
|
|
9
|
+
|
|
10
|
+
async "POST /upload"(req, res) {
|
|
11
|
+
const parts = await parseMultipart(req, { useMemory: false });
|
|
12
|
+
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
console.log(part.body);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
res.send("OK");
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
}
|
|
9
21
|
|
|
10
|
-
TestService.start(
|
|
22
|
+
TestService.start({
|
|
23
|
+
middleware: {
|
|
24
|
+
raw: true,
|
|
25
|
+
json: false,
|
|
26
|
+
cookies: false,
|
|
27
|
+
}
|
|
28
|
+
});
|
|
11
29
|
Gateway.start({
|
|
12
30
|
beforeRequest: (req, res) => {
|
|
13
31
|
res.set("X-Test", "Test");
|