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
|
// Import required files by the plugin
|
||||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
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 . '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 . 'explorer.php';
|
||||||
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'postman.php';
|
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'postman.php';
|
||||||
|
|
||||||
@ -163,6 +164,7 @@ class ActivityPubPlugin extends Plugin
|
|||||||
*/
|
*/
|
||||||
public function onRouterInitialized(URLMapper $m)
|
public function onRouterInitialized(URLMapper $m)
|
||||||
{
|
{
|
||||||
|
if (ActivityPubURLMapperOverwrite::should()) {
|
||||||
ActivityPubURLMapperOverwrite::variable(
|
ActivityPubURLMapperOverwrite::variable(
|
||||||
$m,
|
$m,
|
||||||
'user/:id',
|
'user/:id',
|
||||||
@ -184,6 +186,7 @@ class ActivityPubPlugin extends Plugin
|
|||||||
['id' => '[0-9]+'],
|
['id' => '[0-9]+'],
|
||||||
'apNotice'
|
'apNotice'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$m->connect(
|
$m->connect(
|
||||||
'user/:id/liked.json',
|
'user/:id/liked.json',
|
||||||
@ -944,59 +947,6 @@ class ActivityPubReturn
|
|||||||
echo json_encode($res, JSON_UNESCAPED_SLASHES);
|
echo json_encode($res, JSON_UNESCAPED_SLASHES);
|
||||||
exit;
|
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
|
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)
|
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);
|
$m->connect($path, array('action' => $newaction), $paramPatterns);
|
||||||
$regex = self::makeRegex($path, $paramPatterns);
|
$regex = self::makeRegex($path, $paramPatterns);
|
||||||
foreach ($m->variables as $n => $v) {
|
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) {
|
} catch (AlreadyFulfilledException $e) {
|
||||||
// Notice URI already exists
|
// Notice URI already exists
|
||||||
common_debug('ActivityPub Inbox Create Note: Note already exists: '.$e->getMessage());
|
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) {
|
} catch (Exception $e) {
|
||||||
common_debug('ActivityPub Inbox Create Note: Failed Create Note: '.$e->getMessage());
|
common_debug('ActivityPub Inbox Create Note: Failed Create Note: '.$e->getMessage());
|
||||||
ActivityPubReturn::error($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