From 95fc92f8683f767c4c4e46eb89151e526ec1f69a Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 12:00:24 +0100 Subject: [PATCH 1/6] Verify the signature of the fit and adjust the URL Signed-off-by: Joas Schilling --- lib/Cron/Crawler.php | 90 ++++++++++++++++++++++++--- resources/nextcloud_announcements.crt | 24 +++++++ 2 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 resources/nextcloud_announcements.crt diff --git a/lib/Cron/Crawler.php b/lib/Cron/Crawler.php index 8223bdf..578b550 100644 --- a/lib/Cron/Crawler.php +++ b/lib/Cron/Crawler.php @@ -31,10 +31,11 @@ use OCP\IGroupManager; use OCP\IUser; use OCP\Notification\IManager as INotificationManager; +use phpseclib\File\X509; class Crawler extends TimedJob { - const FEED_URL = 'https://nextcloud.com/blogfeed/'; + const FEED_URL = 'https://pushfeed.nextcloud.com/feed'; /** @var string */ protected $appName; @@ -65,20 +66,22 @@ public function __construct($appName, IConfig $config, IGroupManager $groupManag $this->clientService = $clientService; // Run once per day - $this->setInterval(1); // FIXME 24 * 60 * 60); + $this->setInterval(24 * 60 * 60); } protected function run($argument) { - $client = $this->clientService->newClient(); - $response = $client->get(self::FEED_URL); - - if ($response->getStatusCode() !== Http::STATUS_OK) { + try { + $feedBody = $this->loadFeed(); + $rss = simplexml_load_string($feedBody); + if ($rss === false) { + throw new \Exception('Invalid XML feed'); + } + } catch (\Exception $e) { + // Something is wrong šŸ™Š return; } - $rss = simplexml_load_string($response->getBody()); - /** * TODO: https://github.com/contribook/main/issues/8 if ($rss->channel->pubDate === $this->config->getAppValue($this->appName, 'pub_date', '')) { @@ -110,6 +113,77 @@ protected function run($argument) { $this->config->setAppValue($this->appName, 'pub_date', $rss->channel->pubDate); } + /** + * @return string + * @throws \Exception + */ + protected function loadFeed() { + $signature = $this->readFile('.signature'); + + if (!$signature) { + throw new \Exception('Invalid signature fetched from the server'); + } + + $certificate = new X509(); + $certificate->loadCA(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt')); + $loadedCertificate = $certificate->loadX509(file_get_contents(__DIR__ . '/../../resources/nextcloud_announcements.crt')); + + // Verify if the certificate has been revoked + $crl = new X509(); + $crl->loadCA(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt')); + $crl->loadCRL(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crl')); + if ($crl->validateSignature() !== true) { + throw new \Exception('Could not validate CRL signature'); + } + $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString(); + $revoked = $crl->getRevoked($csn); + if ($revoked !== false) { + throw new \Exception(sprintf('Certificate "%s" has been revoked', $csn)); + } + + // Verify if the certificate has been issued by the Nextcloud Code Authority CA + if($certificate->validateSignature() !== true) { + throw new \Exception('App with id nextcloud_announcements has a certificate not issued by a trusted Code Signing Authority'); + } + + // Verify if the certificate is issued for the requested app id + $certInfo = openssl_x509_parse(file_get_contents(__DIR__ . '/../../resources/nextcloud_announcements.crt')); + if(!isset($certInfo['subject']['CN'])) { + throw new \Exception('App with id nextcloud_announcements has a cert with no CN'); + } + if($certInfo['subject']['CN'] !== 'nextcloud_announcements') { + throw new \Exception(sprintf('App with id nextcloud_announcements has a cert issued to %s', $certInfo['subject']['CN'])); + } + + $feedBody = $this->readFile('.rss'); + + // Check if the signature actually matches the downloaded content + $certificate = openssl_get_publickey(file_get_contents(__DIR__ . '/../../resources/nextcloud_announcements.crt')); + $verified = (bool)openssl_verify($feedBody, base64_decode($signature), $certificate, OPENSSL_ALGO_SHA512); + openssl_free_key($certificate); + + if (!$verified) { + // Signature does not match + throw new \Exception('App with id nextcloud_announcements has invalid signature'); + } + + return $feedBody; + } + + /** + * @param string $file + * @return string + * @throws \Exception + */ + protected function readFile($file) { + $client = $this->clientService->newClient(); + $response = $client->get(self::FEED_URL . $file); + if ($response->getStatusCode() !== Http::STATUS_OK) { + throw new \Exception('Could not load file'); + } + return $response->getBody(); + } + /** * Get the list of users to notify * @return string[] diff --git a/resources/nextcloud_announcements.crt b/resources/nextcloud_announcements.crt new file mode 100644 index 0000000..26f8f21 --- /dev/null +++ b/resources/nextcloud_announcements.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEjCCAvoCAhAeMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMTE4MDk1OTU5WhcNMjcwMjI0MDk1OTU5WjAiMSAwHgYD +VQQDFBduZXh0Y2xvdWRfYW5ub3VuY2VtZW50czCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBANFPvX6AkI/EmwIU4z37bYxdXt4iIQSn3abXgZ7GXNSaEvfH +XIR4f/HIiee5vqdGC/cXFdCrJ93xEt84dqLzp58T1I4bsrizAANAmRvIZwvoCCUg +XNLhTJ9vLT3KY+CHKKWvB1I1GezcPeisOTz1cr6Avrek455yBn/+2rIr9NgxzDAQ +aazIw8dhYWo6dLU1dYmaUH+ECTTBdeggPvLtCPuYRtiX46DtSXuSPu5OsSlPNiLM +ViHZoDOoE89cGxzooV8BSzuVI6qRld19vJGLwg40ieTxpLXgpJxSaV3zZdHCdN8P +vmW2njIsG/9Mb0TuSQGrQtIify2nZNg7+kov3xKrDi2IjiePyYJ+e5i56WikPLDy +N6Mc+clNwLg5rqszkPRrTfoUoHUg4dVe5V/lHV3WH+pA92YbAWYhalB2Bl1jNHhA +S5LEoDbYUckXgc79Et+VxEsDjE1NBBk6q86tQh48Adr6DxqQzBpO7FQIRGHUma2/ +Zv1z4QNSgkT/PdjWYhx9XChCq+oqEnYKL7iC9UP+J0+XZ8fnKMBU1W7w4t801cFV +XxJySBfgLYu4hUSu1CQEamva4H6PW3dYOHz5F7/u+nbw2b93zawE+87hqki3EX7+ +kMUytxZn1VHwMoERn5R2D+exegUU0ag3iE9r0VTa+IAONpGGTsMKZB3ws6AJAgMB +AAEwDQYJKoZIhvcNAQELBQADggEBACGryCPDF0LmYQ2/pNq7mNEE+E8qFG8cJND7 +2zYW7upEd1ohpCUBTyf0sSvcBqUBYwXh9fEkfh72XdnbPQUvuanDJkeMzEsXe2FX +2JCV7Kq+5cRIFHopqDTDYEb+FYRKLYcryE+MmTW4zQkDUY/LmIGQQ9Yc9qxvdTAB +TiSmckVtkDiTTThQG7ATqLoT2Oxartv7W3fnAFzg/VlLFTy8uOZiixwFe6Z/vdaF +A/Pscy9CqJHH4xml3Ag2LLvuPUtHiA6lDcezCSaMAQtk2yUAI9ykCbUEU5W5Nt4H +h05GGDaWvfE3CalylW2+nYaUCPN/kb1Mteegsj8AXheuy0oPyO4= +-----END CERTIFICATE----- From 6e57f1533e1759b5d8040aaa5a4eadf275651b49 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 12:03:41 +0100 Subject: [PATCH 2/6] Use the pub_date Signed-off-by: Joas Schilling --- lib/Cron/Crawler.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Cron/Crawler.php b/lib/Cron/Crawler.php index 578b550..ccd4660 100644 --- a/lib/Cron/Crawler.php +++ b/lib/Cron/Crawler.php @@ -82,12 +82,9 @@ protected function run($argument) { return; } - /** - * TODO: https://github.com/contribook/main/issues/8 if ($rss->channel->pubDate === $this->config->getAppValue($this->appName, 'pub_date', '')) { return; } - */ foreach ($rss->channel->item as $item) { $id = md5((string) $item->guid); From 5b79c8ac0bf13d5e6a4d3b80cb91e92e800b6a0b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 14:26:44 +0100 Subject: [PATCH 3/6] There is no author Signed-off-by: Joas Schilling --- lib/Cron/Crawler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Cron/Crawler.php b/lib/Cron/Crawler.php index ccd4660..b9c3fb1 100644 --- a/lib/Cron/Crawler.php +++ b/lib/Cron/Crawler.php @@ -96,7 +96,7 @@ protected function run($argument) { $notification->setApp($this->appName) ->setDateTime(new \DateTime((string) $item->pubDate)) ->setObject($this->appName, $id) - ->setSubject(Notifier::SUBJECT, [(string) $item->author, (string) $item->title]) + ->setSubject(Notifier::SUBJECT, [(string) $item->title]) ->setLink((string) $item->link); foreach ($this->getUsersToNotify() as $uid) { @@ -135,7 +135,7 @@ protected function loadFeed() { $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString(); $revoked = $crl->getRevoked($csn); if ($revoked !== false) { - throw new \Exception(sprintf('Certificate "%s" has been revoked', $csn)); + throw new \Exception('Certificate has been revoked'); } // Verify if the certificate has been issued by the Nextcloud Code Authority CA @@ -161,7 +161,7 @@ protected function loadFeed() { if (!$verified) { // Signature does not match - throw new \Exception('App with id nextcloud_announcements has invalid signature'); + throw new \Exception('Feed has an invalid signature'); } return $feedBody; From 2bb82f3d836b1da3a8ef16a2721566345b02e6e9 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 14:28:21 +0100 Subject: [PATCH 4/6] FIx the text and the icon of the announcements Signed-off-by: Joas Schilling --- img/app-dark.svg | 4 ++++ img/app.svg | 4 ++++ lib/Notification/Notifier.php | 16 ++++++++++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 img/app-dark.svg create mode 100644 img/app.svg diff --git a/img/app-dark.svg b/img/app-dark.svg new file mode 100644 index 0000000..866a991 --- /dev/null +++ b/img/app-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/img/app.svg b/img/app.svg new file mode 100644 index 0000000..94936d9 --- /dev/null +++ b/img/app.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 409c2c3..cc99710 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -24,6 +24,7 @@ namespace OCA\NextcloudAnnouncements\Notification; +use OCP\IURLGenerator; use OCP\L10N\IFactory; use OCP\Notification\INotification; use OCP\Notification\INotifier; @@ -38,13 +39,18 @@ class Notifier implements INotifier { /** @var IFactory */ protected $l10nFactory; + /** @var IURLGenerator */ + protected $url; + /** * @param string $appName * @param IFactory $l10nFactory + * @param IURLGenerator $url */ - public function __construct($appName, IFactory $l10nFactory) { + public function __construct($appName, IFactory $l10nFactory, IURLGenerator $url) { $this->appName = $appName; $this->l10nFactory = $l10nFactory; + $this->url = $url; } /** @@ -72,7 +78,13 @@ public function prepare(INotification $notification, $languageCode) { $parameters[0] = trim(substr($parameters[0], 0, $openingBracket)); } - $notification->setParsedSubject($l->t('%s announced ā€œ%sā€', $parameters)); + $notification->setParsedSubject($l->t('Nextcloud announcement')) + ->setParsedMessage($parameters[1]); + + if (method_exists($notification, 'setIcon')) { + $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath($this->appName, 'app-dark.svg'))); + } + return $notification; default: From 69dd03425fb5536ad4c93fb374621417c9607e4c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 14:35:36 +0100 Subject: [PATCH 5/6] Don't spam the user on installation Signed-off-by: Joas Schilling --- lib/Cron/Crawler.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/Cron/Crawler.php b/lib/Cron/Crawler.php index b9c3fb1..24b3406 100644 --- a/lib/Cron/Crawler.php +++ b/lib/Cron/Crawler.php @@ -82,19 +82,32 @@ protected function run($argument) { return; } - if ($rss->channel->pubDate === $this->config->getAppValue($this->appName, 'pub_date', '')) { + $lastPubDate = $this->config->getAppValue($this->appName, 'pub_date', 'now'); + if ($lastPubDate === 'now') { + // First call, don't spam the user with old stuff... + $this->config->setAppValue($this->appName, 'pub_date', $rss->channel->pubDate); + return; + } else if ($rss->channel->pubDate === $lastPubDate) { + // Nothing new here... return; } + $lastPubDateTime = new \DateTime($lastPubDate); + foreach ($rss->channel->item as $item) { $id = md5((string) $item->guid); if ($this->config->getAppValue($this->appName, $id, '') === 'published') { continue; } + $pubDate = new \DateTime((string) $item->pubDate); + + if ($pubDate < $lastPubDateTime) { + continue; + } $notification = $this->notificationManager->createNotification(); $notification->setApp($this->appName) - ->setDateTime(new \DateTime((string) $item->pubDate)) + ->setDateTime($pubDate) ->setObject($this->appName, $id) ->setSubject(Notifier::SUBJECT, [(string) $item->title]) ->setLink((string) $item->link); From c503fba5d9ae7f66757e92be8b6c5902fb25e0d9 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Nov 2016 14:43:32 +0100 Subject: [PATCH 6/6] Clean up the root folder Signed-off-by: Joas Schilling --- .../nextcloud_announcements.crt => appinfo/certificate.crt | 0 lib/Cron/Crawler.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename resources/nextcloud_announcements.crt => appinfo/certificate.crt (100%) diff --git a/resources/nextcloud_announcements.crt b/appinfo/certificate.crt similarity index 100% rename from resources/nextcloud_announcements.crt rename to appinfo/certificate.crt diff --git a/lib/Cron/Crawler.php b/lib/Cron/Crawler.php index 24b3406..c78d789 100644 --- a/lib/Cron/Crawler.php +++ b/lib/Cron/Crawler.php @@ -136,7 +136,7 @@ protected function loadFeed() { $certificate = new X509(); $certificate->loadCA(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt')); - $loadedCertificate = $certificate->loadX509(file_get_contents(__DIR__ . '/../../resources/nextcloud_announcements.crt')); + $loadedCertificate = $certificate->loadX509(file_get_contents(__DIR__ . '/../../appinfo/certificate.crt')); // Verify if the certificate has been revoked $crl = new X509();