@webqit/webflo 0.11.33 → 0.11.35
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/package.json +1 -1
- package/src/config-pi/deployment/Virtualization.js +10 -4
- package/src/config-pi/runtime/Server.js +18 -20
- package/src/runtime-pi/Router.js +3 -3
- package/src/runtime-pi/Runtime.js +21 -0
- package/src/runtime-pi/RuntimeClient.js +29 -0
- package/src/runtime-pi/client/Runtime.js +30 -41
- package/src/runtime-pi/client/RuntimeClient.js +10 -13
- package/src/runtime-pi/client/Workport.js +14 -2
- package/src/runtime-pi/client/index.js +1 -3
- package/src/runtime-pi/client/worker/Worker.js +21 -36
- package/src/runtime-pi/client/worker/WorkerClient.js +8 -11
- package/src/runtime-pi/client/worker/Workport.js +11 -1
- package/src/runtime-pi/client/worker/index.js +1 -3
- package/src/runtime-pi/server/Router.js +1 -0
- package/src/runtime-pi/server/Runtime.js +253 -216
- package/src/runtime-pi/server/RuntimeClient.js +10 -7
- package/src/runtime-pi/server/index.js +1 -3
- package/src/services-pi/cert/http-auth-hook.js +1 -1
- package/src/services-pi/cert/http-cleanup-hook.js +1 -1
- package/src/webflo.js +1 -1
|
@@ -23,6 +23,7 @@ import xRequest from "../xRequest.js";
|
|
|
23
23
|
import xResponse from "../xResponse.js";
|
|
24
24
|
import xfetch from '../xfetch.js';
|
|
25
25
|
import xHttpEvent from '../xHttpEvent.js';
|
|
26
|
+
import _Runtime from '../Runtime.js';
|
|
26
27
|
|
|
27
28
|
const URL = xURL(whatwag.URL);
|
|
28
29
|
const FormData = xFormData(whatwag.FormData);
|
|
@@ -47,7 +48,7 @@ export {
|
|
|
47
48
|
Observer,
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export default class Runtime {
|
|
51
|
+
export default class Runtime extends _Runtime {
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Runtime
|
|
@@ -58,202 +59,212 @@ export default class Runtime {
|
|
|
58
59
|
* @return void
|
|
59
60
|
*/
|
|
60
61
|
constructor(cx, clientCallback) {
|
|
62
|
+
super(cx, clientCallback);
|
|
61
63
|
// ---------------
|
|
62
|
-
this.cx = cx;
|
|
63
|
-
this.clients = new Map;
|
|
64
|
-
this.mockSessionStore = {};
|
|
65
64
|
this.ready = (async () => {
|
|
66
|
-
|
|
67
65
|
// ---------------
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
return client;
|
|
66
|
+
const resolveContextObj = async (cx, force = false) => {
|
|
67
|
+
if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
|
|
68
|
+
if (_isEmpty(cx.layout) || force) { cx.layout = await (new cx.config.deployment.Layout(cx)).read(); }
|
|
69
|
+
if (_isEmpty(cx.env) || force) { cx.env = await (new cx.config.deployment.Env(cx)).read(); }
|
|
73
70
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
|
|
78
|
-
if (_isEmpty(cx.env)) {
|
|
79
|
-
let env = await (new this.cx.config.deployment.Env(cx)).read();
|
|
80
|
-
cx.env = env.entries;
|
|
81
|
-
meta.envAutoloading = env.autoload;
|
|
82
|
-
}
|
|
83
|
-
return meta;
|
|
84
|
-
};
|
|
85
|
-
let loadMeta = await loadContextObj(this.cx);
|
|
86
|
-
if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
|
|
87
|
-
if (_isEmpty(this.cx.vcontexts)) {
|
|
88
|
-
this.cx.vcontexts = {};
|
|
89
|
-
const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
|
|
90
|
-
await Promise.all((vhosts.entries || []).map(vhost => async () => {
|
|
91
|
-
this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
|
|
92
|
-
await loadContextObj(this.cx.vcontexts[vhost.host]);
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
_each(this.cx.vcontexts, (host, vcontext) => {
|
|
96
|
-
this.clients.set(vhost.host, execClientCallback(vcontext, host));
|
|
97
|
-
});
|
|
98
|
-
} else {
|
|
99
|
-
this.clients.set('*', execClientCallback(this.cx, '*'));
|
|
100
|
-
}
|
|
101
|
-
// Always populate... regardless whether shared setup
|
|
102
|
-
if (loadMeta.envAutoloading !== false) {
|
|
103
|
-
Object.keys(this.cx.env).forEach(key => {
|
|
71
|
+
await resolveContextObj(this.cx);
|
|
72
|
+
if (this.cx.env.autoload !== false) {
|
|
73
|
+
Object.keys(this.cx.env.entries).forEach(key => {
|
|
104
74
|
if (!(key in process.env)) {
|
|
105
|
-
process.env[key] = this.cx.env[key];
|
|
75
|
+
process.env[key] = this.cx.env.entries[key];
|
|
106
76
|
}
|
|
107
77
|
});
|
|
108
78
|
}
|
|
109
|
-
|
|
110
79
|
// ---------------
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
80
|
+
const parseDomains = domains => _arrFrom(domains).reduce((arr, str) => arr.concat(str.split(',')), []).map(str => str.trim()).filter(str => str);
|
|
81
|
+
const selectDomains = (serverDefs, matchingPort = null) => serverDefs.reduce((doms, def) => doms.length ? doms : (((!matchingPort || def.port === matchingPort) && parseDomains(def.domains || def.hostnames)) || []), []);
|
|
82
|
+
// ---------------
|
|
83
|
+
this.vhosts = new Map;
|
|
84
|
+
if (this.cx.config.deployment.Virtualization) {
|
|
85
|
+
const vhosts = await (new this.cx.config.deployment.Virtualization(this.cx)).read();
|
|
86
|
+
await Promise.all((vhosts.entries || []).map(async vhost => {
|
|
87
|
+
let cx, hostnames = parseDomains(vhost.hostnames), port = parseInt(vhost.port);
|
|
88
|
+
if (vhost.path) {
|
|
89
|
+
cx = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
|
|
90
|
+
await resolveContextObj(cx, true);
|
|
91
|
+
// From the server that's most likely to be active
|
|
92
|
+
port || (port = parseInt(cx.server.port || cx.server.https.port));
|
|
93
|
+
// The domain list that corresponds to the specified resolved port
|
|
94
|
+
hostnames.length || (hostnames = selectDomains([cx.server, cx.server.https], port));
|
|
95
|
+
// Or anyone available... hoping that the remote configs can eventually be in sync
|
|
96
|
+
hostnames.length || (hostnames = selectDomains([cx.server, cx.server.https]));
|
|
97
|
+
}
|
|
98
|
+
hostnames.length || (hostnames = ['*']);
|
|
99
|
+
this.vhosts.set(hostnames.sort().join('|'), { cx, hostnames, port });
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
// ---------------
|
|
103
|
+
this.servers = new Map;
|
|
104
|
+
// ---------------
|
|
105
|
+
if (!this.cx.flags['test-only'] && !this.cx.flags['https-only'] && this.cx.server.port) {
|
|
106
|
+
const httpServer = Http.createServer((request, response) => handleRequest('http', request, response));
|
|
107
|
+
httpServer.listen(this.cx.server.port);
|
|
108
|
+
// -------
|
|
109
|
+
let domains = parseDomains(this.cx.server.domains);
|
|
110
|
+
if (!domains.length) { domains = ['*']; }
|
|
111
|
+
this.servers.set('http', {
|
|
112
|
+
instance: httpServer,
|
|
113
|
+
port: this.cx.server.port,
|
|
114
|
+
domains,
|
|
115
|
+
});
|
|
114
116
|
}
|
|
115
|
-
|
|
116
117
|
// ---------------
|
|
117
|
-
|
|
118
118
|
if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
|
|
119
119
|
const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
var domains = _arrFrom(this.cx.server.https.certdoms);
|
|
143
|
-
var cert = {
|
|
144
|
-
key: Fs.readFileSync(this.cx.server.https.keyfile),
|
|
145
|
-
cert: Fs.readFileSync(this.cx.server.https.certfile),
|
|
146
|
-
};
|
|
147
|
-
if (!domains[0]) {
|
|
148
|
-
domains = ['*'];
|
|
149
|
-
}
|
|
150
|
-
domains.forEach(domain => {
|
|
151
|
-
httpsServer.addContext(domain, cert);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
120
|
+
httpsServer.listen(this.cx.server.https.port);
|
|
121
|
+
// -------
|
|
122
|
+
const addSSLContext = (serverConfig, domains) => {
|
|
123
|
+
if (!Fs.existsSync(serverConfig.https.keyfile)) return;
|
|
124
|
+
const cert = {
|
|
125
|
+
key: Fs.readFileSync(serverConfig.https.keyfile),
|
|
126
|
+
cert: Fs.readFileSync(serverConfig.https.certfile),
|
|
127
|
+
};
|
|
128
|
+
domains.forEach(domain => { httpsServer.addContext(domain, cert); });
|
|
129
|
+
}
|
|
130
|
+
// -------
|
|
131
|
+
let domains = parseDomains(this.cx.server.https.domains);
|
|
132
|
+
if (!domains.length) { domains = ['*']; }
|
|
133
|
+
this.servers.set('https', {
|
|
134
|
+
instance: httpsServer,
|
|
135
|
+
port: this.cx.server.https.port,
|
|
136
|
+
domains,
|
|
137
|
+
});
|
|
138
|
+
// -------
|
|
139
|
+
addSSLContext(this.cx.server, domains);
|
|
140
|
+
for (const [ /*id*/, vhost ] of this.vhosts) {
|
|
141
|
+
vhost.cx && addSSLContext(vhost.cx.server, vhost.hostnames);
|
|
154
142
|
}
|
|
155
|
-
httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
|
|
156
143
|
}
|
|
157
|
-
|
|
158
144
|
// ---------------
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// --------
|
|
162
|
-
// Parse request
|
|
163
|
-
const fullUrl = protocol + '://' + request.headers.host + request.url;
|
|
164
|
-
const requestInit = { method: request.method, headers: request.headers };
|
|
165
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
166
|
-
requestInit.body = await new Promise((resolve, reject) => {
|
|
167
|
-
var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: true, keepExtensions: true });
|
|
168
|
-
formidable.parse(request, (error, fields, files) => {
|
|
169
|
-
if (error) {
|
|
170
|
-
reject(error);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (request.headers['content-type'] === 'application/json') {
|
|
174
|
-
return resolve(fields);
|
|
175
|
-
}
|
|
176
|
-
const formData = new FormData;
|
|
177
|
-
Object.keys(fields).forEach(name => {
|
|
178
|
-
if (Array.isArray(fields[name])) {
|
|
179
|
-
const values = Array.isArray(fields[name][0])
|
|
180
|
-
? fields[name][0]/* bugly a nested array when there are actually more than entry */
|
|
181
|
-
: fields[name];
|
|
182
|
-
values.forEach(value => {
|
|
183
|
-
formData.append(!name.endsWith(']') ? name + '[]' : name, value);
|
|
184
|
-
});
|
|
185
|
-
} else {
|
|
186
|
-
formData.append(name, fields[name]);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
Object.keys(files).forEach(name => {
|
|
190
|
-
const fileCompat = file => {
|
|
191
|
-
// IMPORTANT
|
|
192
|
-
// Path up the "formidable" file in a way that "formdata-node"
|
|
193
|
-
// to can translate it into its own file instance
|
|
194
|
-
file[Symbol.toStringTag] = 'File';
|
|
195
|
-
file.stream = () => Fs.createReadStream(file.path);
|
|
196
|
-
// Done pathcing
|
|
197
|
-
return file;
|
|
198
|
-
}
|
|
199
|
-
if (Array.isArray(files[name])) {
|
|
200
|
-
files[name].forEach(value => {
|
|
201
|
-
formData.append(name, fileCompat(value));
|
|
202
|
-
});
|
|
203
|
-
} else {
|
|
204
|
-
formData.append(name, fileCompat(files[name]));
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
resolve(formData);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// --------
|
|
213
|
-
// Run Application
|
|
145
|
+
const handleRequest = async (proto, request, response) => {
|
|
146
|
+
const [ fullUrl, requestInit ] = await this.parseNodeRequest(proto, request);
|
|
214
147
|
let clientResponse = await this.go(fullUrl, requestInit, { request, response });
|
|
215
148
|
if (response.headersSent) return;
|
|
216
|
-
|
|
217
149
|
// --------
|
|
218
|
-
// Set headers
|
|
219
150
|
_each(clientResponse.headers.json(), (name, value) => {
|
|
220
151
|
response.setHeader(name, value);
|
|
221
152
|
});
|
|
222
|
-
|
|
223
153
|
// --------
|
|
224
|
-
// Send
|
|
225
154
|
response.statusCode = clientResponse.status;
|
|
226
155
|
response.statusMessage = clientResponse.statusText;
|
|
227
|
-
if (clientResponse.headers.
|
|
228
|
-
response.end();
|
|
229
|
-
} else {
|
|
230
|
-
var body = clientResponse.body;
|
|
231
|
-
if ((body instanceof ReadableStream)) {
|
|
232
|
-
body.pipe(response);
|
|
233
|
-
} else {
|
|
234
|
-
// The default
|
|
235
|
-
if (clientResponse.headers.contentType === 'application/json') {
|
|
236
|
-
body += '';
|
|
237
|
-
}
|
|
238
|
-
response.end(body);
|
|
239
|
-
}
|
|
156
|
+
if (clientResponse.headers.location) {
|
|
157
|
+
return response.end();
|
|
240
158
|
}
|
|
159
|
+
if ((clientResponse.body instanceof ReadableStream)) {
|
|
160
|
+
return clientResponse.body.pipe(response);
|
|
161
|
+
}
|
|
162
|
+
let body = clientResponse.body;
|
|
163
|
+
if (clientResponse.headers.contentType === 'application/json') {
|
|
164
|
+
body += '';
|
|
165
|
+
}
|
|
166
|
+
return response.end(body);
|
|
241
167
|
};
|
|
242
|
-
|
|
243
168
|
})();
|
|
169
|
+
|
|
244
170
|
// ---------------
|
|
245
171
|
Observer.set(this, 'location', {});
|
|
246
172
|
Observer.set(this, 'network', {});
|
|
247
173
|
// ---------------
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
174
|
+
|
|
175
|
+
// -------------
|
|
176
|
+
// Initialize
|
|
177
|
+
(async () => {
|
|
178
|
+
await this.ready;
|
|
179
|
+
if (this.cx.logger) {
|
|
180
|
+
if (this.servers.size) {
|
|
181
|
+
this.cx.logger.info(`> Server running! (${this.cx.app.title || ''})`);
|
|
182
|
+
for (let [ proto, def ] of this.servers) {
|
|
183
|
+
this.cx.logger.info(`> ${ proto.toUpperCase() } / ${ def.domains.concat('').join(`:${ def.port } / `)}`);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
this.cx.logger.info(`> Server not running! No port specified.`);
|
|
187
|
+
}
|
|
188
|
+
if (this.vhosts.size) {
|
|
189
|
+
this.cx.logger.info(`> Reverse proxy active.`);
|
|
190
|
+
for (let [ id, def ] of this.vhosts) {
|
|
191
|
+
this.cx.logger.info(`> ${ id } >>> ${ def.port }`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.cx.logger.info(``);
|
|
254
195
|
}
|
|
255
|
-
|
|
256
|
-
|
|
196
|
+
if (this.client && this.client.init) {
|
|
197
|
+
const request = this.generateRequest('/');
|
|
198
|
+
const httpEvent = new HttpEvent(request, { srcType: 'initialization' }, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
|
|
199
|
+
return this.getSession(this.cx, httpEvent, id, options, callback);
|
|
200
|
+
});
|
|
201
|
+
await this.client.init(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
// ---------------
|
|
205
|
+
|
|
206
|
+
// ---------------
|
|
207
|
+
this.mockSessionStore = {};
|
|
208
|
+
// ---------------
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse Nodejs's IncomingMessage to WHATWAG request params.
|
|
214
|
+
*
|
|
215
|
+
* @param String proto
|
|
216
|
+
* @param Http.IncomingMessage request
|
|
217
|
+
*
|
|
218
|
+
* @return Array
|
|
219
|
+
*/
|
|
220
|
+
async parseNodeRequest(proto, request) {
|
|
221
|
+
const fullUrl = proto + '://' + request.headers.host + request.url;
|
|
222
|
+
const requestInit = { method: request.method, headers: request.headers };
|
|
223
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
224
|
+
requestInit.body = await new Promise((resolve, reject) => {
|
|
225
|
+
var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: true, keepExtensions: true });
|
|
226
|
+
formidable.parse(request, (error, fields, files) => {
|
|
227
|
+
if (error) { return reject(error); }
|
|
228
|
+
if (request.headers['content-type'] === 'application/json') {
|
|
229
|
+
return resolve(fields);
|
|
230
|
+
}
|
|
231
|
+
const formData = new FormData;
|
|
232
|
+
Object.keys(fields).forEach(name => {
|
|
233
|
+
if (Array.isArray(fields[name])) {
|
|
234
|
+
const values = Array.isArray(fields[name][0])
|
|
235
|
+
? fields[name][0]/* bugly a nested array when there are actually more than entry */
|
|
236
|
+
: fields[name];
|
|
237
|
+
values.forEach(value => {
|
|
238
|
+
formData.append(!name.endsWith(']') ? name + '[]' : name, value);
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
formData.append(name, fields[name]);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
Object.keys(files).forEach(name => {
|
|
245
|
+
const fileCompat = file => {
|
|
246
|
+
// IMPORTANT
|
|
247
|
+
// Path up the "formidable" file in a way that "formdata-node"
|
|
248
|
+
// to can translate it into its own file instance
|
|
249
|
+
file[Symbol.toStringTag] = 'File';
|
|
250
|
+
file.stream = () => Fs.createReadStream(file.path);
|
|
251
|
+
// Done pathcing
|
|
252
|
+
return file;
|
|
253
|
+
}
|
|
254
|
+
if (Array.isArray(files[name])) {
|
|
255
|
+
files[name].forEach(value => {
|
|
256
|
+
formData.append(name, fileCompat(value));
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
formData.append(name, fileCompat(files[name]));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
resolve(formData);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return [ fullUrl, requestInit ];
|
|
267
|
+
}
|
|
257
268
|
|
|
258
269
|
/**
|
|
259
270
|
* Performs a request.
|
|
@@ -271,28 +282,30 @@ export default class Runtime {
|
|
|
271
282
|
url = typeof url === 'string' ? new whatwag.URL(url) : url;
|
|
272
283
|
init = { referrer: this.location.href, ...init };
|
|
273
284
|
// ------------
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
const hosts = [];
|
|
286
|
+
this.servers.forEach(server => hosts.push(...server.domains));
|
|
287
|
+
// ------------
|
|
288
|
+
for (const [ /*id*/, vhost ] of this.vhosts) {
|
|
289
|
+
if (vhost.hostnames.includes(url.hostname) || (vhost.hostnames.includes('*') && !hosts.includes('*'))) {
|
|
290
|
+
return this.proxyFetch(vhost, url, init);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// ------------
|
|
294
|
+
let exit;
|
|
295
|
+
if (!hosts.includes(url.hostname) && !hosts.includes('*')) {
|
|
296
|
+
exit = { status: 500, statusText: 'Unrecognized host' };
|
|
297
|
+
} else if (url.protocol === 'http:' && this.cx.server.https.force) {
|
|
298
|
+
exit = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
|
|
299
|
+
} else if (url.hostname.startsWith('www.') && this.cx.server.force_www === 'remove') {
|
|
300
|
+
exit = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
|
|
301
|
+
} else if (!url.hostname.startsWith('www.') && this.cx.server.force_www === 'add') {
|
|
302
|
+
exit = { status: 302, headers: { Location: ( url.hostname = `www.${ url.hostname }`, url.href ) } };
|
|
303
|
+
} else if (this.cx.config.runtime.server.Redirects) {
|
|
304
|
+
exit = ((await (new this.cx.config.runtime.server.Redirects(this.cx)).read()).entries || []).reduce((_rdr, entry) => {
|
|
287
305
|
return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
|
|
288
306
|
}, null);
|
|
289
307
|
}
|
|
290
|
-
if (
|
|
291
|
-
return new Response(null, rdr);
|
|
292
|
-
}
|
|
293
|
-
const autoHeaders = _context.config.runtime.server.Headers
|
|
294
|
-
? ((await (new _context.config.runtime.server.Headers(_context)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
|
|
295
|
-
: [];
|
|
308
|
+
if (exit) { return new Response(null, exit); }
|
|
296
309
|
// ------------
|
|
297
310
|
|
|
298
311
|
// ------------
|
|
@@ -300,27 +313,29 @@ export default class Runtime {
|
|
|
300
313
|
Observer.set(this.network, 'redirecting', null);
|
|
301
314
|
// ------------
|
|
302
315
|
|
|
316
|
+
// ------------
|
|
317
|
+
// Automatically-added headers
|
|
318
|
+
const autoHeaders = this.cx.config.runtime.server.Headers
|
|
319
|
+
? ((await (new this.cx.config.runtime.server.Headers(this.cx)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
|
|
320
|
+
: [];
|
|
303
321
|
// The request object
|
|
304
|
-
|
|
322
|
+
const request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
|
|
305
323
|
// The navigation event
|
|
306
|
-
|
|
307
|
-
return this.getSession(
|
|
324
|
+
const httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
|
|
325
|
+
return this.getSession(this.cx, httpEvent, id, options, callback);
|
|
308
326
|
});
|
|
309
327
|
// Response
|
|
310
|
-
let
|
|
311
|
-
if (this.cx.server.shared) {
|
|
312
|
-
client = this.clients.get(url.hostname);
|
|
313
|
-
}
|
|
328
|
+
let response, finalResponse;
|
|
314
329
|
try {
|
|
315
|
-
response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
|
|
316
|
-
finalResponse = await this.handleResponse(
|
|
330
|
+
response = await this.client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
|
|
331
|
+
finalResponse = await this.handleResponse(this.cx, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
|
|
317
332
|
} catch(e) {
|
|
318
333
|
finalResponse = new Response(null, { status: 500, statusText: e.message });
|
|
319
334
|
console.error(e);
|
|
320
335
|
}
|
|
321
336
|
// Logging
|
|
322
337
|
if (this.cx.logger) {
|
|
323
|
-
|
|
338
|
+
const log = this.generateLog(httpEvent.request, finalResponse);
|
|
324
339
|
this.cx.logger.log(log);
|
|
325
340
|
}
|
|
326
341
|
// ------------
|
|
@@ -328,9 +343,31 @@ export default class Runtime {
|
|
|
328
343
|
return finalResponse;
|
|
329
344
|
}
|
|
330
345
|
|
|
346
|
+
// Fetch from proxied host
|
|
347
|
+
async proxyFetch(vhost, url, init) {
|
|
348
|
+
const url2 = new whatwag.URL(url);
|
|
349
|
+
url2.port = vhost.port;
|
|
350
|
+
const init2 = { ...init, compress: false };
|
|
351
|
+
if (!init2.headers) init2.headers = {};
|
|
352
|
+
init2.headers.host = url2.host;
|
|
353
|
+
let response;
|
|
354
|
+
try {
|
|
355
|
+
response = await this.remoteFetch(url2, init2);
|
|
356
|
+
} catch(e) {
|
|
357
|
+
response = new Response(null, { status: 500, statusText: e.message });
|
|
358
|
+
console.error(e);
|
|
359
|
+
}
|
|
360
|
+
if (this.cx.logger) {
|
|
361
|
+
const log = this.generateLog({ url: url2.href, ...init2 }, response);
|
|
362
|
+
this.cx.logger.log(log);
|
|
363
|
+
}
|
|
364
|
+
return response;
|
|
365
|
+
|
|
366
|
+
}
|
|
367
|
+
|
|
331
368
|
// Generates request object
|
|
332
|
-
generateRequest(href, init, autoHeaders = []) {
|
|
333
|
-
|
|
369
|
+
generateRequest(href, init = {}, autoHeaders = []) {
|
|
370
|
+
const request = new Request(href, init);
|
|
334
371
|
this._autoHeaders(request.headers, autoHeaders);
|
|
335
372
|
return request;
|
|
336
373
|
}
|
|
@@ -374,11 +411,11 @@ export default class Runtime {
|
|
|
374
411
|
let href = request;
|
|
375
412
|
if (request instanceof Request) {
|
|
376
413
|
href = request.url;
|
|
377
|
-
} else if (request instanceof
|
|
414
|
+
} else if (request instanceof whatwag.URL) {
|
|
378
415
|
href = request.href;
|
|
379
416
|
}
|
|
380
417
|
Observer.set(this.network, 'remote', href);
|
|
381
|
-
|
|
418
|
+
const _response = fetch(request, ...args);
|
|
382
419
|
// This catch() is NOT intended to handle failure of the fetch
|
|
383
420
|
_response.catch(e => Observer.set(this.network, 'error', e.message));
|
|
384
421
|
// Save a reference to this
|
|
@@ -410,16 +447,16 @@ export default class Runtime {
|
|
|
410
447
|
|
|
411
448
|
// ----------------
|
|
412
449
|
// Redirects
|
|
413
|
-
if (response.headers.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
450
|
+
if (response.headers.location) {
|
|
451
|
+
const xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
|
|
452
|
+
const xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
|
|
453
|
+
const destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
|
|
454
|
+
const isSameOriginRedirect = destinationUrl.origin === e.url.origin;
|
|
418
455
|
let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
|
|
419
456
|
if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
|
|
420
457
|
// Longest-first sorting
|
|
421
|
-
|
|
422
|
-
|
|
458
|
+
const sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
|
|
459
|
+
const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
|
|
423
460
|
isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(e.url.pathname);
|
|
424
461
|
}
|
|
425
462
|
if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
|
|
@@ -461,8 +498,8 @@ export default class Runtime {
|
|
|
461
498
|
if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
|
|
462
499
|
&& ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
|
|
463
500
|
// ...in partials
|
|
464
|
-
|
|
465
|
-
|
|
501
|
+
const totalLength = response.headers.contentLength || 0;
|
|
502
|
+
const ranges = await rangeRequest.reduce(async (_ranges, range) => {
|
|
466
503
|
_ranges = await _ranges;
|
|
467
504
|
if (range[0] < 0 || (totalLength && range[0] > totalLength)
|
|
468
505
|
|| (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
|
|
@@ -475,7 +512,7 @@ export default class Runtime {
|
|
|
475
512
|
}
|
|
476
513
|
if (_ranges.error) return _ranges;
|
|
477
514
|
if (totalLength) { range.clamp(totalLength); }
|
|
478
|
-
|
|
515
|
+
const partLength = range[1] - range[0] + 1;
|
|
479
516
|
_ranges.parts.push({
|
|
480
517
|
body: body.pipe(_streamSlice(range[0], range[1] + 1)),
|
|
481
518
|
range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
|
|
@@ -508,24 +545,24 @@ export default class Runtime {
|
|
|
508
545
|
}
|
|
509
546
|
|
|
510
547
|
// Generates log
|
|
511
|
-
generateLog(
|
|
548
|
+
generateLog(request, response) {
|
|
512
549
|
let log = [];
|
|
513
550
|
// ---------------
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
551
|
+
const style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
|
|
552
|
+
const errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
|
|
553
|
+
const xRedirectCode = response.headers.get('X-Redirect-Code');
|
|
554
|
+
const redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
|
|
555
|
+
const statusCode = xRedirectCode || response.status;
|
|
519
556
|
// ---------------
|
|
520
557
|
log.push(`[${style.comment((new Date).toUTCString())}]`);
|
|
521
|
-
log.push(style.keyword(
|
|
522
|
-
log.push(style.url(
|
|
558
|
+
log.push(style.keyword(request.method));
|
|
559
|
+
log.push(style.url(request.url));
|
|
523
560
|
if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
|
|
524
561
|
if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
|
|
525
562
|
if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
|
|
526
563
|
if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
|
|
527
564
|
else log.push(style.val(`${statusCode} ${response.statusText}`));
|
|
528
|
-
if (redirectCode) log.push(`- ${style.url(response.headers.
|
|
565
|
+
if (redirectCode) log.push(`- ${style.url(response.headers.location)}`);
|
|
529
566
|
|
|
530
567
|
return log.join(' ');
|
|
531
568
|
}
|