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
37AbstractServerProcess::AbstractServerProcess(uint32 options)
38	:
39	AbstractProcess(),
40	fOptions(options),
41	fRequest(NULL)
42{
43}
44
45
46AbstractServerProcess::~AbstractServerProcess()
47{
48}
49
50
51bool
52AbstractServerProcess::HasOption(uint32 flag)
53{
54	return (fOptions & flag) == flag;
55}
56
57
58bool
59AbstractServerProcess::ShouldAttemptNetworkDownload(bool hasDataAlready)
60{
61	return
62		!HasOption(SERVER_PROCESS_NO_NETWORKING)
63		&& !(HasOption(SERVER_PROCESS_PREFER_CACHE) && hasDataAlready);
64}
65
66
67status_t
68AbstractServerProcess::StopInternal()
69{
70	if (fRequest != NULL) {
71		return fRequest->Stop();
72	}
73
74	return AbstractProcess::StopInternal();
75}
76
77
78status_t
79AbstractServerProcess::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
95status_t
96AbstractServerProcess::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
134status_t
135AbstractServerProcess::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
161AbstractServerProcess::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
173status_t
174AbstractServerProcess::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
220status_t
221AbstractServerProcess::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
254status_t
255AbstractServerProcess::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
354status_t
355AbstractServerProcess::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
370status_t
371AbstractServerProcess::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
393bool
394AbstractServerProcess::IsSuccess(status_t e) {
395	return e == B_OK || e == HD_ERR_NOT_MODIFIED;
396}
397