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 || url.path().starts_with(path_prefix + "/");
88 }
89
HandlePrefixedRequest(const std::string & prefix,const EmbeddedTestServer::HandleRequestCallback & handler,const HttpRequest & request)90 std::unique_ptr<HttpResponse> HandlePrefixedRequest(
91 const std::string& prefix,
92 const EmbeddedTestServer::HandleRequestCallback& handler,
93 const HttpRequest& request) {
94 if (ShouldHandle(request, prefix))
95 return handler.Run(request);
96 return nullptr;
97 }
98
ParseQuery(const GURL & url)99 RequestQuery ParseQuery(const GURL& url) {
100 RequestQuery queries;
101 for (QueryIterator it(url); !it.IsAtEnd(); it.Advance()) {
102 std::string unescaped_query = base::UnescapeBinaryURLComponent(
103 it.GetKey(), base::UnescapeRule::REPLACE_PLUS_WITH_SPACE);
104 queries[unescaped_query].push_back(it.GetUnescapedValue());
105 }
106 return queries;
107 }
108
GetFilePathWithReplacements(const std::string & original_file_path,const base::StringPairs & text_to_replace)109 std::string GetFilePathWithReplacements(
110 const std::string& original_file_path,
111 const base::StringPairs& text_to_replace) {
112 std::string new_file_path = original_file_path;
113 for (const auto& replacement : text_to_replace) {
114 const std::string& old_text = replacement.first;
115 const std::string& new_text = replacement.second;
116 std::string base64_old = base::Base64Encode(old_text);
117 std::string base64_new = base::Base64Encode(new_text);
118 if (new_file_path == original_file_path)
119 new_file_path += "?";
120 else
121 new_file_path += "&";
122 new_file_path += "replace_text=";
123 new_file_path += base64_old;
124 new_file_path += ":";
125 new_file_path += base64_new;
126 }
127
128 return new_file_path;
129 }
130
131 // Returns false if there were errors, otherwise true.
UpdateReplacedText(const RequestQuery & query,std::string * data)132 bool UpdateReplacedText(const RequestQuery& query, std::string* data) {
133 auto replace_text = query.find("replace_text");
134 if (replace_text == query.end())
135 return true;
136
137 for (const auto& replacement : replace_text->second) {
138 if (replacement.find(":") == std::string::npos)
139 return false;
140 std::string find;
141 std::string with;
142 base::Base64Decode(replacement.substr(0, replacement.find(":")), &find);
143 base::Base64Decode(replacement.substr(replacement.find(":") + 1), &with);
144 base::ReplaceSubstringsAfterOffset(data, 0, find, with);
145 }
146
147 return true;
148 }
149
150 // Handles |request| by serving a file from under |server_root|.
HandleFileRequest(const base::FilePath & server_root,const HttpRequest & request)151 std::unique_ptr<HttpResponse> HandleFileRequest(
152 const base::FilePath& server_root,
153 const HttpRequest& request) {
154 // This is a test-only server. Ignore I/O thread restrictions.
155 // TODO(svaldez): Figure out why thread is I/O restricted in the first place.
156 base::ScopedAllowBlockingForTesting allow_blocking;
157
158 if (request.method == METHOD_CONNECT) {
159 return nullptr;
160 }
161
162 // A proxy request will have an absolute path. Simulate the proxy by stripping
163 // the scheme, host, and port.
164 GURL request_url = request.GetURL();
165 std::string relative_path(request_url.path());
166
167 std::string_view post_prefix("/post/");
168 if (relative_path.starts_with(post_prefix)) {
169 if (request.method != METHOD_POST)
170 return nullptr;
171 relative_path = relative_path.substr(post_prefix.size() - 1);
172 }
173
174 RequestQuery query = ParseQuery(request_url);
175
176 auto failed_response = std::make_unique<BasicHttpResponse>();
177 failed_response->set_code(HTTP_NOT_FOUND);
178
179 if (query.find("expected_body") != query.end()) {
180 if (request.content.find(query["expected_body"].front()) ==
181 std::string::npos) {
182 return failed_response;
183 }
184 }
185
186 if (query.find("expected_headers") != query.end()) {
187 for (const auto& header : query["expected_headers"]) {
188 if (header.find(":") == std::string::npos)
189 return failed_response;
190 std::string key = header.substr(0, header.find(":"));
191 std::string value = header.substr(header.find(":") + 1);
192 if (request.headers.find(key) == request.headers.end() ||
193 request.headers.at(key) != value) {
194 return failed_response;
195 }
196 }
197 }
198
199 // Trim the first byte ('/').
200 DCHECK(relative_path.starts_with("/"));
201 std::string request_path = relative_path.substr(1);
202 base::FilePath file_path(server_root.AppendASCII(request_path));
203 std::string file_contents;
204 if (!base::ReadFileToString(file_path, &file_contents)) {
205 file_path = file_path.AppendASCII("index.html");
206 if (!base::ReadFileToString(file_path, &file_contents))
207 return nullptr;
208 }
209
210 if (request.method == METHOD_HEAD)
211 file_contents = "";
212
213 if (!UpdateReplacedText(query, &file_contents))
214 return failed_response;
215
216 base::FilePath headers_path(
217 file_path.AddExtension(kMockHttpHeadersExtension));
218
219 if (base::PathExists(headers_path)) {
220 std::string headers_contents;
221
222 if (!base::ReadFileToString(headers_path, &headers_contents) ||
223 !UpdateReplacedText(query, &headers_contents)) {
224 return nullptr;
225 }
226
227 return std::make_unique<RawHttpResponse>(headers_contents, file_contents);
228 }
229
230 auto http_response = std::make_unique<BasicHttpResponse>();
231 http_response->set_code(HTTP_OK);
232
233 if (request.headers.find("Range") != request.headers.end()) {
234 std::vector<HttpByteRange> ranges;
235
236 if (HttpUtil::ParseRangeHeader(request.headers.at("Range"), &ranges) &&
237 ranges.size() == 1) {
238 ranges[0].ComputeBounds(file_contents.size());
239 size_t start = ranges[0].first_byte_position();
240 size_t end = ranges[0].last_byte_position();
241
242 http_response->set_code(HTTP_PARTIAL_CONTENT);
243 http_response->AddCustomHeader(
244 "Content-Range",
245 base::StringPrintf("bytes %" PRIuS "-%" PRIuS "/%" PRIuS, start, end,
246 file_contents.size()));
247
248 file_contents = file_contents.substr(start, end - start + 1);
249 }
250 }
251
252 http_response->set_content_type(GetContentType(file_path));
253 http_response->AddCustomHeader("Accept-Ranges", "bytes");
254 http_response->AddCustomHeader("ETag", "'" + file_path.MaybeAsASCII() + "'");
255 http_response->set_content(file_contents);
256 return http_response;
257 }
258
259 } // namespace net::test_server
260