1 // Copyright 2015 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/test/embedded_test_server/request_handler_util.h"
6
7 #include <stdlib.h>
8
9 #include <ctime>
10 #include <sstream>
11 #include <utility>
12
13 #include "base/base64.h"
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/format_macros.h"
17 #include "base/strings/escape.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/stringprintf.h"
20 #include "base/strings/utf_string_conversions.h"
21 #include "base/threading/thread_restrictions.h"
22 #include "build/build_config.h"
23 #include "net/base/url_util.h"
24 #include "net/http/http_byte_range.h"
25 #include "net/http/http_util.h"
26 #include "net/test/embedded_test_server/http_request.h"
27 #include "url/gurl.h"
28
29 namespace net::test_server {
30 constexpr base::FilePath::CharType kMockHttpHeadersExtension[] =
31 FILE_PATH_LITERAL("mock-http-headers");
32
GetContentType(const base::FilePath & path)33 std::string GetContentType(const base::FilePath& path) {
34 if (path.MatchesExtension(FILE_PATH_LITERAL(".crx")))
35 return "application/x-chrome-extension";
36 if (path.MatchesExtension(FILE_PATH_LITERAL(".css")))
37 return "text/css";
38 if (path.MatchesExtension(FILE_PATH_LITERAL(".exe")))
39 return "application/octet-stream";
40 if (path.MatchesExtension(FILE_PATH_LITERAL(".gif")))
41 return "image/gif";
42 if (path.MatchesExtension(FILE_PATH_LITERAL(".gzip")) ||
43 path.MatchesExtension(FILE_PATH_LITERAL(".gz"))) {
44 return "application/x-gzip";
45 }
46 if (path.MatchesExtension(FILE_PATH_LITERAL(".jpeg")) ||
47 path.MatchesExtension(FILE_PATH_LITERAL(".jpg"))) {
48 return "image/jpeg";
49 }
50 if (path.MatchesExtension(FILE_PATH_LITERAL(".js")))
51 return "application/javascript";
52 if (path.MatchesExtension(FILE_PATH_LITERAL(".json")))
53 return "application/json";
54 if (path.MatchesExtension(FILE_PATH_LITERAL(".pdf")))
55 return "application/pdf";
56 if (path.MatchesExtension(FILE_PATH_LITERAL(".svg")))
57 return "image/svg+xml";
58 if (path.MatchesExtension(FILE_PATH_LITERAL(".txt")))
59 return "text/plain";
60 if (path.MatchesExtension(FILE_PATH_LITERAL(".wav")))
61 return "audio/wav";
62 if (path.MatchesExtension(FILE_PATH_LITERAL(".webp")))
63 return "image/webp";
64 if (path.MatchesExtension(FILE_PATH_LITERAL(".mp4")))
65 return "video/mp4";
66 if (path.MatchesExtension(FILE_PATH_LITERAL(".webm")))
67 return "video/webm";
68 if (path.MatchesExtension(FILE_PATH_LITERAL(".xml")))
69 return "text/xml";
70 if (path.MatchesExtension(FILE_PATH_LITERAL(".mhtml")))
71 return "multipart/related";
72 if (path.MatchesExtension(FILE_PATH_LITERAL(".mht")))
73 return "message/rfc822";
74 if (path.MatchesExtension(FILE_PATH_LITERAL(".html")) ||
75 path.MatchesExtension(FILE_PATH_LITERAL(".htm"))) {
76 return "text/html";
77 }
78 return "";
79 }
80
ShouldHandle(const HttpRequest & request,const std::string & path_prefix)81 bool ShouldHandle(const HttpRequest& request, const std::string& path_prefix) {
82 if (request.method == METHOD_CONNECT) {
83 return false;
84 }
85
86 GURL url = request.GetURL();
87 return url.path() == path_prefix ||
88 base::StartsWith(url.path(), path_prefix + "/",
89 base::CompareCase::SENSITIVE);
90 }
91
HandlePrefixedRequest(const std::string & prefix,const EmbeddedTestServer::HandleRequestCallback & handler,const HttpRequest & request)92 std::unique_ptr<HttpResponse> HandlePrefixedRequest(
93 const std::string& prefix,
94 const EmbeddedTestServer::HandleRequestCallback& handler,
95 const HttpRequest& request) {
96 if (ShouldHandle(request, prefix))
97 return handler.Run(request);
98 return nullptr;
99 }
100
ParseQuery(const GURL & url)101 RequestQuery ParseQuery(const GURL& url) {
102 RequestQuery queries;
103 for (QueryIterator it(url); !it.IsAtEnd(); it.Advance()) {
104 std::string unescaped_query = base::UnescapeBinaryURLComponent(
105 it.GetKey(), base::UnescapeRule::REPLACE_PLUS_WITH_SPACE);
106 queries[unescaped_query].push_back(it.GetUnescapedValue());
107 }
108 return queries;
109 }
110
GetFilePathWithReplacements(const std::string & original_file_path,const base::StringPairs & text_to_replace)111 std::string GetFilePathWithReplacements(
112 const std::string& original_file_path,
113 const base::StringPairs& text_to_replace) {
114 std::string new_file_path = original_file_path;
115 for (const auto& replacement : text_to_replace) {
116 const std::string& old_text = replacement.first;
117 const std::string& new_text = replacement.second;
118 std::string base64_old;
119 std::string base64_new;
120 base::Base64Encode(old_text, &base64_old);
121 base::Base64Encode(new_text, &base64_new);
122 if (new_file_path == original_file_path)
123 new_file_path += "?";
124 else
125 new_file_path += "&";
126 new_file_path += "replace_text=";
127 new_file_path += base64_old;
128 new_file_path += ":";
129 new_file_path += base64_new;
130 }
131
132 return new_file_path;
133 }
134
135 // Returns false if there were errors, otherwise true.
UpdateReplacedText(const RequestQuery & query,std::string * data)136 bool UpdateReplacedText(const RequestQuery& query, std::string* data) {
137 auto replace_text = query.find("replace_text");
138 if (replace_text == query.end())
139 return true;
140
141 for (const auto& replacement : replace_text->second) {
142 if (replacement.find(":") == std::string::npos)
143 return false;
144 std::string find;
145 std::string with;
146 base::Base64Decode(replacement.substr(0, replacement.find(":")), &find);
147 base::Base64Decode(replacement.substr(replacement.find(":") + 1), &with);
148 base::ReplaceSubstringsAfterOffset(data, 0, find, with);
149 }
150
151 return true;
152 }
153
154 // Handles |request| by serving a file from under |server_root|.
HandleFileRequest(const base::FilePath & server_root,const HttpRequest & request)155 std::unique_ptr<HttpResponse> HandleFileRequest(
156 const base::FilePath& server_root,
157 const HttpRequest& request) {
158 // This is a test-only server. Ignore I/O thread restrictions.
159 // TODO(svaldez): Figure out why thread is I/O restricted in the first place.
160 base::ScopedAllowBlockingForTesting allow_blocking;
161
162 if (request.method == METHOD_CONNECT) {
163 return nullptr;
164 }
165
166 // A proxy request will have an absolute path. Simulate the proxy by stripping
167 // the scheme, host, and port.
168 GURL request_url = request.GetURL();
169 std::string relative_path(request_url.path());
170
171 std::string post_prefix("/post/");
172 if (base::StartsWith(relative_path, post_prefix,
173 base::CompareCase::SENSITIVE)) {
174 if (request.method != METHOD_POST)
175 return nullptr;
176 relative_path = relative_path.substr(post_prefix.size() - 1);
177 }
178
179 RequestQuery query = ParseQuery(request_url);
180
181 auto failed_response = std::make_unique<BasicHttpResponse>();
182 failed_response->set_code(HTTP_NOT_FOUND);
183
184 if (query.find("expected_body") != query.end()) {
185 if (request.content.find(query["expected_body"].front()) ==
186 std::string::npos) {
187 return failed_response;
188 }
189 }
190
191 if (query.find("expected_headers") != query.end()) {
192 for (const auto& header : query["expected_headers"]) {
193 if (header.find(":") == std::string::npos)
194 return failed_response;
195 std::string key = header.substr(0, header.find(":"));
196 std::string value = header.substr(header.find(":") + 1);
197 if (request.headers.find(key) == request.headers.end() ||
198 request.headers.at(key) != value) {
199 return failed_response;
200 }
201 }
202 }
203
204 // Trim the first byte ('/').
205 DCHECK(base::StartsWith(relative_path, "/", base::CompareCase::SENSITIVE));
206 std::string request_path = relative_path.substr(1);
207 base::FilePath file_path(server_root.AppendASCII(request_path));
208 std::string file_contents;
209 if (!base::ReadFileToString(file_path, &file_contents)) {
210 file_path = file_path.AppendASCII("index.html");
211 if (!base::ReadFileToString(file_path, &file_contents))
212 return nullptr;
213 }
214
215 if (request.method == METHOD_HEAD)
216 file_contents = "";
217
218 if (!UpdateReplacedText(query, &file_contents))
219 return failed_response;
220
221 base::FilePath headers_path(
222 file_path.AddExtension(kMockHttpHeadersExtension));
223
224 if (base::PathExists(headers_path)) {
225 std::string headers_contents;
226
227 if (!base::ReadFileToString(headers_path, &headers_contents) ||
228 !UpdateReplacedText(query, &headers_contents)) {
229 return nullptr;
230 }
231
232 return std::make_unique<RawHttpResponse>(headers_contents, file_contents);
233 }
234
235 auto http_response = std::make_unique<BasicHttpResponse>();
236 http_response->set_code(HTTP_OK);
237
238 if (request.headers.find("Range") != request.headers.end()) {
239 std::vector<HttpByteRange> ranges;
240
241 if (HttpUtil::ParseRangeHeader(request.headers.at("Range"), &ranges) &&
242 ranges.size() == 1) {
243 ranges[0].ComputeBounds(file_contents.size());
244 size_t start = ranges[0].first_byte_position();
245 size_t end = ranges[0].last_byte_position();
246
247 http_response->set_code(HTTP_PARTIAL_CONTENT);
248 http_response->AddCustomHeader(
249 "Content-Range",
250 base::StringPrintf("bytes %" PRIuS "-%" PRIuS "/%" PRIuS, start, end,
251 file_contents.size()));
252
253 file_contents = file_contents.substr(start, end - start + 1);
254 }
255 }
256
257 http_response->set_content_type(GetContentType(file_path));
258 http_response->AddCustomHeader("Accept-Ranges", "bytes");
259 http_response->AddCustomHeader("ETag", "'" + file_path.MaybeAsASCII() + "'");
260 http_response->set_content(file_contents);
261 return http_response;
262 }
263
264 } // namespace net::test_server
265