@verdant-web/server 2.0.7 → 2.1.0
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/cjs/ClientConnection.d.ts +2 -0
- package/dist/cjs/ClientConnection.js +33 -0
- package/dist/cjs/ClientConnection.js.map +1 -1
- package/dist/cjs/Server.d.ts +39 -3
- package/dist/cjs/Server.js +269 -134
- package/dist/cjs/Server.js.map +1 -1
- package/dist/esm/ClientConnection.d.ts +2 -0
- package/dist/esm/ClientConnection.js +33 -0
- package/dist/esm/ClientConnection.js.map +1 -1
- package/dist/esm/Server.d.ts +39 -3
- package/dist/esm/Server.js +269 -134
- package/dist/esm/Server.js.map +1 -1
- package/dist/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/ClientConnection.ts +57 -1
- package/src/Server.ts +356 -155
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
1
2
|
import { ServerMessage } from '@verdant-web/common';
|
|
2
3
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
4
|
import { WebSocket } from 'ws';
|
|
@@ -7,6 +8,7 @@ export declare class ClientConnectionManager {
|
|
|
7
8
|
private getLibraryConnections;
|
|
8
9
|
addSocket: (libraryId: string, key: string, socket: WebSocket, info: TokenInfo) => void;
|
|
9
10
|
addRequest: (libraryId: string, key: string, request: IncomingMessage, response: ServerResponse, info: TokenInfo) => () => void;
|
|
11
|
+
addFetch: (libraryId: string, key: string, req: Request, info: TokenInfo) => (() => Response);
|
|
10
12
|
remove: (libraryId: string, key: string) => void;
|
|
11
13
|
respond: (libraryId: string, key: string, message: ServerMessage) => void;
|
|
12
14
|
/**
|
|
@@ -28,6 +28,29 @@ class RequestClientConnection extends ClientConnection {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
class FetchClientConnection extends ClientConnection {
|
|
32
|
+
constructor(key, req, info) {
|
|
33
|
+
super(key);
|
|
34
|
+
this.req = req;
|
|
35
|
+
this.info = info;
|
|
36
|
+
this.type = 'fetch';
|
|
37
|
+
this.responses = [];
|
|
38
|
+
this.res = new Response('', { status: 200 });
|
|
39
|
+
this.respond = (message) => {
|
|
40
|
+
this.responses.push(message);
|
|
41
|
+
};
|
|
42
|
+
this.end = () => {
|
|
43
|
+
this.res = new Response(JSON.stringify({
|
|
44
|
+
messages: this.responses,
|
|
45
|
+
}), {
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
class WebSocketClientConnection extends ClientConnection {
|
|
32
55
|
constructor(key, ws, info) {
|
|
33
56
|
super(key);
|
|
@@ -65,6 +88,16 @@ class ClientConnectionManager {
|
|
|
65
88
|
lib.delete(key);
|
|
66
89
|
};
|
|
67
90
|
};
|
|
91
|
+
this.addFetch = (libraryId, key, req, info) => {
|
|
92
|
+
const lib = this.getLibraryConnections(libraryId);
|
|
93
|
+
const connection = new FetchClientConnection(key, req, info);
|
|
94
|
+
lib.set(key, connection);
|
|
95
|
+
return () => {
|
|
96
|
+
connection.end();
|
|
97
|
+
lib.delete(key);
|
|
98
|
+
return connection.res;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
68
101
|
this.remove = (libraryId, key) => {
|
|
69
102
|
const lib = this.getLibraryConnections(libraryId);
|
|
70
103
|
lib.delete(key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientConnection.js","sourceRoot":"","sources":["../../src/ClientConnection.ts"],"names":[],"mappings":";;;AAKA,MAAe,gBAAgB;IAC9B,YAAqB,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;CAGpC;AAED,MAAM,uBAAwB,SAAQ,gBAAgB;IAKrD,YACC,GAAW,EACM,GAAoB,EACpB,GAAmB,EAC3B,IAAe;QAExB,KAAK,CAAC,GAAG,CAAC,CAAC;QAJM,QAAG,GAAH,GAAG,CAAiB;QACpB,QAAG,GAAH,GAAG,CAAgB;QAC3B,SAAI,GAAJ,IAAI,CAAW;QARhB,SAAI,GAAG,SAAS,CAAC;QAElB,cAAS,GAAoB,EAAE,CAAC;QAWxC,YAAO,GAAG,CAAC,OAAsB,EAAE,EAAE;YACpC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC,CAAC;QAEF,QAAG,GAAG,GAAG,EAAE;YACV,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;gBACvB,cAAc,EAAE,kBAAkB;aAClC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,KAAK,CACb,IAAI,CAAC,SAAS,CAAC;gBACd,QAAQ,EAAE,IAAI,CAAC,SAAS;aACxB,CAAC,CACF,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QAChB,CAAC,CAAC;IAhBF,CAAC;CAiBD;AAED,MAAM,yBAA0B,SAAQ,gBAAgB;IAGvD,
|
|
1
|
+
{"version":3,"file":"ClientConnection.js","sourceRoot":"","sources":["../../src/ClientConnection.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/Server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
5
|
import { DocumentBaseline, Operation, ServerMessage } from '@verdant-web/common';
|
|
4
6
|
import EventEmitter from 'events';
|
|
5
7
|
import { IncomingMessage, Server as HttpServer, ServerResponse } from 'http';
|
|
@@ -7,6 +9,7 @@ import { UserProfiles } from './Profiles.js';
|
|
|
7
9
|
import { MessageSender } from './MessageSender.js';
|
|
8
10
|
import { TokenInfo } from './TokenVerifier.js';
|
|
9
11
|
import { FileStorage } from './files/FileStorage.js';
|
|
12
|
+
import internal from 'stream';
|
|
10
13
|
import { FileMetadataConfig } from './files/FileMetadata.js';
|
|
11
14
|
export interface ServerOptions {
|
|
12
15
|
/**
|
|
@@ -68,7 +71,7 @@ export declare interface Server {
|
|
|
68
71
|
emit<Event extends keyof ServerEvents>(ev: Event, ...args: Parameters<ServerEvents[Event]>): boolean;
|
|
69
72
|
}
|
|
70
73
|
export declare class Server extends EventEmitter implements MessageSender {
|
|
71
|
-
|
|
74
|
+
private httpServer;
|
|
72
75
|
private wss;
|
|
73
76
|
private fileStorage?;
|
|
74
77
|
private fileMetadata;
|
|
@@ -80,17 +83,50 @@ export declare class Server extends EventEmitter implements MessageSender {
|
|
|
80
83
|
private log;
|
|
81
84
|
private __testMode;
|
|
82
85
|
constructor(options: ServerOptions);
|
|
86
|
+
/**
|
|
87
|
+
* Attaches the Verdant server to an HttpServer instance, which
|
|
88
|
+
* allows it to handle requests and websockets required for the
|
|
89
|
+
* Verdant protocol.
|
|
90
|
+
*
|
|
91
|
+
* You can pass httpPath = false if you want to handle HTTP requests
|
|
92
|
+
* yourself, and this will only attach the websocket handling.
|
|
93
|
+
*/
|
|
94
|
+
attach: (server: HttpServer, options?: {
|
|
95
|
+
httpPath?: string | false;
|
|
96
|
+
}) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Handles an HTTP upgrade request to a websocket connection.
|
|
99
|
+
*/
|
|
100
|
+
handleUpgrade: (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => Promise<void>;
|
|
83
101
|
private authorizeRequest;
|
|
84
102
|
private getRequestToken;
|
|
85
|
-
private
|
|
103
|
+
private createInternalRequestHandler;
|
|
104
|
+
/**
|
|
105
|
+
* Handles an HTTP request from a verdant client.
|
|
106
|
+
*/
|
|
86
107
|
handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
87
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Handles a "fetch" style request. Complement to handleRequest, for servers
|
|
110
|
+
* that use Request/Response style handlers.
|
|
111
|
+
*/
|
|
112
|
+
handleFetch: (req: Request) => Promise<Response>;
|
|
113
|
+
private handleRequestBody;
|
|
114
|
+
private writeErrorResponse;
|
|
115
|
+
private getErrorResponse;
|
|
88
116
|
/**
|
|
89
117
|
* Handles a multipart upload of a file from a verdant client. The upload
|
|
90
118
|
* will include parameters for the file's ID, name, and type. The request
|
|
91
119
|
* must be authenticated with a token to tie it to a library.
|
|
92
120
|
*/
|
|
93
121
|
handleFileRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Handles a "fetch" style file request. Complement to handleFileRequest,
|
|
124
|
+
* for servers that use Request/Response style handlers.
|
|
125
|
+
*/
|
|
126
|
+
handleFileFetch: (req: Request) => Promise<Response>;
|
|
127
|
+
private getFileStorageOrThrow;
|
|
128
|
+
private streamIncomingFile;
|
|
129
|
+
private getFileData;
|
|
94
130
|
broadcast: (libraryId: string, message: ServerMessage, omitKeys?: string[]) => void;
|
|
95
131
|
send: (libraryId: string, key: string, message: ServerMessage) => void;
|
|
96
132
|
private handleMessage;
|
package/dist/cjs/Server.js
CHANGED
|
@@ -30,6 +30,7 @@ const stream_1 = require("stream");
|
|
|
30
30
|
const FileMetadata_js_1 = require("./files/FileMetadata.js");
|
|
31
31
|
const ServerLibrary_js_1 = require("./ServerLibrary.js");
|
|
32
32
|
const migrations_js_1 = require("./migrations.js");
|
|
33
|
+
const web_1 = require("node:stream/web");
|
|
33
34
|
class DefaultProfiles {
|
|
34
35
|
constructor() {
|
|
35
36
|
this.get = async (userId) => {
|
|
@@ -45,10 +46,58 @@ class Server extends events_1.default {
|
|
|
45
46
|
this.connectionToReplicaIdMap = new WeakMap();
|
|
46
47
|
this.log = () => { };
|
|
47
48
|
this.__testMode = false;
|
|
49
|
+
/**
|
|
50
|
+
* Attaches the Verdant server to an HttpServer instance, which
|
|
51
|
+
* allows it to handle requests and websockets required for the
|
|
52
|
+
* Verdant protocol.
|
|
53
|
+
*
|
|
54
|
+
* You can pass httpPath = false if you want to handle HTTP requests
|
|
55
|
+
* yourself, and this will only attach the websocket handling.
|
|
56
|
+
*/
|
|
57
|
+
this.attach = (server, options = {}) => {
|
|
58
|
+
if (this.httpServer) {
|
|
59
|
+
this.httpServer.off('upgrade', this.handleUpgrade);
|
|
60
|
+
}
|
|
61
|
+
this.httpServer = server;
|
|
62
|
+
this.httpServer.on('upgrade', this.handleUpgrade);
|
|
63
|
+
if (options.httpPath !== false) {
|
|
64
|
+
this.httpServer.on('request', this.createInternalRequestHandler(options.httpPath));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Handles an HTTP upgrade request to a websocket connection.
|
|
69
|
+
*/
|
|
70
|
+
this.handleUpgrade = async (req, socket, head) => {
|
|
71
|
+
try {
|
|
72
|
+
const info = this.authorizeRequest(req);
|
|
73
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
74
|
+
this.wss.emit('connection', ws, req, info);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
this.emit('error', e);
|
|
79
|
+
if (e instanceof common_1.VerdantError && e.httpStatus === 401) {
|
|
80
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n' +
|
|
81
|
+
'Connection: close\r\n' +
|
|
82
|
+
'Content-Length: 0\r\n' +
|
|
83
|
+
'\r\n');
|
|
84
|
+
}
|
|
85
|
+
socket.destroy();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
48
88
|
this.authorizeRequest = (req) => {
|
|
49
89
|
return this.tokenVerifier.verifyToken(this.getRequestToken(req));
|
|
50
90
|
};
|
|
51
91
|
this.getRequestToken = (req) => {
|
|
92
|
+
if (isFetch(req)) {
|
|
93
|
+
const authHeader = req.headers.get('Authorization');
|
|
94
|
+
(0, common_1.assert)(authHeader, 'Token is required');
|
|
95
|
+
const [type, token] = authHeader.split(' ');
|
|
96
|
+
if (type === 'Bearer') {
|
|
97
|
+
return token;
|
|
98
|
+
}
|
|
99
|
+
return token;
|
|
100
|
+
}
|
|
52
101
|
if (req.headers.authorization) {
|
|
53
102
|
const [type, token] = req.headers.authorization.split(' ');
|
|
54
103
|
if (type === 'Bearer') {
|
|
@@ -68,21 +117,27 @@ class Server extends events_1.default {
|
|
|
68
117
|
(0, common_1.assert)(token, 'Token is required');
|
|
69
118
|
return token;
|
|
70
119
|
};
|
|
71
|
-
this.
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (url.pathname
|
|
75
|
-
|
|
120
|
+
this.createInternalRequestHandler = (pathPrefix = '/sync') => {
|
|
121
|
+
const handleRequest = (req, res) => {
|
|
122
|
+
const url = new url_1.URL(req.url || '', 'http://localhost');
|
|
123
|
+
if (url.pathname.startsWith('sync')) {
|
|
124
|
+
if (url.pathname === 'sync') {
|
|
125
|
+
return this.handleRequest(req, res);
|
|
126
|
+
}
|
|
127
|
+
else if (url.pathname.startsWith('/sync/files/')) {
|
|
128
|
+
return this.handleFileRequest(req, res);
|
|
129
|
+
}
|
|
76
130
|
}
|
|
77
|
-
else
|
|
78
|
-
|
|
131
|
+
else {
|
|
132
|
+
res.writeHead(404);
|
|
133
|
+
res.end();
|
|
79
134
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
res.writeHead(404);
|
|
83
|
-
res.end();
|
|
84
|
-
}
|
|
135
|
+
};
|
|
136
|
+
return handleRequest;
|
|
85
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* Handles an HTTP request from a verdant client.
|
|
140
|
+
*/
|
|
86
141
|
this.handleRequest = async (req, res) => {
|
|
87
142
|
var _a;
|
|
88
143
|
try {
|
|
@@ -104,28 +159,45 @@ class Server extends events_1.default {
|
|
|
104
159
|
reject(e);
|
|
105
160
|
});
|
|
106
161
|
}));
|
|
107
|
-
|
|
108
|
-
for (const message of body.messages) {
|
|
109
|
-
await this.handleMessage(key, info, message);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
this.emit('error', e);
|
|
114
|
-
res.writeHead(500);
|
|
115
|
-
res.end();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
// update our keepalive timers for presence management
|
|
119
|
-
const firstMessage = body.messages[0];
|
|
120
|
-
if (firstMessage) {
|
|
121
|
-
this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
|
|
122
|
-
}
|
|
162
|
+
await this.handleRequestBody(key, info, body);
|
|
123
163
|
finish();
|
|
124
164
|
this.emit('request', info);
|
|
125
165
|
}
|
|
126
166
|
}
|
|
127
167
|
catch (e) {
|
|
128
|
-
return this.
|
|
168
|
+
return this.writeErrorResponse(e, res);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Handles a "fetch" style request. Complement to handleRequest, for servers
|
|
173
|
+
* that use Request/Response style handlers.
|
|
174
|
+
*/
|
|
175
|
+
this.handleFetch = async (req) => {
|
|
176
|
+
try {
|
|
177
|
+
const info = this.authorizeRequest(req);
|
|
178
|
+
const key = (0, common_1.generateId)();
|
|
179
|
+
const finish = this.clientConnections.addFetch(info.libraryId, key, req, info);
|
|
180
|
+
const body = (await req.json());
|
|
181
|
+
if (!body) {
|
|
182
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.BodyRequired);
|
|
183
|
+
}
|
|
184
|
+
await this.handleRequestBody(key, info, body);
|
|
185
|
+
const res = finish();
|
|
186
|
+
this.emit('request', info);
|
|
187
|
+
return res;
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return this.getErrorResponse(e);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
this.handleRequestBody = async (key, info, body) => {
|
|
194
|
+
for (const message of body.messages) {
|
|
195
|
+
await this.handleMessage(key, info, message);
|
|
196
|
+
}
|
|
197
|
+
// update our keepalive timers for presence management
|
|
198
|
+
const firstMessage = body.messages[0];
|
|
199
|
+
if (firstMessage) {
|
|
200
|
+
this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
|
|
129
201
|
}
|
|
130
202
|
};
|
|
131
203
|
/**
|
|
@@ -134,70 +206,19 @@ class Server extends events_1.default {
|
|
|
134
206
|
* must be authenticated with a token to tie it to a library.
|
|
135
207
|
*/
|
|
136
208
|
this.handleFileRequest = async (req, res) => {
|
|
137
|
-
const fs = this.fileStorage;
|
|
138
|
-
if (!fs) {
|
|
139
|
-
this.emit('error', new Error('No file storage configured, but a client attempted to upload a file.'));
|
|
140
|
-
res.writeHead(500);
|
|
141
|
-
res.write('File storage is not configured');
|
|
142
|
-
res.end();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
// FIXME: rather than trying to support Express, I should
|
|
146
|
-
// just expose regular methods you call from whatever HTTP handler...
|
|
147
|
-
const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
|
|
148
|
-
const id = url.pathname.split('/').pop();
|
|
149
|
-
if (!id || id === 'files') {
|
|
150
|
-
res.writeHead(400);
|
|
151
|
-
res.write('File ID is required to be in the URL path as the last parameter');
|
|
152
|
-
res.end();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
209
|
try {
|
|
210
|
+
const info = this.authorizeRequest(req);
|
|
211
|
+
const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
|
|
212
|
+
const id = url.pathname.split('/').pop();
|
|
213
|
+
if (!id || id === 'files') {
|
|
214
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound);
|
|
215
|
+
}
|
|
156
216
|
if (req.method === 'POST') {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const lofiFileInfo = {
|
|
163
|
-
id,
|
|
164
|
-
libraryId: info.libraryId,
|
|
165
|
-
fileName: fileInfo.filename,
|
|
166
|
-
type: fileInfo.mimeType,
|
|
167
|
-
};
|
|
168
|
-
// write metadata to storage
|
|
169
|
-
try {
|
|
170
|
-
this.fileMetadata.put(info.libraryId, lofiFileInfo);
|
|
171
|
-
fs.put(stream, lofiFileInfo);
|
|
172
|
-
}
|
|
173
|
-
catch (e) {
|
|
174
|
-
reject(e);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
bb.on('field', (fieldName, value) => {
|
|
178
|
-
if (fieldName === 'file') {
|
|
179
|
-
if (this.__testMode) {
|
|
180
|
-
// this isn't right in the real world, but in testing it's
|
|
181
|
-
// the only way we get file data.
|
|
182
|
-
// we create a stream from the data and pass it as if it
|
|
183
|
-
// were a file stream
|
|
184
|
-
const stream = new stream_1.Readable();
|
|
185
|
-
stream.push(value);
|
|
186
|
-
stream.push(null);
|
|
187
|
-
const fileInfo = {
|
|
188
|
-
filename: 'test.txt',
|
|
189
|
-
mimeType: 'text/plain',
|
|
190
|
-
};
|
|
191
|
-
bb.emit('file', fieldName, stream, fileInfo);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
throw new Error('Invalid file upload');
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
req.pipe(bb);
|
|
199
|
-
bb.on('finish', resolve);
|
|
200
|
-
bb.on('error', reject);
|
|
217
|
+
await this.streamIncomingFile({
|
|
218
|
+
req,
|
|
219
|
+
info,
|
|
220
|
+
headers: req.headers,
|
|
221
|
+
id,
|
|
201
222
|
});
|
|
202
223
|
this.log('File upload complete');
|
|
203
224
|
res.writeHead(200);
|
|
@@ -205,39 +226,147 @@ class Server extends events_1.default {
|
|
|
205
226
|
res.end();
|
|
206
227
|
}
|
|
207
228
|
else if (req.method === 'GET') {
|
|
208
|
-
const
|
|
209
|
-
const fileInfo = this.fileMetadata.get(info.libraryId, id);
|
|
210
|
-
if (!fileInfo) {
|
|
211
|
-
res.writeHead(404);
|
|
212
|
-
res.end();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const url = await fs.getUrl({
|
|
216
|
-
fileName: fileInfo.name,
|
|
217
|
-
id: fileInfo.fileId,
|
|
218
|
-
libraryId: info.libraryId,
|
|
219
|
-
type: fileInfo.type,
|
|
220
|
-
});
|
|
229
|
+
const data = await this.getFileData(info, id);
|
|
221
230
|
res.writeHead(200, {
|
|
222
231
|
'Content-Type': 'application/json',
|
|
223
232
|
});
|
|
224
|
-
// we need to augment that data with the URL from the file backend.
|
|
225
|
-
// and generally enforce the FileData interface here...
|
|
226
|
-
const data = {
|
|
227
|
-
id: fileInfo.fileId,
|
|
228
|
-
url,
|
|
229
|
-
remote: true,
|
|
230
|
-
name: fileInfo.name,
|
|
231
|
-
type: fileInfo.type,
|
|
232
|
-
};
|
|
233
233
|
res.write(JSON.stringify(data));
|
|
234
234
|
res.end();
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
catch (e) {
|
|
238
|
-
return this.
|
|
238
|
+
return this.writeErrorResponse(e, res);
|
|
239
239
|
}
|
|
240
240
|
};
|
|
241
|
+
/**
|
|
242
|
+
* Handles a "fetch" style file request. Complement to handleFileRequest,
|
|
243
|
+
* for servers that use Request/Response style handlers.
|
|
244
|
+
*/
|
|
245
|
+
this.handleFileFetch = async (req) => {
|
|
246
|
+
this.log('info', 'Handling file fetch', req.url, req.method);
|
|
247
|
+
try {
|
|
248
|
+
const info = this.authorizeRequest(req);
|
|
249
|
+
const url = new url_1.URL(req.url, 'http://localhost');
|
|
250
|
+
const id = url.pathname.split('/').pop();
|
|
251
|
+
if (!id || id === 'files') {
|
|
252
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound);
|
|
253
|
+
}
|
|
254
|
+
if (req.method === 'POST') {
|
|
255
|
+
if (!req.body) {
|
|
256
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.InvalidRequest);
|
|
257
|
+
}
|
|
258
|
+
const headersAsRecord = Array.from(req.headers.entries()).reduce((acc, [key, value]) => {
|
|
259
|
+
acc[key] = value;
|
|
260
|
+
return acc;
|
|
261
|
+
}, {});
|
|
262
|
+
// this is needed because Node's webstreams don't
|
|
263
|
+
// like itty's polyfill streams
|
|
264
|
+
const intermediate = new web_1.TransformStream();
|
|
265
|
+
req.body.pipeTo(intermediate.writable);
|
|
266
|
+
await this.streamIncomingFile({
|
|
267
|
+
req: stream_1.Readable.fromWeb(intermediate.readable),
|
|
268
|
+
info,
|
|
269
|
+
headers: headersAsRecord,
|
|
270
|
+
id,
|
|
271
|
+
});
|
|
272
|
+
this.log('File upload complete');
|
|
273
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
274
|
+
status: 200,
|
|
275
|
+
headers: {
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else if (req.method === 'GET') {
|
|
281
|
+
const data = await this.getFileData(info, id);
|
|
282
|
+
return new Response(JSON.stringify(data), {
|
|
283
|
+
status: 200,
|
|
284
|
+
headers: {
|
|
285
|
+
'Content-Type': 'application/json',
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.InvalidRequest);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
return this.getErrorResponse(e);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
this.getFileStorageOrThrow = () => {
|
|
298
|
+
if (!this.fileStorage) {
|
|
299
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.NoFileStorage);
|
|
300
|
+
}
|
|
301
|
+
return this.fileStorage;
|
|
302
|
+
};
|
|
303
|
+
this.streamIncomingFile = ({ id, req, headers, info, }) => {
|
|
304
|
+
const fs = this.getFileStorageOrThrow();
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const bb = (0, busboy_1.default)({ headers });
|
|
307
|
+
bb.on('file', (fieldName, stream, fileInfo) => {
|
|
308
|
+
// too many 'info's....
|
|
309
|
+
const lofiFileInfo = {
|
|
310
|
+
id,
|
|
311
|
+
libraryId: info.libraryId,
|
|
312
|
+
fileName: fileInfo.filename,
|
|
313
|
+
type: fileInfo.mimeType,
|
|
314
|
+
};
|
|
315
|
+
// write metadata to storage
|
|
316
|
+
try {
|
|
317
|
+
this.fileMetadata.put(info.libraryId, lofiFileInfo);
|
|
318
|
+
fs.put(stream, lofiFileInfo);
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
reject(e);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
bb.on('field', (fieldName, value) => {
|
|
325
|
+
if (fieldName === 'file') {
|
|
326
|
+
if (this.__testMode) {
|
|
327
|
+
// this isn't right in the real world, but in testing it's
|
|
328
|
+
// the only way we get file data.
|
|
329
|
+
// we create a stream from the data and pass it as if it
|
|
330
|
+
// were a file stream
|
|
331
|
+
const stream = new stream_1.Readable();
|
|
332
|
+
stream.push(value);
|
|
333
|
+
stream.push(null);
|
|
334
|
+
const fileInfo = {
|
|
335
|
+
filename: 'test.txt',
|
|
336
|
+
mimeType: 'text/plain',
|
|
337
|
+
};
|
|
338
|
+
bb.emit('file', fieldName, stream, fileInfo);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
throw new Error('Invalid file upload');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
req.pipe(bb);
|
|
346
|
+
bb.on('finish', resolve);
|
|
347
|
+
bb.on('error', reject);
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
this.getFileData = async (info, id) => {
|
|
351
|
+
const fs = this.getFileStorageOrThrow();
|
|
352
|
+
const fileInfo = this.fileMetadata.get(info.libraryId, id);
|
|
353
|
+
if (!fileInfo) {
|
|
354
|
+
throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound, undefined, `File ${id} not found`);
|
|
355
|
+
}
|
|
356
|
+
const url = await fs.getUrl({
|
|
357
|
+
fileName: fileInfo.name,
|
|
358
|
+
id: fileInfo.fileId,
|
|
359
|
+
libraryId: info.libraryId,
|
|
360
|
+
type: fileInfo.type,
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
id: fileInfo.fileId,
|
|
364
|
+
url,
|
|
365
|
+
remote: true,
|
|
366
|
+
name: fileInfo.name,
|
|
367
|
+
type: fileInfo.type,
|
|
368
|
+
};
|
|
369
|
+
};
|
|
241
370
|
this.broadcast = (libraryId, message, omitKeys = []) => {
|
|
242
371
|
this.clientConnections.broadcast(libraryId, message, omitKeys);
|
|
243
372
|
};
|
|
@@ -372,28 +501,10 @@ class Server extends events_1.default {
|
|
|
372
501
|
});
|
|
373
502
|
this.wss.on('connection', this.handleConnection);
|
|
374
503
|
this.httpServer =
|
|
375
|
-
options.httpServer || new http_1.Server(this.
|
|
376
|
-
this.httpServer.on('upgrade', async (req, socket, head) => {
|
|
377
|
-
try {
|
|
378
|
-
const info = this.authorizeRequest(req);
|
|
379
|
-
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
380
|
-
this.wss.emit('connection', ws, req, info);
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
catch (e) {
|
|
384
|
-
this.emit('error', e);
|
|
385
|
-
if (e instanceof common_1.VerdantError && e.httpStatus === 401) {
|
|
386
|
-
socket.write('HTTP/1.1 401 Unauthorized\r\n' +
|
|
387
|
-
'Connection: close\r\n' +
|
|
388
|
-
'Content-Length: 0\r\n' +
|
|
389
|
-
'\r\n');
|
|
390
|
-
}
|
|
391
|
-
socket.destroy();
|
|
392
|
-
}
|
|
393
|
-
});
|
|
504
|
+
options.httpServer || new http_1.Server(this.createInternalRequestHandler());
|
|
394
505
|
this.keepalives.subscribe('lost', this.library.remove);
|
|
395
506
|
}
|
|
396
|
-
|
|
507
|
+
writeErrorResponse(e, res) {
|
|
397
508
|
this.emit('error', e);
|
|
398
509
|
this.log('Error handling request', e);
|
|
399
510
|
if (e instanceof common_1.VerdantError) {
|
|
@@ -407,6 +518,30 @@ class Server extends events_1.default {
|
|
|
407
518
|
}
|
|
408
519
|
res.end();
|
|
409
520
|
}
|
|
521
|
+
// for fetch-style
|
|
522
|
+
getErrorResponse(e) {
|
|
523
|
+
this.emit('error', e);
|
|
524
|
+
this.log('Error handling request', e);
|
|
525
|
+
if (e instanceof common_1.VerdantError) {
|
|
526
|
+
return new Response(JSON.stringify(e.toResponse()), {
|
|
527
|
+
status: e.httpStatus,
|
|
528
|
+
headers: {
|
|
529
|
+
'Content-Type': 'application/json',
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
return new Response(JSON.stringify(new common_1.VerdantError(common_1.VerdantError.Code.Unexpected).toResponse()), {
|
|
535
|
+
status: 500,
|
|
536
|
+
headers: {
|
|
537
|
+
'Content-Type': 'application/json',
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
410
542
|
}
|
|
411
543
|
exports.Server = Server;
|
|
544
|
+
function isFetch(request) {
|
|
545
|
+
return 'json' in request;
|
|
546
|
+
}
|
|
412
547
|
//# sourceMappingURL=Server.js.map
|