_connection = $connection; } public function request( Address $address, int $timeout = 15 ): void { // Init request $request = new Request( $address->get() ); // Get connection settings $options = $request->getOptions(); // Apply identity if available if ($identity = $this->matchIdentity($address)) { $crt = tmpfile(); fwrite( $crt, $identity->crt ); $options['ssl']['local_cert'] = stream_get_meta_data( $crt )['uri']; $key = tmpfile(); fwrite( $key, $identity->key ); $options['ssl']['local_pk'] = stream_get_meta_data( $key )['uri']; } // Update connection $request->setOptions( $options ); // Parse response $response = new Response( $request->getResponse( $timeout ) ); // @TODO reset title, mime, data // Route status code // https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes switch ($response->getCode()) { case 10: // response expected case 11: // sensitive input $this->_connection->setTitle( _('Pending...') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( $response->getMeta() ? $response->getMeta() : _('Response expected') ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); $this->_connection->setRequest( $response->getMeta(), 11 !== $response->getCode() ); break; case 20: // ok // Detect MIME type switch (true) { case $mime = self::getMimeByMeta( $response->getMeta() ): break; case $mime = Filesystem::getMimeByPath( $address->getPath() ): break; case $mime = Filesystem::getMimeByData( $response->getData() ): break; default: $mime = Filesystem::MIME_TEXT_GEMINI; } // Set MIME $this->_connection->setMime( $mime ); // Set title $this->_connection->setTitle( $address->getHost() ); // Set subtitle $this->_connection->setSubtitle( $address->getHost() ); // Set tooltip $this->_connection->setTooltip( $address->get() ); // Set data $this->_connection->setData( $response->getBody() ); break; case 31: // redirect // show link, no follow $this->_connection->setTitle( _('Redirect...') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( sprintf( _('Redirect to %s'), $response->getMeta() ) ); $this->_connection->setData( sprintf( '=> %s', $response->getMeta() ) ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); break; case 60: // authorization certificate required $this->_connection->setAuth( true ); $this->_connection->setTitle( _('Authorization') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( sprintf( 'Authorization required (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setData( sprintf( 'Authorization required (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); break; case 61: // certificate not authorized $this->_connection->setAuth( true ); $this->_connection->setTitle( _('Oops!') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( sprintf( 'Authorization certificate not authorized (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setData( sprintf( 'Authorization certificate not authorized (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); break; case 62: // certificate not valid $this->_connection->setAuth( true ); $this->_connection->setTitle( _('Oops!') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( sprintf( 'Authorization certificate not valid (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setData( sprintf( 'Authorization certificate not valid (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); break; default: // Try cache if ($cache = $this->_connection->database->cache->get($address->get())) { $this->_connection->setTitle( $cache->title ); $this->_connection->setSubtitle( date( 'c', $cache->time ) # $cache->subtitle ); $this->_connection->setTooltip( $cache->tooltip ); $this->_connection->setData( $cache->data ); $this->_connection->setMime( $cache->mime ); } else { $this->_connection->setTitle( _('Oops!') ); $this->_connection->setSubtitle( $address->getHost() ); $this->_connection->setTooltip( sprintf( 'Could not open request (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setData( sprintf( 'Could not open request (code: %d)', intval( $response->getCode() ) ) ); $this->_connection->setMime( Filesystem::MIME_TEXT_GEMINI ); } } $this->_connection->setCompleted( true ); } /** * Return identity match request | NULL * * https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates * */ public function matchIdentity( Address $address, array $identities = [] ): ?object { foreach ( // Select host records $this->_connection->database->auth->like( sprintf( '%s%%', $address->get( true, true, true, true, true, false, false, false ) ) ) as $auth ) { // Parse result address $request = new Address( $auth->request ); // Filter results match current path prefix if (str_starts_with($address->getPath(), $request->getPath())) { $identities[ $auth->identity ] = $auth->request; } } // Results found if ($identities) { uasort( // max-level $identities, function ($a, $b) { return mb_strlen($b) <=> mb_strlen($a); } ); return $this->_connection->database->identity->get( intval( array_key_first( $identities ) ) ); } return null; } public static function getMimeByMeta( ?string $meta = null ): ?string { if ($meta) { preg_match( '/(?([\w]+\/[\w]+))/m', $meta, $match ); if (isset($match['mime'])) { return $match['mime']; } } return null; } }