xref: /haiku/src/apps/haikudepot/server/AbstractServerProcess.cpp (revision 5629675a326ecf2ff3fd23f154beb525c171048d)
1 /*
2  * Copyright 2017-2018, 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 		fprintf(stderr, "unable to parse the meta-data date and time from [%s]"
126 			" - cannot set the 'If-Modified-Since' header\n",
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 		fprintf(stderr, "the meta data was read from [%s], but no values "
152 			"were extracted\n", 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 		printf("[%s] unable to find the meta data file at [%s]\n", 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 				printf("[%s] did rename [%s] --> [%s]\n",
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 		printf("[%s] exceeded %d redirects --> failure\n", Name(),
263 			MAX_REDIRECTS);
264 		return B_IO_ERROR;
265 	}
266 
267 	if (failures > MAX_FAILURES) {
268 		printf("[%s] exceeded %d failures\n", Name(), MAX_FAILURES);
269 		return B_IO_ERROR;
270 	}
271 
272 	printf("[%s] will stream '%s' to [%s]\n", 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 		fprintf(stdout, "[%s] did complete streaming data [%"
318 			B_PRIdSSIZE " bytes]\n", Name(), listener.ContentLength());
319 		return B_OK;
320 	} else if (statusCode == B_HTTP_STATUS_NOT_MODIFIED) {
321 		fprintf(stdout, "[%s] remote data has not changed since [%s]\n",
322 			Name(), 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 			fprintf(stdout, "[%s] will redirect to; %s\n",
331 				Name(), redirectUrl.UrlString().String());
332 			return DownloadToLocalFile(targetFilePath, redirectUrl,
333 				redirects + 1, 0);
334 		}
335 
336 		fprintf(stdout, "[%s] unable to find 'Location' header for redirect\n",
337 			Name());
338 		return B_IO_ERROR;
339 	} else {
340 		if (statusCode == 0 || (statusCode / 100) == 5) {
341 			fprintf(stdout, "error response from server [%" B_PRId32 "] --> "
342 				"retry...\n", statusCode);
343 			return DownloadToLocalFile(targetFilePath, url, redirects,
344 				failures + 1);
345 		}
346 
347 		fprintf(stdout, "[%s] unexpected response from server [%" B_PRId32 "]\n",
348 			Name(), statusCode);
349 		return B_IO_ERROR;
350 	}
351 }
352 
353 
354 status_t
355 AbstractServerProcess::DeleteLocalFile(const BPath& currentFilePath)
356 {
357 	if (0 == remove(currentFilePath.Path()))
358 		return B_OK;
359 
360 	return B_IO_ERROR;
361 }
362 
363 
364 /*!	When a file is damaged or corrupted in some way, the file should be 'moved
365     aside' so that it is not involved in the next update.  This method will
366     create such an alternative 'damaged' file location and move this file to
367     that location.
368 */
369 
370 status_t
371 AbstractServerProcess::MoveDamagedFileAside(const BPath& currentFilePath)
372 {
373 	BPath damagedFilePath;
374 	BString damagedLeaf;
375 
376 	damagedLeaf.SetToFormat("%s__damaged", currentFilePath.Leaf());
377 	currentFilePath.GetParent(&damagedFilePath);
378 	damagedFilePath.Append(damagedLeaf.String());
379 
380 	if (0 != rename(currentFilePath.Path(), damagedFilePath.Path())) {
381 		printf("[%s] unable to move damaged file [%s] aside to [%s]\n",
382 			Name(), currentFilePath.Path(), damagedFilePath.Path());
383 		return B_IO_ERROR;
384 	}
385 
386 	printf("[%s] did move damaged file [%s] aside to [%s]\n",
387 		Name(), currentFilePath.Path(), damagedFilePath.Path());
388 
389 	return B_OK;
390 }
391 
392 
393 bool
394 AbstractServerProcess::IsSuccess(status_t e) {
395 	return e == B_OK || e == HD_ERR_NOT_MODIFIED;
396 }
397