Skip to content
This repository was archived by the owner on Sep 14, 2021. It is now read-only.

Commit d291937

Browse files
committed
Add http2-push support
Added support for pushing files with h2 requests, including CLI flags: -m, --manifest string Path to h2-push manifest Usage: 1. Generate push manifest with http2-push-manifest node module 2. Run: polyserve -m push_manifest.json -h h2
1 parent 7a6921c commit d291937

File tree

4 files changed

+104
-1
lines changed

4 files changed

+104
-1
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"command-line-args": "^2.1.6",
2323
"express": "^4.8.5",
2424
"find-port": "^1.0.1",
25+
"mime": "^1.3.4",
2526
"mz": "^2.4.0",
2627
"opn": "^3.0.2",
2728
"pem": "^1.8.3",

src/args.ts

+6
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,10 @@ export let args : ArgDescriptor[] = [
9393
defaultValue: 'cert.pem',
9494
type: String,
9595
},
96+
{
97+
name: 'manifest',
98+
alias: 'm',
99+
description: 'Path to h2-push manifest',
100+
type: String,
101+
},
96102
];

src/cli.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function run(): Promise<void> {
4444
protocol: cliOptions['protocol'],
4545
keyPath: cliOptions['key'],
4646
certPath: cliOptions['cert'],
47+
pushManifestPath: cliOptions['manifest'],
4748
};
4849

4950
if (cliOptions.help) {

src/start_server.ts

+96-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
'use strict';
1212

13+
import * as assert from 'assert';
1314
import * as express from 'express';
1415
import * as findPort from 'find-port';
16+
import * as mime from 'mime';
1517
// TODO: Switch to node-http2 when compatible with express
1618
// https://github.com/molnarg/node-http2/issues/100
1719
import * as http from 'spdy';
@@ -24,6 +26,9 @@ import * as fs from 'mz/fs';
2426

2527
import { makeApp } from './make_app';
2628

29+
/** h2 push manifest cache */
30+
let _pushManifest = {};
31+
2732
export interface ServerOptions {
2833

2934
/** The root directory to serve **/
@@ -58,6 +63,9 @@ export interface ServerOptions {
5863

5964
/** Path to TLS certificate for HTTPS */
6065
certPath?: string;
66+
67+
/** Path to H2 push-manifest file */
68+
pushManifestPath?: string;
6169
}
6270

6371
function applyDefaultOptions(options: ServerOptions): ServerOptions {
@@ -121,9 +129,17 @@ export function getApp(options: ServerOptions) {
121129

122130
app.use('/components/', polyserve);
123131

132+
// Preload the h2-push manifest to avoid the cost on first push
133+
if (options.pushManifestPath) {
134+
getPushManifest(options.root, options.pushManifestPath);
135+
}
136+
124137
app.get('/*', (req, res) => {
138+
139+
pushResources(options, req, res);
140+
125141
let filePath = req.path;
126-
send(req, filePath, {root: root,})
142+
send(req, filePath, {root: root})
127143
.on('error', (error: send.SendError) => {
128144
if ((error).status == 404 && !filePath.endsWith('.html')) {
129145
send(req, '/', {root: root}).pipe(res);
@@ -340,3 +356,82 @@ function startWithPort(userOptions: ServerOptions): Promise<http.Server> {
340356
})
341357
);
342358
}
359+
360+
/**
361+
* Asserts file existence for all specified files in a push-manifest
362+
* @param {string} root path to root directory
363+
* @param {string} manifest manifest object
364+
*/
365+
function assertValidManifest(root: string, manifest: {[path: string]: any}) {
366+
367+
function assertExists(filename: string) {
368+
const fname = path.join(root, filename);
369+
try {
370+
// fs.statSync also verifies file existence
371+
assert(fs.statSync(fname).isFile(), `file not found: ${fname}`);
372+
} catch (err) {
373+
console.error('invalid h2-push manifest');
374+
throw err;
375+
}
376+
}
377+
378+
for (const refFile of Object.keys(manifest)) {
379+
assertExists(refFile);
380+
for (const pushFile of Object.keys(manifest[refFile])) {
381+
assertExists(pushFile);
382+
}
383+
}
384+
}
385+
386+
/**
387+
* Reads a push-manifest from the specified path, or a cached version
388+
* of the file
389+
* @param {string} root path to root directory
390+
* @param {string} manifestPath path to manifest file
391+
* @returns {any} the manifest
392+
*/
393+
function getPushManifest(root: string, manifestPath: string): {[path: string]: any} {
394+
if (!_pushManifest[manifestPath]) {
395+
const data = fs.readFileSync(manifestPath);
396+
const manifest = JSON.parse(data.toString());
397+
assertValidManifest(root, manifest);
398+
_pushManifest[manifestPath] = manifest;
399+
}
400+
return _pushManifest[manifestPath];
401+
}
402+
403+
/**
404+
* Pushes any resources for the requested file
405+
* @param options server options
406+
* @param req HTTP request
407+
* @param res HTTP response
408+
*/
409+
function pushResources(options: ServerOptions, req: any, res: any) {
410+
if (res.push
411+
&& options.protocol === 'h2'
412+
&& options.pushManifestPath
413+
&& !req.get('x-is-push')) {
414+
415+
const pushManifest = getPushManifest(options.root, options.pushManifestPath);
416+
const resources = pushManifest[req.path];
417+
if (resources) {
418+
const root = options.root;
419+
for (const filename of Object.keys(resources)) {
420+
const stream: NodeJS.WritableStream = res.push(filename,
421+
{
422+
request: {
423+
accept: '*/*'
424+
},
425+
response: {
426+
'content-type': mime.lookup(filename),
427+
428+
// Add an X-header to the pushed request so we don't trigger pushes for pushes
429+
'x-is-push': 'true'
430+
}
431+
})
432+
.on('error', (err: any) => console.error('failed to push', filename, err));
433+
fs.createReadStream(path.join(root, filename)).pipe(stream);
434+
}
435+
}
436+
}
437+
}

0 commit comments

Comments
 (0)