Skip to content
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

Add whitelist for custom headers that will be forwarded to the micros… #27

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -66,7 +66,7 @@ See Laravel/Lumen documentation for the list of supported databases.

#### APP_KEY

Lumen application key
Lumen application key

### Gateway variables

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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*

Expand All @@ -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
Expand Down Expand Up @@ -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!

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions app/Console/Commands/ParseServices.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Middleware/Authenticate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 3 additions & 2 deletions app/Presenters/JSONPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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'])) {
Expand All @@ -81,4 +82,4 @@ private function formatArray($input)

return self::safeEncode($output);
}
}
}
43 changes: 28 additions & 15 deletions app/Services/RestClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm if I understand correctly, this is not really a white list (a list of allowed headers to pass through) but a pre-defined list of headers to pass? then it should be called something else, eg extra headers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok...you're right. I think then we should go with the suggestion of @MrDarkSkil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the PR to match the real whitelist functionality.

}

$this->setHeaders($headers);
}

/**
Expand Down Expand Up @@ -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;
}, []);

Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -320,7 +333,7 @@ private function injectBodyParams(array $body, array $params, $prefix = '')
}
}
return $body;
}
}

/**
* @param ActionContract $action
Expand All @@ -336,4 +349,4 @@ private function buildUrl(ActionContract $action, $parametersJar)
return $this->services->resolveInstance($action->getService()) . $url;
}

}
}
8 changes: 4 additions & 4 deletions ci/site.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions config/cors.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => []
];
];
Empty file modified storage/app/.gitignore
100644 → 100755
Empty file.
Empty file modified storage/framework/cache/.gitignore
100644 → 100755
Empty file.
Empty file modified storage/framework/views/.gitignore
100644 → 100755
Empty file.
Empty file modified storage/logs/.gitignore
100644 → 100755
Empty file.