From 4240482cdc17fc26ea9db4937a2f76b2d0f02898 Mon Sep 17 00:00:00 2001 From: cindreta Date: Sat, 5 Mar 2022 11:59:47 +0100 Subject: [PATCH] Initial commit --- .editorconfig | 15 ++ .gitattributes | 10 ++ .php-cs-fixer.dist.php | 94 ++++++++++ CHANGELOG.md | 10 ++ README.md | 114 +++++++++++- composer.json | 46 +++++ config/treblle.php | 34 ++++ phpunit.xml.dist | 11 ++ src/Middlewares/TreblleMiddleware.php | 244 ++++++++++++++++++++++++++ 9 files changed, 576 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .php-cs-fixer.dist.php create mode 100644 CHANGELOG.md create mode 100644 composer.json create mode 100644 config/treblle.php create mode 100644 phpunit.xml.dist create mode 100644 src/Middlewares/TreblleMiddleware.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6f313c6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..455116e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto + +/.github export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitignore export-ignore +.php-cs-fixer.dist export-ignore +phpunit.xml.dist export-ignore diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..92a872d --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,94 @@ +in([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->name('*.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config())->setRules([ + '@PSR2' => true, + '@PSR12' => true, + '@PHP71Migration' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'blank_line_after_namespace' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'class_definition' => false, + 'concat_space' => [ + 'spacing' => 'none', + ], + 'ereg_to_preg' => true, + 'general_phpdoc_tag_rename' => true, + 'is_null' => true, + 'line_ending' => true, + 'modernize_types_casting' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_extra_blank_lines' => true, + 'no_short_bool_cast' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_whitespace_in_blank_line' => true, + 'ordered_imports' => true, + 'php_unit_method_casing' => [ + 'case' => 'camel_case', + ], + 'php_unit_test_annotation' => [ + 'style' => 'prefix', + ], + 'php_unit_test_case_static_method_calls' => [ + 'call_type' => 'this', + ], + 'phpdoc_align' => [ + 'align' => 'vertical', + 'tags' => [ + 'param', + 'return', + 'throws', + 'type', + 'var', + ], + ], + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_tag_type' => [ + 'tags' => [ + 'inheritdoc' => 'inline', + ], + ], + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'self_accessor' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => false, + 'yoda_style' => [ + 'always_move_variable' => false, + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + ]) + ->setFinder($finder) + ->setRiskyAllowed(true); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..687b283 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.1] - 2022-03-05 +### Changed +- initial release diff --git a/README.md b/README.md index 5da0734..b626a7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,112 @@ -# treblle-lumen -The offical Treblle package for Lumen + +Treblle for Lumen + +# Treblle for Lumen + +[![Latest Version](https://img.shields.io/packagist/v/treblle/treblle-lumen)](https://packagist.org/packages/treblle/treblle-lumen) +[![Total Downloads](https://img.shields.io/packagist/dt/treblle/treblle-lumen)](https://packagist.org/packages/treblle/treblle-lumen) +[![MIT Licence](https://img.shields.io/packagist/l/treblle/treblle-lumen)](LICENSE.md) + +Treblle makes it super easy to understand what’s going on with your APIs and the apps that use them. Just by adding +Treblle to your API out of the box you get: + +* Real-time API monitoring and logging +* Auto-generated API docs with OAS support +* API analytics +* Quality scoring +* One-click testing +* API management on the go +* and more... + +## Requirements + +* PHP 7.2+ +* Lumen 7+ + +## Dependencies + +* [`laravel/lumen`](https://packagist.org/packages/laravel/lumen) +* [`guzzlehttp/guzzle`](https://packagist.org/packages/guzzlehttp/guzzle) +* [`nesbot/carbon`](https://packagist.org/packages/nesbot/carbon) + +## Installation + +Install Treblle for Lumen via [Composer](http://getcomposer.org/) by running the following command in your console: + +```bash +composer require treblle/treblle-lumen +``` + +## Getting started +Installing Lumen packages is a lot more complicated than Laravel packages and requires a few manual steps. If you want a completely automated process please use Laravel. + +### Step 1: Publish config files +The first thing we need to do is publish the Treblle config file and make sure Lumen loads it. To do that we need to copy/paste the package config file like so: + +```bash +mkdir -p config +cp vendor/treblle/treblle-lumen/config/treblle.php config/treblle.php +``` +Now we can have Lumen load the config file. We do that by adding a new line in `bootstrap/app.php`, under the *Register Config Files* section, like so: + +```php +$app->configure('treblle'); +``` + +### Step 2: Register middleware +We need to register the Treblle middleware in Lumen. To do add a new line of code to `bootstrap/app.php`, under the Register Middleware section, like so: +```php +$app->routeMiddleware([ + 'treblle' => Treblle\Middlewares\TreblleMiddleware::class +]); +``` +### Step 3: Configure Treblle +You need an API KEY and PROJECT ID for Treblle to work. You can get those by creating a FREE account on and your first project. You'll get the two keys which you need to add to your .ENV file like so: + +```bash +TREBLLE_API_KEY=YOUR_API_KEY +TREBLLE_PROJECT_ID=YOUR_PROJECT_ID +``` + +## Enable Treblle on your API + +Now that we've installed the package we simply need to enable it. Open **routes/web.php** and assign the **treblle** middleware to your API routes like so: + +```php +$router->group(['prefix' => 'api', 'middleware' => 'treblle'], function () use ($router) { + $router->get('users', ['uses' => 'UserController@index']); + $router->post('users', ['uses' => 'TestController@store']); +}); +``` +**You're all set**. Next time someone makes a request to your API you will see it in real-time on your Treblle dashboard +alongside other features like: auto-generated documentation, error tracking, analytics and API quality scoring. + +## Configuration options + +You can configure Treblle using just .ENV variables: + +```shell +TREBLLE_IGNORED_ENV=local,dev,test +``` + +Define which environments Treblle should NOT LOG at all. By default, Treblle will log all environments except local, dev +and test. If you want to change that you can define your own ignored environments by using a comma separated list, or +allow all environments by leaving the value empty. + +### Masked fields + +Treblle **masks sensitive information** from both the request and response data as well as the request headers data +**before it even leaves your server**. The following parameters are automatically masked: password, pwd, secret, +password_confirmation, cc, card_number, ccv, ssn, credit_score. + +You can customize this list by editing the array configuration file. + +## Support + +If you have problems of any kind feel free to reach out via or email vedran@treblle.com, and we'll +do our best to help you out. + +## License + +Copyright 2021, Treblle Limited. Licensed under the MIT license: +http://www.opensource.org/licenses/mit-license.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..04ded4c --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "treblle/treblle-lumen", + "description": "Stay in tune with your APIs", + "license": "MIT", + "type": "library", + "keywords": [ + "api", + "debuging", + "documentation", + "lumen", + "monitoring", + "treblle" + ], + "authors": [ + { + "name": "Vedran Cindrić", + "email": "vedran@treblle.com", + "homepage": "https://treblle.com/", + "role": "Developer" + } + ], + "homepage": "https://treblle.com/", + "require": { + "php": "^7.2.5 || ^8.0", + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.0 || ^7.0", + "illuminate/contracts": "^7.0 || ^8.0 || ^9.0", + "illuminate/http": "^7.0 || ^8.0 || ^9.0", + "illuminate/support": "^7.0 || ^8.0 || ^9.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "orchestra/testbench": "^5.0 || ^6.0 || ^7.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Treblle\\": "src/" + } + }, + "config": { + "preferred-install": "dist", + "sort-packages": true + } +} diff --git a/config/treblle.php b/config/treblle.php new file mode 100644 index 0000000..90ba4b0 --- /dev/null +++ b/config/treblle.php @@ -0,0 +1,34 @@ + env('TREBLLE_API_KEY'), + + /* + * A valid Treblle project ID. Create your first project on https://treblle.com/ + */ + 'project_id' => env('TREBLLE_PROJECT_ID'), + + /* + * Define which environments should Treblle ignore and not monitor + */ + 'ignored_environments' => env('TREBLLE_IGNORED_ENV', 'dev,test'), + + /* + * Define which fields should be masked before leaving the server + */ + 'masked_fields' => [ + 'password', + 'pwd', + 'secret', + 'password_confirmation', + 'cc', + 'card_number', + 'ccv', + 'ssn', + 'credit_score', + 'api_key', + ], +]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7201011 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + + ./tests + + + diff --git a/src/Middlewares/TreblleMiddleware.php b/src/Middlewares/TreblleMiddleware.php new file mode 100644 index 0000000..3015b27 --- /dev/null +++ b/src/Middlewares/TreblleMiddleware.php @@ -0,0 +1,244 @@ +payload = [ + 'api_key' => config('treblle.api_key'), + 'project_id' => config('treblle.project_id'), + 'version' => 0.9, + 'sdk' => 'laravel', + 'data' => [ + 'server' => [ + 'ip' => null, + 'timezone' => config('app.timezone'), + 'os' => [ + 'name' => php_uname('s'), + 'release' => php_uname('r'), + 'architecture' => php_uname('m'), + ], + 'software' => null, + 'signature' => null, + 'protocol' => null, + 'encoding' => null, + ], + 'language' => [ + 'name' => 'php', + 'version' => phpversion(), + 'expose_php' => $this->getPHPConfigValue('expose_php'), + 'display_errors' => $this->getPHPConfigValue('display_errors'), + ], + 'request' => [ + 'timestamp' => Carbon::now('UTC')->format('Y-m-d H:i:s'), + 'ip' => null, + 'url' => null, + 'user_agent' => null, + 'method' => null, + 'headers' => '', + 'body' => '', + ], + 'response' => [ + 'headers' => '', + 'code' => null, + 'size' => 0, + 'load_time' => 0, + 'body' => '', + ], + 'errors' => [], + ], + ]; + } + + public function handle($request, Closure $next) + { + $response = $next($request); + + /* + * The terminate method is automatically called when the server supports the FastCGI protocol. + * In the case the server does not support it, we fall back to manually calling the terminate method. + * + * @see https://laravel.com/docs/middleware#terminable-middleware + */ + if (! str_contains(php_sapi_name(), 'fcgi')) { + $this->terminate($request, $response); + } + + return $response; + } + + public function terminate($request, $response) + { + if (! config('treblle.api_key') && config('treblle.project_id')) { + return; + } + + if (config('treblle.ignored_environments')) { + if (in_array(config('app.env'), explode(',', config('treblle.ignored_environments')))) { + return; + } + } + + $this->payload['data']['server']['ip'] = $request->server('SERVER_ADDR'); + $this->payload['data']['server']['software'] = $request->server('SERVER_SOFTWARE'); + $this->payload['data']['server']['signature'] = $request->server('SERVER_SIGNATURE'); + $this->payload['data']['server']['protocol'] = $request->server('SERVER_PROTOCOL'); + $this->payload['data']['server']['encoding'] = $request->server('HTTP_ACCEPT_ENCODING'); + + $this->payload['data']['request']['user_agent'] = $request->server('HTTP_USER_AGENT'); + $this->payload['data']['request']['ip'] = $request->ip(); + $this->payload['data']['request']['url'] = $request->url(); + $this->payload['data']['request']['method'] = $request->method(); + + $this->payload['data']['response']['load_time'] = $this->getLoadTime(); + $this->payload['data']['request']['body'] = $this->maskFields($request->all()); + + $this->payload['data']['request']['headers'] = $this->maskFields( + collect($request->headers->all())->transform(function ($item) { + return $item[0]; + }) + ->toArray() + ); + + $this->payload['data']['response']['code'] = $response->status(); + + $this->payload['data']['response']['headers'] = $this->maskFields( + collect($response->headers->all())->transform(function ($item) { + return $item[0]; + }) + ->toArray() + ); + + if (empty($response->exception)) { + $this->payload['data']['response']['body'] = json_decode($response->content()); + $this->payload['data']['response']['size'] = strlen($response->content()); + } else { + array_push( + $this->payload['data']['errors'], + [ + 'source' => 'onException', + 'type' => 'UNHANDLED_EXCEPTION', + 'message' => $response->exception->getMessage(), + 'file' => $response->exception->getFile(), + 'line' => $response->exception->getLine(), + ] + ); + } + + try { + (new Client()) + ->request('POST', 'https://rocknrolla.treblle.com', [ + 'connect_timeout' => 1, + 'timeout' => 1, + 'verify' => false, + 'http_errors' => false, + 'headers' => [ + 'Content-Type' => 'application/json', + 'x-api-key' => config('treblle.api_key'), + ], + 'body' => json_encode($this->payload), + ]); + } catch (RequestException | ConnectException $e) { + } + } + + public function getLoadTime(): float + { + + if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { + return (float) microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']; + } + + return 0.0000; + } + + public function maskFields($data): array + { + if (! is_array($data)) { + return []; + } + + // PROVIDE A FALLBACK IN CASE SOMEONE FORGOT TO CLEAR CACHE + $fields = config( + 'treblle.masked_fields', + [ + 'password', + 'pwd', + 'secret', + 'password_confirmation', + 'cc', + 'card_number', + 'ccv', + 'ssn', + 'credit_score', + 'api_key', + ] + ); + + if (!empty($fields)) { + foreach ($data as $key => $value) { + if (is_array($value)) { + $this->maskFields($value); + } else { + foreach ($fields as $field) { + if (preg_match('/\b'.$field.'\b/mi', (string) $key)) { + if (strtolower($field) === 'authorization') { + $authStringParts = explode(' ', $value); + + if (count($authStringParts) > 1) { + if (in_array(strtolower($authStringParts[0]), ['basic', 'bearer', 'negotiate'])) { + $data[$key] = $authStringParts[0].' '.str_repeat('*', strlen($authStringParts[1])); + } + } + } else { + $data[$key] = str_repeat('*', strlen($value)); + } + } + } + } + } + } + + return $data; + } + + public function getPHPConfigValue($variable): string + { + $isBooleanValue = filter_var(ini_get($variable), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + if (is_bool($isBooleanValue)) { + return ini_get($variable) ? 'On' : 'Off'; + } + + return ini_get($variable); + } + + public function getResponseHeaders(): array + { + $data = []; + $headers = headers_list(); + + if (is_array($headers) && ! empty($headers)) { + foreach ($headers as $header) { + $header = explode(':', $header); + $data[array_shift($header)] = trim(implode(':', $header)); + } + } + + return $data; + } + +}