Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish up the app for the release #11

Merged
merged 6 commits into from
Nov 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions appinfo/certificate.crt
Original file line number Diff line number Diff line change
@@ -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-----
4 changes: 4 additions & 0 deletions img/app-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions img/app.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 98 additions & 14 deletions lib/Cron/Crawler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,38 +66,50 @@ 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', '')) {
$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->author, (string) $item->title])
->setSubject(Notifier::SUBJECT, [(string) $item->title])
->setLink((string) $item->link);

foreach ($this->getUsersToNotify() as $uid) {
Expand All @@ -110,6 +123,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__ . '/../../appinfo/certificate.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('Certificate has been revoked');
}

// 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('Feed has an 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[]
Expand Down
16 changes: 14 additions & 2 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
namespace OCA\NextcloudAnnouncements\Notification;


use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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:
Expand Down