diff --git a/scripts/ssh/ssh-connect.php b/scripts/ssh/ssh-connect.php --- a/scripts/ssh/ssh-connect.php +++ b/scripts/ssh/ssh-connect.php @@ -53,6 +53,12 @@ $pattern[] = '-o'; $pattern[] = 'UserKnownHostsFile=/dev/null'; +// This suppresses the "Permanently added ... to the list of known hosts" +// message, which is always output because we set `UserKnownHostsFile` to +// `/dev/null`. +$pattern[] = '-o'; +$pattern[] = 'LogLevel=ERROR'; + $as_device = getenv('PHABRICATOR_AS_DEVICE'); $credential_phid = getenv('PHABRICATOR_CREDENTIAL'); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3148,6 +3148,7 @@ 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php', 'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php', + 'PhabricatorMetaMTASNSReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASNSReceiveController.php', 'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php', 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php', 'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php', @@ -8468,6 +8469,7 @@ 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', 'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase', + 'PhabricatorMetaMTASNSReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', diff --git a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php --- a/src/applications/metamta/application/PhabricatorMetaMTAApplication.php +++ b/src/applications/metamta/application/PhabricatorMetaMTAApplication.php @@ -42,6 +42,7 @@ 'detail/(?P[1-9]\d*)/' => 'PhabricatorMetaMTAMailViewController', 'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController', 'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController', + 'sns/' => 'PhabricatorMetaMTASNSReceiveController', ), ); } diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASNSReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASNSReceiveController.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/controller/PhabricatorMetaMTASNSReceiveController.php @@ -0,0 +1,192 @@ +getRequest(); + + // SNS incorrectly sets the `Content-Type` to `text/plain` + // instead of `application/json` (see + // https://forums.aws.amazon.com/thread.jspa?threadID=69413). + try { + $request_data = phutil_json_decode($input); + $request->setRequestData($request_data); + } catch (PhutilJSONParserException $ex) { + throw new PhutilProxyException( + pht('Request body should be JSON encoded.'), + $ex); + } + } + + /** + * @phutil-external-symbol class MimeMailParser + * @phutil-external-symbol class PhabricatorStartup + */ + public function handleRequest(AphrontRequest $request) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + try { + PhutilTypeSpec::checkMap( + $data, + array( + 'Message' => 'string', + 'MessageAttributes' => 'optional map>', + 'MessageId' => 'string', + 'Signature' => 'string', + 'SignatureVersion' => 'string', + 'SigningCertURL' => 'string', + 'Subject' => 'optional string', + 'SubscribeURL' => 'optional string', + 'Timestamp' => 'string', + 'Token' => 'optional string', + 'TopicArn' => 'string', + 'Type' => 'string', + 'UnsubscribeURL' => 'optional string', + )); + } catch (PhutilTypeCheckException $ex) { + return new Aphront400Response(); + } catch (PhutilTypeExtraParametersException $ex) { + return new Aphront400Response(); + } + + if (!$this->verifyMessage()) { + throw new Exception(pht('Signature is not valid.')); + } + + switch ($data['Type']) { + case 'SubscriptionConfirmation': + id(new HTTPSFuture($data['SubscribeURL'])) + ->setTimeout(5) + ->resolvex(); + break; + + case 'Notification': + $message = phutil_json_decode($data['Message']); + + PhutilTypeSpec::checkMap( + $message, + array( + 'content' => 'string', + 'mail' => 'map', + 'notificationType' => 'string', + 'receipt' => 'map', + )); + + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/mimemailparser/MimeMailParser.class.php'; + + $parser = new MimeMailParser(); + $parser->setText($message['content']); + + $headers = $parser->getHeaders(); + $headers['subject'] = $message['mail']['commonHeaders']['subject']; + $headers['from'] = $message['mail']['commonHeaders']['from']; + + $received = new PhabricatorMetaMTAReceivedMail(); + $received->setHeaders($headers); + $received->setBodies(array( + 'text' => $parser->getMessageBody('text'), + 'html' => $parser->getMessageBody('html'), + )); + + $attachments = array(); + foreach ($parser->getAttachments() as $attachment) { + $file = PhabricatorFile::newFromFileData( + $attachment->getContent(), + array( + 'name' => $attachment->getFilename(), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + )); + $attachments[] = $file->getPHID(); + } + + $received->setAttachments($attachments); + $received->save(); + $received->processReceivedMail(); + break; + + default: + return new Aphront400Response(); + } + + return id(new AphrontJSONResponse()) + ->setAddJSONShield(false) + ->setContent(array()); + } + + private function verifyMessage() { + $request = $this->getRequest(); + $data = $request->getRequestData(); + + $certificate_url = idx($data, 'SigningCertURL'); + + if (!$certificate_url) { + return false; + } + + // TODO: Check that certificate URL is a subdomain of `amazonaws.com`. + + $certificate_future = id(new HTTPSFuture($certificate_url)) + ->setTimeout(5); + + list($certificate) = $certificate_future->resolvex(); + $public_key = openssl_pkey_get_public($certificate); + + if (!$public_key) { + throw new Exception(pht('Cannot get the public key from the certificate.')); + } + + // Verify the signature of the message. + $content = $this->getStringToSign($data); + $signature = base64_decode($data['Signature']); + return openssl_verify($content, $signature, $public_key, OPENSSL_ALGO_SHA1); + } + + /** + * Builds string-to-sign according to the SNS message specification. + * + * See http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html. + * + * @param map + * @return string + */ + private function getStringToSign(array $message) { + static $signable_keys = array( + 'Message', + 'MessageId', + 'Subject', + 'SubscribeURL', + 'Timestamp', + 'Token', + 'TopicArn', + 'Type', + ); + + if ($message['SignatureVersion'] != 1) { + throw new Exception( + pht( + 'The signature version (%s) is not supported.', + $message['SignatureVersion'])); + } + + $string_to_sign = ''; + + foreach ($signable_keys as $key) { + if (isset($message[$key])) { + $string_to_sign .= "{$key}\n{$message[$key]}\n"; + } + } + + return $string_to_sign; + } + +}