Page MenuHomePhabricator

D18205.id43872.diff
No OneTemporary

D18205.id43872.diff

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<id>[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 @@
+<?php
+
+final class PhabricatorMetaMTASNSReceiveController
+ extends PhabricatorMetaMTAController {
+
+ public function shouldRequireLogin() {
+ return false;
+ }
+
+ /**
+ * @phutil-external-symbol class PhabricatorStartup
+ */
+ public function willProcessRequest(array $uri_data) {
+ $input = PhabricatorStartup::getRawInput();
+ $request = $this->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<string, map<string, string>>',
+ '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<string, wild>',
+ 'notificationType' => 'string',
+ 'receipt' => 'map<string, wild>',
+ ));
+
+ $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<string,wild>
+ * @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;
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Wed, Jan 22, 3:35 PM (12 h, 35 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7031301
Default Alt Text
D18205.id43872.diff (8 KB)

Event Timeline