Use the Index router in Uiharu.

This commit is contained in:
flash 2022-07-15 21:41:28 +00:00
parent 0a668992d9
commit 9948642a5a
5 changed files with 695 additions and 575 deletions

2
public/index.html Normal file
View file

@ -0,0 +1,2 @@
<!doctype html>
<pre>Metadata lookup service - OK</pre>

View file

@ -2,98 +2,24 @@
namespace Uiharu;
use stdClass;
use InvalidArgumentException;
use Index\Http\HttpFx;
use Index\MediaType;
use Index\Performance\Stopwatch;
require_once __DIR__ . '/../uiharu.php';
define('UIH_CACHE', !UIH_DEBUG || isset($_GET['_cache']));
define('UIH_INCLUDE_RAW', UIH_DEBUG || isset($_GET['include_raw']));
function uih_origin_allowed(string $origin): bool {
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
function uih_media_type_json(MediaType $mediaType): array {
$parts = [
'string' => (string)$mediaType,
'type' => $mediaType->getCategory(),
'subtype' => $mediaType->getKind(),
];
if($origin === $_SERVER['HTTP_HOST'])
return true;
if(!empty($suffix = $mediaType->getSuffix()))
$parts['suffix'] = $suffix;
$allowed = Config::get('CORS', 'origins', []);
if(empty($allowed))
return true;
if(!empty($params = $mediaType->getParams()))
$parts['params'] = $params;
return $parts;
}
function uih_parse_url(string $url): array|false {
$parts = parse_url($url);
if($parts === false)
return false;
// v1 compat
$parts['uri'] = uih_build_url($parts);
if(isset($parts['pass']))
$parts['password'] = $parts['pass'];
return $parts;
}
function uih_build_url(array $parts): string {
$string = '';
if(!empty($parts['scheme']))
$string .= $parts['scheme'] . ':';
$authority = '';
if(isset($parts['user']) || isset($parts['pass'])) {
if(isset($parts['user']))
$authority .= $parts['user'];
if(isset($parts['pass']))
$authority .= ':' . $parts['pass'];
$authority .= '@';
}
if(isset($parts['host'])) {
$authority .= $parts['host'];
if(isset($parts['port']))
$authority .= ':' . $parts['port'];
}
$hasAuthority = !empty($authority);
if($hasAuthority)
$string .= '//' . $authority;
$path = $parts['path'] ?? '';
$hasPath = !empty($path);
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
$string .= '/';
elseif(!$hasAuthority && $path[1] === '/')
$path = '/' . trim($path, '/');
$string .= $path;
if(!empty($parts['query'])) {
$string .= '?';
$queryParts = explode('&', $parts['query']);
foreach($queryParts as $queryPart) {
$kvp = explode('=', $queryPart, 2);
$string .= rawurlencode($kvp[0]);
if(isset($kvp[1]))
$string .= '=' . rawurlencode($kvp[1]);
$string .= '&';
}
$string = substr($string, 0, -1);
}
if(!empty($parts['fragment']))
$string .= '#' . rawurlencode($parts['fragment']);
return $string;
return in_array($origin, $allowed);
}
function uih_eeprom_lookup(stdClass $resp, string $eepromFileId, string $domain = 'flashii'): void {
@ -126,108 +52,85 @@ function uih_eeprom_lookup(stdClass $resp, string $eepromFileId, string $domain
$resp->site_name = 'Flashii EEPROM';
}
if(!is_dir(UIH_SEM_PATH))
mkdir(UIH_SEM_PATH, 0777, true);
header('X-Powered-By: Uiharu');
$db->execute('DELETE FROM `uih_metadata_cache` WHERE `metadata_created` < NOW() - INTERVAL 7 DAY');
$reqMethod = filter_input(INPUT_SERVER, 'REQUEST_METHOD');
$reqPath = '/' . trim(parse_url(filter_input(INPUT_SERVER, 'REQUEST_URI'), PHP_URL_PATH), '/');
$reqHead = false;
/*apis = [
new \Uiharu\APIs\v1_0,
];*/
if($reqMethod == 'HEAD') {
$reqMethod = 'GET';
$reqHead = true;
}
$router = new HttpFx;
$router->use('/', function($response) {
$response->setPoweredBy('Uiharu');
});
if(!empty($_SERVER['HTTP_ORIGIN'])) {
$originLast12 = substr($_SERVER['HTTP_ORIGIN'], -12, 12);
$originLast10 = substr($_SERVER['HTTP_ORIGIN'], -10, 10);
if($originLast12 !== '/flashii.net' && $originLast12 !== '.flashii.net'
&& $originLast10 !== '/edgii.net' && $originLast10 !== '.edgii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'https://flashii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'http://flashii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'https://edgii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'http://edgii.net') {
http_response_code(403);
return;
$router->use('/', function($response, $request) {
$origin = $request->getHeaderLine('Origin');
if(!empty($origin)) {
if(!uih_origin_allowed($origin))
return 403;
$response->setHeader('Access-Control-Allow-Origin', $origin);
$response->setHeader('Vary', 'Origin');
}
});
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header('Vary: Origin');
}
if($reqMethod === 'OPTIONS') {
http_response_code(204);
//header('Access-Control-Allow-Credentials: true');
//header('Access-Control-Allow-Headers: Authorization');
header('Access-Control-Allow-Methods: OPTIONS, GET, POST');
return;
}
if($reqPath === '/metadata') {
// Allow using POST for ridiculous urls.
if($reqMethod !== 'GET' && $reqMethod !== 'POST') {
http_response_code(405);
return;
$router->use('/', function($response, $request) {
if($request->getMethod() === 'OPTIONS') {
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
return 204;
}
});
header('Content-Type: application/json; charset=utf-8');
if($reqHead)
$router->get('/', function($response) {
$response->accelRedirect('/index.html');
$response->setContentType('text/html; charset=utf-8');
});
$metaDataHandlerV1 = function($response, $request) use ($db) {
$response->setContentType('application/json; charset=utf-8');
if($request->getMethod() === 'HEAD')
return;
$sw = Stopwatch::startNew();
$resp = new stdClass;
if($_SERVER['HTTP_HOST'] === 'mii.flashii.net') {
$resp->type = 'object';
$resp->content_type = [];
$resp->content_type['string'] = 'application/x-update-your-script-to-use-uiharu.flashii.net-instead-of-mii.flashii.net';
$resp->content_type['type'] = 'text';
$resp->content_type['subtype'] = 'deprecation';
$resp->title = 'Update your URLs: mii.flashii.net -> uiharu.flashii.net';
$resp->description = 'Update your URLs: mii.flashii.net -> uiharu.flashii.net';
$resp->site_name = 'Deprecation notice';
$resp->took = 35.1;
echo json_encode($resp);
return;
if($request->getMethod() === 'POST') {
if(!$request->isStreamContent()) {
$response->setStatusCode(400);
return $resp;
}
if($reqMethod === 'POST') {
$targetUrl = substr((string)file_get_contents('php://input'), 0, 1000);
$targetUrl = $request->getContent()->getStream()->read(1000);
} else {
$targetUrl = (string)filter_input(INPUT_GET, 'url');
$targetUrl = (string)$request->getParam('url');
}
$parsedUrl = uih_parse_url($targetUrl);
if($parsedUrl === false) {
http_response_code(400);
$resp->error = 'metadata:uri';
echo json_encode($resp);
return;
if(empty($targetUrl)) {
$response->setStatusCode(400);
return $resp;
}
$resp->uri = $parsedUrl;
// if no scheme is specified, try https
if(empty($parsedUrl['scheme'])) {
$parsedUrl['scheme'] = 'https';
$parsedUrl = uih_parse_url(uih_build_url($parsedUrl));
}
$urlHash = hash('sha256', uih_build_url($parsedUrl));
try {
$semPath = UIH_SEM_PATH . DIRECTORY_SEPARATOR . $urlHash;
if(!is_file($semPath))
touch($semPath);
$ftok = ftok($semPath, UIH_SEM_NAME);
$semaphore = sem_get($ftok, 1);
while(!sem_acquire($semaphore)) usleep(100);
$parsedUrl = Url::parse($targetUrl);
} catch(InvalidArgumentException $ex) {
$response->setStatusCode(400);
$resp->error = 'metadata:uri';
return $resp;
}
if(UIH_CACHE) {
// if no scheme is specified, try https
if(!$parsedUrl->hasScheme())
$parsedUrl->setScheme('https');
$resp->uri = $parsedUrl->toV1();
$urlHash = $parsedUrl->calculateHash(false);
$enableCache = !UIH_DEBUG || $request->hasParam('_cache');
$includeRawResult = UIH_DEBUG || $request->hasParam('include_raw');
if($enableCache) {
$cacheFetch = $db->prepare('SELECT `metadata_resp` FROM `uih_metadata_cache` WHERE `metadata_url` = UNHEX(?) AND `metadata_created` > NOW() - INTERVAL 10 MINUTE');
$cacheFetch->addParameter(1, $urlHash);
$cacheFetch->execute();
@ -240,19 +143,21 @@ if($reqPath === '/metadata') {
}
if(empty($resp->type)) {
$urlScheme = strtolower($parsedUrl['scheme']);
$urlHost = strtolower($parsedUrl['host']);
$urlPath = '/' . trim($parsedUrl['path'], '/');
$urlScheme = strtolower($parsedUrl->getScheme());
$urlHost = strtolower($parsedUrl->getHost());
$urlPath = '/' . trim($parsedUrl->getPath(), '/');
if($urlScheme === 'eeprom') {
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl['path'], $matches)) {
$resp->uri = $parsedUrl = uih_parse_url('https://i.fii.moe/' . $matches[1]);
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) {
$parsedUrl = Url::parse('https://i.fii.moe/' . $matches[1]);
$resp->uri = $parsedUrl->toV1();
$continueRaw = true;
uih_eeprom_lookup($resp, $matches[1]);
}
} elseif($urlScheme === 'devrom') {
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl['path'], $matches)) {
$resp->uri = $parsedUrl = uih_parse_url('https://i.edgii.net/' . $matches[1]);
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) {
$parsedUrl = Url::parse('https://i.edgii.net/' . $matches[1]);
$resp->uri = $parsedUrl->toV1();
$continueRaw = true;
uih_eeprom_lookup($resp, $matches[1], 'edgii');
}
@ -359,7 +264,7 @@ if($reqPath === '/metadata') {
$youtubeVideoId = substr($urlPath, 1);
case 'youtube.com': case 'www.youtube.com':
case 'youtube-nocookie.com': case 'www.youtube-nocookie.com':
parse_str($parsedUrl['query'], $queryString);
parse_str($parsedUrl->getQuery(), $queryString);
if(!isset($youtubeVideoId) && $urlPath === '/watch')
$youtubeVideoId = $queryString['v'] ?? null;
@ -406,12 +311,13 @@ if($reqPath === '/metadata') {
break;
}
} else {
http_response_code(404);
$resp->error = 'metadata:scheme';
$response->setStatusCode(400);
return $resp;
}
if((empty($resp->type) || isset($continueRaw)) && in_array($parsedUrl['scheme'], ['http', 'https'])) {
$curl = curl_init(uih_build_url($parsedUrl));
if((empty($resp->type) || isset($continueRaw)) && in_array($parsedUrl->getScheme(), ['http', 'https'])) {
$curl = curl_init((string)$parsedUrl);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => true,
CURLOPT_CERTINFO => false,
@ -466,7 +372,7 @@ if($reqPath === '/metadata') {
$contentType = MediaType::parse('application/octet-stream');
}
$resp->content_type = uih_media_type_json($contentType);
$resp->content_type = MediaTypeExts::toV1($contentType);
$isHTML = $contentType->equals('text/html');
$isXHTML = $contentType->equals('application/xhtml+xml');
@ -560,7 +466,7 @@ if($reqPath === '/metadata') {
if($isImage || $isAudio || $isVideo) {
curl_close($curl);
$resp->media = new stdClass;
$ffmpeg = json_decode(shell_exec(sprintf('ffprobe -show_streams -show_format -print_format json -v quiet -i %s', escapeshellarg(uih_build_url($parsedUrl)))));
$ffmpeg = json_decode(shell_exec(sprintf('ffprobe -show_streams -show_format -print_format json -v quiet -i %s', escapeshellarg((string)$parsedUrl))));
if(!empty($ffmpeg)) {
if(!empty($ffmpeg->format)) {
@ -639,7 +545,7 @@ if($reqPath === '/metadata') {
}
}
if(UIH_INCLUDE_RAW)
if($includeRawResult)
$resp->ffmpeg = $ffmpeg;
} else curl_close($curl);
}
@ -654,30 +560,15 @@ if($reqPath === '/metadata') {
$replaceCache->addParameter(2, $respJson);
$replaceCache->execute();
}
} finally {
if(!empty($semaphore))
sem_release($semaphore);
if(is_file($semPath))
unlink($semPath);
}
echo $respJson ?? json_encode($resp);
return;
}
if(!empty($respJson))
$response->setContent($respJson);
else
return $resp;
};
if($reqPath === '/') {
if($reqMethod !== 'GET') {
http_response_code(405);
return;
}
// Allow using POST for ridiculous urls.
$router->get('/metadata', $metaDataHandlerV1);
$router->post('/metadata', $metaDataHandlerV1);
header('Content-Type: text/plain');
if($reqHead)
return;
echo 'Metadata lookup service - OK';
return;
}
http_response_code(404);
$router->dispatch();

22
src/MediaTypeExts.php Normal file
View file

@ -0,0 +1,22 @@
<?php
namespace Uiharu;
use Index\MediaType;
final class MediaTypeExts {
public static function toV1(MediaType $mediaType): array {
$parts = [
'string' => (string)$mediaType,
'type' => $mediaType->getCategory(),
'subtype' => $mediaType->getKind(),
];
if(!empty($suffix = $mediaType->getSuffix()))
$parts['suffix'] = $suffix;
if(!empty($params = $mediaType->getParams()))
$parts['params'] = $params;
return $parts;
}
}

207
src/Url.php Normal file
View file

@ -0,0 +1,207 @@
<?php
namespace Uiharu;
use InvalidArgumentException;
final class Url {
private string $scheme = '';
private string $host = '';
private int $port = 0;
private string $user = '';
private string $pass = '';
private string $path = '';
private string $query = '';
private string $fragment = '';
private ?string $formatted = null;
public function __construct(array $parts) {
if(isset($parts['scheme']))
$this->scheme = $parts['scheme'];
if(isset($parts['host']))
$this->host = $parts['host'];
if(isset($parts['port']))
$this->port = $parts['port'];
if(isset($parts['user']))
$this->user = $parts['user'];
if(isset($parts['pass']))
$this->pass = $parts['pass'];
if(isset($parts['path']))
$this->path = $parts['path'];
if(isset($parts['query']))
$this->query = $parts['query'];
if(isset($parts['fragment']))
$this->fragment = $parts['fragment'];
}
public static function parse(string $urlString): Url {
$parts = parse_url($urlString);
if($parts === false)
throw new InvalidArgumentException('Invalid URL provided.');
return new Url($parts);
}
public function resetString(): void {
$this->formatted = null;
}
public function setScheme(string $scheme): void {
$this->scheme = $scheme;
$this->resetString();
}
public function discardFragment(): void {
$this->fragment = '';
$this->resetString();
}
public function hasScheme(): bool {
return $this->scheme !== '';
}
public function hasHost(): bool {
return $this->host !== '';
}
public function hasPort(): bool {
return $this->port !== 0;
}
public function hasUser(): bool {
return $this->user !== '';
}
public function hasPassword(): bool {
return $this->pass !== '';
}
public function hasPath(): bool {
return $this->path !== '';
}
public function hasQuery(): bool {
return $this->query !== '';
}
public function hasFragment(): bool {
return $this->fragment !== '';
}
public function getScheme(): string {
return $this->scheme;
}
public function getHost(): string {
return $this->host;
}
public function getPort(): int {
return $this->port;
}
public function getUser(): string {
return $this->user;
}
public function getPassword(): string {
return $this->pass;
}
public function getPath(): string {
return $this->path;
}
public function getQuery(): string {
return $this->query;
}
public function getFragment(): string {
return $this->fragment;
}
public function hasUserInfo(): bool {
return $this->hasUser()
|| $this->hasPassword();
}
public function hasAuthority(): bool {
return $this->hasUserInfo()
|| $this->hasHost();
}
public function getUserInfo(): string {
$userInfo = $this->user;
if($this->pass !== '')
$userInfo .= ':' . $this->pass;
return $userInfo;
}
public function getAuthority(): string {
$authority = '//';
if($this->hasUserInfo())
$authority .= $this->getUserInfo() . '@';
$authority .= $this->host;
if($this->port !== 0)
$authority .= ':' . $this->port;
return $authority;
}
public function calculateHash(bool $raw = true): string {
return hash('sha256', (string)$this, $raw);
}
public function __toString(): string {
if($this->formatted === null) {
$string = '';
if($this->hasScheme())
$string .= $this->getScheme() . ':';
$hasAuthority = $this->hasAuthority();
if($hasAuthority)
$string .= $this->getAuthority();
$hasPath = $this->hasPath();
$path = $this->getPath();
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
$string .= '/';
elseif(!$hasAuthority && $path[1] === '/')
$path = '/' . trim($path, '/');
$string .= $path;
// is all this necessary...?
if($this->hasQuery()) {
$string .= '?';
$parts = explode('&', $this->getQuery());
foreach($parts as $part) {
$param = explode('=', $part, 2);
$string .= rawurlencode($param[0]);
if(isset($param[1]))
$string .= '=' . rawurlencode($param[1]);
$string .= '&';
}
$string = substr($string, 0, -1);
}
if($this->hasFragment())
$string .= '#' . rawurlencode($this->getFragment());
$this->formatted = $string;
}
return $this->formatted;
}
public function toV1(): array {
$parts = ['uri' => (string)$this];
if($this->hasScheme())
$parts['scheme'] = $this->getScheme();
if($this->hasHost())
$parts['host'] = $this->getHost();
if($this->hasPort())
$parts['port'] = $this->getPort();
if($this->hasUser())
$parts['user'] = $this->getUser();
if($this->hasPassword())
$parts['pass'] = $parts['password'] = $this->getPassword();
if($this->hasPath())
$parts['path'] = $this->getPath();
if($this->hasQuery())
$parts['query'] = $this->getQuery();
if($this->hasFragment())
$parts['fragment'] = $this->getFragment();
return $parts;
}
}

View file

@ -12,9 +12,7 @@ define('UIH_DEBUG', is_file(UIH_ROOT . '/.debug'));
define('UIH_PUBLIC', UIH_ROOT . '/public');
define('UIH_SOURCE', UIH_ROOT . '/src');
define('UIH_LIBRARY', UIH_ROOT . '/lib');
define('UIH_VERSION', '20220714');
define('UIH_SEM_NAME', 'U');
define('UIH_SEM_PATH', sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'uiharu');
define('UIH_VERSION', '20220715');
require_once UIH_LIBRARY . '/index/index.php';