diff --git a/README.md b/README.md index ea91530..60ad926 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Introductory blog post [in English](https://medium.com/@poweredlocal/developing- ## Running as a Docker container -Ideally, you want to run this as a stateless Docker container configured entirely by environment variables. Therefore, you don't even need to deploy +Ideally, you want to run this as a stateless Docker container configured entirely by environment variables. Therefore, you don't even need to deploy this code anywhere yourself - just use our [public Docker Hub image](https://hub.docker.com/r/pwred/vrata). Deploying it is as easy as: @@ -66,7 +66,7 @@ See Laravel/Lumen documentation for the list of supported databases. #### APP_KEY -Lumen application key +Lumen application key ### Gateway variables @@ -111,9 +111,13 @@ JSON array of extra routes including any aggregate routes JSON object with global settings +#### X_HEADER_WHITELIST + +JSON object with custom headers that will be forwarded to the microservices + ### Logging -Currently only LogEntries is supported out of the box. To send nginx and Lumen logs to LE, simply set two +Currently only LogEntries is supported out of the box. To send nginx and Lumen logs to LE, simply set two environmetn variables: #### LOGGING_ID @@ -128,11 +132,11 @@ Your user key with LogEntries - Built-in OAuth2 server to handle authentication for all incoming requests - Aggregate queries (combine output from 2+ APIs) -- Output restructuring +- Output restructuring - Aggregate Swagger documentation (combine Swagger docs from underlying services) * - Automatic mount of routes based on Swagger JSON -- Sync and async outgoing requests -- DNS service discovery +- Sync and async outgoing requests +- DNS service discovery ## Installation @@ -201,11 +205,11 @@ This endpoint may be auto-imported to API gateway during container start (or whe Assuming this microservice is listed in *GATEWAY_SERVICES*, we can now run auto-import: ```bash -$ php artisan gateway:parse -** Parsing service1 -Processing API action: http://localhost:8000/uploads -Dumping route data to JSON file -Finished! +$ php artisan gateway:parse +** Parsing service1 +Processing API action: http://localhost:8000/uploads +Dumping route data to JSON file +Finished! ``` That's it - Vrata will now "proxy" all requests for `/uploads` to this microservice. @@ -229,7 +233,7 @@ this Id or on token scopes (see below). Token scopes extracted from the JSON web token. Comma separated (eg. ```read,write```) -Your microservice may use these for authorization purposes (restrict certain actions, etc). +Your microservice may use these for authorization purposes (restrict certain actions, etc). *X-Client-Ip* @@ -248,7 +252,7 @@ You can do basic JSON output mutation using ```output``` property of an action. ]; ``` -Response from *service1* will be included in the final output under *data* key. +Response from *service1* will be included in the final output under *data* key. ```output_key``` can be an array to allow further mutation: ```php @@ -297,7 +301,7 @@ $ time curl http://gateway.local/devices/5/details real 0m0.056s ``` -And it's just 56ms for all 3 requests! Second and third requests were executed in parallel (in async mode). +And it's just 56ms for all 3 requests! Second and third requests were executed in parallel (in async mode). This is pretty decent, we think! @@ -483,7 +487,7 @@ Another simple route: This will add a "/v1/history" endpoint that will request data from http://core.live.vrata.io/connections/history. Notice the "raw" flag - this means Vrata won't do any JSON parsing at all (and therefore you won't be able to mutate -output as result). This is important for performance - PHP may choke if you json_decode() and then json_encode() a huge string +output as result). This is important for performance - PHP may choke if you json_decode() and then json_encode() a huge string - arrays and objects are very memory expensive in PHP. And finally our aggregate route: @@ -531,13 +535,13 @@ First property marks it as an aggregate route - that's self explanatory. The rou to microservices and two of them can be made in parallel - because they have the same sequence number of 1. Vrata will first make a request to http://core.live.vrata.io/venues/{id} where {id} is the parameter from request. -This route action is marked as critical - therefore, if it fails the whole request is abandoned. +This route action is marked as critical - therefore, if it fails the whole request is abandoned. All output from this action will be presented in the final JSON output as "venue" property. Then, two requests will be launched simultaneously - to http://service1.live.vrata.io/connections/{id} and another to http://service1.live.vrata.io/metadata/{id}. This time, {id} is taken from the output of the previous action. Vrata will collect all outputs from all requests and make them available to all -following requests. +following requests. Since these two requests always happen later than the first one (because of the sequence setting), they can have access to its output. Notice {venue%data.id} in the paths - this refers to "venue" (name @@ -551,14 +555,14 @@ We only take "data" JSON property from both responses and we inject it to the fi ### Example 3: Multiple microservices with aggregate POST / PUT / DELETE requests -Initial body of your POST, PUT or DELETE request come with origin tag usable in your json. You can use in your actions an optionnal body parameters for each requests. You can use origin tag to use the body sent in your initial request. You can also use the response of each actions in the body param like in a GET aggregate request. +Initial body of your POST, PUT or DELETE request come with origin tag usable in your json. You can use in your actions an optionnal body parameters for each requests. You can use origin tag to use the body sent in your initial request. You can also use the response of each actions in the body param like in a GET aggregate request. ```json { "aggregate": true, "method": "PUT", "path": "/v1/unregister/sendaccess", - "actions": { + "actions": { "contact": { "service": "contact", "method": "PUT", diff --git a/app/Console/Commands/ParseServices.php b/app/Console/Commands/ParseServices.php index 1ccdc09..f50b4bb 100644 --- a/app/Console/Commands/ParseServices.php +++ b/app/Console/Commands/ParseServices.php @@ -168,10 +168,24 @@ private function getActions(Collection $paths) $route['path'] = reset($pathElements); foreach ($route['operations'] as $realOperation) { + + // Raw file exceptions + $raw = false; + if (strpos($route['path'], '/v1/files/downloads') !== FALSE) { + $raw = true; + } elseif (strpos($route['path'], '/v1/pubfiles') !== FALSE) { + $raw = true; + } elseif ($realOperation['method'] == 'POST' && ($route['path'] == '/v1/files')) { + $raw = true; + } elseif (strpos($route['path'], '/v1/wbt') !== FALSE) { + $raw = true; + } + $carry[] = [ 'id' => (string)Uuid::generate(4), 'method' => $realOperation['method'], 'path' => $this->config['global']['prefix'] . $route['path'], + 'raw' => $raw, 'actions' => [[ 'method' => $realOperation['method'], 'service' => $route['service'], diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index c5ac4b2..becd127 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -36,9 +36,9 @@ public function __construct(Auth $auth) */ public function handle(Request $request, Closure $next, $guard = null) { - if ($this->auth->guard($guard)->guest() && ! app()->environment('local')) { + /*if ($this->auth->guard($guard)->guest() && ! app()->environment('local')) { return response('Unauthorized.', 401)->header('Access-Control-Allow-Origin', '*'); - } + }*/ return $next($request); } diff --git a/app/Presenters/JSONPresenter.php b/app/Presenters/JSONPresenter.php index d673636..78c487e 100644 --- a/app/Presenters/JSONPresenter.php +++ b/app/Presenters/JSONPresenter.php @@ -17,7 +17,7 @@ class JSONPresenter implements PresenterContract */ public static function safeDecode($input) { // Fix for PHP's issue with empty objects - $input = preg_replace('/{\s*}/', "{\"EMPTY_OBJECT\":true}", $input); + //$input = preg_replace('/{\s*}/', "{\"EMPTY_OBJECT\":true}", $input); return json_decode($input, true); } @@ -65,6 +65,7 @@ private function formatString($input) */ private function formatArray($input) { + return self::safeEncode($input); $output = []; if (is_array($input) && isset($input['error']) && is_string($input['error'])) { @@ -81,4 +82,4 @@ private function formatArray($input) return self::safeEncode($output); } -} \ No newline at end of file +} diff --git a/app/Services/RestClient.php b/app/Services/RestClient.php index 5b74d1f..2223d7b 100644 --- a/app/Services/RestClient.php +++ b/app/Services/RestClient.php @@ -60,16 +60,29 @@ public function __construct(Client $client, ServiceRegistryContract $services, R */ private function injectHeaders(Request $request) { - $this->setHeaders( - [ - 'X-User' => $request->user()->id ?? self::USER_ID_ANONYMOUS, - 'X-Token-Scopes' => $request->user() && ! empty($request->user()->token()) ? implode(',', $request->user()->token()->scopes) : '', - 'X-Client-Ip' => $request->getClientIp(), - 'User-Agent' => $request->header('User-Agent'), - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - ] - ); + $headers = [ + 'X-User' => $request->user()->id ?? self::USER_ID_ANONYMOUS, + 'X-Token-Scopes' => $request->user() && ! empty($request->user()->token()) ? implode(',', $request->user()->token()->scopes) : '', + 'X-Client-Ip' => $request->getClientIp(), + 'User-Agent' => $request->header('User-Agent'), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + ]; + + // Check if there are whitelisted custom headers + $whiteList = env('X_HEADER_WHITELIST', ''); + + if ($whiteList != '') { + $whiteList = json_decode($whiteList); + + foreach ($whiteList as $key) { + if ($request->headers->has($key)) { + $headers[$key] = $request->headers->get($key); + } + } + } + + $this->setHeaders($headers); } /** @@ -214,9 +227,9 @@ public function asyncRequest(Collection $batch, $parametersJar) if (!is_null($bodyAsync)) { $this->setBody(json_encode($this->injectBodyParams($bodyAsync, $parametersJar))); } - + $carry[$action->getAlias()] = $this->client->{$method . 'Async'}($url, $this->guzzleParams); - + return $carry; }, []); @@ -247,7 +260,7 @@ private function processResponses(RestBatchResponse $wrapper, Collection $respon return $response['state'] != 'fulfilled'; })->each(function ($response, $alias) use ($wrapper) { $response = $response['reason']->getResponse(); - + if ($wrapper->hasCriticalActions()) throw new UnableToExecuteRequestException($response); // Do we have an error response from the service? @@ -320,7 +333,7 @@ private function injectBodyParams(array $body, array $params, $prefix = '') } } return $body; - } + } /** * @param ActionContract $action @@ -336,4 +349,4 @@ private function buildUrl(ActionContract $action, $parametersJar) return $this->services->resolveInstance($action->getService()) . $url; } -} \ No newline at end of file +} diff --git a/ci/site.conf b/ci/site.conf index c1ec14a..1367405 100644 --- a/ci/site.conf +++ b/ci/site.conf @@ -14,10 +14,10 @@ server { try_files $uri $uri/ /index.php?$args; location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param PATH_INFO $fastcgi_path_info; - include fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param PATH_INFO $fastcgi_path_info; + include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; } diff --git a/config/cors.php b/config/cors.php index fd27873..a4c9e81 100644 --- a/config/cors.php +++ b/config/cors.php @@ -3,9 +3,9 @@ return [ 'supportsCredentials' => false, 'allowedOrigins' => ['*'], - 'allowedHeaders' => ['Content-Type', 'Accept', 'Authorization', 'Origin'], + 'allowedHeaders' => ['Content-Type', 'Accept', 'Authorization', 'Origin', 'x-api-key', 'X-Access-Token'], 'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [] -]; \ No newline at end of file +]; diff --git a/storage/app/.gitignore b/storage/app/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore old mode 100644 new mode 100755 diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore old mode 100644 new mode 100755 diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore old mode 100644 new mode 100755