From 1e8d7d10ab45747afe043d0636cb3c30f2e46b68 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 17 Mar 2018 19:21:25 -0400 Subject: [PATCH] implement this.fetch (#178) --- package.json | 1 + src/middleware.ts | 40 +++++++++++++++++++++++- src/runtime/index.ts | 2 ++ test/app/app/server.js | 7 +++++ test/app/routes/credentials/index.html | 12 +++++++ test/app/routes/credentials/test.json.js | 28 +++++++++++++++++ test/app/routes/index.html | 1 + test/common/test.js | 26 +++++++++++++++ 8 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/app/routes/credentials/index.html create mode 100644 test/app/routes/credentials/test.json.js diff --git a/package.json b/package.json index 37a0b75a8..981bf2629 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "cheerio": "^1.0.0-rc.2", "chokidar": "^2.0.2", "clorox": "^1.0.3", + "cookie": "^0.3.1", "devalue": "^1.0.1", "glob": "^7.1.2", "html-minifier": "^3.5.11", diff --git a/src/middleware.ts b/src/middleware.ts index 63ad84ac0..7935ec77f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,12 @@ import * as fs from 'fs'; import * as path from 'path'; -import { resolve } from 'url'; +import { resolve, URL } from 'url'; import { ClientRequest, ServerResponse } from 'http'; +import cookie from 'cookie'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; import devalue from 'devalue'; +import fetch from 'node-fetch'; import { lookup } from './middleware/mime'; import { create_routes, create_compilers } from './core'; import { locations, dev } from './config'; @@ -42,6 +44,7 @@ interface Req extends ClientRequest { method: string; path: string; params: Record; + headers: Record; } export default function middleware({ routes, store }: { @@ -163,6 +166,41 @@ function get_route_handler(chunks: Record, routes: RouteObject[] error: (statusCode: number, message: Error | string) => { error = { statusCode, message }; }, + fetch: (url: string, opts?: any) => { + const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl}${req.path}`); + + if (opts) { + opts = Object.assign({}, opts); + + const include_cookies = ( + opts.credentials === 'include' || + opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}` + ); + + if (include_cookies) { + const cookies: Record = {}; + if (!opts.headers) opts.headers = {}; + + const str = [] + .concat( + cookie.parse(req.headers.cookie || ''), + cookie.parse(opts.headers.cookie || ''), + cookie.parse(res.getHeader('Set-Cookie') || '') + ) + .map(cookie => { + return Object.keys(cookie) + .map(name => `${name}=${encodeURIComponent(cookie[name])}`) + .join('; '); + }) + .filter(Boolean) + .join(', '); + + opts.headers.cookie = str; + } + } + + return fetch(parsed.href, opts); + }, store }, req) : {} ).catch(err => { diff --git a/src/runtime/index.ts b/src/runtime/index.ts index bf7ab86f7..347fbeef9 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -92,6 +92,8 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) { } return Promise.resolve(Component.preload.call({ + store, + fetch: (url: string, opts?: any) => window.fetch(url, opts), redirect: (statusCode: number, location: string) => { redirect = { statusCode, location }; }, diff --git a/test/app/app/server.js b/test/app/app/server.js index 3e7c46e5a..01b21539b 100644 --- a/test/app/app/server.js +++ b/test/app/app/server.js @@ -43,6 +43,13 @@ global.fetch = (url, opts) => { const middlewares = [ serve('assets'), + // set test cookie + (req, res, next) => { + res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600'); + next(); + }, + + // emit messages so we can capture requests (req, res, next) => { if (!pending) return next(); diff --git a/test/app/routes/credentials/index.html b/test/app/routes/credentials/index.html new file mode 100644 index 000000000..fa29efa84 --- /dev/null +++ b/test/app/routes/credentials/index.html @@ -0,0 +1,12 @@ +

{{message}}

+ + \ No newline at end of file diff --git a/test/app/routes/credentials/test.json.js b/test/app/routes/credentials/test.json.js new file mode 100644 index 000000000..3e7a549e0 --- /dev/null +++ b/test/app/routes/credentials/test.json.js @@ -0,0 +1,28 @@ +export function get(req, res) { + const cookies = req.headers.cookie + ? req.headers.cookie.split(/,\s+/).reduce((cookies, cookie) => { + const [pair] = cookie.split('; '); + const [name, value] = pair.split('='); + cookies[name] = value; + return cookies; + }, {}) + : {}; + + if (cookies.test) { + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + + res.end(JSON.stringify({ + message: cookies.test + })); + } else { + res.writeHead(403, { + 'Content-Type': 'application/json' + }); + + res.end(JSON.stringify({ + message: 'unauthorized' + })); + } +} \ No newline at end of file diff --git a/test/app/routes/index.html b/test/app/routes/index.html index 29febe7bd..2aec30824 100644 --- a/test/app/routes/index.html +++ b/test/app/routes/index.html @@ -10,6 +10,7 @@

Great success!

redirect broken link error link +credentials blog
diff --git a/test/common/test.js b/test/common/test.js index c3626eed4..ca4a063d3 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -533,6 +533,32 @@ function run({ mode, basepath = '' }) { assert.equal(title, 'Stored title'); }); }); + + it('sends cookies when using this.fetch with credentials: "include"', () => { + return nightmare.goto(`${base}/credentials?creds=include`) + .page.title() + .then(title => { + assert.equal(title, 'woohoo!'); + }); + }); + + it('does not send cookies when using this.fetch without credentials', () => { + return nightmare.goto(`${base}/credentials`) + .page.title() + .then(title => { + assert.equal(title, 'unauthorized'); + }); + }); + + it('delegates to fetch on the client', () => { + return nightmare.goto(base).init() + .click('[href="credentials?creds=include"]') + .wait(100) + .page.title() + .then(title => { + assert.equal(title, 'woohoo!'); + }); + }); }); describe('headers', () => {