1 | "use strict";
|
2 |
|
3 | const assert = require("assert");
|
4 | const NodeFetch = require("node-fetch");
|
5 | const supertest = require("supertest");
|
6 | const url = require("url");
|
7 | const BrowserTab = require("./lib/BrowserTab");
|
8 | const {version} = require("./package.json");
|
9 | const {CookieAccessInfo} = require("cookiejar");
|
10 | const {normalizeHeaders, getLocationHost} = require("./lib/getHeaders");
|
11 | const saveToJar = require("./lib/saveToJar");
|
12 |
|
13 | module.exports = Tallahassee;
|
14 |
|
15 | const responseSymbol = Symbol.for("response");
|
16 |
|
17 | class 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 |
|
33 | class 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 |
|
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 |
|
152 | function 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 |
|
159 | Tallahassee.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 | };
|