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