Issues (1502)

1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2025 grommunio GmbH
7
 *
8
 * This is the entry point through which all requests are processed.
9
 */
10
11
ob_start(null, 1048576);
12
13
// ignore user abortions because this can lead to weird errors
14
ignore_user_abort(true);
15
16
require_once 'vendor/autoload.php';
17
18
if (!defined('GSYNC_CONFIG')) {
19
	define('GSYNC_CONFIG', 'config.php');
20
}
21
22
include_once GSYNC_CONFIG;
23
24
// Attempt to set maximum execution time
25
ini_set('max_execution_time', SCRIPT_TIMEOUT);
26
set_time_limit(SCRIPT_TIMEOUT);
27
28
try {
29
	// check config & initialize the basics
30
	GSync::CheckConfig();
31
	Request::Initialize();
32
	SLog::Initialize();
33
34
	SLog::Write(LOGLEVEL_DEBUG, "-------- Start");
35
	SLog::Write(
36
		LOGLEVEL_DEBUG,
37
		sprintf(
38
			"cmd='%s' devType='%s' devId='%s' getUser='%s' from='%s' version='%s' method='%s'",
39
			Request::GetCommand(),
40
			Request::GetDeviceType(),
41
			Request::GetDeviceID(),
42
			Request::GetGETUser(),
43
			Request::GetRemoteAddr(),
44
			@constant('GROMMUNIOSYNC_VERSION'),
45
			Request::GetMethod()
46
		)
47
	);
48
49
	// always request the authorization header
50
	if (!Request::HasAuthenticationInfo() || !Request::GetGETUser()) {
51
		throw new AuthenticationRequiredException("Access denied. Please send authorisation information");
52
	}
53
54
	GSync::CheckAdvancedConfig();
55
56
	// Process request headers and look for AS headers
57
	Request::ProcessHeaders();
58
59
	// Stop here if this is an OPTIONS request
60
	if (Request::IsMethodOPTIONS()) {
61
		RequestProcessor::Authenticate();
62
63
		throw new NoPostRequestException("Options request", NoPostRequestException::OPTIONS_REQUEST);
64
	}
65
66
	// Check required GET parameters
67
	if (Request::IsMethodPOST() && (Request::GetCommandCode() === false || !Request::GetDeviceID() || !Request::GetDeviceType())) {
68
		throw new FatalException("Requested the grommunio-sync URL without the required GET parameters");
69
	}
70
71
	// Load the backend
72
	$backend = GSync::GetBackend();
73
74
	// check the provisioning information
75
	if (
76
		PROVISIONING === true &&
77
		Request::IsMethodPOST() &&
78
		GSync::CommandNeedsProvisioning(Request::GetCommandCode()) &&
79
		(
80
			(Request::WasPolicyKeySent() && Request::GetPolicyKey() == 0) ||
81
			GSync::GetProvisioningManager()->ProvisioningRequired(Request::GetPolicyKey())
82
		) && (
83
			LOOSE_PROVISIONING === false ||
84
			(LOOSE_PROVISIONING === true && Request::WasPolicyKeySent())
85
		)) {
86
		// TODO for AS 14 send a wbxml response
87
		throw new ProvisioningRequiredException();
88
	}
89
90
	// most commands require an authenticated user
91
	if (GSync::CommandNeedsAuthentication(Request::GetCommandCode())) {
92
		RequestProcessor::Authenticate();
93
	}
94
95
	// Do the actual processing of the request
96
	if (Request::IsMethodGET()) {
97
		throw new NoPostRequestException("This is the grommunio-sync location and can only be accessed by Microsoft ActiveSync-capable devices", NoPostRequestException::GET_REQUEST);
98
	}
99
100
	// Do the actual request
101
	header(GSync::GetServerHeader());
102
103
	if (RequestProcessor::isUserAuthenticated()) {
104
		header("X-Grommunio-Sync-Version: " . @constant('GROMMUNIOSYNC_VERSION'));
105
		GSync::TrackConnection();
106
107
		// announce the supported AS versions (if not already sent to device)
108
		if (GSync::GetDeviceManager()->AnnounceASVersion()) {
109
			$versions = GSync::GetSupportedProtocolVersions(true);
110
			SLog::Write(LOGLEVEL_INFO, sprintf("Announcing latest AS version to device: %s", $versions));
111
			header("X-MS-RP: " . $versions);
112
		}
113
	}
114
115
	RequestProcessor::Initialize();
116
	RequestProcessor::HandleRequest();
117
118
	// eventually the RequestProcessor wants to send other headers to the mobile
119
	foreach (RequestProcessor::GetSpecialHeaders() as $header) {
120
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Special header: %s", $header));
121
		header($header);
122
	}
123
124
	// stream the data
125
	$len = ob_get_length();
126
	$data = ob_get_contents();
127
	ob_end_clean();
128
129
	// log amount of data transferred
130
	// TODO check $len when streaming more data (e.g. Attachments), as the data will be send chunked
131
	if (GSync::GetDeviceManager(false)) {
132
		GSync::GetDeviceManager()->SentData($len);
133
	}
134
135
	// Unfortunately, even though grommunio-sync can stream the data to the client
136
	// with a chunked encoding, using chunked encoding breaks the progress bar
137
	// on the PDA. So the data is de-chunk here, written a content-length header and
138
	// data send as a 'normal' packet. If the output packet exceeds 1MB (see ob_start)
139
	// then it will be sent as a chunked packet anyway because PHP will have to flush
140
	// the buffer.
141
	if (!headers_sent()) {
142
		header("Content-Length: {$len}");
143
	}
144
145
	// send vnd.ms-sync.wbxml content type header if there is no content
146
	// otherwise text/html content type is added which might break some devices
147
	if (!headers_sent() && $len == 0) {
148
		header("Content-Type: application/vnd.ms-sync.wbxml");
149
	}
150
151
	echo $data;
152
153
	// destruct backend after all data is on the stream
154
	$backend->Logoff();
155
}
156
catch (NoPostRequestException $nopostex) {
157
	if ($nopostex->getCode() == NoPostRequestException::OPTIONS_REQUEST) {
158
		header(GSync::GetServerHeader());
159
		header(GSync::GetSupportedProtocolVersions());
160
		header(GSync::GetSupportedCommands());
161
		header("X-AspNet-Version: 4.0.30319");
162
		SLog::Write(LOGLEVEL_INFO, $nopostex->getMessage());
163
	}
164
	elseif ($nopostex->getCode() == NoPostRequestException::GET_REQUEST) {
165
		if (Request::GetUserAgent()) {
166
			SLog::Write(LOGLEVEL_INFO, sprintf("User-agent: '%s'", Request::GetUserAgent()));
167
		}
168
		if (!headers_sent() && $nopostex->showLegalNotice()) {
169
			GSync::PrintGrommunioSyncLegal('GET not supported', $nopostex->getMessage());
170
		}
171
	}
172
}
173
catch (Exception $ex) {
174
	// Extract any previous exception message for logging purpose.
175
	$exclass = $ex::class;
176
	$exception_message = $ex->getMessage();
177
	if ($ex->getPrevious()) {
178
		do {
179
			$current_exception = $ex->getPrevious();
180
			$exception_message .= ' -> ' . $current_exception->getMessage();
181
		}
182
		while ($current_exception->getPrevious());
183
	}
184
185
	if (Request::GetUserAgent()) {
186
		SLog::Write(LOGLEVEL_INFO, sprintf("User-agent: '%s'", Request::GetUserAgent()));
187
	}
188
189
	SLog::Write(LOGLEVEL_FATAL, sprintf('Exception: (%s) - %s', $exclass, $exception_message));
190
191
	if (!headers_sent()) {
192
		if ($ex instanceof GSyncException) {
193
			header('HTTP/1.1 ' . $ex->getHTTPCodeString());
194
			foreach ($ex->getHTTPHeaders() as $h) {
195
				header($h);
196
			}
197
		}
198
		// something really unexpected happened!
199
		else {
200
			header('HTTP/1.1 500 Internal Server Error');
201
		}
202
	}
203
204
	if ($ex instanceof AuthenticationRequiredException) {
205
		// Only print GSync legal message for GET requests because
206
		// some devices send unauthorized OPTIONS requests
207
		// and don't expect anything in the response body
208
		if (Request::IsMethodGET()) {
209
			GSync::PrintGrommunioSyncLegal($exclass, sprintf('<pre>%s</pre>', $ex->getMessage()));
210
		}
211
212
		// log the failed login attempt e.g. for fail2ban
213
		if (defined('LOGAUTHFAIL') && LOGAUTHFAIL !== false) {
0 ignored issues
show
The condition LOGAUTHFAIL !== false is always false.
Loading history...
214
			SLog::Write(LOGLEVEL_WARN, sprintf("IP: %s failed to authenticate user '%s'", Request::GetRemoteAddr(), Request::GetAuthUser() ?: Request::GetGETUser()));
215
		}
216
	}
217
218
	// This could be a WBXML problem.. try to get the complete request
219
	elseif ($ex instanceof WBXMLException) {
220
		SLog::Write(LOGLEVEL_FATAL, "Request could not be processed correctly due to a WBXMLException. Please report this including the 'WBXML debug data' logged. Be aware that the debug data could contain confidential information.");
221
	}
222
223
	// Try to output some kind of error information. This is only possible if
224
	// the output had not started yet. If it has started already, we can't show the user the error, and
225
	// the device will give its own (useless) error message.
226
	elseif (!($ex instanceof GSyncException) || $ex->showLegalNotice()) {
227
		$cmdinfo = (Request::GetCommand()) ? sprintf(" processing command <i>%s</i>", Request::GetCommand()) : "";
228
		$extrace = $ex->getTrace();
229
		$trace = (!empty($extrace)) ? "\n\nTrace:\n" . print_r($extrace, 1) : "";
230
		GSync::PrintGrommunioSyncLegal($exclass . $cmdinfo, sprintf('<pre>%s</pre>', $ex->getMessage() . $trace));
231
	}
232
233
	// Announce exception to process loop detection
234
	if (GSync::GetDeviceManager(false)) {
235
		GSync::GetDeviceManager()->AnnounceProcessException($ex);
236
	}
237
238
	// Announce exception if the TopCollector if available
239
	GSync::GetTopCollector()->AnnounceInformation($ex::class, true);
240
}
241
242
// save device data if the DeviceManager is available
243
if (GSync::GetDeviceManager(false)) {
244
	GSync::GetDeviceManager()->Save();
245
}
246
247
// end gracefully
248
SLog::Write(
249
	LOGLEVEL_INFO,
250
	sprintf(
251
		"cmd='%s' memory='%s/%s' time='%ss' devType='%s' devId='%s' getUser='%s' from='%s' idle='%ss' version='%s' method='%s' httpcode='%s'",
252
		Request::GetCommand(),
253
		Utils::FormatBytes(memory_get_peak_usage(false)),
254
		Utils::FormatBytes(memory_get_peak_usage(true)),
255
		number_format(microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"], 2),
256
		Request::GetDeviceType(),
257
		Request::GetDeviceID(),
258
		Request::GetGETUser(),
259
		Request::GetRemoteAddr(),
260
		RequestProcessor::GetWaitTime(),
261
		@constant('GROMMUNIOSYNC_VERSION'),
262
		Request::GetMethod(),
263
		http_response_code()
264
	)
265
);
266
267
SLog::Write(LOGLEVEL_DEBUG, "-------- End");
268