xref: /aosp_15_r20/external/cronet/net/test/embedded_test_server/request_handler_util.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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