xref: /haiku/src/apps/haikudepot/server/AbstractServerProcess.cpp (revision 5889cb5e7e8e7bfea6072ddfe881f55d364a0cf0)
1 /*
2  * Copyright 2017-2020, Andrew Lindesay <apl@lindesay.co.nz>.
3  * All rights reserved. Distributed under the terms of the MIT License.
4  */
5 
6 
7 #include "AbstractServerProcess.h"
8 
9 #include <unistd.h>
10 #include <errno.h>
11 #include <string.h>
12 
13 #include <AutoDeleter.h>
14 #include <FileIO.h>
15 #include <HttpTime.h>
16 #include <UrlProtocolRoster.h>
17 
18 #include <support/ZlibCompressionAlgorithm.h>
19 
20 #include "HaikuDepotConstants.h"
21 #include "Logger.h"
22 #include "ServerHelper.h"
23 #include "ServerSettings.h"
24 #include "StandardMetaDataJsonEventListener.h"
25 #include "StorageUtils.h"
26 #include "ToFileUrlProtocolListener.h"
27 
28 
29 #define MAX_REDIRECTS 3
30 #define MAX_FAILURES 2
31 
32 
33 // 30 seconds
34 #define TIMEOUT_MICROSECONDS 3e+7
35 
36 
37 AbstractServerProcess::AbstractServerProcess(uint32 options)
38 	:
39 	AbstractProcess(),
40 	fOptions(options),
41 	fRequest(NULL)
42 {
43 }
44 
45 
46 AbstractServerProcess::~AbstractServerProcess()
47 {
48 }
49 
50 
51 bool
52 AbstractServerProcess::HasOption(uint32 flag)
53 {
54 	return (fOptions & flag) == flag;
55 }
56 
57 
58 bool
59 AbstractServerProcess::ShouldAttemptNetworkDownload(bool hasDataAlready)
60 {
61 	return
62 		!HasOption(SERVER_PROCESS_NO_NETWORKING)
63 		&& !(HasOption(SERVER_PROCESS_PREFER_CACHE) && hasDataAlready);
64 }
65 
66 
67 status_t
68 AbstractServerProcess::StopInternal()
69 {
70 	if (fRequest != NULL) {
71 		return fRequest->Stop();
72 	}
73 
74 	return AbstractProcess::StopInternal();
75 }
76 
77 
78 status_t
79 AbstractServerProcess::IfModifiedSinceHeaderValue(BString& headerValue) const
80 {
81 	BPath metaDataPath;
82 	BString jsonPath;
83 
84 	status_t result = GetStandardMetaDataPath(metaDataPath);
85 
86 	if (result != B_OK)
87 		return result;
88 
89 	GetStandardMetaDataJsonPath(jsonPath);
90 
91 	return IfModifiedSinceHeaderValue(headerValue, metaDataPath, jsonPath);
92 }
93 
94 
95 status_t
96 AbstractServerProcess::IfModifiedSinceHeaderValue(BString& headerValue,
97 	const BPath& metaDataPath, const BString& jsonPath) const
98 {
99 	headerValue.SetTo("");
100 	struct stat s;
101 
102 	if (-1 == stat(metaDataPath.Path(), &s)) {
103 		if (ENOENT != errno)
104 			 return B_ERROR;
105 
106 		return B_FILE_NOT_FOUND;
107 	}
108 
109 	if (s.st_size == 0)
110 		return B_BAD_VALUE;
111 
112 	StandardMetaData metaData;
113 	status_t result = PopulateMetaData(metaData, metaDataPath, jsonPath);
114 
115 	if (result == B_OK) {
116 
117 		// An example of this output would be; 'Fri, 24 Oct 2014 19:32:27 +0000'
118 
119 		BDateTime modifiedDateTime = metaData
120 			.GetDataModifiedTimestampAsDateTime();
121 		BPrivate::BHttpTime modifiedHttpTime(modifiedDateTime);
122 		headerValue.SetTo(modifiedHttpTime
123 			.ToString(BPrivate::B_HTTP_TIME_FORMAT_COOKIE));
124 	} else {
125 		HDERROR("unable to parse the meta-data date and time from [%s]"
126 			" - cannot set the 'If-Modified-Since' header",
127 			metaDataPath.Path());
128 	}
129 
130 	return result;
131 }
132 
133 
134 status_t
135 AbstractServerProcess::PopulateMetaData(
136 	StandardMetaData& metaData, const BPath& path,
137 	const BString& jsonPath) const
138 {
139 	StandardMetaDataJsonEventListener listener(jsonPath, metaData);
140 	status_t result = ParseJsonFromFileWithListener(&listener, path);
141 
142 	if (result != B_OK)
143 		return result;
144 
145 	result = listener.ErrorStatus();
146 
147 	if (result != B_OK)
148 		return result;
149 
150 	if (!metaData.IsPopulated()) {
151 		HDERROR("the meta data was read from [%s], but no values "
152 			"were extracted", path.Path());
153 		return B_BAD_DATA;
154 	}
155 
156 	return B_OK;
157 }
158 
159 
160 /* static */ bool
161 AbstractServerProcess::LooksLikeGzip(const char *pathStr)
162 {
163 	int l = strlen(pathStr);
164 	return l > 4 && 0 == strncmp(&pathStr[l - 3], ".gz", 3);
165 }
166 
167 
168 /*!	Note that a B_OK return code from this method may not indicate that the
169 	listening process went well.  One has to see if there was an error in
170 	the listener.
171 */
172 
173 status_t
174 AbstractServerProcess::ParseJsonFromFileWithListener(
175 	BJsonEventListener *listener,
176 	const BPath& path) const
177 {
178 	const char* pathStr = path.Path();
179 	FILE* file = fopen(pathStr, "rb");
180 
181 	if (file == NULL) {
182 		HDERROR("[%s] unable to find the meta data file at [%s]", Name(),
183 			path.Path());
184 		return B_FILE_NOT_FOUND;
185 	}
186 
187 	BFileIO rawInput(file, true); // takes ownership
188 
189 		// if the file extension ends with '.gz' then the data will be
190 		// compressed and the algorithm needs to decompress the data as
191 		// it is parsed.
192 
193 	if (LooksLikeGzip(pathStr)) {
194 		BDataIO* gzDecompressedInput = NULL;
195 		BZlibDecompressionParameters* zlibDecompressionParameters
196 			= new BZlibDecompressionParameters();
197 
198 		status_t result = BZlibCompressionAlgorithm()
199 			.CreateDecompressingInputStream(&rawInput,
200 				zlibDecompressionParameters, gzDecompressedInput);
201 
202 		if (B_OK != result)
203 			return result;
204 
205 		ObjectDeleter<BDataIO> gzDecompressedInputDeleter(gzDecompressedInput);
206 		BPrivate::BJson::Parse(gzDecompressedInput, listener);
207 	} else {
208 		BPrivate::BJson::Parse(&rawInput, listener);
209 	}
210 
211 	return B_OK;
212 }
213 
214 
215 /*! In order to reduce the chance of failure half way through downloading a
216     large file, this method will download the file to a temporary file and
217     then it can rename the file to the final target file.
218 */
219 
220 status_t
221 AbstractServerProcess::DownloadToLocalFileAtomically(
222 	const BPath& targetFilePath,
223 	const BUrl& url)
224 {
225 	BPath temporaryFilePath(tmpnam(NULL), NULL, true);
226 	status_t result = DownloadToLocalFile(
227 		temporaryFilePath, url, 0, 0);
228 
229 		// not copying if the data has not changed because the data will be
230 		// zero length.  This is if the result is APP_ERR_NOT_MODIFIED.
231 	if (result == B_OK) {
232 
233 			// if the file is zero length then assume that something has
234 			// gone wrong.
235 		off_t size;
236 		bool hasFile;
237 
238 		result = StorageUtils::ExistsObject(temporaryFilePath, &hasFile, NULL,
239 			&size);
240 
241 		if (result == B_OK && hasFile && size > 0) {
242 			if (rename(temporaryFilePath.Path(), targetFilePath.Path()) != 0) {
243 				HDINFO("[%s] did rename [%s] --> [%s]",
244 					Name(), temporaryFilePath.Path(), targetFilePath.Path());
245 				result = B_IO_ERROR;
246 			}
247 		}
248 	}
249 
250 	return result;
251 }
252 
253 
254 status_t
255 AbstractServerProcess::DownloadToLocalFile(const BPath& targetFilePath,
256 	const BUrl& url, uint32 redirects, uint32 failures)
257 {
258 	if (WasStopped())
259 		return B_CANCELED;
260 
261 	if (redirects > MAX_REDIRECTS) {
262 		HDINFO("[%s] exceeded %d redirects --> failure", Name(),
263 			MAX_REDIRECTS);
264 		return B_IO_ERROR;
265 	}
266 
267 	if (failures > MAX_FAILURES) {
268 		HDINFO("[%s] exceeded %d failures", Name(), MAX_FAILURES);
269 		return B_IO_ERROR;
270 	}
271 
272 	HDINFO("[%s] will stream '%s' to [%s]", Name(), url.UrlString().String(),
273 		targetFilePath.Path());
274 
275 	ToFileUrlProtocolListener listener(targetFilePath, Name(),
276 		Logger::IsTraceEnabled());
277 
278 	BHttpHeaders headers;
279 	ServerSettings::AugmentHeaders(headers);
280 
281 	BString ifModifiedSinceHeader;
282 	status_t ifModifiedSinceHeaderStatus = IfModifiedSinceHeaderValue(
283 		ifModifiedSinceHeader);
284 
285 	if (ifModifiedSinceHeaderStatus == B_OK &&
286 		ifModifiedSinceHeader.Length() > 0) {
287 		headers.AddHeader("If-Modified-Since", ifModifiedSinceHeader);
288 	}
289 
290 	thread_id thread;
291 
292 	{
293 		fRequest = dynamic_cast<BHttpRequest *>(
294 			BUrlProtocolRoster::MakeRequest(url, &listener));
295 		fRequest->SetHeaders(headers);
296 		fRequest->SetMaxRedirections(0);
297 		fRequest->SetTimeout(TIMEOUT_MICROSECONDS);
298 		thread = fRequest->Run();
299 	}
300 
301 	wait_for_thread(thread, NULL);
302 
303 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
304 		fRequest->Result());
305 	int32 statusCode = result.StatusCode();
306 	const BHttpHeaders responseHeaders = result.Headers();
307 	const char *locationC = responseHeaders["Location"];
308 	BString location;
309 
310 	if (locationC != NULL)
311 		location.SetTo(locationC);
312 
313 	delete fRequest;
314 	fRequest = NULL;
315 
316 	if (BHttpRequest::IsSuccessStatusCode(statusCode)) {
317 		HDINFO("[%s] did complete streaming data [%"
318 			B_PRIdSSIZE " bytes]", Name(), listener.ContentLength());
319 		return B_OK;
320 	} else if (statusCode == B_HTTP_STATUS_NOT_MODIFIED) {
321 		HDINFO("[%s] remote data has not changed since [%s]", Name(),
322 			ifModifiedSinceHeader.String());
323 		return HD_ERR_NOT_MODIFIED;
324 	} else if (statusCode == B_HTTP_STATUS_PRECONDITION_FAILED) {
325 		ServerHelper::NotifyClientTooOld(responseHeaders);
326 		return HD_CLIENT_TOO_OLD;
327 	} else if (BHttpRequest::IsRedirectionStatusCode(statusCode)) {
328 		if (location.Length() != 0) {
329 			BUrl redirectUrl(result.Url(), location);
330 			HDINFO("[%s] will redirect to; %s",
331 				Name(), redirectUrl.UrlString().String());
332 			return DownloadToLocalFile(targetFilePath, redirectUrl,
333 				redirects + 1, 0);
334 		}
335 
336 		HDERROR("[%s] unable to find 'Location' header for redirect", Name());
337 		return B_IO_ERROR;
338 	} else {
339 		if (statusCode == 0 || (statusCode / 100) == 5) {
340 			HDERROR("error response from server [%" B_PRId32 "] --> retry...",
341 				statusCode);
342 			return DownloadToLocalFile(targetFilePath, url, redirects,
343 				failures + 1);
344 		}
345 
346 		HDERROR("[%s] unexpected response from server [%" B_PRId32 "]",
347 			Name(), statusCode);
348 		return B_IO_ERROR;
349 	}
350 }
351 
352 
353 status_t
354 AbstractServerProcess::DeleteLocalFile(const BPath& currentFilePath)
355 {
356 	if (0 == remove(currentFilePath.Path()))
357 		return B_OK;
358 
359 	return B_IO_ERROR;
360 }
361 
362 
363 /*!	When a file is damaged or corrupted in some way, the file should be 'moved
364     aside' so that it is not involved in the next update.  This method will
365     create such an alternative 'damaged' file location and move this file to
366     that location.
367 */
368 
369 status_t
370 AbstractServerProcess::MoveDamagedFileAside(const BPath& currentFilePath)
371 {
372 	BPath damagedFilePath;
373 	BString damagedLeaf;
374 
375 	damagedLeaf.SetToFormat("%s__damaged", currentFilePath.Leaf());
376 	currentFilePath.GetParent(&damagedFilePath);
377 	damagedFilePath.Append(damagedLeaf.String());
378 
379 	if (0 != rename(currentFilePath.Path(), damagedFilePath.Path())) {
380 		HDERROR("[%s] unable to move damaged file [%s] aside to [%s]",
381 			Name(), currentFilePath.Path(), damagedFilePath.Path());
382 		return B_IO_ERROR;
383 	}
384 
385 	HDINFO("[%s] did move damaged file [%s] aside to [%s]",
386 		Name(), currentFilePath.Path(), damagedFilePath.Path());
387 
388 	return B_OK;
389 }
390 
391 
392 bool
393 AbstractServerProcess::IsSuccess(status_t e) {
394 	return e == B_OK || e == HD_ERR_NOT_MODIFIED;
395 }
396