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