-
-
Notifications
You must be signed in to change notification settings - Fork 102
Use the backend to perform routing API requests #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a174595
getOptionsValues: Don't expose URLs and API keys
aszlig fa543fd
Add proxy handlers for external routing services
aszlig abfc644
script: Switch to new proxying routes
aszlig 135a01f
Remove OSRM/GraphHopper/Mapbox URLs from CSP
aszlig 9d3752c
proxy: Replace usage of cURL by OCP\Http\Client
aszlig 159471d
Rename "features" variable to "routingProviders"
aszlig 339fd6c
routing: Add rate limiting for GraphHopper/MapBox
aszlig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add proxy handlers for external routing services
This adds three handlers for OSRM, GraphHopper and Mapbox, which essentially proxy the requests made to the routing APIs and inject the required API keys if needed. The reason for doing this is to offer protection for both the user and administrator, because first of all, external services can then no longer track usage down to individual users but essentially get requests from one source only. Second, the administrator no longer has his/her API keys exposed to all of the users. Note that this change only adds the routes and handlers for proxying and it's not yet integrated into the frontend. Unfortunately, when passing along and injecting API keys into the query string we need to parse query string arguments by ourselves because GraphHopper uses the "point" query string argument multiple times and PHP by default treats them as unique keys. Signed-off-by: aszlig <[email protected]>
- Loading branch information
commit fa543fd5fb72c8553c773ebdb9500d46d4a10721
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| <?php declare(strict_types=1); | ||
| /** | ||
| * Nextcloud - Maps | ||
| * | ||
| * This file is licensed under the Affero General Public License version 3 or | ||
| * later. See the COPYING file. | ||
| * | ||
| * @author aszlig <[email protected]> | ||
| * @copyright aszlig 2019 | ||
| */ | ||
|
|
||
| namespace OCA\Maps\Controller; | ||
|
|
||
| use OCP\IConfig; | ||
| use OCP\ILogger; | ||
| use OCP\IRequest; | ||
| use OCP\AppFramework\Controller; | ||
| use OCP\AppFramework\Http; | ||
| use OCP\AppFramework\Http\Response; | ||
|
|
||
| use OCA\Maps\Http\ProxyResponse; | ||
|
|
||
| class RoutingProxyController extends Controller { | ||
| private $logger; | ||
| private $config; | ||
|
|
||
| public function __construct(string $appname, IRequest $request, | ||
| ILogger $logger, IConfig $config) { | ||
| parent::__construct($appname, $request); | ||
| $this->logger = $logger; | ||
| $this->config = $config; | ||
| } | ||
|
|
||
| /** | ||
| * Build a query string from the current request combined with $extraQuery | ||
| * and return it in a way that can be directly appended to an URL (eg. with | ||
| * a leading '?'). | ||
| */ | ||
| private function buildQueryStringArg(array $extraQuery = []): string { | ||
| // Unfortunately, we can't use $this->request->getParams() here, | ||
| // because some services like GraphHopper use the same query string | ||
| // arguments twice, like eg.: point=12.34,56.78&point=43.21,87.65 | ||
| $queryComponents = explode('&', $_SERVER['QUERY_STRING'] ?? ''); | ||
|
|
||
| if (count($queryComponents) == 0) { | ||
| return ''; | ||
| } | ||
|
|
||
| $query = []; | ||
| foreach ($queryComponents as $comp) { | ||
| $keyval = explode('=', $comp, 2); | ||
| $key = rawurldecode($keyval[0]); | ||
| $val = rawurldecode($keyval[1] ?? ''); | ||
| $query[$key][] = $val; | ||
| } | ||
|
|
||
| // XXX: PHP's array() "function" is *not* a ZEND_FUNCTION, so we can't | ||
| // simply do array_map('array', ...). | ||
| $toSingleton = function ($a) { return [$a]; }; | ||
|
|
||
| $query = array_merge($query, array_map($toSingleton, $extraQuery)); | ||
|
|
||
| $result = []; | ||
| foreach ($query as $key => $values) { | ||
| foreach ($values as $value) { | ||
| $keyEnc = rawurlencode($key); | ||
| if ($value === null) { | ||
| $result[] = $keyEnc; | ||
| } else { | ||
| $result[] = $keyEnc . '=' . rawurlencode($value); | ||
| } | ||
| } | ||
| } | ||
| return '?' . implode('&', $result); | ||
| } | ||
|
|
||
| /** | ||
| * Send a request to the service at $baseUrl with path $path and the | ||
| * current request query string params and return the response from the | ||
| * remote server. | ||
| */ | ||
| private function proxyResponse(string $baseUrl, string $path, | ||
| array $extraQuery = []): Response { | ||
| if ($baseUrl === '') { | ||
| $response = new Response(); | ||
| $response->setStatus(Http::STATUS_NOT_ACCEPTABLE); | ||
| return $response; | ||
| } | ||
| $url = $baseUrl . '/' . ltrim($path, '/'); | ||
| $url .= $this->buildQueryStringArg($extraQuery); | ||
| $proxy = new ProxyResponse($url); | ||
| $proxy->sendRequest($this->logger); | ||
| return $proxy; | ||
| } | ||
|
|
||
| /** | ||
| * Proxy routing request to either a configured OSRM instance or the demo | ||
| * instance. | ||
| * | ||
| * @NoAdminRequired | ||
| * @NoCSRFRequired | ||
| */ | ||
| public function requestOsrmRoute(string $profile, string $path): Response { | ||
| if ($profile === 'demo') { | ||
| $url = 'https://router.project-osrm.org/route/v1'; | ||
| } elseif ($profile === 'car') { | ||
| $url = $this->config->getAppValue('maps', 'osrmCarURL'); | ||
| } elseif ($profile === 'bicycle') { | ||
| $url = $this->config->getAppValue('maps', 'osrmBikeURL'); | ||
| } elseif ($profile === 'foot') { | ||
| $url = $this->config->getAppValue('maps', 'osrmFootURL'); | ||
| } else { | ||
| $this->logger->error( | ||
| 'Unknown profile '.$profile.' selected for OSRM.' | ||
| ); | ||
| $response = new Response(); | ||
| $response->setStatus(Http::STATUS_BAD_REQUEST); | ||
| return $response; | ||
| } | ||
| return $this->proxyResponse($url, $path); | ||
| } | ||
|
|
||
| /** | ||
| * Proxy routing request to GraphHopper, injecting the API key. | ||
| * | ||
| * @NoAdminRequired | ||
| * @NoCSRFRequired | ||
| */ | ||
| public function requestGraphHopperRoute(string $path): Response { | ||
| $url = $this->config->getAppValue( | ||
| 'maps', 'graphhopperURL', 'https://graphhopper.com/api/1/route' | ||
| ); | ||
| $apiKey = $this->config->getAppValue('maps', 'graphhopperAPIKEY'); | ||
| return $this->proxyResponse($url, $path, ['key' => $apiKey]); | ||
| } | ||
|
|
||
| /** | ||
| * Proxy routing request to Mapbox, injecting the API key. | ||
| * | ||
| * @NoAdminRequired | ||
| * @NoCSRFRequired | ||
| */ | ||
| public function requestMapboxRoute(string $path): Response { | ||
| $url = 'https://api.mapbox.com/directions/v5'; | ||
| $apiKey = $this->config->getAppValue('maps', 'mapboxAPIKEY'); | ||
| return $this->proxyResponse($url, $path, ['access_token' => $apiKey]); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <?php declare(strict_types=1); | ||
| /** | ||
| * Nextcloud - Maps | ||
| * | ||
| * This file is licensed under the Affero General Public License version 3 or | ||
| * later. See the COPYING file. | ||
| * | ||
| * @author aszlig <[email protected]> | ||
| * @copyright aszlig 2019 | ||
| */ | ||
|
|
||
| namespace OCA\Maps\Http; | ||
|
|
||
| use OCP\ILogger; | ||
| use OCP\AppFramework\Http; | ||
| use OCP\AppFramework\Http\Response; | ||
|
|
||
| class ProxyResponse extends Response { | ||
| const USER_AGENT = 'Nextcloud Maps (https://github.com/nextcloud/maps)'; | ||
| const REQUEST_TIMEOUT = 20; | ||
|
|
||
| // NOTE: These need to be lower-case! | ||
| const ALLOWED_HEADERS = ['content-type', 'content-length']; | ||
|
|
||
| private $url; | ||
| private $responseBody = ''; | ||
|
|
||
| public function __construct(string $url) { | ||
| $this->url = $url; | ||
| } | ||
|
|
||
| /** | ||
| * Send the API request to the given URL and set headers for our response | ||
| * appropriately. | ||
| */ | ||
| public function sendRequest(ILogger $logger): bool { | ||
| if (($curl = curl_init()) === false) { | ||
| $logger->error('Unable to initialise cURL'); | ||
| $this->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR); | ||
| return false; | ||
| } | ||
|
|
||
| curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); | ||
| curl_setopt($curl, CURLOPT_URL, $this->url); | ||
| curl_setopt($curl, CURLOPT_USERAGENT, self::USER_AGENT); | ||
| curl_setopt($curl, CURLOPT_TIMEOUT, self::REQUEST_TIMEOUT); | ||
|
|
||
| curl_setopt($curl, CURLOPT_HEADERFUNCTION, function ($_, string $hl) { | ||
| $keyval = explode(':', $hl, 2); | ||
| if (count($keyval) === 2 && in_array(strtolower($keyval[0]), | ||
| self::ALLOWED_HEADERS)) { | ||
| $this->addHeader(trim($keyval[0]), ltrim($keyval[1])); | ||
| } | ||
| return strlen($hl); | ||
| }); | ||
|
|
||
| $response = curl_exec($curl); | ||
|
|
||
| if ($response === false) { | ||
| $logger->error('Error while proxying request to '.$this->url.': '. | ||
| curl_error($curl)); | ||
| curl_close($curl); | ||
| $this->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR); | ||
| return false; | ||
| } | ||
|
|
||
| $this->setStatus(curl_getinfo($curl, CURLINFO_RESPONSE_CODE)); | ||
| $this->responseBody = $response; | ||
| curl_close($curl); | ||
| return true; | ||
| } | ||
|
|
||
| public function render(): string { | ||
| return $this->responseBody; | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.