1 // Copyright (c) 2012- PPSSPP Project.
2 
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, version 2.0 or later versions.
6 
7 // This program is distributed in the hope that it will be useful,
8 // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10 // GNU General Public License 2.0 for more details.
11 
12 // A copy of the GPL 2.0 should have been included with the program.
13 // If not, see http://www.gnu.org/licenses/
14 
15 // Official git repository and contact information can be found at
16 // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17 
18 #include <algorithm>
19 
20 #include "Common/Common.h"
21 #include "Common/Log.h"
22 #include "Common/StringUtils.h"
23 #include "Core/Config.h"
24 #include "Core/FileLoaders/HTTPFileLoader.h"
25 
HTTPFileLoader(const::Path & filename)26 HTTPFileLoader::HTTPFileLoader(const ::Path &filename)
27 	: url_(filename.ToString()), progress_(&cancel_), filename_(filename) {
28 }
29 
Prepare()30 void HTTPFileLoader::Prepare() {
31 	std::call_once(preparedFlag_, [this](){
32 		client_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION));
33 
34 		std::vector<std::string> responseHeaders;
35 		Url resourceURL = url_;
36 		int redirectsLeft = 20;
37 		while (redirectsLeft > 0) {
38 			responseHeaders.clear();
39 			int code = SendHEAD(resourceURL, responseHeaders);
40 			if (code == -400) {
41 				// Already reported the error.
42 				return;
43 			}
44 
45 			if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
46 				Disconnect();
47 
48 				std::string redirectURL;
49 				if (http::GetHeaderValue(responseHeaders, "Location", &redirectURL)) {
50 					Url url(resourceURL);
51 					url = url.Relative(redirectURL);
52 
53 					if (url.ToString() == url_.ToString() || url.ToString() == resourceURL.ToString()) {
54 						ERROR_LOG(LOADER, "HTTP request failed, hit a redirect loop");
55 						latestError_ = "Could not connect (redirect loop)";
56 						return;
57 					}
58 
59 					resourceURL = url;
60 					redirectsLeft--;
61 					continue;
62 				}
63 
64 				// No Location header?
65 				ERROR_LOG(LOADER, "HTTP request failed, invalid redirect");
66 				latestError_ = "Could not connect (invalid response)";
67 				return;
68 			}
69 
70 			if (code != 200) {
71 				// Leave size at 0, invalid.
72 				ERROR_LOG(LOADER, "HTTP request failed, got %03d for %s", code, filename_.c_str());
73 				latestError_ = "Could not connect (invalid response)";
74 				Disconnect();
75 				return;
76 			}
77 
78 			// We got a good, non-redirect response.
79 			redirectsLeft = 0;
80 			url_ = resourceURL;
81 		}
82 
83 		// TODO: Expire cache via ETag, etc.
84 		bool acceptsRange = false;
85 		for (std::string header : responseHeaders) {
86 			if (startsWithNoCase(header, "Content-Length:")) {
87 				size_t size_pos = header.find_first_of(' ');
88 				if (size_pos != header.npos) {
89 					size_pos = header.find_first_not_of(' ', size_pos);
90 				}
91 				if (size_pos != header.npos) {
92 					filesize_ = atoll(&header[size_pos]);
93 				}
94 			}
95 			if (startsWithNoCase(header, "Accept-Ranges:")) {
96 				std::string lowerHeader = header;
97 				std::transform(lowerHeader.begin(), lowerHeader.end(), lowerHeader.begin(), tolower);
98 				// TODO: Delimited.
99 				if (lowerHeader.find("bytes") != lowerHeader.npos) {
100 					acceptsRange = true;
101 				}
102 			}
103 		}
104 
105 		// TODO: Keepalive instead.
106 		Disconnect();
107 
108 		if (!acceptsRange) {
109 			WARN_LOG(LOADER, "HTTP server did not advertise support for range requests.");
110 		}
111 		if (filesize_ == 0) {
112 			ERROR_LOG(LOADER, "Could not determine file size for %s", filename_.c_str());
113 		}
114 
115 		// If we didn't end up with a filesize_ (e.g. chunked response), give up.  File invalid.
116 	});
117 }
118 
SendHEAD(const Url & url,std::vector<std::string> & responseHeaders)119 int HTTPFileLoader::SendHEAD(const Url &url, std::vector<std::string> &responseHeaders) {
120 	if (!url.Valid()) {
121 		ERROR_LOG(LOADER, "HTTP request failed, invalid URL");
122 		latestError_ = "Invalid URL";
123 		return -400;
124 	}
125 
126 	if (!client_.Resolve(url.Host().c_str(), url.Port())) {
127 		ERROR_LOG(LOADER, "HTTP request failed, unable to resolve: |%s| port %d", url.Host().c_str(), url.Port());
128 		latestError_ = "Could not connect (name not resolved)";
129 		return -400;
130 	}
131 
132 	client_.SetDataTimeout(20.0);
133 	Connect();
134 	if (!connected_) {
135 		ERROR_LOG(LOADER, "HTTP request failed, failed to connect: %s port %d", url.Host().c_str(), url.Port());
136 		latestError_ = "Could not connect (refused to connect)";
137 		return -400;
138 	}
139 
140 	http::RequestParams req(url.Resource(), "*/*");
141 	int err = client_.SendRequest("HEAD", req, nullptr, &progress_);
142 	if (err < 0) {
143 		ERROR_LOG(LOADER, "HTTP request failed, failed to send request: %s port %d", url.Host().c_str(), url.Port());
144 		latestError_ = "Could not connect (could not request data)";
145 		Disconnect();
146 		return -400;
147 	}
148 
149 	net::Buffer readbuf;
150 	return client_.ReadResponseHeaders(&readbuf, responseHeaders, &progress_);
151 }
152 
~HTTPFileLoader()153 HTTPFileLoader::~HTTPFileLoader() {
154 	Disconnect();
155 }
156 
Exists()157 bool HTTPFileLoader::Exists() {
158 	Prepare();
159 	return url_.Valid() && filesize_ > 0;
160 }
161 
ExistsFast()162 bool HTTPFileLoader::ExistsFast() {
163 	return url_.Valid();
164 }
165 
IsDirectory()166 bool HTTPFileLoader::IsDirectory() {
167 	// Only files.
168 	return false;
169 }
170 
FileSize()171 s64 HTTPFileLoader::FileSize() {
172 	Prepare();
173 	return filesize_;
174 }
175 
GetPath() const176 Path HTTPFileLoader::GetPath() const {
177 	return filename_;
178 }
179 
ReadAt(s64 absolutePos,size_t bytes,void * data,Flags flags)180 size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) {
181 	Prepare();
182 	std::lock_guard<std::mutex> guard(readAtMutex_);
183 
184 	s64 absoluteEnd = std::min(absolutePos + (s64)bytes, filesize_);
185 	if (absolutePos >= filesize_ || bytes == 0) {
186 		// Read outside of the file or no read at all, just fail immediately.
187 		return 0;
188 	}
189 
190 	Connect();
191 	if (!connected_) {
192 		return 0;
193 	}
194 
195 	char requestHeaders[4096];
196 	// Note that the Range header is *inclusive*.
197 	snprintf(requestHeaders, sizeof(requestHeaders),
198 		"Range: bytes=%lld-%lld\r\n", absolutePos, absoluteEnd - 1);
199 
200 	http::RequestParams req(url_.Resource(), "*/*");
201 	int err = client_.SendRequest("GET", req, requestHeaders, &progress_);
202 	if (err < 0) {
203 		latestError_ = "Invalid response reading data";
204 		Disconnect();
205 		return 0;
206 	}
207 
208 	net::Buffer readbuf;
209 	std::vector<std::string> responseHeaders;
210 	int code = client_.ReadResponseHeaders(&readbuf, responseHeaders, &progress_);
211 	if (code != 206) {
212 		ERROR_LOG(LOADER, "HTTP server did not respond with range, received code=%03d", code);
213 		latestError_ = "Invalid response reading data";
214 		Disconnect();
215 		return 0;
216 	}
217 
218 	// TODO: Expire cache via ETag, etc.
219 	// We don't support multipart/byteranges responses.
220 	bool supportedResponse = false;
221 	for (std::string header : responseHeaders) {
222 		if (startsWithNoCase(header, "Content-Range:")) {
223 			// TODO: More correctness.  Whitespace can be missing or different.
224 			s64 first = -1, last = -1, total = -1;
225 			std::string lowerHeader = header;
226 			std::transform(lowerHeader.begin(), lowerHeader.end(), lowerHeader.begin(), tolower);
227 			if (sscanf(lowerHeader.c_str(), "content-range: bytes %lld-%lld/%lld", &first, &last, &total) >= 2) {
228 				if (first == absolutePos && last == absoluteEnd - 1) {
229 					supportedResponse = true;
230 				} else {
231 					ERROR_LOG(LOADER, "Unexpected HTTP range: got %lld-%lld, wanted %lld-%lld.", first, last, absolutePos, absoluteEnd - 1);
232 				}
233 			} else {
234 				ERROR_LOG(LOADER, "Unexpected HTTP range response: %s", header.c_str());
235 			}
236 		}
237 	}
238 
239 	// TODO: Would be nice to read directly.
240 	net::Buffer output;
241 	int res = client_.ReadResponseEntity(&readbuf, responseHeaders, &output, &progress_);
242 	if (res != 0) {
243 		ERROR_LOG(LOADER, "Unable to read HTTP response entity: %d", res);
244 		// Let's take anything we got anyway.  Not worse than returning nothing?
245 	}
246 
247 	// TODO: Keepalive instead.
248 	Disconnect();
249 
250 	if (!supportedResponse) {
251 		ERROR_LOG(LOADER, "HTTP server did not respond with the range we wanted.");
252 		latestError_ = "Invalid response reading data";
253 		return 0;
254 	}
255 
256 	size_t readBytes = output.size();
257 	output.Take(readBytes, (char *)data);
258 	filepos_ = absolutePos + readBytes;
259 	return readBytes;
260 }
261 
Connect()262 void HTTPFileLoader::Connect() {
263 	if (!connected_) {
264 		cancel_ = false;
265 		// Latency is important here, so reduce the timeout.
266 		connected_ = client_.Connect(3, 10.0, &cancel_);
267 	}
268 }
269