Browse Source

first commit of helper functions for writing a WebSub client

master
Aaron Parecki 4 years ago
commit
9edfe6ee18
No known key found for this signature in database GPG Key ID: 276C2817346D6056
6 changed files with 331 additions and 0 deletions
  1. +3
    -0
      .gitignore
  2. +1
    -0
      CONTRIBUTING.md
  3. +21
    -0
      LICENSE
  4. +49
    -0
      README.md
  5. +29
    -0
      composer.json
  6. +228
    -0
      src/p3k/WebSub/Client.php

+ 3
- 0
.gitignore View File

@ -0,0 +1,3 @@
vendor/
.DS_Store
composer.lock

+ 1
- 0
CONTRIBUTING.md View File

@ -0,0 +1 @@
By submitting code to this project, you agree to irrevocably release it under the same license as this project. See README.md for more details.

+ 21
- 0
LICENSE View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Aaron Parecki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 49
- 0
README.md View File

@ -0,0 +1,49 @@
# p3k-websub
## Usage
### Initialize the client
```php
$http = new p3k\HTTP('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) p3k-websub/0.1.0 example');
$client = new p3k\WebSub\Client($http);
```
### Discover the hub and self URLs for a topic URL
```php
// Returns false unless both hub and self were found
$endpoints = $client->discover($topic);
// $endpoints['hub']
// $endpoints['self']
```
### Send the subscription request
```php
$secret = p3k\random_string(32);
$id = p3k\random_string(32);
$callback = 'http://localhost:8080/subscriber.php?id='.$id;
$subscription = $client->subscribe($endpoints['hub'], $endpoints['self'], $callback, [
'lease_seconds' => 300,
'secret' => $secret
]);
```
### Verify the signature
```php
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE'];
$document = file_get_contents('php://input');
$valid = p3k\WebSub\Client::verify_signature($document, $signature, $secret);
```
## License
Copyright 2017 by Aaron Parecki
Available under the MIT license.

+ 29
- 0
composer.json View File

@ -0,0 +1,29 @@
{
"name": "p3k/websub",
"type": "library",
"description": "A library for subscribing to and publishing WebSub feeds",
"keywords": ["websub","pubsubhubbub","pubsub","indieweb","p3k","feed"],
"license": "MIT",
"homepage": "https://github.com/aaronpk/p3k-websub",
"authors": [
{
"name": "Aaron Parecki",
"homepage": "https://aaronparecki.com"
}
],
"require": {
"indieweb/link-rel-parser": "0.1.*",
"p3k/http": "0.1.*",
"p3k/utils": "1.*"
},
"require-dev": {
"predis/predis": "1.*"
},
"autoload": {
"psr-4": {
"p3k\\WebSub\\": "src/p3k/WebSub"
}
},
"autoload-dev": {
}
}

+ 228
- 0
src/p3k/WebSub/Client.php View File

@ -0,0 +1,228 @@
<?php
namespace p3k\WebSub;
use p3k\HTTP;
use p3k;
use DOMXPath;
class Client {
private $http;
public function __construct($http=false) {
if($http) {
$this->http = $http;
} else {
$this->http = new HTTP('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) p3k-websub/0.1.0');
}
}
public function discover($url, $headfirst=true, $verbose=false) {
$hub = false;
$self = false;
$type = 'unknown';
$http = [
'hub' => [],
'self' => [],
'type' => $type,
];
$body = [
'hub' => [],
'self' => [],
'type' => $type,
];
if($headfirst) {
// First make a HEAD request, and if the links are found there, stop.
$topic = $this->http->head($url);
// Get the values from the Link headers
if(array_key_exists('hub', $topic['rels'])) {
$http['hub'] = $topic['rels']['hub'];
$hub = $http['hub'][0];
}
if(array_key_exists('self', $topic['rels'])) {
$http['self'] = $topic['rels']['self'];
$self = $http['self'][0];
}
$content_type = '';
if(array_key_exists('Content-Type', $topic['headers'])) {
$content_type = $topic['headers']['Content-Type'];
if(is_array($content_type))
$content_type = $content_type[count($content_type)-1];
if(strpos($content_type, 'text/html') !== false) {
$type = $http['type'] = 'html';
} else if(strpos($content_type, 'xml') !== false) {
if(strpos('rss', $content_type) !== false) {
$type = $http['type'] = 'rss';
} else if(strpos($content_type, 'atom') !== false) {
$type = $http['type'] = 'atom';
}
}
}
}
if(!$hub || !$self) {
// If we're missing hub or self, now make a GET request
$topic = $this->http->get($url);
$content_type = '';
if(array_key_exists('Content-Type', $topic['headers'])) {
$content_type = $topic['headers']['Content-Type'];
if(is_array($content_type))
$content_type = $content_type[count($content_type)-1];
// Get the values from the Link headers
if(array_key_exists('hub', $topic['rels'])) {
$http['hub'] = $topic['rels']['hub'];
}
if(array_key_exists('self', $topic['rels'])) {
$http['self'] = $topic['rels']['self'];
}
if(preg_match('|text/html|', $content_type)) {
$type = $body['type'] = 'html';
$dom = p3k\html_to_dom_document($topic['body']);
$xpath = new DOMXPath($dom);
foreach($xpath->query('*/link[@href]') as $link) {
$rel = $link->getAttribute('rel');
$url = $link->getAttribute('href');
if($rel == 'hub') {
$body['hub'][] = $url;
} else if($rel == 'self') {
$body['self'][] = $url;
}
}
} else if(preg_match('|xml|', $content_type)) {
$dom = p3k\xml_to_dom_document($topic['body']);
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
if($xpath->query('/rss')->length) {
$type = $body['type'] = 'rss';
} elseif($xpath->query('/atom:feed')->length) {
$type = $body['type'] = 'atom';
}
// Look for atom link elements in the feed
foreach($xpath->query('/atom:feed/atom:link[@href]') as $link) {
$rel = $link->getAttribute('rel');
$url = $link->getAttribute('href');
if($rel == 'hub') {
$body['hub'][] = $url;
} else if($rel == 'self') {
$body['self'][] = $url;
}
}
// Some RSS feeds include the link element as an atom attribute
foreach($xpath->query('/rss/channel/atom:link[@href]') as $link) {
$rel = $link->getAttribute('rel');
$url = $link->getAttribute('href');
if($rel == 'hub') {
$body['hub'][] = $url;
} else if($rel == 'self') {
$body['self'][] = $url;
}
}
}
}
}
// Prioritize the HTTP headers
if($http['hub']) {
$hub = $http['hub'][0];
$hub_source = 'http';
}
elseif($body['hub']) {
$hub = $body['hub'][0];
$hub_source = 'body';
} else {
$hub_source = false;
}
if($http['self']) {
$self = $http['self'][0];
$self_source = 'http';
}
elseif($body['self']) {
$self = $body['self'][0];
$self_source = 'body';
} else {
$self_source = false;
}
if(!($hub && $self)) {
return false;
}
$response = [
'hub' => $hub,
'hub_source' => $hub_source,
'self' => $self,
'self_source' => $self_source,
'type' => $type,
];
if($verbose) {
$response['details'] = [
'http' => $http,
'body' => $body
];
}
return $response;
}
public function subscribe($hub, $topic, $callback, $options=[]) {
$params = [
'hub.mode' => 'subscribe',
'hub.topic' => $topic,
'hub.callback' => $callback,
];
if(isset($options['lease_seconds'])) {
$params['hub.lease_seconds'] = $options['lease_seconds'];
}
if(isset($options['secret'])) {
$params['hub.secret'] = $options['secret'];
}
$response = $this->http->post($hub, $params);
// TODO: Check for HTTP 307/308 and subscribe at the new location
return $response;
}
public function unsubscribe($hub, $topic, $callback) {
$params = [
'hub.mode' => 'unsubscribe',
'hub.topic' => $topic,
'hub.callback' => $callback,
];
$response = $this->http->post($hub, $params);
// TODO: Check for HTTP 307/308 and unsubscribe at the new location
return $response;
}
public static function verify_signature($body, $signature_header, $secret) {
if($signature_header && is_string($signature_header)
&& preg_match('/(sha(?:1|256|384|512))=(.+)/', $signature_header, $match)) {
$alg = $match[1];
$sig = $match[2];
$expected_signature = hash_hmac($alg, $body, $secret);
return $sig == $expected_signature;
} else {
return false;
}
}
}

Loading…
Cancel
Save