Royce  Reinger

Royce Reinger

1660135980

Faraday-decode_xml: Faraday Middleware for Decoding XML Requests

Faraday Decode XML  

Faraday middleware for decoding XML requests.

Installation

Add this line to your application's Gemfile:

gem "faraday-decode_xml"

And then execute:

bundle install

Or install it yourself as:

gem install faraday-decode_xml

Usage

require "faraday/decode_xml"

Faraday.new { |faraday| faraday.response :xml }

Development

After checking out the repo, run bin/setup to install dependencies.

Then, run bin/test to run the tests.

To install this gem onto your local machine, run rake build.

To release a new version, make a commit with a message such as "Bumped to 0.0.2" and then run rake release. See how it works here.

To run prettier, run rake prettier

Contributing

Bug reports and pull requests are welcome on GitHub.

Download Details:

Author: Soberstadt
Source Code: https://github.com/soberstadt/faraday-decode_xml 
License: MIT license

#ruby #xml #decode #middleware 

Faraday-decode_xml: Faraday Middleware for Decoding XML Requests
Royce  Reinger

Royce Reinger

1660120740

Faraday Middleware Which Turns Hashes into Hashie::Mash::Rash Objects

Faraday::Rashify

This is a Faraday middleware which turns Hashes into Hashie::Mash::Rash objects, using the rash_alt gem.

This very specific middleware has been extracted from the faraday_middleware project.

Original code created by @mislav with contributions by @shishi.

Installation

Add this line to your application's Gemfile:

gem 'faraday-rashify'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install faraday-rashify

Usage

This is a Faraday middleware, and you need to mount it after parsing middlewares, such as :json.

Example:

Faraday.new(options) do |conn|
  conn.request :json

  conn.response :rashify
  conn.response :json, content_type: /\bjson$/
  conn.response :logger, logger, bodies: true

  conn.adapter Faraday.default_adapter
end

That is, this middleware only acts on Ruby Hashes and Arrays and makes the Hashes in there into Hashie::Mash::Rash objects.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at lostisland/faraday-rashify. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

Code of Conduct

Everyone interacting in the Faraday::Rashify project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Download Details:

Author: lostisland
Source Code: https://github.com/lostisland/faraday-rashify 
License: MIT license

#ruby #middleware 

Faraday Middleware Which Turns Hashes into Hashie::Mash::Rash Objects
Royce  Reinger

Royce Reinger

1660113300

Faraday-multipart: Perform Multipart-post Requests using Faraday

Faraday Multipart 

The Multipart middleware converts a Faraday::Request#body Hash of key/value pairs into a multipart form request, but only under these conditions:

  • The request's Content-Type is "multipart/form-data"
  • Content-Type is unspecified, AND one of the values in the Body responds to #content_type.

Faraday contains a couple helper classes for multipart values:

  • Faraday::Multipart::FilePart wraps binary file data with a Content-Type. The file data can be specified with a String path to a local file, or an IO object.
  • Faraday::Multipart::ParamPart wraps a String value with a Content-Type, and optionally a Content-ID.

Installation

Add this line to your application's Gemfile:

gem 'faraday-multipart'

And then execute:

bundle install

Or install it yourself as:

gem install faraday-multipart

Usage

First of all, you'll need to add the multipart middleware to your Faraday connection:

require 'faraday'
require 'faraday/multipart'

conn = Faraday.new(...) do |f|
  f.request :multipart, **options
  # ...
end

Payload can be a mix of POST data and multipart values.

# regular POST form value
payload = { string: 'value' }

# filename for this value is File.basename(__FILE__)
payload[:file] = Faraday::Multipart::FilePart.new(__FILE__, 'text/x-ruby')

# specify filename because IO object doesn't know it
payload[:file_with_name] = Faraday::Multipart::FilePart.new(
  File.open(__FILE__),
  'text/x-ruby',
  File.basename(__FILE__)
)

# Sets a custom Content-Disposition:
# nil filename still defaults to File.basename(__FILE__)
payload[:file_with_header] = Faraday::Multipart::FilePart.new(
  __FILE__,
  'text/x-ruby',
  nil,
  'Content-Disposition' => 'form-data; foo=1'
)

# Upload raw json with content type
payload[:raw_data] = Faraday::Multipart::ParamPart.new(
  { a: 1 }.to_json,
  'application/json'
)

# optionally sets Content-ID too
payload[:raw_with_id] = Faraday::Multipart::ParamPart.new(
  { a: 1 }.to_json,
  'application/json',
  'foo-123'
)

conn.post('/', payload)

Sending an array of documents

Sometimes, the server you're calling will expect an array of documents or other values for the same key. The multipart middleware will automatically handle this scenario for you:

payload = {
  files: [
    Faraday::Multipart::FilePart.new(__FILE__, 'text/x-ruby'),
    Faraday::Multipart::FilePart.new(__FILE__, 'text/x-pdf')
  ],
  url: [
    'http://mydomain.com/callback1',
    'http://mydomain.com/callback2'
  ]
}

conn.post(url, payload)
#=> POST url[]=http://mydomain.com/callback1&url[]=http://mydomain.com/callback2
#=>   and includes both files in the request under the `files[]` name

However, by default these will be sent with files[] key and the URLs with url[], similarly to arrays in URL parameters. Some servers (e.g. Mailgun) expect each document to have the same parameter key instead. You can instruct the multipart middleware to do so by providing the flat_encode option:

require 'faraday'
require 'faraday/multipart'

conn = Faraday.new(...) do |f|
  f.request :multipart, flat_encode: true
  # ...
end

payload = ... # see example above

conn.post(url, payload)
#=> POST url=http://mydomain.com/callback1&url=http://mydomain.com/callback2
#=>   and includes both files in the request under the `files` name

This works for both UploadIO and normal parameters alike.

Development

After checking out the repo, run bin/setup to install dependencies.

Then, run bin/test to run the tests.

To install this gem onto your local machine, run rake build.

Releasing a new version

To release a new version, make a commit with a message such as "Bumped to 0.0.2", and change the Unreleased heading in CHANGELOG.md to a heading like "0.0.2 (2022-01-01)", and then use GitHub Releases to author a release. A GitHub Actions workflow then publishes a new gem to RubyGems.org.

Contributing

Bug reports and pull requests are welcome on GitHub.

Download Details:

Author: lostisland
Source Code: https://github.com/lostisland/faraday-multipart 
License: MIT license

#ruby #middleware 

Faraday-multipart: Perform Multipart-post Requests using Faraday
Royce  Reinger

Royce Reinger

1660098180

A Detailed Request & Response Logger for Faraday

Faraday::DetailedLogger

A Faraday middleware used for providing debug- and info-level logging information. The request and response logs follow very closely with cURL output for ease of understanding.

Caution: Be careful about your log level settings when using this middleware, especially in a production environment. With a DEBUG level log enabled, there will be information security concerns.

At a DEBUG level, the request and response headers and their bodies will be logged. This means that if you have Authorization information or API keys in your headers or are passing around sensitive information in the bodies, only an INFO level or above should be used.

No headers or bodies are logged at an INFO or greater log level.

Installation

Add this line to your application's Gemfile:

gem "faraday-detailed_logger"

And then execute:

$ bundle

Or install it yourself as:

$ gem install faraday-detailed_logger

Usage

Once required, the logger can be added to any Faraday connection by inserting it into your connection's request/response stack:

require 'faraday'
require 'faraday/detailed_logger'

connection = Faraday.new(:url => "http://sushi.com") do |faraday|
  faraday.request  :url_encoded
  faraday.response :detailed_logger # <-- Inserts the logger into the connection.
  faraday.adapter  Faraday.default_adapter
end

By default, the Faraday::DetailedLogger will log to STDOUT. If this is not your desired log location, simply provide any Logger-compatible object as a parameter to the middleware definition:

require 'faraday'
require 'faraday/detailed_logger'
require 'logger'

my_logger = Logger.new("logfile.log")
my_logger.level = Logger::INFO

connection = Faraday.new(:url => "http://sushi.com") do |faraday|
  faraday.request  :url_encoded
  faraday.response :detailed_logger, my_logger # <-- sets a custom logger.
  faraday.adapter  Faraday.default_adapter
end

Or, perhaps use your Rails logger:

faraday.response :detailed_logger, Rails.logger

Further, you might like to tag logged output to make it easily located in your logs:

faraday.response :detailed_logger, Rails.logger, "Sushi Request"

Example output

Because logs generally work best with a single line of data per entry, the DEBUG-level output which contains the headers and bodies is inspected prior to logging. This crushes down and slightly manipulates the multi-line output one would expect when performing a verbose cURL operation into a log-compatible single line.

Below is a contrived example showing how this works. Presuming cURL generated the following request and received the associated response:

$ curl -v -d "requestbody=content" http://sushi.com/temaki
> GET /temaki HTTP/1.1
> User-Agent: Faraday::DetailedLogger
> Host: sushi.com
> Content-Type: application/x-www-form-urlencoded
> 
> requestbody=content
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< 
< {"order_id":"1"}

The Faraday::DetailedLogger would log something similar to the following, with DEBUG-level logging enabled:

POST http://sushi.com/nigirizushi
"User-Agent: Faraday::DetailedLogger\nContent-Type: application/x-www-form-urlencoded\n\nrequestbody=content"
HTTP 200
"Content-Type: application/json\n\n{\"order_id\":\"1\"}"

Request logging

Log output for the request-portion of an HTTP interaction:

POST http://sushi.com/temaki
"User-Agent: Faraday v0.9.0\nAccept: application/json\nContent-Type: application/json\n\n{\"name\":\"Polar Bear\",\"ingredients\":[\"Tuna\",\"Salmon\",\"Cream Cheese\",\"Tempura Flakes\"],\"garnish\":\"Eel Sauce\"}"

The POST line is logged at an INFO level just before the request is transmitted to the remote system. The second line containing the request headers and body are logged at a DEBUG level.

Response logging

Log output for the response-portion of an HTTP interaction: Response portion:

HTTP 202
"server: nginx\ndate: Tue, 01 Jul 2014 21:56:52 GMT\ncontent-type: application/json\ncontent-length: 17\nconnection: close\nstatus: 202 Accepted\n\n{\"order_id\":\"1\"}"

The HTTP status line is logged at an INFO level at the same time the response is returned from the remote system. The second line containing the response headers and body are logged at a DEBUG level.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/envylabs/faraday-detailed_logger.

Download Details:

Author: Envylabs
Source Code: https://github.com/envylabs/faraday-detailed_logger 
License: MIT license

#ruby #logger #middleware 

A Detailed Request & Response Logger for Faraday
Royce  Reinger

Royce Reinger

1660061054

Various Faraday Middlewares for Faraday-based API Wrappers

Faraday Middleware 

A collection of useful Faraday middleware. 

gem install faraday_middleware

⚠️ DEPRECATION WARNING ⚠️

As highlighted in Faraday's UPGRADING guide, faraday_middleware is DEPRECATED, and will not be updated to support Faraday 2.0. If you rely on faraday_middleware in your project and would like to support Faraday 2.0:

  • The json middleware (request and response) are now both bundled with Faraday 🙌
  • The instrumentation middleware is bundled with Faraday
  • All other middlewares, they'll be re-released as independent gems compatible with both Faraday v1 and v2, look for awesome-faraday

Most of the middleare are looking for adoption, contributors that would like to maintain them. If you'd like to maintain any middleware, have any question or need any help, we're here! Please reach out opening an issue or a discussion.

Dependencies

Ruby >= 2.3.0

As of v0.16.0, faraday and faraday_middleware no longer officially support JRuby or Rubinius.

Some dependent libraries are needed only when using specific middleware:

MiddlewareLibraryNotes
FaradayMiddleware::Instrumentationactivesupport 
FaradayMiddleware::OAuthsimple_oauth 
FaradayMiddleware::ParseXmlmulti_xml 
FaradayMiddleware::ParseYamlsafe_yamlNot backwards compatible with versions of this middleware prior to faraday_middleware v0.12. See code comments for alternatives.
FaradayMiddleware::Mashifyhashie 
FaradayMiddleware::Rashifyrash_altMake sure to uninstall original rash gem to avoid conflict.

Examples

require 'faraday_middleware'

connection = Faraday.new 'http://example.com/api' do |conn|
  conn.request :oauth2, 'TOKEN'
  conn.request :json

  conn.response :xml,  content_type: /\bxml$/
  conn.response :json, content_type: /\bjson$/

  conn.use :instrumentation
  conn.adapter Faraday.default_adapter
end

See the documentation.

Download Details:

Author: lostisland
Source Code: https://github.com/lostisland/faraday_middleware 
License: MIT license

#ruby #middleware #api #wrapper 

Various Faraday Middlewares for Faraday-based API Wrappers
Royce  Reinger

Royce Reinger

1660049700

A Faraday Middleware Sets Body Encoding When Specified By Server

Faraday::Encoding 

A Faraday Middleware sets body encoding when specified by server.

Motivation

Response body's encoding is set always ASCII-8BIT using with net/http adapter. Net::HTTP doesn't handle encoding when server specifies encoding in content-type header. Sometimes we caught an Error such as the following:

body = Faraday.new(url: 'https://example.com').get('/').body
# body contains utf-8 string. ex: "赤坂"
body.to_json
# => raise Encoding::UndefinedConversionError: "\xE8" from ASCII-8BIT to UTF-8

That's why I wrote Farday::Encoding gem.

SEE ALSO: response.body is ASCII-8BIT when Content-Type is text/xml; charset=utf-8

Installation

Add this line to your application's Gemfile:

gem 'faraday-encoding'

And then execute:

$ bundle

Or install it yourself as:

$ gem install faraday-encoding

Usage

require 'faraday/encoding'

conn = Faraday.new do |connection|
  connection.response :encoding  # use Faraday::Encoding middleware
  connection.adapter Faraday.default_adapter # net/http
end

response = conn.get '/nya.html'  # content-type is specified as 'text/plain; charset=utf-8'
response.body.encoding
# => #<Encoding:UTF-8>

Contributing

  1. Fork it ( https://github.com/ma2gedev/faraday-encoding/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Download Details:

Author: ma2gedev
Source Code: https://github.com/ma2gedev/faraday-encoding 
License: MIT license

#ruby #encode #middleware 

A Faraday Middleware Sets Body Encoding When Specified By Server
Royce  Reinger

Royce Reinger

1660042320

Faraday-encode_xml: Faraday Middleware for Encoding Requests As XML

Faraday Encode XML   

This repo is a Faraday middleware for encoding requests as XML. Faraday is an HTTP client library that provides a common interface for making requests.

Installation

Add this line to your application's Gemfile:

gem 'faraday-encode_xml'

And then execute:

bundle install

Or install it yourself as:

gem install faraday-encode_xml

Usage

require 'faraday/encode_xml'

connection = Faraday.new do |faraday|
  ## This gem only encodes requests
  faraday.request :xml

  ## For responses deconding use `faraday_middleware` gem and such code:
  # require 'faraday_middleware'
  # faraday.response :xml

  ## For example `httpbingo` responses as JSON, so let's enable it:
  require 'faraday_middleware'
  faraday.response :json
end

response = connection.post('https://httpbingo.org/post', { a: 1, b: 'foo', c: true })

puts response.body['data'] ## => <a>1</a><b>foo</b><c>true</c>

Development

After checking out the repo, run bundle install to install dependencies.

Then, run bundle exec rspec to run the tests.

To install this gem onto your local machine, run toys gem install.

To release a new version, run toys gem release %version%. See how it works here.

Contributing

Bug reports and pull requests are welcome on GitHub.

Download Details:

Author: AlexWayfer
Source Code: https://github.com/AlexWayfer/faraday-encode_xml 
License: MIT license

#ruby #middleware #xml 

Faraday-encode_xml: Faraday Middleware for Encoding Requests As XML
Elian  Harber

Elian Harber

1660005480

Souin: An HTTP Cache System, RFC Compliant, Compatible

Project description

Souin is a new HTTP cache system suitable for every reverse-proxy. It can be either placed on top of your current reverse-proxy whether it's Apache, Nginx or as plugin in your favorite reverse-proxy like Træfik, Caddy or Tyk.
Since it's written in go, it can be deployed on any server and thanks to the docker integration, it will be easy to install on top of a Swarm, or a kubernetes instance.
It's RFC compatible, supporting Vary, request coalescing, stale cache-control and other specifications related to the RFC-7234.
It also supports the Cache-Status HTTP response header and the YKey group such as Varnish.

Configuration

The configuration file is store at /anywhere/configuration.yml. You can supply your own as long as you use one of the minimal configurations below.

Required configuration

Souin as plugin

default_cache: # Required
  ttl: 10s # Default TTL

Souin out-of-the-box

default_cache: # Required
  ttl: 10s # Default TTL
reverse_proxy_url: 'http://traefik' # If it's in the same network you can use http://your-service, otherwise just use https://yourdomain.com
KeyDescriptionValue example
default_cache.ttlDuration to cache request (in seconds)10

Besides, it's highly recommended to set default_cache.default_cache_control (see it below) to avoid undesired caching for responses without Cache-Control header.

Optional configuration

# /anywhere/configuration.yml
api:
  basepath: /souin-api # Default route basepath for every additional APIs to avoid conflicts with existing routes
  prometheus: # Prometheus exposed metrics
    basepath: /anything-for-prometheus-metrics # Change the prometheus endpoint basepath
  souin: # Souin listing keys and cache management
    basepath: /anything-for-souin # Change the souin endpoint basepath
cache_keys:
  '.*\.css':
    disable_body: true
    disable_host: true
    disable_method: true
cdn: # If Souin is set after a CDN fill these informations
  api_key: XXXX # Your provider API key if mandatory
  provider: fastly # The provider placed before Souin (e.g. fastly, cloudflare, akamai, varnish)
  strategy: soft # The strategy to purge the CDN cache based on tags (e.g. soft, hard)
  dynamic: true # If true, you'll be able to add custom keys than the ones defined under the surrogate_keys key
default_cache:
  allowed_http_verbs: # Allowed HTTP verbs to cache (default GET, HEAD).
    - GET
    - POST
    - HEAD
  cache_name: Souin # Override the cache name to use in the Cache-Status header
  distributed: true # Use Olric or Etcd distributed storage
  headers: # Default headers concatenated in stored keys
    - Authorization
  key:
    disable_body: true
    disable_host: true
    disable_method: true
  etcd: # If distributed is set to true, you'll have to define either the etcd or olric section
    configuration: # Configure directly the Etcd client
      endpoints: # Define multiple endpoints
        - etcd-1:2379 # First node
        - etcd-2:2379 # Second node
        - etcd-3:2379 # Third node
  olric: # If distributed is set to true, you'll have to define either the etcd or olric section
    url: 'olric:3320' # Olric server
  regex:
    exclude: 'ARegexHere' # Regex to exclude from cache
  stale: 1000s # Stale duration
  timeout: # Timeout configuration
    backend: 10s # Backend timeout before returning an HTTP unavailable response
    cache: 20ms # Cache provider (badger, etcd, nutsdb, olric, depending the configuration you set) timeout before returning a miss
  ttl: 1000s # Default TTL
  default_cache_control: no-store # Set default value for Cache-Control response header if not set by upstream
log_level: INFO # Logs verbosity [ DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL ], case do not matter
ssl_providers: # The {providers}.json to use
  - traefik
urls:
  'https:\/\/domain.com\/first-.+': # First regex route configuration
    ttl: 1000s # Override default TTL
  'https:\/\/domain.com\/second-route': # Second regex route configuration
    ttl: 10s # Override default TTL
    headers: # Override default headers
    - Authorization
  'https?:\/\/mysubdomain\.domain\.com': # Third regex route configuration
    ttl: 50s # Override default TTL
    headers: # Override default headers
    - Authorization
    - 'Content-Type'
    default_cache_control: public, max-age=86400 # Override default default Cache-Control
ykeys:
  The_First_Test:
    headers:
      Content-Type: '.+'
  The_Second_Test:
    url: 'the/second/.+'
  The_Third_Test:
  The_Fourth_Test:
surrogate_keys:
  The_First_Test:
    headers:
      Content-Type: '.+'
  The_Second_Test:
    url: 'the/second/.+'
  The_Third_Test:
  The_Fourth_Test:
KeyDescriptionValue example
apiThe cache-handler API cache management 
api.basepathBasePath for all APIs to avoid conflicts/your-non-conflicting-route

(default: /souin-api)
api.{api}.enable(DEPRECATED) Enable the API with related routestrue

(default: true if you define the api name, false then)
api.{api}.security(DEPRECATED) Enable the JWT Authentication token verificationtrue

(default: false)
api.security.secret(DEPRECATED) JWT secret keyAny_charCanW0rk123
api.security.users(DEPRECATED) Array of authorized users with username x password combo- username: admin

password: admin
api.souin.securityEnable JWT validation to access the resourcetrue

(default: false)
cache_keysDefine the key generation rules for each URI matching the key regexp 
cache_keys.{your regexp}Regexp that the URI should match to override the key generation.+\.css
default_cache.key.disable_bodyDisable the body part in the key matching the regexp (GraphQL context)true

(default: false)
default_cache.key.disable_hostDisable the host part in the key matching the regexptrue

(default: false)
default_cache.key.disable_methodDisable the method part in the key matching the regexptrue

(default: false)
cdnThe CDN management, if you use any cdn to proxy your requests Souin will handle that 
cdn.providerThe provider placed before Souinakamai

fastly

souin
cdn.api_keyThe api key used to access to the providerXXXX
cdn.dynamicEnable the dynamic keys returned by your backend applicationtrue

(default: false)
cdn.emailThe api key used to access to the provider if required, depending the providerXXXX
cdn.hostnameThe hostname if required, depending the providerdomain.com
cdn.networkThe network if required, depending the provideryour_network
cdn.strategyThe strategy to use to purge the cdn cache, soft will keep the content as a stale resourcehard

(default: soft)
cdn.service_idThe service id if required, depending the provider123456_id
cdn.zone_idThe zone id if required, depending the provideranywhere_zone
default_cache.allowed_http_verbsThe HTTP verbs to support cache- GET

- POST

(default: GET, HEAD)
default_cache.badgerConfigure the Badger cache storage 
default_cache.badger.pathConfigure Badger with a file/anywhere/badger_configuration.json
default_cache.badger.configurationConfigure Badger directly in the Caddyfile or your JSON caddy configurationSee the Badger configuration for the options
default_cache.etcdConfigure the Etcd cache storage 
default_cache.etcd.configurationConfigure Etcd directly in the Caddyfile or your JSON caddy configurationSee the Etcd configuration for the options
default_cache.headersList of headers to include to the cache- Authorization

- Content-Type

- X-Additional-Header
default_cache.keyOverride the key generation with the ability to disable unecessary parts 
default_cache.key.disable_bodyDisable the body part in the key (GraphQL context)true

(default: false)
default_cache.key.disable_hostDisable the host part in the keytrue

(default: false)
default_cache.key.disable_methodDisable the method part in the keytrue

(default: false)
default_cache.etcdConfigure the Etcd cache storage 
default_cache.etcd.configurationConfigure Etcd directly in the Caddyfile or your JSON caddy configurationSee the Etcd configuration for the options
default_cache.nutsConfigure the Nuts cache storage 
default_cache.nuts.pathSet the Nuts file path storage/anywhere/nuts/storage
default_cache.nuts.configurationConfigure Nuts directly in the Caddyfile or your JSON caddy configurationSee the Nuts configuration for the options
default_cache.olricConfigure the Olric cache storage 
default_cache.olric.pathConfigure Olric with a file/anywhere/olric_configuration.json
default_cache.olric.configurationConfigure Olric directly in the Caddyfile or your JSON caddy configurationSee the Olric configuration for the options
default_cache.port.{web,tls}The device's local HTTP/TLS port that Souin should be listening onRespectively 80 and 443
default_cache.regex.excludeThe regex used to prevent paths being cached^[A-z]+.*$
default_cache.staleThe stale duration25m
default_cache.timeoutThe timeout configuration 
default_cache.timeout.backendThe timeout duration to consider the backend as unreachable10s
default_cache.timeout.cacheThe timeout duration to consider the cache provider as unreachable10ms
default_cache.ttlThe TTL duration120s
default_cache.default_cache_controlSet the default value of Cache-Control response header if not set by upstream (Souin treats empty Cache-Control as public if omitted)no-store
log_levelThe log levelOne of DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL it's case insensitive
reverse_proxy_urlThe reverse-proxy's instance URL (Apache, Nginx, Træfik...)- http://yourservice (Container way)
http://localhost:81 (Local way)
http://yourdomain.com:81 (Network way)
ssl_providersList of your providers handling certificates- traefik

- nginx

- apache
urls.{your url or regex}List of your custom configuration depending each URL or regex'https://yourdomain.com'
urls.{your url or regex}.ttlOverride the default TTL if defined90s

10m
urls.{your url or regex}.default_cache_controlOverride the default default Cache-Control if definedpublic, max-age=86400
urls.{your url or regex}.headersOverride the default headers if defined- Authorization

- 'Content-Type'
surrogate_keys.{key name}.headersHeaders that should match to be part of the surrogate key groupAuthorization: ey.+

Content-Type: json
surrogate_keys.{key name}.headers.{header name}Header name that should be present a match the regex to be part of the surrogate key groupContent-Type: json
surrogate_keys.{key name}.urlUrl that should match to be part of the surrogate key group.+
ykeys.{key name}.headers(DEPRECATED) Headers that should match to be part of the ykey groupAuthorization: ey.+

Content-Type: json
ykeys.{key name}.headers.{header name}(DEPRECATED) Header name that should be present a match the regex to be part of the ykey groupContent-Type: json
ykeys.{key name}.url(DEPRECATED) Url that should match to be part of the ykey group.+

APIs

All endpoints are accessible through the api.basepath configuration line or by default through /souin-api to avoid named route conflicts. Be sure to define an unused route to not break your existing application.

Prometheus API

Prometheus API expose some metrics about the cache.
The base path for the prometheus API is /metrics. Not supported inside Træfik because the deny the unsafe library usage inside plugins

MethodEndpointDescription
GET/Expose the different keys listed below.
KeyDefinition
souin_request_counterCount the incoming requests
souin_no_cached_response_counterCount the uncacheable responses
souin_cached_response_counterCount the cacheable responses
souin_avg_response_timeAverage response time

Souin API

Souin API allow users to manage the cache.
The base path for the souin API is /souin.
The Souin API supports the invalidation by surrogate keys such as Fastly which will replace the Varnish system. You can read the doc about this system. This system is able to invalidate by tags your cloud provider cache. Actually it supports Akamai and Fastly but in a near future some other providers would be implemented like Cloudflare or Varnish.

MethodEndpointHeadersDescription
GET/-List stored keys cache
GET/surrogate_keys-List stored keys cache
PURGE/{id or regexp}-Purge selected item(s) depending. The parameter can be either a specific key or a regexp
PURGE/?ykey={key}-Purge selected item(s) corresponding to the target ykey such as Varnish (deprecated)
PURGE/Surrogate-Key: Surrogate-Key-First, Surrogate-Key-SecondPurge selected item(s) belong to the target key in the header Surrogate-Key (see Surrogate-Key system)
PURGE/flush-Purge all providers and surrogate storages

Security API

DEPRECATED
Security API allows users to protect other APIs with JWT authentication.
The base path for the security API is /authentication.

MethodEndpointBodyHeadersDescription
POST/login{"username":"admin", "password":"admin"}['Content-Type' => 'json']Try to login, it returns a response which contains the cookie name souin-authorization-token with the JWT if succeed
POST/refresh-['Content-Type' => 'json', 'Cookie' => 'souin-authorization-token=the-token']Refreshes the token, replaces the old with a new one

Diagrams

Sequence diagram

See the sequence diagram for the minimal version below Sequence diagram

Cache systems

Supported providers

The cache system sits on top of three providers at the moment. It provides two in-memory storage solutions (badger and nuts), and two distributed storages Olric and Etcd because setting, getting, updating and deleting keys in these providers is as easy as it gets.
The Badger provider (default one): you can tune its configuration using the badger configuration inside your Souin configuration. In order to do that, you have to declare the badger block. See the following json example.

"badger": {
  "configuration": {
    "ValueDir": "default",
    "ValueLogFileSize": 16777216,
    "MemTableSize": 4194304,
    "ValueThreshold": 524288,
    "BypassLockGuard": true
  }
}

The Nuts provider: you can tune its configuration using the nuts configuration inside your Souin configuration. In order to do that, you have to declare the nuts block. See the following json example.

"nuts": {
  "configuration": {
    "Dir": "default",
    "EntryIdxMode": 1,
    "RWMode": 0,
    "SegmentSize": 1024,
    "NodeNum": 42,
    "SyncEnable": true,
    "StartFileLoadingMode": 1
  }
}

The Olric provider: you can tune its configuration using the olric configuration inside your Souin configuration and declare Souin has to use the distributed provider. In order to do that, you have to declare the olric block and the distributed directive. See the following json example.

"distributed": true,
"olric": {
  "configuration": {
    # Olric configuration here...
  }
}

In order to do that, the Olric provider need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible.

The Etcd provider: you can tune its configuration using the etcd configuration inside your Souin configuration and declare Souin has to use the distributed provider. In order to do that, you have to declare the etcd block and the distributed directive. See the following json example.

"distributed": true,
"etcd": {
  "configuration": {
    # Etcd configuration here...
  }
}

In order to do that, the Etcd provider need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible. Souin will return at first the response from the choosen provider when it gives a non-empty response, or fallback to the reverse proxy otherwise. Since v1.4.2, Souin supports Olric and since v1.6.10 it supports Etcd to handle distributed cache.

GraphQL

This feature is currently in beta.
Souin can partially cache your GraphQL requests. It automatically handles the data retrieval and omit the caching for the mutations.
However, it will invalidate whole cache keys with a body when you send a mutation request due to the inability to read and understand automatically which cached endpoint should be deleted.
You can enable the GraphQL support with the default_cache.allowed_http_verbs key to define the list of supported HTTP verbs like GET, POST, DELETE.

default_cache:
  allowed_http_verbs:
    - GET
    - POST
    - HEAD

Cache invalidation

The cache invalidation is built for CRUD requests, if you're doing a GET HTTP request, it will serve the cached response when it exists, otherwise the reverse-proxy response will be served.
If you're doing a POST, PUT, PATCH or DELETE HTTP request, the related cache GET request, and the list endpoint will be dropped.
It also supports invalidation via Souin API to invalidate the cache programmatically.

Examples

Træfik container

Træfik is a modern reverse-proxy which helps you to manage full container architecture projects.

# your-traefik-instance/docker-compose.yml
version: '3.7'

x-networks: &networks
  networks:
    - your_network

services:
  traefik:
    image: traefik:v2.5.6
    command: --providers.docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /anywhere/traefik.json:/acme.json
    <<: *networks

  # your other services here...

networks:
  your_network:
    external: true
# your-souin-instance/docker-compose.yml
version: '3.7'

x-networks: &networks
  networks:
    - your_network

services:
  souin:
    image: darkweak/souin:latest
    ports:
      - 80:80
      - 443:443
    environment:
      GOPATH: /app
    volumes:
      - /anywhere/traefik.json:/ssl/traefik.json
      - /anywhere/configuration.yml:/configuration/configuration.yml
    <<: *networks

networks:
  your_network:
    external: true

Plugins

Beego filter

To use Souin as beego filter, you can refer to the Beego filter integration folder to discover how to configure it.
You just have to define a new beego router and tell to the instance to use the Handle method like below:

import (
    "net/http"

    httpcache "github.com/darkweak/souin/plugins/beego"
)

func main(){

    // ...
    web.InsertFilterChain("/*", httpcache.NewHTTPCacheFilter())
    // ...

}

Caddy module

To use Souin as caddy module, you can refer to the Caddy module integration folder to discover how to configure it.
The related Caddyfile can be found here.
Then you just have to run the following command:

xcaddy build --with github.com/darkweak/souin/plugins/caddy

There is the fully configuration below

{
    order cache before rewrite
    log {
        level debug
    }
    cache {
        allowed_http_verbs GET POST PATCH
        api {
            basepath /some-basepath
            prometheus {
                security
            }
            souin {
                security
            }
        }
        badger {
            path the_path_to_a_file.json
        }
        cache_name Souin
        cache_keys {
            .*\.something {
                disable_body
                disable_host
                disable_method
            }
        }
        cdn {
            api_key XXXX
            dynamic
            email darkweak@protonmail.com
            hostname domain.com
            network your_network
            provider fastly
            strategy soft
            service_id 123456_id
            zone_id anywhere_zone
        }
        headers Content-Type Authorization
        key {
            disable_body
            disable_host
            disable_method
        }
        log_level debug
        etcd {
            configuration {
                # Your Etcd configuration here
            }
        }
        olric {
            url url_to_your_cluster:3320
            path the_path_to_a_file.yaml
            configuration {
                # Your Olric configuration here
            }
        }
        regex {
            exclude /test2.*
        }
        stale 200s
        timeout {
          backend 20s
          cache 5ms
        }
        ttl 1000s
        default_cache_control no-store
    }
}

:4443
respond "Hello World!"

@match path /test1*
@match2 path /test2*
@matchdefault path /default
@souin-api path /souin-api*

cache @match {
    ttl 5s
    badger {
        path /tmp/badger/first-match
        configuration {
            # Required value
            ValueDir <string>

            # Optional
            SyncWrites <bool>
            NumVersionsToKeep <int>
            ReadOnly <bool>
            Compression <int>
            InMemory <bool>
            MetricsEnabled <bool>
            MemTableSize <int>
            BaseTableSize <int>
            BaseLevelSize <int>
            LevelSizeMultiplier <int>
            TableSizeMultiplier <int>
            MaxLevels <int>
            VLogPercentile <float>
            ValueThreshold <int>
            NumMemtables <int>
            BlockSize <int>
            BloomFalsePositive <float>
            BlockCacheSize <int>
            IndexCacheSize <int>
            NumLevelZeroTables <int>
            NumLevelZeroTablesStall <int>
            ValueLogFileSize <int>
            ValueLogMaxEntries <int>
            NumCompactors <int>
            CompactL0OnClose <bool>
            LmaxCompaction <bool>
            ZSTDCompressionLevel <int>
            VerifyValueChecksum <bool>
            EncryptionKey <string>
            EncryptionKey <Duration>
            BypassLockGuard <bool>
            ChecksumVerificationMode <int>
            DetectConflicts <bool>
            NamespaceOffset <int>
        }
    }
}

cache @match2 {
    ttl 50s
    badger {
        path /tmp/badger/second-match
        configuration {
            ValueDir match2
            ValueLogFileSize 16777216
            MemTableSize 4194304
            ValueThreshold 524288
            BypassLockGuard true
        }
    }
    headers Authorization
    default_cache_control "public, max-age=86400"
}

cache @matchdefault {
    ttl 5s
    badger {
        path /tmp/badger/default-match
        configuration {
            ValueDir default
            ValueLogFileSize 16777216
            MemTableSize 4194304
            ValueThreshold 524288
            BypassLockGuard true
        }
    }
}

route /no-method-and-domain.css {
    cache {
        cache_keys {
            .*\.css {
                disable_host
                disable_method
            }
        }
    }
    respond "Hello without storing method and domain cache key"
}

cache @souin-api {}

Chi middleware

To use Souin as chi middleware, you can refer to the Chi middleware integration folder to discover how to configure it.
You just have to define a new chi router and tell to the instance to use the Handle method like below:

import (
    "net/http"

    cache "github.com/darkweak/souin/plugins/chi"
    "github.com/go-chi/chi/v5"
)

func main(){

    // ...
    router := chi.NewRouter()
    httpcache := cache.NewHTTPCache(cache.DevDefaultConfiguration)
    router.Use(httpcache.Handle)
    router.Get("/*", defaultHandler)
    // ...

}

Dotweb middleware

To use Souin as dotweb middleware, you can refer to the Dotweb plugin integration folder to discover how to configure it.
You just have to define a new dotweb router and tell to the instance to use the process method like below:

import (
    cache "github.com/darkweak/souin/plugins/dotweb"
    "github.com/go-dotweb/dotweb/v5"
)

func main(){

    // ...
    httpcache := cache.NewHTTPCache(cache.DevDefaultConfiguration)
    app.HttpServer.GET("/:p", func(ctx dotweb.Context) error {
        return ctx.WriteString("Hello, World 👋!")
    }).Use(httpcache)
    // ...

}

Echo middleware

To use Souin as echo middleware, you can refer to the Echo plugin integration folder to discover how to configure it.
You just have to define a new echo router and tell to the instance to use the process method like below:

import (
    "net/http"

    souin_echo "github.com/darkweak/souin/plugins/echo"
    "github.com/labstack/echo/v4"
)

func main(){

    // ...
    e := echo.New()
    s := souin_echo.New(souin_echo.DefaultConfiguration)
    e.Use(s.Process)
    // ...

}

Fiber middleware

To use Souin as fiber middleware, you can refer to the Fiber plugin integration folder to discover how to configure it.
You just have to define a new fiber router and tell to the instance to use the process method like below:

import (
    cache "github.com/darkweak/souin/plugins/fiber"
    "github.com/gofiber/fiber/v2"
)

func main(){

    // ...
    httpcache := cache.NewHTTPCache(cache.DevDefaultConfiguration)
    app.Use(httpcache.Handle)
    // ...

}

Gin middleware

To use Souin as gin middleware, you can refer to the Gin plugin integration folder to discover how to configure it.
You just have to define a new gin router and tell to the instance to use the process method like below:

import (
    "net/http"

    souin_gin "github.com/darkweak/souin/plugins/gin"
    "github.com/gin-gonic/gin"
)

func main(){

    // ...
    r := gin.New()
    s := souin_gin.New(souin_gin.DefaultConfiguration)
    r.Use(s.Process())
    // ...

}

Go-zero middleware

To use Souin as go-zero middleware, you can refer to the Go-zero plugin integration folder to discover how to configure it.
You just have to give a Condfiguration object to the NewHTTPCache method to get a new HTTP cache instance and use the Handle method as a GlobalMiddleware:

import (
    "net/http"

    cache "github.com/darkweak/souin/plugins/go-zero"
)

func main(){

    // ...
    httpcache := cache.NewHTTPCache(cache.DevDefaultConfiguration)
    server.Use(httpcache.Handle)
    // ...

}

Goyave middleware

To use Souin as goyave middleware, you can refer to the Goyave plugin integration folder to discover how to configure it.
You just have to start Goyave, define a new goyave router and tell to the router instance to use the Handle method as GlobalMiddleware like below:

import (
    "net/http"

    cache "github.com/darkweak/souin/plugins/goyave"
    "goyave.dev/goyave/v4"
)

func main() {
    // ...
    goyave.Start(func(r *goyave.Router) {
        r.GlobalMiddleware(cache.NewHTTPCache(cache.DevDefaultConfiguration).Handle)
        // ...
    })
}

Kratos filter

To use Souin as Kratos filter, you can refer to the Kratos plugin integration folder to discover how to configure it.
You just have to start the Kratos HTTP server with the Souin filter like below:

import (
    httpcache "github.com/darkweak/souin/plugins/kratos"
    kratos_http "github.com/go-kratos/kratos/v2/transport/http"
)

func main() {
    kratos_http.NewServer(
        kratos_http.Filter(
            httpcache.NewHTTPCacheFilter(httpcache.DevDefaultConfiguration),
        ),
    )
}

You can also use the configuration file to configuration the HTTP cache. Refer to the code block below:

server: #...
data: #...
# HTTP cache part
httpcache:
  api:
    souin: {}
  default_cache:
    regex:
      exclude: /excluded
    ttl: 5s
  log_level: debug

After that you have to edit your server instanciation to use the HTTP cache configuration parser

import (
    httpcache "github.com/darkweak/souin/plugins/kratos"
    kratos_http "github.com/go-kratos/kratos/v2/transport/http"
)

func main() {
  c := config.New(
        config.WithSource(file.NewSource("examples/configuration.yml")),
        config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
            return yaml.Unmarshal(kv.Value, v)
        }),
    )
    if err := c.Load(); err != nil {
        panic(err)
    }

    server := kratos_http.NewServer(
        kratos_http.Filter(
            httpcache.NewHTTPCacheFilter(httpcache.ParseConfiguration(c)),
        ),
    )
  // ...
}

Roadrunner middleware

To use Souin as Roadrunner middleware, you can refer to the Roadrunner plugin integration folder to discover how to configure it.
Ysou have to build your rr binary with the souin dependency.

[velox]
build_args = ['-trimpath', '-ldflags', '-s -X github.com/roadrunner-server/roadrunner/v2/internal/meta.version=${VERSION} -X github.com/roadrunner-server/roadrunner/v2/internal/meta.buildTime=${TIME}']

[roadrunner]
ref = "master"

[github]
    [github.token]
    token = "GH_TOKEN"

    [github.plugins]
    logger = { ref = "master", owner = "roadrunner-server", repository = "logger" }
    cache = { ref = "master", owner = "darkweak", repository = "souin/plugins/roadrunner" }
    # others ...

[log]
level = "debug"
mode = "development"

After that, you'll be able to set each Souin configuration key under the http.cache key.

# .rr.yaml
http:
  # Other http sub keys
  cache:
    api:
      basepath: /httpcache_api
      prometheus:
        basepath: /anything-for-prometheus-metrics
      souin: {}
    default_cache:
      allowed_http_verbs:
        - GET
        - POST
        - HEAD
      cdn:
        api_key: XXXX
        dynamic: true
        hostname: XXXX
        network: XXXX
        provider: fastly
        strategy: soft
      headers:
        - Authorization
      regex:
        exclude: '/excluded'
      timeout:
        backend: 5s
        cache: 1ms
      ttl: 5s
      stale: 10s
    log_level: debug
    ykeys:
      The_First_Test:
        headers:
          Content-Type: '.+'
      The_Second_Test:
        url: 'the/second/.+'
    surrogate_keys:
      The_First_Test:
        headers:
          Content-Type: '.+'
      The_Second_Test:
        url: 'the/second/.+'
  middleware:
    - cache
    # Other middlewares

Skipper filter

To use Souin as skipper filter, you can refer to the Skipper plugin integration folder to discover how to configure it.
You just have to add to your Skipper instance the Souin filter like below:

package main

import (
    souin_skipper "github.com/darkweak/souin/plugins/skipper"
    "github.com/zalando/skipper"
    "github.com/zalando/skipper/filters"
)

func main() {
    skipper.Run(skipper.Options{
        Address:       ":9090",
        RoutesFile:    "example.yaml",
        CustomFilters: []filters.Spec{souin_skipper.NewSouinFilter()}},
    )
}

After that you will be able to declare the httpcache filter in your eskip file.

hello: Path("/hello") 
  -> httpcache(`{"api":{"basepath":"/souin-api","security":{"secret":"your_secret_key","enable":true,"users":[{"username":"user1","password":"test"}]},"souin":{"security":true,"enable":true}},"default_cache":{"headers":["Authorization"],"regex":{"exclude":"ARegexHere"},"ttl":"10s","stale":"10s"},"log_level":"INFO"}`)
  -> "https://www.example.org"

Træfik plugin

To use Souin as Træfik plugin, you can refer to the pilot documentation and the Træfik plugin integration folder to discover how to configure it.
You have to declare the experimental block in your traefik static configuration file. Keep in mind Træfik run their own interpreter and they often break any dependances (such as the yaml.v3 support).

# anywhere/traefik.yml
experimental:
  plugins:
    souin:
      moduleName: github.com/darkweak/souin
      version: v1.6.18

After that you can declare either the whole configuration at once in the middleware block or by service. See the examples below.

# anywhere/dynamic-configuration
http:
  routers:
    whoami:
      middlewares:
        - http-cache
      service: whoami
      rule: Host(`domain.com`)
  middlewares:
    http-cache:
      plugin:
        souin:
          api:
            prometheus: {}
            souin: {}
          default_cache:
            headers:
              - Authorization
              - Content-Type
            regex:
              exclude: '/test_exclude.*'
            ttl: 5s
            default_cache_control: no-store
          log_level: debug
          urls:
            'domain.com/testing':
              ttl: 5s
              headers:
                - Authorization
            'mysubdomain.domain.com':
              ttl: 50s
              headers:
                - Authorization
                - 'Content-Type'
              default_cache_control: public, max-age=86400
          ykeys:
            The_First_Test:
              headers:
                Content-Type: '.+'
            The_Second_Test:
              url: 'the/second/.+'
            The_Third_Test:
            The_Fourth_Test:
          surrogate_keys:
            The_First_Test:
              headers:
                Content-Type: '.+'
            The_Second_Test:
              url: 'the/second/.+'
            The_Third_Test:
            The_Fourth_Test:
# anywhere/docker-compose.yml
services:
#...
  whoami:
    image: traefik/whoami
    labels:
      # other labels...
      - traefik.http.routers.whoami.middlewares=http-cache
      - traefik.http.middlewares.http-cache.plugin.souin.api.souin
      - traefik.http.middlewares.http-cache.plugin.souin.default_cache.headers=Authorization,Content-Type
      - traefik.http.middlewares.http-cache.plugin.souin.default_cache.ttl=10s
      - traefik.http.middlewares.http-cache.plugin.souin.log_level=debug

Tyk plugin

To use Souin as a Tyk plugin, you can refer to the Tyk plugin integration folder to discover how to configure it.
You have to define the use of Souin as post and response custom middleware. You can compile your own Souin integration using the Makefile and the docker-compose inside the tyk integration directory and place your generated souin-plugin.so file inside your middleware directory.

{
  "name":"httpbin.org",
  "api_id":"3",
  "org_id":"3",
  "use_keyless": true,
  "version_data": {
    "not_versioned": true,
    "versions": {
      "Default": {
        "name": "Default",
        "use_extended_paths": true
      }
    }
  },
  "custom_middleware": {
    "pre": [],
    "post": [
      {
        "name": "SouinRequestHandler",
        "path": "/opt/tyk-gateway/middleware/souin-plugin.so"
      }
    ],
    "post_key_auth": [],
    "auth_check": {
      "name": "",
      "path": "",
      "require_session": false
    },
    "response": [
      {
        "name": "SouinResponseHandler",
        "path": "/opt/tyk-gateway/middleware/souin-plugin.so"
      }
    ],
    "driver": "goplugin",
    "id_extractor": {
      "extract_from": "",
      "extract_with": "",
      "extractor_config": {}
    }
  },
  "proxy":{
    "listen_path":"/httpbin/",
    "target_url":"http://httpbin.org/",
    "strip_listen_path":true
  },
  "active":true,
  "config_data": {
    "httpcache": {
      "api": {
        "souin": {
          "enable": true
        }
      },
      "cdn": {
        "api_key": "XXXX",
        "provider": "fastly",
        "strategy": "soft"
      },
      "default_cache": {
        "ttl": "5s"
      }
    }
  }
}

Webgo middleware

To use Souin as webgo middleware, you can refer to the Webgo middleware integration folder to discover how to configure it.
You just have to define a new webgo router and tell to the instance to use the process method like below:

import (
    "net/http"

    "github.com/bnkamalesh/webgo/v6"
    cache "github.com/darkweak/souin/plugins/webgo"
)

func main(){

    // ...
    httpcache := cache.NewHTTPCache(cache.DevDefaultConfiguration)
    router.Use(httpcache.Middleware)
    // ...

}

Prestashop plugin

A repository called prestashop-souin has been started by lucmichalski. You can manage your Souin instance through the admin panel UI.

Wordpress plugin

A repository called wordpress-souin to be able to manage your Souin instance through the admin panel UI.

Credits

Thanks to these users for contributing or helping this project in any way

Author: Darkweak
Source Code: https://github.com/darkweak/Souin 
License: MIT license

#go #golang #middleware #cache 

Souin: An HTTP Cache System, RFC Compliant, Compatible
Rupert  Beatty

Rupert Beatty

1659144120

Laravel-cors: Adds CORS Headers Support in Your Laravel Application

CORS Middleware for Laravel

About

The laravel-cors package allows you to send Cross-Origin Resource Sharing headers with Laravel middleware configuration.

If you want to have a global overview of CORS workflow, you can browse this image.

Upgrading from 0.x (barryvdh/laravel-cors)

When upgrading from 0.x versions, there are some breaking changes:

  • A new 'paths' property is used to enable/disable CORS on certain routes. This is empty by default, so fill it correctly!
  • Group middleware is no longer supported, use the global middleware
  • The vendor name has changed (see installation/usage)
  • The casing on the props in cors.php has changed from camelCase to snake_case, so if you already have a cors.php file you will need to update the props in there to match the new casing.

Features

  • Handles CORS pre-flight OPTIONS requests
  • Adds CORS headers to your responses
  • Match routes to only add CORS to certain Requests

Installation

Require the fruitcake/laravel-cors package in your composer.json and update your dependencies:

composer require fruitcake/laravel-cors

If you get a conflict, this could be because an older version of barryvdh/laravel-cors or fruitcake/laravel-cors is installed. Remove the conflicting package first, then try install again:

composer remove barryvdh/laravel-cors fruitcake/laravel-cors
composer require fruitcake/laravel-cors

Global usage

To allow CORS for all your routes, add the HandleCors middleware at the top of the $middleware property of app/Http/Kernel.php class:

protected $middleware = [
  \Fruitcake\Cors\HandleCors::class,
    // ...
];

Now update the config to define the paths you want to run the CORS service on, (see Configuration below):

'paths' => ['api/*'],

Configuration

The defaults are set in config/cors.php. Publish the config to copy the file to your own config:

php artisan vendor:publish --tag="cors"

Note: When using custom headers, like X-Auth-Token or X-Requested-With, you must set the allowed_headers to include those headers. You can also set it to ['*'] to allow all custom headers.

Note: If you are explicitly whitelisting headers, you must include Origin or requests will fail to be recognized as CORS.

Options

OptionDescriptionDefault value
pathsYou can enable CORS for 1 or multiple paths, eg. ['api/*'][]
allowed_originsMatches the request origin. Wildcards can be used, eg. *.mydomain.com or mydomain.com:*['*']
allowed_origins_patternsMatches the request origin with preg_match.[]
allowed_methodsMatches the request method.['*']
allowed_headersSets the Access-Control-Allow-Headers response header.['*']
exposed_headersSets the Access-Control-Expose-Headers response header.false
max_ageSets the Access-Control-Max-Age response header.0
supports_credentialsSets the Access-Control-Allow-Credentials header.false

allowed_origins, allowed_headers and allowed_methods can be set to ['*'] to accept any value.

Note: For allowed_origins you must include the scheme when not using a wildcard, eg. ['http://example.com', 'https://example.com']. You must also take into account that the scheme will be present when using allowed_origins_patterns.

Note: Try to be a specific as possible. You can start developing with loose constraints, but it's better to be as strict as possible!

Note: Because of http method overriding in Laravel, allowing POST methods will also enable the API users to perform PUT and DELETE requests as well.

Note: Sometimes it's necessary to specify the port (when you're coding your app in a local environment for example). You can specify the port or using a wildcard here too, eg. localhost:3000, localhost:* or even using a FQDN app.mydomain.com:8080

Lumen

On Lumen, just register the ServiceProvider manually in your bootstrap/app.php file:

$app->register(Fruitcake\Cors\CorsServiceProvider::class);

Also copy the cors.php config file to config/cors.php and put it into action:

$app->configure('cors');

Global usage for Lumen

To allow CORS for all your routes, add the HandleCors middleware to the global middleware and set the paths property in the config.

$app->middleware([
    // ...
    Fruitcake\Cors\HandleCors::class,
]);

Common problems

Wrong config

Make sure the path option in the config is correct and actually matches the route you are using. Remember to clear the config cache as well.

Error handling, Middleware order

Sometimes errors/middleware that return own responses can prevent the CORS Middleware from being run. Try changing the order of the Middleware and make sure it's the first entry in the global middleware, not a route group. Also check your logs for actual errors, because without CORS, the errors will be swallowed by the browser, only showing CORS errors. Also try running it without CORS to make sure it actually works.

Authorization headers / Credentials

If your Request includes an Authorization header or uses Credentials mode, set the supports_credentials value in the config to true. This will set the Access-Control-Allow-Credentials Header to true.

Echo/die

If you echo(), dd(), die(), exit(), dump() etc in your code, you will break the Middleware flow. When output is sent before headers, CORS cannot be added. When the scripts exits before the CORS middleware finished, CORS headers will not be added. Always return a proper response or throw an Exception.

Disabling CSRF protection for your API

If possible, use a route group with CSRF protection disabled. Otherwise you can disable CSRF for certain requests in App\Http\Middleware\VerifyCsrfToken:

protected $except = [
    'api/*',
    'sub.domain.zone' => [
      'prefix/*'
    ],
];

Duplicate headers

The CORS Middleware should be the only place you add these headers. If you also add headers in .htaccess, nginx or your index.php file, you will get duplicate headers and unexpected results.

Implements https://github.com/fruitcake/php-cors for Laravel

Since Laravel 9.2, this Middleware is included in laravel/framework. You can use the provided middleware, which should be compatible with the Middleware and config provided in this package. See https://github.com/laravel/laravel/pull/5825/files for the changes.

Author: fruitcake
Source Code: https://github.com/fruitcake/laravel-cors 
License: MIT license

#laravel #cors #php #middleware 

Laravel-cors: Adds CORS Headers Support in Your Laravel Application
Rupert  Beatty

Rupert Beatty

1658057520

Defender: Roles & Permissions for Laravel

Defender


Defender is an Access Control List (ACL) Solution for Laravel 5 / 6 / 7 / 8 / 9 (single auth). (Not compatible with multi-auth)
With security and usability in mind, this project aims to provide you a safe way to control your application access without losing the fun of coding.  

Contribution welcome

Defender is looking for maintainers and contributors.

Installation

1. Dependency

Using composer, execute the following command to automatically update your composer.json, using the corresponding package version:

Version ConstraintPackage Version
>= 5.0.* && <= 5.3.*0.6.*
~5.4, ~5.50.7.*
>= 5.6.*0.8.*
^6.00.9.*
^7.00.10.*
^8.00.11.*
^9.00.12.*
composer require artesaos/defender

or manually update your composer.json file

{
    "require": {
        "artesaos/defender": "~0.10.0"
    }
}

2. Provider

If you are using Laravel >= 5.5 skip this section since our package support auto-discovery.

You need to update your application configuration in order to register the package, so it can be loaded by Laravel. Just update your config/app.php file adding the following code at the end of your 'providers' section:

// file START ommited
    'providers' => [
        // other providers ommited
        \Artesaos\Defender\Providers\DefenderServiceProvider::class,
    ],
// file END ommited

3. User Class

On your User class, add the trait Artesaos\Defender\Traits\HasDefender to enable the creation of permissions and roles:

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Artesaos\Defender\Traits\HasDefender;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
    use Authenticatable, CanResetPassword, HasDefender;
...

If you are using laravel 5.2+, there is a small difference:

<?php

namespace App;

use Artesaos\Defender\Traits\HasDefender;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasDefender;
...

4. Publishing configuration file and migrations

To publish the default configuration file and database migrations, execute the following command:

php artisan vendor:publish

Execute the migrations, so that the tables on you database are created:

php artisan migrate

You can also publish only the configuration file or the migrations:

php artisan vendor:publish --tag=config

Or

php artisan vendor:publish --tag=migrations

If you already published defender files, but for some reason you want to override previous published files, add the --force flag.

5. Facade (optional)

In order to use the Defender facade, you need to register it on the config/app.php file, you can do that the following way:

// config.php file
// file START ommited
    'aliases' => [
        // other Facades ommited
        'Defender' => \Artesaos\Defender\Facades\Defender::class,
    ],
// file END ommited

6. Defender Middlewares (optional)

If you have to control the access Defender provides middlewares to protect your routes. If you have to control the access through the Laravel routes, Defender has some built-in middlewares for the trivial tasks. To use them, just put it in your app/Http/Kernel.php file.

protected $routeMiddleware = [
    'auth'            => \App\Http\Middleware\Authenticate::class,
    'auth.basic'      => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'guest'           => \App\Http\Middleware\RedirectIfAuthenticated::class,

    // Access control using permissions
    'needsPermission' => \Artesaos\Defender\Middlewares\NeedsPermissionMiddleware::class,

    // Simpler access control, uses only the groups
    'needsRole' => \Artesaos\Defender\Middlewares\NeedsRoleMiddleware::class
];

You'll see how to use the middlewares below.

6.1 - Create your own middleware

If the built-in middlewares doesn't fit your needs, you can make your own by using Defender's API to control the access.

Usage

Defender handles only access control. The authentication is still made by Laravel's Auth.

Note: If you are using a different model for your users or has changed the namespace, please update the user_model key on your defender config file

Creating roles and permissions

With commands

You can use these commands to create the roles and permissions for you application.

php artisan defender:make:role admin  # creates the role admin
php artisan defender:make:role admin --user=1 # creates the role admin and attaches this role to the user where id=1
php artisan defender:make:permission users.index "List all the users" # creates the permission
php artisan defender:make:permission users.create "Create user" --user=1 # creates the permission and attaches it to user where id=1
php artisan defender:make:permission users.destroy "Delete user" --role=admin # creates the permission and attaches it to the role admin

With the seeder or artisan tinker

You can also use the Defender's API. You can create a Laravel Seeder or use php artisan tinker.


use App\User;

$roleAdmin = Defender::createRole('admin');

// The first parameter is the permission name
// The second is the "friendly" version of the name. (usually for you to show it in your application).
$permission =  Defender::createPermission('user.create', 'Create Users');

// You can assign permission directly to a user.
$user = User::find(1);
$user->attachPermission($permission);

// or you can add the user to a group and that group has the power to rule create users.
$roleAdmin->attachPermission($permission);

// Now this user is in the Administrators group.
$user->attachRole($roleAdmin);

Using the middleware

To protect your routes, you can use the built-in middlewares.

Defender requires Laravel's Auth, so, use the auth middleware before the Defender's middleware that you intend to use.

Checking Permissions: needsPermissionMiddleware

Route::get('foo', ['middleware' => ['auth', 'needsPermission'], 'shield' => 'user.create', function()
{
    return 'Yes I can!';
}]);

If you're using Laravel 5.1+ it's possible to use Middleware Parameters.

Route::get('foo', ['middleware' => ['auth', 'needsPermission:user.index'], function() {
    return 'Yes I can!';
}]);

With this syntax it's also possible to use the middleware within your controllers.

$this->middleware('needsPermission:user.index');

You can pass an array of permissions to check on.

Route::get('foo', ['middleware' => ['auth', 'needsPermission'], 'shield' => ['user.index', 'user.create'], function()
{
    return 'Yes I can!';
}]);

When using middleware parameters, use a | to separate multiple permissions.

Route::get('foo', ['middleware' => ['auth', 'needsPermission:user.index|user.create'], function() {
    return 'Yes I can!';
}]);

Or within controllers:

$this->middleware('needsPermission:user.index|user.create');

When you pass an array of permissions, the route will be fired only if the user has all the permissions. However, if you want to allow the access to the route when the user has at least one of the permissions, just add 'any' => true.

Route::get('foo', ['middleware' => ['auth', 'needsPermission'], 'shield' => ['user.index', 'user.create'], 'any' => true, function()
{
    return 'Yes I can!';
}]);

Or, with middleware parameters, pass it as the 2nd parameter

Route::get('foo', ['middleware' => ['auth', 'needsPermission:user.index|user.create,true'], function() {
    return 'Yes I can!';
}]);

Or within controllers:

$this->middleware('needsPermission:user.index|user.create,true');

Checking Roles: needsRoleMiddleware

This is similar to the previous middleware, but only the roles are checked, it means that it doesn't check the permissions.

Route::get('foo', ['middleware' => ['auth', 'needsRole'], 'is' => 'admin', function()
{
    return 'Yes I am!';
}]);

If you're using Laravel 5.1 it's possible to use Middleware Parameters.

Route::get('foo', ['middleware' => ['auth', 'needsRole:admin'], function() {
    return 'Yes I am!';
}]);

With this syntax it's also possible to use the middleware within your controllers.

$this->middleware('needsRole:admin');

You can pass an array of permissions to check on.

Route::get('foo', ['middleware' => ['auth', 'needsRole'], 'shield' => ['admin', 'member'], function()
{
    return 'Yes I am!';
}]);

When using middleware parameters, use a | to separate multiple roles.

Route::get('foo', ['middleware' => ['auth', 'needsRole:admin|editor'], function() {
    return 'Yes I am!';
}]);

Or within controllers:

$this->middleware('needsRole:admin|editor');

When you pass an array of permissions, the route will be fired only if the user has all the permissions. However, if you want to allow the access to the route when the user has at least one of the permissions, just add 'any' => true.

Route::get('foo', ['middleware' => ['auth', 'needsRole'], 'is' => ['admin', 'member'], 'any' => true, function()
{
    return 'Yes I am!';
}]);

Or, with middleware parameters, pass it as the 2nd parameter

Route::get('foo', ['middleware' => ['auth', 'needsRole:admin|editor,true'], function() {
    return 'Yes I am!';
}]);

Or within controllers:

$this->middleware('needsRole:admin|editor,true');

Using in Views

Laravel's Blade extension for using Defender.

@shield

@shield('user.index')
    shows your protected stuff
@endshield
@shield('user.index')
    shows your protected stuff
@else
    shows the data for those who doesn't have the user.index permission
@endshield

You can also use wildcard(*)

@shield('user.*')
    shows your protected stuff
@else
    shows the data for those who doesn't have the any permission with 'user' prefix
@endshield

@is

@is('admin')
    Shows data for the logged user and that belongs to the admin role
@endis
@is('admin')
    Shows data for the logged user and that belongs to the admin role
@else
    shows the data for those who doesn't have the admin permission
@endis
@is(['role1', 'role2'])
    Shows data for the logged user and that belongs to the admin role
@else
    shows the data for those who doesn't have the admin permission
@endis

Using javascript helper

The stand provides helper for when you need to interact with the user permissions on the front-end.

echo Defender::javascript()->render();
// or
echo app('defender')->javascript()->render();
// or
echo app('defender.javascript')->render();

This helper injects a javascript code with all permissions and roles of the current user.


Using the Facade

With the Defender's Facade you can access the API and use it at any part of your application.


Defender::hasPermission($permission):

Check if the logged user has the $permission.


Defender::canDo($permission):

Check if the logged user has the $permission. If the role superuser returns true


Defender::roleHasPermission($permission):

Check if the logged user has the $permission checking only the role permissions.


Defender::hasRole($roleName):

Check if the logged user belongs to the role $roleName.


Defender::roleExists($roleName):

Check if the role $roleName exists in the database.


Defender::permissionExists($permissionName):

Check if the permission $permissionName exists in the database.


Defender::findRole($roleName):

Find the role in the database by the name $roleName.


Defender::findRoleById($roleId):

Find the role in the database by the role ID roleId.


Defender::findPermission($permissionName):

Find the permission in the database by the name $permissionName.


Defender::findPermissionById($permissionId):

Find the permission in the database by the ID $permissionId.


Defender::createRole($roleName):

Create a new role in the database.


Defender::createPermission($permissionName):

Create a new permission in the database.

Defender::is($roleName):

Check whether the current user belongs to the role.

Defender::javascript()->render():

Returns a javascript script with a list of all roles and permissions of the current user. The variable name can be modified.


Using the trait

To add the Defender's features, you need to add the trait HasDefender in you User model (usually App\User).

<?php namespace App;

// Declaration of other omitted namespaces
use Artesaos\Defender\Traits\HasDefender;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract {

    use Authenticatable, CanResetPassword, HasDefender;

    // Rest of the class
}

This trait, beyond configuring the relationships, will add the following methods to your object App\User:

public function hasPermission($permission):

This method checks if the logged user has the permission $permission

In Defender, there are 2 kind of permissions: User permissions and Role permissions. By default, the permissions that the user inherits, are permissions of the roles that it belongs to. However, always that a user pemission is set, it will take precedence of role permission.

public function foo(Authenticable $user)
{
    if ($user->hasPermission('user.create'));
}

public function roleHasPermission($permission):

This method works the same way the previous one, the only diference is that the user permissions are not considered, however, only the role's permissions that the user belongs are used to check the access.

public function foo(Authenticable $user)
{
    if ($user->roleHasPermission('user.create');
}

public function attachRole($role):

Attach the user to the role $role. The $role variable might be an object of the type Artesaos\Defender\Role or an array containing the ids of the roles.

public function foo(Authenticable $user)
{
    $role = Defender::findRole('admin'); // Returns an Artesao\Defender\Role
    $user->attachRole($role);

    // or

    $roles = [1, 2, 3]; // Using an array of ids
    $user->attachRole($roles);
}

public function detachRole($role):

Detach the role $role from the user (inverse to attachRole()).

public function foo(Authenticable $user)
{
    $role = Defender::findRole('admin'); // Returns an Artesao\Defender\Role
    $user->detachRole($role);

    // ou

    $roles = [1, 2, 3]; // Using an array of ids
    $user->detachRole($roles);
}

public function syncRoles(array $roles = array()):

This is like the attachRole() method, but only the roles in the array $roles will be on the relationship after the method runs. $roles is an array of ids for the needed roles.

public function foo(Authenticable $user)
{
    $roles = [1, 2, 3]; // Using an array of ids

    $user->syncRoles($roles);
}

public function attachPermission($permission, array $options = array()):

Attach the user to the permission $permission. The $permission variable is an instance of the Artesaos\Defender\Permission class.

public function foo(Authenticable $user)
{
    $permission = Defender::findPermission('user.create');

    $user->attachPermission($permission, [
        'value' => true // true = has the permission, false = doesn't have the permission,
    ]);
}

public function detachPermission($permission):

Remove the permission $permission from the user. The $permission variable might be an instance of the Artesaos\Defender\Permission class or an array of ids with the ids of the permissions to be removed.

public function foo(Authenticable $user)
{
    $permission = Defender::findPermission('user.create');
    $user->detachPermission($permission);

    // or

    $permissions = [1, 3];
    $user->detachPermission($permissions);
}

public function syncPermissions(array $permissions):

This is like the method syncRoles, but only the roles in the array $permissions be on the relationship after the method runs.

public function foo(Authenticable $user)
{
    $permissions = [
        1 => ['value' => false],
        2 => ['value' => true,
        3 => ['value' => true]
    ];

    $user->syncPermissions($permissions);
}

public function revokePermissions():

Remove all the user permissions.

public function foo(Authenticable $user)
{
    $user->revokePermissions();
}

public function revokeExpiredPermissions():

Remove all the temporary expired pemissions from the user. More about temporary permissions below.

public function foo(Authenticable $user)
{
    $user->revokeExpiredPermissions();
}

Temporary permissions

One of Defender's coolest features is to add temporary permissions to a group or an user.

For example

The user John belongs to the role 'admins', however I want to temporaly remove the John's permission to create new users

In this case we need to attach an permission with the value equal to false, explicitly prohibiting the user to perform that action. You must add this permission, with the false value, since by default, the user permissions are inherited of the permissions of their roles. When you assign a user permission, this will always take precedence.

For instance. Below we revoke the permission user.create for the user during 7 days.

public function foo()
{
    $userX = App\User::find(3);
    $permission = Defender::findPermission('user.create');


    $userX->attachPermission($permission, [
        'value' => false, // false means that he will not have the permission,
        'expires' => \Carbon\Carbon::now()->addDays(7) // Set the permission's expiration date
    ]);

}

After 7 days, the user will take the permission again.


Allow that a user can perform some action by a period of time.

To allow that a user have temporary access to perform a given action, just set the expires key. The value key will be true by default.

public function foo()
{
    $user = App\User::find(1);
    $permission = Defender::findPermission('user.create');

    $user->attachPermission($permission, [
        'expires' => \Carbon\Carbon::now()->addDays(7)
    ];
}

It's also possible to extend an existing temporary: Just use the $user->extendPermission($permissionName, array $options) method.

Using custom Role and Permission models

To use your own classes for Role and Permission models, first set the role_model and permission_model keys at defender.php config.

Following are two examples of how Role and Permission models must be implemented for MongoDB using jenssegers/laravel-mongodb driver:

    <?php
    
    // Role model
    
    namespace App;
    
    use Jenssegers\Mongodb\Eloquent\Model;
    use Artesaos\Defender\Traits\Models\Role;
    use Artesaos\Defender\Contracts\Role as RoleInterface;
    
    /**
     * Class Role.
     */
    class Role extends Model implements RoleInterface {
        use Role;
    }
    <?php
    
    // Permission model
    
    namespace App;
    
    use Jenssegers\Mongodb\Eloquent\Model;
    use Artesaos\Defender\Traits\Models\Permission;
    use Artesaos\Defender\Contracts\Permission as PermissionInterface;
    
    /**
     * Class Permission.
     */
    class Permission extends Model implements PermissionInterface
    {
        use Permission;    
    }

You must use the correct traits and each class has to implemet the corresponding interface contract.

Author: Artesaos
Source Code: https://github.com/artesaos/defender 
License: MIT license

#laravel #php #middleware 

Defender: Roles & Permissions for Laravel

Throttle: (API) Rate Limiting Requests in CakePHP

Throttle

(API) Rate limiting requests in CakePHP

This plugin allows you to limit the number of requests a client can make to your app in a given time frame.

Installation

composer require muffin/throttle

To make your application load the plugin either run:

./bin/cake plugin load Muffin/Throttle

Configuration

In your config/app.php add a cache config named throttle under the Cache key with required config. For e.g.:

'throttle' => [
    'className' => 'Apcu',
    'prefix' => 'throttle_'
],

Using the Middleware

Add the middleware to the queue and pass your custom configuration:

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    // Various other middlewares for error handling, routing etc. added here.

    $throttleMiddleware = new \Muffin\Throttle\Middleware\ThrottleMiddleware([
        // Data used to generate response with HTTP code 429 when limit is exceeded.
        'response' => [
            'body' => 'Rate limit exceeded',
        ],
        // Time period as number of seconds
        'period' => 60,
        // Number of requests allowed within the above time period
        'limit' => 100,
        // Client identifier
        'identifier' => function ($request) {
            if (!empty($request->getHeaderLine('Authorization'))) {
                return str_replace('Bearer ', '', $request->getHeaderLine('Authorization'));
            }

            return $request->clientIp();
        }
    ]);

    $middlewareQueue->add($throttleMiddleware);

    return $middlewareQueue;
}

The above example would allow 100 requests/minute/token and would first try to identify the client by JWT Bearer token before falling back to (Throttle default) IP address based identification.

Events

The middleware also dispatches following event which effectively allows you to have multiple rate limits:

Throttle.beforeThrottle

This is the first event that is triggered before a request is processed by the middleware. All rate limiting process will be bypassed if this event is stopped.

\Cake\Event\EventManager::instance()->on(
    \Muffin\Throttle\Middleware\ThrottleMiddleware::EVENT_BEFORE_THROTTLE,
    function ($event, $request) {
        if (/* check for something here, most likely using $request */) {
            $event->stopPropogation();
        }
    }
);

Throttle.getIdentifier

Instead of using the indentifer config you can also setup a listener for the Throttle.getIdentifier event. The event's callback would receive a request instance as argument and must return an identifier string.

Throttle.getThrottleInfo

The Throttle.getThrottleInfo event allows you to customize the period and limit configs for a request as well as the cache key used to store the rate limiting info.

This allows you to set multiple rate limit as per your app's needs.

Here's an example:

\Cake\Event\EventManager::instance()->on(
    \Muffin\Throttle\Middleware\ThrottleMiddleware::EVENT_GET_THROTTLE_INFO,
    function ($event, $request, \Muffin\Throttle\ValueObject\ThrottleInfo $throttle) {
        // Set a different period for POST request.
        if ($request->is('POST')) {
            // This will change the cache key from default "{identifer}" to "{identifer}.post".
            $throttle->appendToKey('post');
            $throttle->setPeriod(30);
        }

        // Modify limit for logged in user
        $identity = $request->getAttribute('identity');
        if ($identity) {
            $throttle->appendToKey($identity->get('role'));
            $throttle->setLimit(200);
        }
    }
);

Throtttle.beforeCacheSet

The Throtttle.beforeCacheSet event allows you to observe result of middleware configuration and previous Throtttle.getIdentifier and Throttle.getThrottleInfo events results.

You can also use this event to modify cached $rateLimit and $ttl values, modifying $throttleInfo in this event has no effect.

Example:

\Cake\Event\EventManager::instance()->on(
    \Muffin\Throttle\Middleware\ThrottleMiddleware::EVENT_BEFORE_CACHE_SET,
    function ($event, \Muffin\Throttle\ValueObject\RateLimitInfo $rateLimit, int $ttl, \Muffin\Throttle\ValueObject\ThrottleInfo $throttleInfo) {
        \Cake\Log\Log::debug(sprintf("key(%s) remaining(%d) resetTimestamp(%d) ttl(%d)", $throttleInfo->getKey(), $rateLimit->getRemaining(), $rateLimit->getResetTimestamp(), $ttl));
    }
);

X-headers

By default Throttle will add X-headers with rate limiting information to all responses:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1438434161

To customize the header names simply pass (all of them) under headers key in your configuration array:

'headers' => [
    'limit' => 'X-MyRateLimit-Limit',
    'remaining' => 'X-MyRateLimit-Remaining',
    'reset' => 'X-MyRateLimit-Reset',
]

To disable the headers set headers key to false.

Customize response object

You may use type and headers subkeys of the response array (as you would do with a Response object) if you want to return a different message as the default one:

new \Muffin\Throttle\Middleware\ThrottleMiddleware([
    'response' => [
        'body' => json_encode(['error' => 'Rate limit exceeded']),
        'type' => 'json',
        'headers' => [
            'Custom-Header' => 'custom_value',
        ]
    ],
    'limit' => 300,
]);

Patches & Features

  • Fork
  • Mod, fix
  • Test - this is important, so it's not unintentionally broken
  • Commit - do not mess with license, todo, version, etc. (if you do change any, bump them into commits of their own that I can ignore when I pull)
  • Pull request - bonus point for topic branches

To ensure your PRs are considered for upstream, you MUST follow the CakePHP coding standards.

Bugs & Feedback

http://github.com/usemuffin/throttle/issues

Author: usemuffin
Source Code: https://github.com/usemuffin/throttle 
License: MIT license

#php #cakephp #middleware 

Throttle: (API) Rate Limiting Requests in CakePHP

Wrench: CakePHP 3 Maintenance Mode Plugin

Wrench : CakePHP 3 Maintenance mode plugin 

Wrench is a CakePHP 3.X plugin that aims to provide an easy way to implement a Maintenance Mode for your CakePHP website / applications.

Requirements

  • PHP >= 5.5.9
  • CakePHP >= 3.3.0

About the plugin versions

CakePHP < 3.3.0CakePHP >= 3.3.0CakePHP >= 3.5.0
Wrench 1.XWrench 2.XWrench 3.X
PHP >= 5.4.16PHP >= 5.5.9PHP >= 5.6.0
Uses CakePHP DispatcherFilter mecanismUses CakePHP Middleware Stack and PSR-7 Request / Response implementationUses CakePHP Middleware Stack and PSR-7 Request / Response implementation + no deprecation warning from CakePHP 3.6.X

Recommanded package

If you want to create your own maintenance mode, you can use the CakePHP 3 Bake plugin

Installation

You can install this plugin into your CakePHP application using composer.

The recommended way to install composer packages is:

composer require havokinspiration/wrench

Loading the plugin

You can load the plugin using the shell command:

bin/cake plugin load Wrench

Or you can manually add the loading statement in the boostrap.php file of your application:

Plugin::load('Wrench');

Usage

The plugin is built around a Middleware that will intercept the current request to return a customized response to warn the user that the website / app is undergoing maintenance.

To use the Maintenance mode, you need to add the MaintenanceMiddleware to the MiddlewareStack in your Application file by adding the following elements :

use Wrench\Middleware\MaintenanceMiddleware;

// ...

public function middleware($middleware)
{
    $middleware->add(new MaintenanceMiddleware());

    // Other middleware configuration

    return $middleware;
}

Since this Middleware is here to prevent the application from responding, it should be the first to be treated by the Dispatcher and should, as such, be configured as the first one, either by adding it in the beginning of the method with the push() method or using the prepend() method anywhere you want in your middlewares configuration.

By default, only adding it with the previous line will make use of the Redirect mode. More informations on maintenance Modes below.

The Middleware is only active when the Configure key Wrench.enable is equal to true. To enable the maintenance mode, use the following statement in your bootstrap.php file :

Configure::write('Wrench.enable', true);

Modes

The plugin is built around the concept of "modes". Modes are special classes which will have the task of processing the request and return the proper response in order to warn the user that the website / application is undergoing maintenance.

The plugin comes packaged with four maintenance modes : Redirect, Output, Callback and View.

You can configure it to use specific modes when adding the Middleware to the Middleware stack by passing parameters to the Middleware constructor. The will result in a call looking like this :

$middleware->add(new MaintenanceMiddleware([
    'mode' => [
        'className' => 'Full\Namespace\To\Mode',
        'config' => [
            // Specific configuration parameters for the Mode
        ]
    ]
]);

If you need it, you can directly pass an instance of a Mode to the mode array key of the filter's config:

$middleware->add(new MaintenanceMiddleware([
    'mode' => new \Wrench\Mode\Redirect([
        'url' => 'http://example.com/maintenance'
    ])
]);

IP Whitelisting

While you put your application under maintenance, you might want, as the project administrator or developer, to be able to access the application. You can do so using the IP whitelisting feature. When configuring the MaintenanceMiddleware, just pass an array of allowed IP addresses to the whitelist key in the Middleware configuration array. All those IP will be allowed to access the application, even if the maintenance mode is on:

$middleware->add(new MaintenanceMiddleware([
    'whitelist' => ['1.2.3.4', '5.6.7.8'],
]));

In the above example, clients connecting with the IP address 1.2.3.4 or 5.6.7.8 will be able to access the project, even if the maintenance mode is on.

Redirect Mode

The Redirect Mode is the default one. It will perform a redirect to a specific URL. The Redirect Mode accepts the following parameters :

  • url : The URL where the redirect should point to. Default to the app base path pointing to a maintenance.html page.
  • code : The HTTP status code of the redirect response. The code should be in the 3XX range, otherwise, it might get overwritten. Default to 307.
  • headers : Array of additional headers to pass along the redirect response. Default to empty.

You can customize all those parameters :

$middleware->add(new MaintenanceMiddleware([
    'mode' => [
        'className' => 'Wrench\Mode\Redirect',
        'config' => [
            'url' => 'http://example.com/maintenance',
            'code' => 303,
            'headers' => ['someHeader' => 'someValue']
        ]
    ]
]);

Output Mode

The Output Mode allows you to display the content of a static file as a response for the maintenance status. It accepts multiple parameters :

  • path : the absolute path to the file that will be served. Default to {ROOT}/maintenance.html.
  • code : The HTTP status code of the redirect response. Default to 503.
  • headers : Array of additional headers to pass along the redirect response. Default to empty.

You can customize all those parameters :

$middleware->add(new MaintenanceMiddleware([
    'mode' => [
        'className' => 'Wrench\Mode\Output',
        'config' => [
            'path' => '/path/to/my/file',
            'code' => 404,
            'headers' => ['someHeader' => 'someValue']
        ]
    ]
]);

Callback Mode

The Callback Mode gives you the ability to use a custom callable. It accepts only one parameter callback which should be a callable. The callable will take two arguments :

  • request : A \Psr\Http\Message\ServerRequestInterface instance
  • response : A \Psr\Http\Message\ResponseInterface instance

The callable is expected to return a \Psr\Http\Message\ResponseInterface if the request is to be stopped.

$middleware->add(new MaintenanceMiddleware([
    'mode' => [
        'className' => 'Wrench\Mode\Callback',
        'config' => [
            'callback' => function($request, $response) {
                $string = 'Some content from a callback';

                $stream = new Stream(fopen('php://memory', 'r+'));
                $stream->write($string);
                $response = $response->withBody($stream);
                $response = $response->withStatus(503);
                $response = $response->withHeader('someHeader', 'someValue');
                return $response;
            }
        ]
    ]
]);

View Mode

The View Mode gives you the ability to use a View to render the maintenance page. This gives you the ability to leverage helpers and the layout / template system of the framework. It accepts multiple parameters :

  • code : The HTTP status code of the redirect response. Default to 503.
  • headers : Array of additional headers to pass along the redirect response. Default to empty.
  • view : Array of parameters to pass to the View class constructor. Only the following options are supported :
    • className : Fully qualified class name of the View class to use. Default to AppView
    • templatePath : Path to the template you wish to display (relative to your src/Template directory). You can use plugin dot notation.
    • template : Template name to use. Default to "maintenance".
    • plugin : Theme where to find the layout and template
    • theme : Same thing than plugin
    • layout : Layout name to use. Default to "default"
    • layoutPath : Path to the layout you wish to display (relative to your src/Template directory). You can use plugin dot notation. Default to "Layout"
// Will load a template ``src/Template/Maintenance/maintenance.ctp``
// in a layout ``src/Template/Layout/Maintenance/maintenance.ctp``
$middleware->add(new MaintenanceMiddleware([
    'mode' => [
        'className' => 'Wrench\Mode\View',
        'config' => [
            'view' => [
                 'templatePath' => 'Maintenance',
                 'layout' => 'maintenance',
                 'layoutPath' => 'Maintenance'
            ]
        ]
    ]
]);

Creating a custom mode

If you have special needs, you can create your own maintenance mode. To get started quickly, you can use the bake console tool to generate a skeleton:

bin/cake bake maintenance_mode MyCustomMode

This will generate a MyCustomMode class file under the App\Maintenance\Mode namespace (as well as a test file). Your skeleton will only contain one method process() returning a \Psr\Http\Message\ResponseInterface object. This is where the logic of your maintenance mode goes. You can either make the method return a ResponseInterface object which will shortcut the request cycle and use the returned ResponseInterface object to respond to the request. Any other returned value will make the maintenance mode no-op and the request cycle will go on. This is useful if you need to display the maintenance status only on specific conditions.

The Mode implements the InstanceConfigTrait which allows you to easily define default configuration parameters and gives you easy access to them.

Keep in mind that the ResponseInterface you need to return is PSR-7 compliant. You can get more details about the implementation and how to interact with it on the PHP-FIG website as well as on the CakePHP documentation

You can check out the implemented modes to have some examples.

Conditionally applying the maintenance mode

Conditionally applying a middleware is currently not possible with the current implementation of the Middleware stack in CakePHP 3.3. A documentation on how to do this will be added when and if this feature is implemented in the core.

Contributing

If you find a bug or would like to ask for a feature, please use the GitHub issue tracker. If you would like to submit a fix or a feature, please fork the repository and submit a pull request.

Coding standards

Since this plugin is tangled with features from the CakePHP Core and to provide consistency, it follows the CakePHP coding standards. When submitting a pull request, make sure your code follows these standards. You can check it by installing the code sniffer :

composer require cakephp/cakephp-codesniffer:dev-master

And then running the sniff :

./vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests

Author: HavokInspiration
Source Code: https://github.com/HavokInspiration/wrench 
License: View license

#php #cakephp #middleware 

Wrench: CakePHP 3 Maintenance Mode Plugin

CakeMiddlewares: This is A Collection Of Cakephp Middlewares

CakeMiddlewares

This is a collection of Cakephp Middlewares.

This plugin provides commonly used middlewares to make it easier to integrate into your cakephp project.

Requirements

Installation

composer require chrisshick/cakemiddlewares

To make your application load the plugin either run:

./bin/cake plugin load chrisShick/CakeMiddlewares

or add the following line to config/bootstrap.php:

Plugin::load('chrisShick/CakeMiddlewares');

Available Middlewares

Patches & Features

  • Fork
  • Mod, fix
  • Test - This is important, so it's not unintentionally broken
  • Commit - Please do not mess with license, todo, version, etc.
  • Pull request

To ensure your PRs are considered for upstream, you MUST follow the CakePHP coding standards.

Bugs & Feedback

http://github.com/chrisShick/CakeMiddlewares/issues

Credits

This repository was inspired by Oscarotero's PSR-7 Middelwares

The requirements and readme was inspired by UseMuffin's repositories.

Author: ChrisShick
Source Code: https://github.com/chrisShick/CakeMiddlewares 
License: MIT license

#php #cakephp #middleware 

CakeMiddlewares: This is A Collection Of Cakephp Middlewares

CakePHP Plugin for using Glide Image Manipulation Library

CakePHP Glide

CakePHP plugin to help using Glide image manipulation library.

The plugin consists of a middlware, view helper and a Glide response class.

Installation

Install the plugin through composer:

composer require admad/cakephp-glide

Load the plugin using CLI:

bin/cake plugin load ADmad/Glide

Usage

Middleware

In your config/routes.php setup the GlideMiddleware for required scope which intercepts requests and serves images generated by Glide. For e.g.:

$routes->scope('/images', function ($routes) {
    $routes->registerMiddleware('glide', new \ADmad\Glide\Middleware\GlideMiddleware([
        // Run this middleware only for URLs starting with specified string. Default null.
        // Setting this option is required only if you want to setup the middleware
        // in Application::middleware() instead of using router's scoped middleware.
        // It would normally be set to same value as that of server.base_url below.
        'path' => null,

        // Either a callable which returns an instance of League\Glide\Server
        // or config array to be used to create server instance.
        // http://glide.thephpleague.com/1.0/config/setup/
        'server' => [
            // Path or League\Flysystem adapter instance to read images from.
            // http://glide.thephpleague.com/1.0/config/source-and-cache/
            'source' => WWW_ROOT . 'uploads',

            // Path or League\Flysystem adapter instance to write cached images to.
            'cache' => WWW_ROOT . 'cache',

            // URL part to be omitted from source path. Defaults to "/images/"
            // http://glide.thephpleague.com/1.0/config/source-and-cache/#set-a-base-url
            'base_url' => '/images/',

            // Response class for serving images. If unset (default) an instance of
            // \ADmad\Glide\Response\PsrResponseFactory() will be used.
            // http://glide.thephpleague.com/1.0/config/responses/
            'response' => null,
        ],

        // http://glide.thephpleague.com/1.0/config/security/
        'security' => [
            // Boolean indicating whether secure URLs should be used to prevent URL
            // parameter manipulation. Default false.
            'secureUrls' => false,

            // Signing key used to generate / validate URLs if `secureUrls` is `true`.
            // If unset value of Cake\Utility\Security::salt() will be used.
            'signKey' => null,
        ],

        // Cache duration. Default '+1 days'.
        'cacheTime' => '+1 days',

        // Any response headers you may want to set. Default null.
        'headers' => [
            'X-Custom' => 'some-value',
        ],

        // Allowed query string params. If for e.g. you are only using glide presets
        // then you can set allowed params as `['p']` to prevent users from using
        // any other image manipulation params.
        'allowedParams' => null
    ]));

    $routes->applyMiddleware('glide');

    $routes->connect('/*');
});

For the example config shown above, for URL like domain.com/images/user/profile.jpg the source image should be under webroot/uploads/user/profile.jpg.

Note: Make sure the image URL does not directly map to image under webroot. Otherwise as per CakePHP's default URL rewriting rules the image will be served by webserver itself and request won't reach your CakePHP app.

Exceptions

If you have enabled secure URLs and a valid token is not present in query string the middleware will throw a ADmad\Glide\Exception\SignatureException exception.

If a response image could not be generated by the Glide library then by default the middleware will throw a ADmad\Glide\Exception\ResponseException. A Glide.response_failure event will also be triggered in this case. You can setup a listener for this event and return a response instance from it. In this case that response will be sent to client and ResponseException will not be thrown.

\Cake\Event\EventManager::instance()->on(
    \ADmad\Glide\Middleware\GlideMiddleware::RESPONSE_FAILURE_EVENT,
    function ($event) {
        return (new Response())
            ->withFile('/path/to/default-image.jpg');
    }
);

Helper

The provided GlideHelper helps creating URLs and image tags for generating images. You can load the helper in your AppView::initialize() as shown in example below:

public function initialize(): void
{
    // All option values should match the corresponding options for `GlideFilter`.
    $this->loadHelper('ADmad/Glide.Glide', [
        // Base URL.
        'baseUrl' => '/images/',
        // Whether to generate secure URLs.
        'secureUrls' => false,
        // Signing key to use when generating secure URLs.
        'signKey' => null,
    ]);
}

Here are the available methods of GlideHelper:

    /**
     * Creates a formatted IMG element.
     *
     * @param string $path Image path.
     * @param array $params Image manipulation parameters.
     * @param array $options Array of HTML attributes and options.
     *   See `$options` argument of `Cake\View\HtmlHelper::image()`.
     * @return string Complete <img> tag.
     */
    GlideHelper::image(string $path, array $params = [], array $options = []): string

    /**
     * URL with query string based on resizing params.
     *
     * @param string $path Image path.
     * @param array $params Image manipulation parameters.
     * @return string Image URL.
     */
    GlideHelper::url(string $path, array $params = []): string

The main benefit of using this helper is depending on the value of secureUrls config, the generated URLs will contain a token which will be verified by the middleware. The prevents modification of query string params.

Author: ADmad
Source Code: https://github.com/ADmad/cakephp-glide 
License: MIT license

#php #cakephp #middleware 

CakePHP Plugin for using Glide Image Manipulation Library

Gourmet/filters: Dispatcher Filters (middleware) for CakePHP 3

Filters

Dispatcher filters (middlewares) for CakePHP 3.

NOTE: This is the master branch, latest stable is 1.0 branch.

Install

Using Composer:

composer require gourmet/filters:1.1.x-dev

You then need to load the plugin. In boostrap.php, something like:

\Cake\Core\Plugin::load('Gourmet/Filters');

Usage

All the below examples happen in bootstrap.php.

MaintenanceFilter

By default, this filter will look for the ROOT/maintenance.html file and if it exists, it will use it as the response.

DispatcherFactory::add('Gourmet/Filters.Maintenance');

You could customize the path like so:

DispatcherFactory::add('Gourmet/Filters.Maintenance', [
    'path' => '/absolute/path/to/maintenance/file.html'
]);

You could for example do echo 'Scheduled maintenance' > maintenance.html and your site will automatically be set to maintenance mode with the message 'Scheduled maintenance'.

IpFilter

Restrict access to spefic IPs and/or ban other.

DispatcherFactory::add('Gourmet/Filters.Ip', [
    'allow' => [
        '127.0.0.1'
    ]
]);

or

DispatcherFactory::add('Gourmet/Filters.Ip', [
    'disallow' => [
        '127.0.0.1'
    ]
]);

RobotsFilter

This one provides a default robots.txt file for non-production environments. By default, it checks the 'APP_ENV' environment variable and compares it's value to 'production'.

DispatcherFactory::add('Gourmet/Filters.Robots');

On all your non-production environments, robots.txt will look like this:

User-Agent: *
Disallow: /

and your pages' headers will include the X-Robots-Tag with 'noindex. nofollow, noarchive' flags.

You can customize all of that using the configuration keys: priority, when, key, value.

Patches & Features

  • Fork
  • Mod, fix
  • Test - this is important, so it's not unintentionally broken
  • Commit - do not mess with license, todo, version, etc. (if you do change any, bump them into commits of their own that I can ignore when I pull)
  • Pull request - bonus point for topic branches

Bugs & Feedback

http://github.com/gourmet/filters/issues

Author: Gourmet
Source Code: https://github.com/gourmet/filters 
License: MIT license

#php #cakephp #filter #middleware 

Gourmet/filters: Dispatcher Filters (middleware) for CakePHP 3