grommunio /
grommunio-sync
| 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
introduced
by
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 |