Proper Accept Header Validation
This commit is contained in:
parent
1f77983891
commit
f25d8278b1
@ -35,6 +35,7 @@ date_default_timezone_set('UTC');
|
||||
// Import required files by the plugin
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'discoveryhints.php';
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'AcceptHeader.php';
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'explorer.php';
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'postman.php';
|
||||
|
||||
@ -163,6 +164,7 @@ class ActivityPubPlugin extends Plugin
|
||||
*/
|
||||
public function onRouterInitialized(URLMapper $m)
|
||||
{
|
||||
if (ActivityPubURLMapperOverwrite::should()) {
|
||||
ActivityPubURLMapperOverwrite::variable(
|
||||
$m,
|
||||
'user/:id',
|
||||
@ -184,6 +186,7 @@ class ActivityPubPlugin extends Plugin
|
||||
['id' => '[0-9]+'],
|
||||
'apNotice'
|
||||
);
|
||||
}
|
||||
|
||||
$m->connect(
|
||||
'user/:id/liked.json',
|
||||
@ -944,59 +947,6 @@ class ActivityPubReturn
|
||||
echo json_encode($res, JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select content type from HTTP Accept header
|
||||
*
|
||||
* @author Maciej Łebkowski <m.lebkowski@gmail.com>
|
||||
* @param array $mimeTypes Supported Types
|
||||
* @return array|null of supported mime types sorted | null if none valid
|
||||
*/
|
||||
public static function getBestSupportedMimeType($mimeTypes)
|
||||
{
|
||||
// XXX: This function needs improvement!
|
||||
if (!isset($_SERVER['HTTP_ACCEPT'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This mime type was messing everything, thus the special case
|
||||
if ($_SERVER['HTTP_ACCEPT'] == 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Values will be stored in this array
|
||||
$AcceptTypes = [];
|
||||
|
||||
// Accept header is case insensitive, and whitespace isn’t important
|
||||
$accept = strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT']));
|
||||
// divide it into parts in the place of a ","
|
||||
$accept = explode(',', $accept);
|
||||
foreach ($accept as $a) {
|
||||
// the default quality is 1.
|
||||
$q = 1;
|
||||
// check if there is a different quality
|
||||
if (strpos($a, ';q=')) {
|
||||
// divide "mime/type;q=X" into two parts: "mime/type" and "X"
|
||||
list($a, $q) = explode(';q=', $a);
|
||||
}
|
||||
// mime-type $a is accepted with the quality $q
|
||||
// WARNING: $q == 0 means, that mime-type isn’t supported!
|
||||
$AcceptTypes[$a] = $q;
|
||||
}
|
||||
arsort($AcceptTypes);
|
||||
|
||||
$mimeTypes = array_map('strtolower', $mimeTypes);
|
||||
|
||||
// let’s check our supported types:
|
||||
foreach ($AcceptTypes as $mime => $q) {
|
||||
if ($q && in_array($mime, $mimeTypes)) {
|
||||
return $mime;
|
||||
}
|
||||
}
|
||||
|
||||
// no mime-type found
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1004,19 +954,18 @@ class ActivityPubReturn
|
||||
*/
|
||||
class ActivityPubURLMapperOverwrite extends URLMapper
|
||||
{
|
||||
/**
|
||||
* Overwrites a route.
|
||||
*
|
||||
* @author Hannes Mannerheim <h@nnesmannerhe.im>
|
||||
* @param URLMapper $m
|
||||
* @param string $path
|
||||
* @param string $paramPatterns
|
||||
* @param string $newaction
|
||||
* @return void
|
||||
*/
|
||||
public static function variable($m, $path, $paramPatterns, $newaction)
|
||||
{
|
||||
$mimes = [
|
||||
'application/json',
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
if (is_null(ActivityPubReturn::getBestSupportedMimeType($mimes))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$m->connect($path, array('action' => $newaction), $paramPatterns);
|
||||
$regex = self::makeRegex($path, $paramPatterns);
|
||||
foreach ($m->variables as $n => $v) {
|
||||
@ -1025,4 +974,35 @@ class ActivityPubURLMapperOverwrite extends URLMapper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the route should or not be overwrited.
|
||||
* If ACCEPT header isn't set false will be returned.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return boolean true if it should, false otherwise
|
||||
*/
|
||||
public static function should()
|
||||
{
|
||||
// Do not operate without Accept Header
|
||||
if (!isset($_SERVER['HTTP_ACCEPT'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mimes = [
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0,
|
||||
'application/activity+json' => 1,
|
||||
'application/json' => 2,
|
||||
'application/ld+json' => 3
|
||||
];
|
||||
|
||||
$acceptheader = new AcceptHeader($_SERVER['HTTP_ACCEPT']);
|
||||
foreach ($acceptheader as $ah) {
|
||||
if (isset($mimes[$ah['raw']])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ try {
|
||||
} catch (AlreadyFulfilledException $e) {
|
||||
// Notice URI already exists
|
||||
common_debug('ActivityPub Inbox Create Note: Note already exists: '.$e->getMessage());
|
||||
ActivityPubReturn::error('Note already exists.',202);
|
||||
ActivityPubReturn::error('Note already exists.', 202);
|
||||
} catch (Exception $e) {
|
||||
common_debug('ActivityPub Inbox Create Note: Failed Create Note: '.$e->getMessage());
|
||||
ActivityPubReturn::error($e->getMessage());
|
||||
|
43
tests/Unit/AcceptHeaderTest.php
Normal file
43
tests/Unit/AcceptHeaderTest.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
require 'AcceptHeader.php';
|
||||
|
||||
class ContainerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testHeader1()
|
||||
{
|
||||
$acceptHeader = new AcceptHeader('audio/*; q=0.2, audio/basic');
|
||||
$this->assertEquals('audio/basic', $this->_getMedia($acceptHeader[0]));
|
||||
$this->assertEquals('audio/*; q=0.2', $this->_getMedia($acceptHeader[1]));
|
||||
}
|
||||
|
||||
public function testHeader2()
|
||||
{
|
||||
$acceptHeader = new AcceptHeader('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5');
|
||||
$this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0]));
|
||||
$this->assertEquals('text/html; q=0.7', $this->_getMedia($acceptHeader[1]));
|
||||
$this->assertEquals('*/*; q=0.5', $this->_getMedia($acceptHeader[2]));
|
||||
$this->assertEquals('text/html; level=2; q=0.4', $this->_getMedia($acceptHeader[3]));
|
||||
$this->assertEquals('text/*; q=0.3', $this->_getMedia($acceptHeader[4]));
|
||||
}
|
||||
|
||||
public function testHeader3()
|
||||
{
|
||||
$acceptHeader = new AcceptHeader('text/*, text/html, text/html;level=1, */*');
|
||||
$this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0]));
|
||||
$this->assertEquals('text/html', $this->_getMedia($acceptHeader[1]));
|
||||
$this->assertEquals('text/*', $this->_getMedia($acceptHeader[2]));
|
||||
$this->assertEquals('*/*', $this->_getMedia($acceptHeader[3]));
|
||||
}
|
||||
|
||||
private function _getMedia(array $mediaType)
|
||||
{
|
||||
$str = $mediaType['type'] . '/' . $mediaType['subtype'];
|
||||
if (!empty($mediaType['params'])) {
|
||||
foreach ($mediaType['params'] as $k => $v) {
|
||||
$str .= '; ' . $k . '=' . $v;
|
||||
}
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
116
utils/AcceptHeader.php
Normal file
116
utils/AcceptHeader.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Note : Code is released under the GNU LGPL
|
||||
*
|
||||
* Please do not change the header of this file
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
|
||||
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*
|
||||
* See the GNU Lesser General Public License for more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The AcceptHeader page will parse and sort the different
|
||||
* allowed types for the content negociations
|
||||
*
|
||||
* @author Pierrick Charron <pierrick@webstart.fr>
|
||||
*/
|
||||
class AcceptHeader extends \ArrayObject
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $header Value of the Accept header
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($header)
|
||||
{
|
||||
$acceptedTypes = $this->_parse($header);
|
||||
usort($acceptedTypes, [$this, '_compare']);
|
||||
parent::__construct($acceptedTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the accept header and return an array containing
|
||||
* all the informations about the Accepted types
|
||||
*
|
||||
* @param string $header Value of the Accept header
|
||||
* @return array
|
||||
*/
|
||||
private function _parse($data)
|
||||
{
|
||||
$array = [];
|
||||
$items = explode(',', $data);
|
||||
foreach ($items as $item) {
|
||||
$elems = explode(';', $item);
|
||||
|
||||
$acceptElement = [];
|
||||
$mime = current($elems);
|
||||
list($type, $subtype) = explode('/', $mime);
|
||||
$acceptElement['type'] = trim($type);
|
||||
$acceptElement['subtype'] = trim($subtype);
|
||||
$acceptElement['raw'] = $mime;
|
||||
|
||||
$acceptElement['params'] = [];
|
||||
while (next($elems)) {
|
||||
list($name, $value) = explode('=', current($elems));
|
||||
$acceptElement['params'][trim($name)] = trim($value);
|
||||
}
|
||||
|
||||
$array[] = $acceptElement;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Accepted types with their parameters to know
|
||||
* if one media type should be used instead of an other
|
||||
*
|
||||
* @param array $a The first media type and its parameters
|
||||
* @param array $b The second media type and its parameters
|
||||
* @return int
|
||||
*/
|
||||
private function _compare($a, $b)
|
||||
{
|
||||
$a_q = isset($a['params']['q']) ? floatval($a['params']['q']) : 1.0;
|
||||
$b_q = isset($b['params']['q']) ? floatval($b['params']['q']) : 1.0;
|
||||
if ($a_q === $b_q) {
|
||||
$a_count = count($a['params']);
|
||||
$b_count = count($b['params']);
|
||||
if ($a_count === $b_count) {
|
||||
if ($r = $this->_compareSubType($a['subtype'], $b['subtype'])) {
|
||||
return $r;
|
||||
} else {
|
||||
return $this->_compareSubType($a['type'], $b['type']);
|
||||
}
|
||||
} else {
|
||||
return $a_count < $b_count;
|
||||
}
|
||||
} else {
|
||||
return $a_q < $b_q;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two subtypes
|
||||
*
|
||||
* @param string $a First subtype to compare
|
||||
* @param string $b Second subtype to compare
|
||||
* @return int
|
||||
*/
|
||||
private function _compareSubType($a, $b)
|
||||
{
|
||||
if ($a === '*' && $b !== '*') {
|
||||
return 1;
|
||||
} elseif ($b === '*' && $a !== '*') {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user