{"id":21657909,"url":"https://github.com/defense-cr/defense","last_synced_at":"2025-12-28T12:54:48.206Z","repository":{"id":52554618,"uuid":"187209583","full_name":"defense-cr/defense","owner":"defense-cr","description":"🔮 A Crystal HTTP handler for throttling, blocking and tracking malicious requests.","archived":false,"fork":false,"pushed_at":"2023-12-30T13:53:07.000Z","size":65,"stargazers_count":58,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-05-06T00:04:49.806Z","etag":null,"topics":["allow2ban","block","crystal","fail2ban","handler","rack-attack","throttle","throttling"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/defense-cr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-05-17T12:06:31.000Z","updated_at":"2024-04-11T17:04:41.000Z","dependencies_parsed_at":"2023-12-30T14:43:41.650Z","dependency_job_id":null,"html_url":"https://github.com/defense-cr/defense","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/defense-cr%2Fdefense","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/defense-cr%2Fdefense/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/defense-cr%2Fdefense/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/defense-cr%2Fdefense/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/defense-cr","download_url":"https://codeload.github.com/defense-cr/defense/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226304167,"owners_count":17603524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["allow2ban","block","crystal","fail2ban","handler","rack-attack","throttle","throttling"],"created_at":"2024-11-25T09:28:20.302Z","updated_at":"2025-12-28T12:54:48.158Z","avatar_url":"https://github.com/defense-cr.png","language":"Crystal","funding_links":[],"categories":["Crystal"],"sub_categories":[],"readme":"# Defense\n\n[![Build Status](https://github.com/defense-cr/defense/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/defense-cr/defense/actions/workflows/tests.yml/?branch=master)\n\n🔮 *A Crystal HTTP handler for throttling, blocking and tracking malicious requests* 🔮\n\n## Getting started\n\n### Installation\n\nAdd the shard as a dependency to your project's `shards.yml`:\n\n```yaml\ndependencies:\n  defense:\n    github: defense-cr/defense\n```\n\n...and install it:\n\n```sh\nshards install\n```\n\n### Configure the data store\n\nDefense stores its state in a **Redis** database. You can configure this by setting the `REDIS_URL` environment variable or by using the `Defense#store=` method:\n\n```crystal\nDefense.store = Defense::RedisStore.new(url: \"redis://localhost:6379/0\")\n```\n\nFor simple use cases or tests you can also use the **memory store**:\n\n```crystal\nDefense.store = Defense::MemoryStore.new\n```\n\nYou can always implement your own **custom store** by extending the abstract class `Defense::Store`.\n\n### Plugging into the application\n\nDefense is built as a Crystal `HTTP::Handler`. You will need to register the `Defense::Handler` to your web application's handler chain. For more information about *handlers* and the *handler chain* follow [this link](https://crystal-lang.org/api/latest/HTTP/Server.html).\n\nUsually the earlier you register the handler to your handler chain, the better. This ensures that malicious requests are blocked early own, before other layers (handlers) of your application are reached.\n\nHere's how to plug Defense into some of the **most popular Crystal web frameworks**:\n\n#### Kemal\n\nIn Kemal you would use the `add_handler` method to register the Defense handler:\n\n```crystal\nrequire \"kemal\"\nrequire \"defense\"\n\nadd_handler Defense::Handler.new\n\n# Other handlers...\nadd_handler SomeOtherHandler.new\n\nget \"/\" do\n  \"hello world\"\nend\n\nKemal.run\n```\n\nFor more details, check out the [kemal-defense-example repository](https://github.com/defense-cr/kemal-defense-example).\n\n#### Amber\n\nIn Amber you register handlers as part of a pipeline in your `config/routes.cr` file:\n\n```crystal\nAmber::Server.configure do |app|\n  pipeline :web do\n    plug Defense::Handler.new\n\n    # Other handlers...\n    plug SomeOtherHandler.new\n  end\n\n  routes :web do\n    get \"/\", HomeController, :index\n  end\nend\n```\n\n#### Lucky\n\nIn Lucky, you would add the `Defense::Handler` within your `src/app_server.cr` file, somewhere before the `Lucky::RouteHandler`:\n\n```crystal\nclass AppServer \u003c Lucky::BaseAppServer\n  def middleware\n    [\n      Defense::Handler.new,\n\n      # Other handlers...\n      SomeOtherHandler.new,\n\n      Lucky::RouteHandler.new,\n    ]\n  end\nend\n```\n\n#### HTTP::Server (Standalone)\n\nWhen using the standard library `HTTP::Server`, any middleware is registered as part of the initializer:\n\n```crystal\nrequire \"defense\"\nrequire \"http/server\"\n\nserver = HTTP::Server.new([Defense::Handler.new]) do |context|\n  context.response.content_type = \"text/plain\"\n  context.response.print \"hello world\"\nend\n\nserver.bind_tcp(8080)\nserver.listen\n```\n\n### Usage\n\nDefense provides a set of configurable rules that you can use to throttle, block and track malicious requests based on your own heuristics:\n\n- [Throttling](#throttling)\n- [Configure the throttled response](#configure-the-throttled-response)\n- [Blocklist](#blocklist)\n- [Configure the blocked response](#configure-the-blocked-response)\n- [Fail2Ban](#fail2ban)\n- [Allow2Ban](#allow2ban)\n- [Safelist](#safelist)\n\n#### Throttling\n\nThe `Defense.throttle` method can be used to throttle clients based on a maximum number of requests (*limit*) over a given time frame specified in seconds (*period*).\n\nThe method takes a block which receives the `request` as an argument. The return value of the block should either be `nil` (in which case the request will not be counted at all) or a `String` which uniquely identifies the client to throttle. A good identifier is usually the IP address.\n\nThe following example throttles clients based on their IP address to a limit of 10 requests per minute:\n\n```crystal\nDefense.throttle(\"throttle requests per minute\", limit: 10, period: 60) do |request|\n  request.remote_address.to_s\nend\n```\n\nThe following example throttles clients in a similar way but will ignore requests coming from `127.0.0.1`:\n\n```crystal\nDefense.throttle(\"throttle requests per minute except localhost\", limit: 10, period: 60) do |request|\n  return nil if request.remote_address.to_s == \"127.0.0.1\"\n\n  request.remote_address.to_s\nend\n```\n\n#### Configure the throttled response\n\nThrottled requests are responded with:\n\n```http\nHTTP/1.1 429 Too Many Requests\ncontent-type: text/plain\ncontent-length: 10\n\nRetry later\n```\n\nYou can override the default response message by using the `Defense.throttled_response=` method:\n\n```crystal\nDefense.throttled_response = -\u003e(response : HTTP::Server::Response) do\n  response.status = HTTP::Status::UNAUTHORIZED\n  response.content_type = \"application/json\"\n  response.puts(\"{'hello':'world'}\")\nend\n```\n\n#### Blocklist\n\nThe `Defense.blocklist` method can be used to block malicious or unwanted requests.\n\nThe method takes a block which receives the `request` as an argument. The return value of the block should either be `true` - in which case the request will be blocked, or `false` - in which case the request will be allowed.\n\nThe following example blocks all requests to `/admin/*`:\n\n```crystal\nDefense.blocklist(\"block requests to the admin\") do |request|\n  request.path.starts_with?(\"/admin/\")\nend\n```\n\nThe following example blocks requests based on a predefined list of malicious IPs:\n\n```crystal\nMALICIOUS_IPS = [\"1.1.1.1\", \"2.2.2.2\", \"3.3.3.3\"]\n\nDefense.blocklist(\"block requests from malicious ips\") do |request|\n  MALICIOUS_IPS.includes?(request.remote_address.to_s)\nend\n```\n\nThe [Spamhaus DROP lists](https://www.spamhaus.org/drop/) are a great resource for malicious IPs to block.\n\n#### Configure the blocked response\n\nBlocked requests are responded with:\n\n```http\nHTTP/1.1 403 Forbidden\ncontent-type: text/plain\ncontent-length: 9\n\nForbidden\n```\n\nYou can override the default response message by using the `Defense.blocked_response=` method:\n\n```crystal\nDefense.blocked_response = -\u003e(response : HTTP::Server::Response) do\n  response.status = HTTP::Status::UNAUTHORIZED\n  response.content_type = \"application/json\"\n  response.puts(\"{'hello':'world'}\")\nend\n```\n\n#### Fail2Ban\n\nThe `Defense::Fail2Ban.filter` method can be used within a `Defense.blocklist` block to ban misbehaving clients for a given period of time (*bantime*) after a sequence of blocked requests (*maxretry*) performed over a particular time range (*findtime*).\n\nThe method's first argument should be a unique identifier of the client - the IP address is usually a safe bet. It's highly recommended to namespace this identifier, in order to avoid conflicts with other `Fail2Ban` or `Allow2Ban` calls - e.g. `my-fancy-filter:#{request.remote_address.to_s}` would be a good identifier.\n\nThe method also takes a block which should return `true` - in which case the request will be blocked and counted for the ban, or `false` - in which case the request will be allowed and excluded from the ban count. Note that the return value of the `#filter` block will also be used as a return value for the `#blocklist` block.\n\nThe following example blocks any requests containing `/etc/passwd` inside the path and, once a particular client identified by IP has accumulated 5 requests wihin 60 seconds, it bans him for the next 24 hours:\n\n```crystal\nDefense.blocklist(\"fail2ban pentesters\") do |request|\n  Defense::Fail2Ban.filter(\"pentesters:#{request.remote_address.to_s}\", maxretry: 5, findtime: 60, bantime: 24 * 60 * 60) do\n    request.path.includes?(\"/etc/passwd\")\n  end\nend\n```\n\n#### Allow2Ban\n\nThe `Defense::Allow2Ban.filter` method works the same way as `Defense::Fail2Ban.filter` except that it allows requests from misbehaving clients until such time as they reach *maxretry* at which they are cut off as per normal.\n\nThe following example allows all `POST /login` requests until a particular client identified by IP has accumulated 5 requests within 60 seconds, at which point it bans him for the next 24 hours:\n\n```crystal\nDefense.blocklist(\"allow2ban too many login attempts\") do |request|\n  Defense::Allow2Ban.filter(\"too-many-login-attempts:#{request.remote_address.to_s}\", maxretry: 5, findtime: 60, bantime: 24 * 60 * 60) do\n    request.method == \"POST\" \u0026\u0026 request.path == \"/login\"\n  end\nend\n```\n\n#### Safelist\n\nThe `Defense.safelist` method can be used to exclude requests from any throttling or blocking rules. This method has precedence over all the other rules.\n\nThe method takes a block which receives the `request` as an argument. The return value of the block should either be `true` - in which case the request will never be throttled or blocked, or `false` - in which case the request will be checked against the other existing rules and might potentially be throttled or blocked.\n\nThe following example marks all requests originating from `127.0.0.1` as safe:\n\n```crystal\nDefense.safelist(\"local requests are safe\") do |request|\n  request.remote_address.to_s == \"127.0.0.1\"\nend\n```\n\n## Contributing \u0026 Development\n\nContributions are welcome. Make sure to check the existing issues (including the closed ones) before requesting a feature, reporting a bug or opening a pull requests.\n\n### Getting started\n\nInstall dependencies:\n\n```sh\nshards install\n```\n\nRun tests using Redis as a backend (requires a running Redis server):\n\n```sh\ncrystal spec\n```\n\nRun tests using the memory store as a backend:\n\n```sh\nSTORE=memory crystal spec\n```\n\nFormat the code:\n\n```sh\ncrystal tool format\n```\n\n### Guidelines\n\n- Keep the public interface small. Anything that doesn't have to be public, should explicitly be marked as protected or\nprivate, including classes.\n- Be explicit about type declaration (especially on public methods).\n- Use the Crystal formatter to format the code.\n- For now, prefer integration/system tests over unit tests.\n\n## Maintainers\n\n- [Florin Lipan](https://github.com/lipanski)\n- [Rodrigo Pinto](https://github.com/rodrigopinto)\n\n## Credits\n\nThis shard is heavily inspired by [rack-attack](https://github.com/kickstarter/rack-attack) ❤\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdefense-cr%2Fdefense","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdefense-cr%2Fdefense","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdefense-cr%2Fdefense/lists"}