UNPKG

6.44 kBJavaScriptView Raw
1"use strict";
2
3const assert = require("assert");
4const NodeFetch = require("node-fetch");
5const supertest = require("supertest");
6const url = require("url");
7const BrowserTab = require("./lib/BrowserTab");
8const {version} = require("./package.json");
9const {CookieAccessInfo} = require("cookiejar");
10const {normalizeHeaders, getLocationHost} = require("./lib/getHeaders");
11const saveToJar = require("./lib/saveToJar");
12
13module.exports = Tallahassee;
14
15const responseSymbol = Symbol.for("response");
16
17class OriginResponse {
18 constructor(uri, response, originHost, protocol) {
19 this[responseSymbol] = response;
20 const status = this.status = response.statusCode;
21 this.ok = status >= 200 && status < 300;
22 this.headers = new Map(Object.entries(response.headers));
23 this.url = originHost ? `${protocol}//${originHost}${uri}` : response.request.url;
24 }
25 text() {
26 return Promise.resolve(this[responseSymbol].text);
27 }
28 json() {
29 return Promise.resolve(this[responseSymbol].body);
30 }
31}
32
33class WebPage {
34 constructor(agent, originRequestHeaders) {
35 this.agent = agent;
36 this.jar = agent.jar;
37 this.originRequestHeaders = originRequestHeaders;
38 this.originHost = getLocationHost(originRequestHeaders);
39 this.userAgent = `Tallahassee/${version}`;
40 this.protocol = `${originRequestHeaders["x-forwarded-proto"] || "http"}:`;
41 this.referrer = originRequestHeaders.referer;
42 }
43 async load(uri, headers, statusCode = 200) {
44 const requestHeaders = normalizeHeaders(headers);
45 if (requestHeaders["user-agent"]) this.userAgent = requestHeaders["user-agent"];
46
47 if (requestHeaders.cookie) {
48 const publicHost = getLocationHost(requestHeaders);
49 const parsedUri = url.parse(uri);
50 const cookieDomain = parsedUri.hostname || publicHost || this.originHost || "127.0.0.1";
51 const isSecure = (parsedUri.protocol || this.protocol) === "https:";
52
53 this.agent.jar.setCookies(requestHeaders.cookie.split(";").map((c) => c.trim()).filter(Boolean), cookieDomain, "/", isSecure);
54 }
55
56 const resp = await this.fetch(uri, {
57 method: "GET",
58 headers: requestHeaders,
59 });
60 assert.equal(resp.status, statusCode, `Unexepected status code. Expected: ${statusCode}. Actual: ${resp.statusCode}`);
61 assert(resp.headers.get("content-type").match(/text\/html/i), `Unexepected content type. Expected: text/html. Actual: ${resp.headers["content-type"]}`);
62 const browser = new BrowserTab(this, resp);
63 return browser.load();
64 }
65 async submit(uri, options) {
66 const res = await this.fetch(uri, options);
67 const response = await this.handleResponse(res, options);
68 const browser = new BrowserTab(this, response);
69 return browser.load();
70 }
71 async fetch(uri, requestOptions = {}) {
72 this.numRedirects = 0;
73 const res = await this.makeRequest(uri, requestOptions);
74 return this.handleResponse(res, requestOptions);
75 }
76 async handleResponse(res, requestOptions) {
77 const setCookieHeader = res.headers.get("set-cookie");
78 if (setCookieHeader) {
79 const cookieDomain = new URL(res.url).hostname;
80 saveToJar(this.jar, setCookieHeader, cookieDomain);
81 }
82
83 if (res.status > 300 && res.status < 309 && requestOptions.redirect !== "manual") {
84 this.numRedirects++;
85 if (this.numRedirects > 20) {
86 throw new Error("Too many redirects");
87 }
88 const location = res.headers.get("location");
89 const redirectOptions = {...requestOptions};
90
91 if (res.status === 307 || res.status === 308) {
92 // NO-OP
93 } else {
94 redirectOptions.method = "GET";
95 delete redirectOptions.body;
96 }
97
98 const redirectedRes = await this.makeRequest(location, redirectOptions);
99 return this.handleResponse(redirectedRes, requestOptions);
100 }
101
102 return res;
103 }
104 makeRequest(uri, requestOptions = {method: "GET", headers: {}}) {
105 const parsedUri = url.parse(uri);
106 let headers = requestOptions.headers = normalizeHeaders(requestOptions.headers);
107 const isLocal = uri.startsWith("/") || parsedUri.hostname === this.originHost;
108 if (isLocal) {
109 headers = requestOptions.headers = {...this.originRequestHeaders, ...headers};
110 } else {
111 headers.host = parsedUri.host;
112 }
113
114 const publicHost = getLocationHost(headers);
115 const cookieDomain = parsedUri.hostname || publicHost || this.originHost || "127.0.0.1";
116 const isSecure = (parsedUri.protocol || this.protocol) === "https:";
117 const accessInfo = CookieAccessInfo(cookieDomain, parsedUri.pathname, isSecure);
118
119 const cookieValue = this.jar.getCookies(accessInfo).toValueString();
120 if (cookieValue) headers.cookie = cookieValue;
121
122 return isLocal ? this.originRequest(parsedUri.path, requestOptions) : NodeFetch(uri, {...requestOptions, redirect: "manual"});
123 }
124 async originRequest(uri, requestOptions) {
125 const req = this.buildRequest(uri, requestOptions);
126 if (requestOptions.headers) {
127 for (const header in requestOptions.headers) {
128 const headerValue = requestOptions.headers[header];
129 if (headerValue) req.set(header, requestOptions.headers[header]);
130 }
131 }
132
133 const res = await req;
134 return new OriginResponse(uri, res, this.originHost, this.protocol);
135 }
136 buildRequest(uri, requestOptions) {
137 switch (requestOptions.method) {
138 case "POST":
139 return this.agent.post(uri).send(requestOptions.body);
140 case "DELETE":
141 return this.agent.delete(uri).send(requestOptions.body);
142 case "PUT":
143 return this.agent.put(uri).send(requestOptions.body);
144 case "HEAD":
145 return this.agent.head(uri);
146 default:
147 return this.agent.get(uri);
148 }
149 }
150}
151
152function Tallahassee(app, options = {}) {
153 if (!(this instanceof Tallahassee)) return new Tallahassee(app, options);
154 const agent = this.agent = supertest.agent(app);
155 this.jar = agent.jar;
156 this.options = options;
157}
158
159Tallahassee.prototype.navigateTo = async function navigateTo(linkUrl, headers = {}, statusCode = 200) {
160 const requestHeaders = {
161 ...normalizeHeaders(this.options.headers),
162 ...normalizeHeaders(headers),
163 };
164
165 if (requestHeaders["set-cookie"]) {
166 const setCookies = requestHeaders["set-cookie"];
167 saveToJar(this.jar, setCookies);
168 requestHeaders["set-cookie"] = undefined;
169 }
170
171 const webPage = new WebPage(this.agent, requestHeaders);
172 return webPage.load(linkUrl, requestHeaders, statusCode);
173};