Compare commits
34 commits
99b1e759ba
...
3248706eb2
Author | SHA1 | Date | |
---|---|---|---|
3248706eb2 | |||
|
749c537e85 | ||
|
75d5ef1046 | ||
|
2d8df1f116 | ||
|
dc031c6074 | ||
|
85fcbbee02 | ||
|
3243b8e0e0 | ||
|
0afbd129f5 | ||
|
1d46fd17d7 | ||
|
5554d3d2f1 | ||
|
6a29240bbe | ||
|
e69bd400b7 | ||
|
52ab623c50 | ||
|
ca7d6e1e64 | ||
|
a15ba83175 | ||
|
b074578c1a | ||
|
7c47bbc150 | ||
|
bef7351000 | ||
|
bb9105f202 | ||
|
9628043e4c | ||
|
0de0e990e9 | ||
|
13fd4f8b2f | ||
|
cb9adcd188 | ||
|
a939a577de | ||
|
c995a2a971 | ||
|
aa004afbbf | ||
|
8ebe171279 | ||
|
84ea6627ac | ||
|
6a10ea8127 | ||
|
19661893ca | ||
|
ae7cda8291 | ||
|
d20dbc783c | ||
|
c8f857dff3 | ||
|
93521b7120 |
28 changed files with 578 additions and 280 deletions
19
.ameba.yml
19
.ameba.yml
|
@ -1,13 +1,24 @@
|
|||
# This configuration file was generated by `ameba --gen-config`
|
||||
# on 2019-08-25 09:29:24 UTC using Ameba version 0.10.0.
|
||||
# on 2023-01-30 12:35:15 UTC using Ameba version 1.4.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the reported problems are removed from the code base.
|
||||
|
||||
# Problems found: 7
|
||||
# Problems found: 2
|
||||
# Run `ameba --only Lint/UselessAssign` for details
|
||||
Lint/UselessAssign:
|
||||
Description: Disallows useless variable assignments
|
||||
Enabled: true
|
||||
Severity: Warning
|
||||
Excluded:
|
||||
- spec/view_spec.cr
|
||||
Enabled: true
|
||||
Severity: Warning
|
||||
|
||||
# Problems found: 6
|
||||
# Run `ameba --only Lint/NotNil` for details
|
||||
Lint/NotNil:
|
||||
Description: Identifies usage of `not_nil!` calls
|
||||
Excluded:
|
||||
- src/kemal/param_parser.cr
|
||||
- src/kemal/static_file_handler.cr
|
||||
- src/kemal/config.cr
|
||||
Enabled: true
|
||||
Severity: Warning
|
||||
|
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.cr text eol=lf
|
||||
*.ecr text eol=lf
|
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
|
@ -11,31 +11,66 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
crystal: [latest, nightly]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Crystal
|
||||
uses: oprypin/install-crystal@v1
|
||||
uses: crystal-lang/install-crystal@v1
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
- name: Download source
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: shards install
|
||||
env:
|
||||
SHARDS_OPTS: --ignore-crystal-version
|
||||
|
||||
- name: Run specs
|
||||
run: |
|
||||
crystal spec
|
||||
crystal spec --release --no-debug
|
||||
|
||||
format:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
crystal: [latest, nightly]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
- name: Download source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check formatting
|
||||
run: crystal tool format --check
|
||||
|
||||
ameba:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
crystal: [latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
- name: Download source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: shards install
|
||||
|
||||
- name: Run ameba linter
|
||||
run: bin/ameba
|
||||
|
381
CHANGELOG.md
381
CHANGELOG.md
|
@ -1,16 +1,45 @@
|
|||
# 1.6.0 (12-10-2024)
|
||||
|
||||
- Crystal 1.14.0 support :tada:
|
||||
- Windows support [#690](https://github.com/kemalcr/kemal/pull/690). Thanks @sdogruyol :pray:
|
||||
- Directory Listing: Add UTF-8 Charset to the response Content type [#679](https://github.com/kemalcr/kemal/pull/679). Thanks @alexkutsan @Sija :pray:
|
||||
- Use context instead of response in static_headers helper [#681](https://github.com/kemalcr/kemal/pull/681). Thanks @sdogruyol :pray:
|
||||
|
||||
# 1.5.0 (10-04-2024)
|
||||
|
||||
- Crystal 1.12.0 support :tada:
|
||||
- Allow HTTP::Server::Context#redirect to take an URL [#659](https://github.com/kemalcr/kemal/pull/659). Thanks @xendk :pray:
|
||||
- Bump `exception_page` dependency [#669](https://github.com/kemalcr/kemal/pull/669). Thanks @Sija :pray:
|
||||
- Add message support to `Kemal::Exceptions::CustomException` [#671](https://github.com/kemalcr/kemal/pull/671). Thanks @sdogruyol :pray:
|
||||
- Add `Date` header to HTTP responses [#676](https://github.com/kemalcr/kemal/pull/676). Thanks @Sija :pray:
|
||||
|
||||
# 1.4.0 (15-04-2023)
|
||||
|
||||
- Crystal 1.8.0 support :tada:
|
||||
- Fix multiple logger handlers when custom logger is used [#653](https://github.com/kemalcr/kemal/pull/653). Thanks @aravindavk :pray:
|
||||
- Add `Kemal::OverrideMethodHandler` [#651](https://github.com/kemalcr/kemal/pull/651). Thanks @sdogruyol :pray:
|
||||
- `HeadRequestHandler`: run GET handler and don't return the body [#655](https://github.com/kemalcr/kemal/pull/655). Thanks @compumike :pray:
|
||||
|
||||
# 1.3.0 (09-10-2022)
|
||||
|
||||
- Crystal 1.6.0 support :tada:
|
||||
- Disable signal trap for usage Kemal with other tools [#642](https://github.com/kemalcr/kemal/pull/642). Thanks @le0pard :pray:
|
||||
- Bump exception_page shard to v0.3.0 [#645](https://github.com/kemalcr/kemal/pull/645). Thanks @Sija :pray:
|
||||
- ***(Security)*** Omitting filters fix for lowercase methods requests [#647](https://github.com/kemalcr/kemal/pull/647). Thanks @sdogruyol @SlayerShadow :pray:
|
||||
|
||||
# 1.2.0 (07-07-2022)
|
||||
|
||||
- Crystal 1.5.0 support :tada:
|
||||
- Eliminated several seconds of delay when loading big mp4 file. Thanks @Athlon64 :pray:
|
||||
- Fix content_for failing to capture the correct block input [#639](https://github.com/kemalcr/kemal/pull/639). Thanks @sdogruyol :pray:
|
||||
- Closes response by default in HTTP::Server::Context#redirect [#641](https://github.com/kemalcr/kemal/pull/641). Thanks @cyangle :pray:
|
||||
- Enable option for index.html to be a directories default [#640](https://github.com/kemalcr/kemal/pull/640). Thanks @ukd1 :pray:
|
||||
- Fix `content_for` failing to capture the correct block input [#639](https://github.com/kemalcr/kemal/pull/639). Thanks @sdogruyol :pray:
|
||||
- Closes response by default in `HTTP::Server::Context#redirect` [#641](https://github.com/kemalcr/kemal/pull/641). Thanks @cyangle :pray:
|
||||
- Enable option for `index.html` to be a directories default [#640](https://github.com/kemalcr/kemal/pull/640). Thanks @ukd1 :pray:
|
||||
|
||||
You can enable it via
|
||||
You can enable it via:
|
||||
|
||||
```crystal
|
||||
```crystal
|
||||
serve_static({"dir_index" => true})
|
||||
```
|
||||
```
|
||||
|
||||
# 1.1.2 (24-02-2022)
|
||||
|
||||
|
@ -18,7 +47,9 @@ You can enable it via
|
|||
|
||||
# 1.1.1 (22-02-2022)
|
||||
|
||||
- Ignore HTTP::Server::Response patching for crystal >= 1.3.0 [#628](https://github.com/kemalcr/kemal/pull/628). Thanks @SamantazFox :pray:
|
||||
- Remove Kilt [#618](https://github.com/kemalcr/kemal/pull/618). Thanks @sdogruyol :pray:
|
||||
- Ignore `HTTP::Server::Response` patching for crystal >= 1.3.0 [#628](https://github.com/kemalcr/kemal/pull/628). Thanks @SamantazFox :pray:
|
||||
|
||||
# 1.1.0 (02-09-2021)
|
||||
|
||||
- You can now set your own application name for startup message [#606](https://github.com/kemalcr/kemal/pull/606). Thanks @aravindavk :pray:
|
||||
|
@ -31,7 +62,7 @@ You can enable it via
|
|||
- Crystal 1.0.0 support :tada:
|
||||
- Update Radix to use latest 0.4.0 [#596](https://github.com/kemalcr/kemal/pull/596). Thanks @luislavena :pray:
|
||||
- Use latest version of Ameba dependency (dev) [#597](https://github.com/kemalcr/kemal/pull/597). Thanks @luislavena :pray:
|
||||
- Fix StaticFileHandler failing spec [#599](https://github.com/kemalcr/kemal/pull/599). Thanks @jinn999 :pray:
|
||||
- Fix `StaticFileHandler` failing spec [#599](https://github.com/kemalcr/kemal/pull/599). Thanks @jinn999 :pray:
|
||||
|
||||
# 0.27.0 (28-11-2020)
|
||||
|
||||
|
@ -57,88 +88,83 @@ You can enable it via
|
|||
# 0.25.2 (08-02-2019)
|
||||
|
||||
- Add option to config to parse or not command line parameters [#483](https://github.com/kemalcr/kemal/pull/483). Thanks @diegogub :pray:
|
||||
|
||||
- Allow to set filename for `send_file` [#512](https://github.com/kemalcr/kemal/pull/512). Thanks @mamantoha :pray:
|
||||
|
||||
|
||||
```ruby
|
||||
send_file env, "./asset/image.jpeg", filename: "image.jpg"
|
||||
```
|
||||
```crystal
|
||||
send_file env, "./asset/image.jpeg", filename: "image.jpg"
|
||||
```
|
||||
|
||||
- Set `status_code` before response [#513](https://github.com/kemalcr/kemal/pull/513). Thanks @mamantohoa :pray:
|
||||
|
||||
- Use Crystal MIME registry. [#516](https://github.com/kemalcr/kemal/pull/516) Thanks @Sija :pray:
|
||||
|
||||
# 0.25.1 (06-10-2018)
|
||||
|
||||
- Fix `params.files` memoization https://github.com/kemalcr/kemal/pull/503. Thanks @mamantoha :pray:
|
||||
- Fix `params.files` memoization [#503](https://github.com/kemalcr/kemal/pull/503). Thanks @mamantoha :pray:
|
||||
|
||||
# 0.25.0 (05-10-2018)
|
||||
|
||||
- Crystal 0.27.0 support.
|
||||
- *[breaking change]* Added back `env.params.files`.
|
||||
|
||||
Here's a fully working sample for reading a image file upload `image1` and saving it under `public/uploads`.
|
||||
Here's a fully working sample for reading a image file upload `image1` and saving it under `public/uploads`.
|
||||
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
file = env.params.files["image1"].tempfile
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(file.path)]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file, f)
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
file = env.params.files["image1"].tempfile
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(file.path)]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file, f)
|
||||
end
|
||||
"Upload ok"
|
||||
end
|
||||
"Upload ok"
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
To test
|
||||
To test
|
||||
|
||||
`curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload`
|
||||
`curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload`
|
||||
|
||||
- Cache HTTP routes to increase performance :rocket: https://github.com/kemalcr/kemal/pull/493
|
||||
- Cache HTTP routes to increase performance :rocket: [#493](https://github.com/kemalcr/kemal/pull/493)
|
||||
|
||||
# 0.24.0 (14-08-2018)
|
||||
|
||||
- *[breaking change]* Removed `env.params.files`. You can use Crystal's built-in `HTTP::FormData.parse` instead
|
||||
- *[breaking change]* Removed `env.params.files`. You can use Crystal's built-in `HTTP::FormData.parse` instead:
|
||||
|
||||
```ruby
|
||||
post "/upload" do |env|
|
||||
HTTP::FormData.parse(env.request) do |upload|
|
||||
filename = file.filename
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
HTTP::FormData.parse(env.request) do |upload|
|
||||
filename = file.filename
|
||||
|
||||
if !filename.is_a?(String)
|
||||
"No filename included in upload"
|
||||
else
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file.tmpfile, f)
|
||||
if !filename.is_a?(String)
|
||||
"No filename included in upload"
|
||||
else
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file.tmpfile, f)
|
||||
end
|
||||
"Upload OK"
|
||||
end
|
||||
"Upload OK"
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
- *[breaking change]* From now on to access dynamic url params in a WebSocket route you have to use:
|
||||
|
||||
```ruby
|
||||
ws "/:id" do |socket, context|
|
||||
id = context.ws_route_lookup.params["id"]
|
||||
end
|
||||
```
|
||||
```crystal
|
||||
ws "/:id" do |socket, context|
|
||||
id = context.ws_route_lookup.params["id"]
|
||||
end
|
||||
```
|
||||
|
||||
- *[breaking change]* Removed `_method` magic param.
|
||||
|
||||
- Added new exception page [#466](https://github.com/kemalcr/kemal/pull/466). Thanks @mamantoha 🙏
|
||||
|
||||
- Support custom port binding. Thanks @straight-shoota 🙏
|
||||
|
||||
```ruby
|
||||
Kemal.run do |config|
|
||||
server = config.server.not_nil!
|
||||
server.bind_tcp "127.0.0.1", 3000, reuse_port: true
|
||||
server.bind_tcp "0.0.0.0", 3001, reuse_port: true
|
||||
end
|
||||
```
|
||||
```crystal
|
||||
Kemal.run do |config|
|
||||
server = config.server.not_nil!
|
||||
server.bind_tcp "127.0.0.1", 3000, reuse_port: true
|
||||
server.bind_tcp "0.0.0.0", 3001, reuse_port: true
|
||||
end
|
||||
```
|
||||
|
||||
# 0.23.0 (17-06-2018)
|
||||
|
||||
|
@ -156,71 +182,69 @@ end
|
|||
- Allow videos to be opened with correct mime type. [#406](https://github.com/kemalcr/kemal/pull/406) thanks @crisward 🙏
|
||||
- Add webm mime type.[#413](https://github.com/kemalcr/kemal/pull/413) thanks @reindeer-cafe 🙏
|
||||
|
||||
|
||||
# 0.21.0 (05-09-2017)
|
||||
|
||||
- Dynamically insert handlers :muscle: Fixes [#376](https://github.com/kemalcr/kemal/pull/376).
|
||||
- Add context to WebSocket. This allows one to use `HTTP::Server::Context` in `ws` declarations :heart_eyes: Fixes [#349](https://github.com/kemalcr/kemal/pull/349).
|
||||
|
||||
```ruby
|
||||
ws "/:room_name" do |socket, env|
|
||||
env.params.url["room_name"]
|
||||
end
|
||||
```
|
||||
```crystal
|
||||
ws "/:room_name" do |socket, env|
|
||||
env.params.url["room_name"]
|
||||
end
|
||||
```
|
||||
|
||||
- Add support for customizing the headers of built-in `Kemal::StaticFileHandler` :hammer: Useful for supporting `CORS` for single page applications :clap:
|
||||
|
||||
```ruby
|
||||
static_headers do |response, filepath, filestat|
|
||||
if filepath =~ /\.html$/
|
||||
```crystal
|
||||
static_headers do |response, filepath, filestat|
|
||||
if filepath =~ /\.html$/
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
end
|
||||
response.headers.add("Content-Size", filestat.size.to_s)
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
- Allow %w in Handler macros [#385](https://github.com/kemalcr/kemal/pull/385). Thanks @will :pray:
|
||||
|
||||
- Security: X-Content-Type-Options: nosniff for static files. Fixes [#379](https://github.com/kemalcr/kemal/issues/379). Thanks @crisward :pray:
|
||||
|
||||
- Performance: [Remove tempfile management to OS](https://github.com/kemalcr/kemal/commit/a1520de7ed3865fa73258343a80fad4f20666a99). This brings %10 - 15 performance boost to Kemal :rocket:
|
||||
- Allow `%w` in Handler macros [#385](https://github.com/kemalcr/kemal/pull/385). Thanks @will :pray:
|
||||
- Security: `X-Content-Type-Options: nosniff` for static files. Fixes [#379](https://github.com/kemalcr/kemal/issues/379). Thanks @crisward :pray:
|
||||
- Performance: [Remove tempfile management to OS](https://github.com/kemalcr/kemal/commit/a1520de7ed3865fa73258343a80fad4f20666a99). This brings 10-15% performance boost to Kemal :rocket:
|
||||
|
||||
# 0.20.0 (01-07-2017)
|
||||
|
||||
- Crystal 0.23.0 support! As always, Kemal is compatible with the latest major release of Crystal 💎
|
||||
- Great news everyone 🎉 All handlers are now completely ***customizable***!. Use the default `Kemal` handlers or go wild, it's all up to you ⛏
|
||||
|
||||
```ruby
|
||||
# Don't forget to add `Kemal::RouteHandler::INSTANCE` or your routes won't work!
|
||||
Kemal.config.handlers = [Kemal::InitHandler.new, YourHandler.new, Kemal::RouteHandler::INSTANCE]
|
||||
```
|
||||
```crystal
|
||||
# Don't forget to add `Kemal::RouteHandler::INSTANCE` or your routes won't work!
|
||||
Kemal.config.handlers = [Kemal::InitHandler.new, YourHandler.new, Kemal::RouteHandler::INSTANCE]
|
||||
```
|
||||
|
||||
You can also insert a handler into a specific position.
|
||||
You can also insert a handler into a specific position.
|
||||
|
||||
```crystal
|
||||
# This adds MyCustomHandler instance to 1 position.
|
||||
# Be aware that the index starts from 0.
|
||||
add_handler MyCustomHandler.new, 1
|
||||
```
|
||||
|
||||
```ruby
|
||||
# This adds MyCustomHandler instance to 1 position. Be aware that the index starts from 0.
|
||||
add_handler MyCustomHandler.new, 1
|
||||
```
|
||||
- Updated [Kilt](https://github.com/jeromegn/kilt) to v0.4.0.
|
||||
- Make `Route` a `Struct`. This improves the performance of route declarations.
|
||||
|
||||
# 0.19.0 (09-05-2017)
|
||||
|
||||
- Return no body for head route fixes #323. (thanks @crisward)
|
||||
- Update `radix` to `0.3.8`. (thanks @waghanza)
|
||||
- Return no body for head route fixes [#323](https://github.com/kemalcr/kemal/issues/323). (thanks @crisward)
|
||||
- Update Radix to `v0.3.8`. (thanks @waghanza)
|
||||
- User defined context store types. (thanks @neovitange)
|
||||
|
||||
```ruby
|
||||
class User
|
||||
property name
|
||||
end
|
||||
```crystal
|
||||
class User
|
||||
property name
|
||||
end
|
||||
|
||||
add_context_storage_type(User)
|
||||
```
|
||||
add_context_storage_type(User)
|
||||
```
|
||||
|
||||
- Prevent `send_file returning filesize. (thanks @crisward)
|
||||
- Dont call setup in `config#add_filter_handler` fixes #338.
|
||||
- Prevent `send_file` returning filesize. (thanks @crisward)
|
||||
- Don't call setup in `config#add_filter_handler` fixes [#338](https://github.com/kemalcr/kemal/issues/338).
|
||||
|
||||
# 0.18.3 (07-03-2017)
|
||||
|
||||
|
@ -228,16 +252,14 @@ add_context_storage_type(User)
|
|||
|
||||
# 0.18.2 (24-02-2017)
|
||||
|
||||
- Fix [Gzip in Kemal Seems broken for static files](https://github.com/kemalcr/kemal/issues/316). This was caused by `Gzip::Writer` in `Crystal 0.21.0` and currently mitigated by monkey patching `Gzip::Header`.
|
||||
- Fix Gzip in Kemal Seems broken for static files [#316](https://github.com/kemalcr/kemal/issues/316). This was caused by `Gzip::Writer` in `Crystal 0.21.0` and currently mitigated by monkey patching `Gzip::Header`.
|
||||
|
||||
# 0.18.1 (21-02-2017)
|
||||
|
||||
- Crystal 0.21.0 support
|
||||
- Drop `multipart.cr` dependency. `multipart` support is now built-into Crystal <3
|
||||
- Since Crystal 0.21.0 comes built-in with `multipart` there are some improvements and deprecations.
|
||||
|
||||
`meta` has been removed from `FileUpload` and it has the following properties
|
||||
|
||||
- `meta` has been removed from `FileUpload` and it has the following properties:
|
||||
+ `tmpfile`: This is temporary file for file upload. Useful for saving the upload file.
|
||||
+ `filename`: File name of the file upload. (logo.png, images.zip e.g)
|
||||
+ `headers`: Headers for the file upload.
|
||||
|
@ -246,40 +268,35 @@ add_context_storage_type(User)
|
|||
+ `read_time`: Read time of the file upload.
|
||||
+ `size`: Size of the file upload.
|
||||
|
||||
|
||||
# 0.18.0 (11-02-2017)
|
||||
|
||||
- Simpler file upload. File uploads can now be access from `HTTP::Server::Context` like `env.params.files["filename"]`.
|
||||
- Simpler file upload. File uploads can now be access from `HTTP::Server::Context` like `env.params.files["filename"]`, which exposes following properties.
|
||||
+ `tmpfile`: This is temporary file for file upload. Useful for saving the upload file.
|
||||
+ `tmpfile_path`: File path of `tmpfile`.
|
||||
+ `filename`: File name of the file upload. (logo.png, images.zip e.g)
|
||||
+ `meta`: Meta information for the file upload.
|
||||
+ `headers`: Headers for the file upload.
|
||||
|
||||
`env.params.files["filename"]` has 5 methods
|
||||
|
||||
- `tmpfile`: This is temporary file for file upload. Useful for saving the upload file.
|
||||
- `tmpfile_path`: File path of `tmpfile`.
|
||||
- `filename`: File name of the file upload. (logo.png, images.zip e.g)
|
||||
- `meta`: Meta information for the file upload.
|
||||
- `headers`: Headers for the file upload.
|
||||
|
||||
Here's a fully working sample for reading a image file upload `image1` and saving it under `public/uploads`.
|
||||
Here's a fully working sample for reading a image file upload `image1` and saving it under `public/uploads`.
|
||||
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
file = env.params.files["image1"].tmpfile
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", file.filename]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file, f)
|
||||
post "/upload" do |env|
|
||||
file = env.params.files["image1"].tmpfile
|
||||
file_path = ::File.join [Kemal.config.public_folder, "uploads/", file.filename]
|
||||
File.open(file_path, "w") do |f|
|
||||
IO.copy(file, f)
|
||||
end
|
||||
"Upload ok"
|
||||
end
|
||||
"Upload ok"
|
||||
end
|
||||
```
|
||||
|
||||
To test
|
||||
To test
|
||||
|
||||
`curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload`
|
||||
`curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload`
|
||||
|
||||
- RF7233 support a.k.a file streaming. (https://github.com/kemalcr/kemal/pull/299) (thanks @denysvitali)
|
||||
|
||||
- Update Radix to 0.3.7. Fixes https://github.com/kemalcr/kemal/issues/293
|
||||
- Configurable startup / shutdown logging. https://github.com/kemalcr/kemal/issues/291 and https://github.com/kemalcr/kemal/issues/292 (thanks @twisterghost).
|
||||
- RF7233 support a.k.a file streaming. [#299](https://github.com/kemalcr/kemal/pull/299) (thanks @denysvitali)
|
||||
- Update Radix to 0.3.7. Fixes [#293](https://github.com/kemalcr/kemal/issues/293)
|
||||
- Configurable startup / shutdown logging. [#291](https://github.com/kemalcr/kemal/issues/291) and [#292](https://github.com/kemalcr/kemal/issues/292) (thanks @twisterghost).
|
||||
|
||||
# 0.17.5 (09-01-2017)
|
||||
|
||||
|
@ -288,133 +305,127 @@ To test
|
|||
# 0.17.4 (24-12-2016)
|
||||
|
||||
- Support for Crystal 0.20.3
|
||||
- Add `Kemal.stop`. Fixes #269.
|
||||
- Add `Kemal.stop`. Fixes [#269](https://github.com/kemalcr/kemal/issues/269).
|
||||
- `HTTP::Handler` is not a class anymore, it's a module. See https://github.com/crystal-lang/crystal/releases/tag/0.20.3
|
||||
|
||||
# 0.17.3 (03-12-2016)
|
||||
|
||||
- Handle missing 404 image. Fixes #263
|
||||
- Handle missing 404 image. Fixes [#263](https://github.com/kemalcr/kemal/issues/263)
|
||||
- Remove basic auth middleware from core and move to [kemalcr/kemal-basic-auth](https://github.com/kemalcr/kemal-basic-auth).
|
||||
|
||||
# 0.17.2 (25-11-2016)
|
||||
|
||||
- Use body.gets_to_end for parse_json. Fixes #260.
|
||||
- Use `body.gets_to_end` for `parse_json`. Fixes #260.
|
||||
- Update Radix to 0.3.5 and lock pessimistically. (thanks @luislavena)
|
||||
|
||||
# 0.17.1 (24-11-2016)
|
||||
|
||||
- Treat `HTTP::Request` body as an `IO`. Fixes [#257](https://github.com/sdogruyol/kemal/issues/257)
|
||||
- Treat `HTTP::Request` body as an `IO`. Fixes [#257](https://github.com/kemalcr/kemal/issues/257)
|
||||
|
||||
# 0.17.0 (23-11-2016)
|
||||
|
||||
- Reimplemented Request middleware / filter routing.
|
||||
|
||||
Now all requests will first go through the Middleware stack then Filters (before_*) and will finally reach the matching route.
|
||||
Now all requests will first go through the Middleware stack then Filters (`before_*`) and will finally reach the matching route.
|
||||
|
||||
Which is illustrated as,
|
||||
|
||||
```
|
||||
Request -> Middleware -> Filter -> Route
|
||||
```
|
||||
Which is illustrated as: `Request -> Middleware -> Filter -> Route`
|
||||
|
||||
- Rename `return_with` as `halt`.
|
||||
- Route declaration must start with `/`. Fixes [#242](https://github.com/sdogruyol/kemal/issues/242)
|
||||
- Set default exception Content-Type to text/html. Fixes [#202](https://github.com/sdogruyol/kemal/issues/242)
|
||||
- Route declaration must start with `/`. Fixes [#242](https://github.com/kemalcr/kemal/issues/242)
|
||||
- Set default exception `Content-Type` to `text/html`. Fixes [#202](https://github.com/kemalcr/kemal/issues/242)
|
||||
- Add `only` and `exclude` paths for `Kemal::Handler`. This change requires that all handlers must inherit from `Kemal::Handler`.
|
||||
|
||||
For example this handler will only work on `/` path. By default the HTTP method is `GET`.
|
||||
For example this handler will only work on `/` path. By default the HTTP method is `GET`.
|
||||
|
||||
```crystal
|
||||
class OnlyHandler < Kemal::Handler
|
||||
only ["/"]
|
||||
|
||||
```crystal
|
||||
class OnlyHandler < Kemal::Handler
|
||||
only ["/"]
|
||||
|
||||
def call(env)
|
||||
return call_next(env) unless only_match?(env)
|
||||
puts "If the path is / i will be doing some processing here."
|
||||
def call(env)
|
||||
return call_next(env) unless only_match?(env)
|
||||
puts "If the path is / i will be doing some processing here."
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
The handlers using `exclude` will work on the paths that isn't specified. For example this handler will work on any routes other than `/`.
|
||||
The handlers using `exclude` will work on the paths that isn't specified. For example this handler will work on any routes other than `/`.
|
||||
|
||||
```crystal
|
||||
class ExcludeHandler < Kemal::Handler
|
||||
exclude ["/"]
|
||||
```crystal
|
||||
class ExcludeHandler < Kemal::Handler
|
||||
exclude ["/"]
|
||||
|
||||
def call(env)
|
||||
return call_next(env) unless only_match?(env)
|
||||
puts "If the path is NOT / i will be doing some processing here."
|
||||
def call(env)
|
||||
return call_next(env) unless only_match?(env)
|
||||
puts "If the path is NOT / i will be doing some processing here."
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
- Close response on `halt`. (thanks @samueleaton).
|
||||
- Update `Radix` to `v0.3.4`.
|
||||
- `error` handler now also yields error. For example you can get the error mesasage like
|
||||
- Update Radix to `v0.3.4`.
|
||||
- `error` handler now also yields error. For example you can get the error mesasage like:
|
||||
|
||||
```crystal
|
||||
```crystal
|
||||
error 500 do |env, err|
|
||||
err.message
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
- Update `multipart.cr` to `v0.1.1`
|
||||
|
||||
# 0.16.1 (12-10-2016)
|
||||
|
||||
- Improved Multipart support with more info on parsed files. `parse_multipart(env)` now yields
|
||||
an `UploadFile` object which has the following properties `field`,`data`,`meta`,`headers.
|
||||
- Improved Multipart support with more info on parsed files. `parse_multipart(env)` now yields an `UploadFile` object which has the following properties: `field`, `data`, `meta` and `headers`.
|
||||
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
parse_multipart(env) do |f|
|
||||
image1 = f.data if f.field == "image1"
|
||||
image2 = f.data if f.field == "image2"
|
||||
puts f.meta
|
||||
puts f.headers
|
||||
"Upload complete"
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
parse_multipart(env) do |f|
|
||||
image1 = f.data if f.field == "image1"
|
||||
image2 = f.data if f.field == "image2"
|
||||
puts f.meta
|
||||
puts f.headers
|
||||
"Upload complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
# 0.16.0
|
||||
|
||||
- Multipart support <3 (thanks @RX14). Now you can handle file uploads.
|
||||
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
parse_multipart(env) do |field, data|
|
||||
image1 = data if field == "image1"
|
||||
image2 = data if field == "image2"
|
||||
"Upload complete"
|
||||
```crystal
|
||||
post "/upload" do |env|
|
||||
parse_multipart(env) do |field, data|
|
||||
image1 = data if field == "image1"
|
||||
image2 = data if field == "image2"
|
||||
"Upload complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
```
|
||||
|
||||
- Make session configurable. Now you can specify session name and expire time wit
|
||||
- Make session configurable. Now you can specify session name and expire time with:
|
||||
|
||||
```crystal
|
||||
Kemal.config.session["name"] = "your_app"
|
||||
Kemal.config.session["expire_time"] = 48.hours
|
||||
```
|
||||
```crystal
|
||||
Kemal.config.session["name"] = "your_app"
|
||||
Kemal.config.session["expire_time"] = 48.hours
|
||||
```
|
||||
|
||||
- Session now supports more types. (String, Int32, Float64, Bool)
|
||||
- Session now supports more types. (`String`, `Int32`, `Float64`, `Bool`)
|
||||
- Add `gzip` helper to enable / disable gzip compression on responses.
|
||||
- Static file caching with etag and gzip (thanks @crisward)
|
||||
- `Kemal.run` now accepts port to listen.
|
||||
|
||||
# 0.15.1 (05-09-2016)
|
||||
|
||||
- Don't forget to call_next on NullLogHandler
|
||||
- Don't forget to `call_next` on `NullLogHandler`
|
||||
|
||||
# 0.15.0 (03-09-2016)
|
||||
|
||||
- Add context store
|
||||
- Add context store.
|
||||
- `KEMAL_ENV` respects to `Kemal.config.env` and needs to be explicitly set.
|
||||
- `Kemal::InitHandler` is introduced. Adds initial configuration, headers like `X-Powered-By`.
|
||||
- Add `send_file` to helpers.
|
||||
- Add mime types.
|
||||
- Fix parsing JSON params when "charset" is present in "Content-Type" header.
|
||||
- Use http-only cookie for session
|
||||
- Inject STDOUT by default in CommonLogHandler
|
||||
- Fix parsing JSON params when "charset" is present in `Content-Type` header.
|
||||
- Use http-only cookie for session.
|
||||
- Inject `STDOUT` by default in `CommonLogHandler`.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: kemal
|
||||
version: 1.2.0
|
||||
version: 1.6.0
|
||||
|
||||
authors:
|
||||
- Serdar Dogruyol <dogruyolserdar@gmail.com>
|
||||
|
@ -10,12 +10,11 @@ dependencies:
|
|||
version: ~> 0.4.0
|
||||
exception_page:
|
||||
github: crystal-loot/exception_page
|
||||
version: ~> 0.2.0
|
||||
version: ~> 0.5.0
|
||||
|
||||
development_dependencies:
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.0
|
||||
|
||||
crystal: ">= 0.36.0"
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ describe "Config" do
|
|||
end
|
||||
|
||||
it "sets default powered_by_header to true" do
|
||||
Kemal::Config.new.powered_by_header.should be_true
|
||||
Kemal::Config.new.powered_by_header?.should be_true
|
||||
end
|
||||
|
||||
it "sets host binding" do
|
||||
|
@ -29,7 +29,7 @@ describe "Config" do
|
|||
config = Kemal.config
|
||||
config.add_handler CustomTestHandler.new
|
||||
Kemal.config.setup
|
||||
config.handlers.size.should eq(7)
|
||||
config.handlers.size.should eq(8)
|
||||
end
|
||||
|
||||
it "toggles the shutdown message" do
|
||||
|
|
37
spec/filters_spec.cr
Normal file
37
spec/filters_spec.cr
Normal file
|
@ -0,0 +1,37 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe "Kemal::FilterHandler" do
|
||||
it "handles with upcased 'POST'" do
|
||||
filter_handler = Kemal::FilterHandler.new
|
||||
filter_handler._add_route_filter("POST", "*", :before) do |env|
|
||||
env.set "sensitive", "1"
|
||||
end
|
||||
Kemal.config.add_filter_handler(filter_handler)
|
||||
|
||||
post "/sensitive_post" do |env|
|
||||
env.get "sensitive"
|
||||
end
|
||||
|
||||
request = HTTP::Request.new("POST", "/sensitive_post")
|
||||
client_response = call_request_on_app(request)
|
||||
client_response.status_code.should eq(200)
|
||||
client_response.body.should eq("1")
|
||||
end
|
||||
|
||||
it "handles with downcased 'post'" do
|
||||
filter_handler = Kemal::FilterHandler.new
|
||||
filter_handler._add_route_filter("POST", "*", :before) do |env|
|
||||
env.set "sensitive", "1"
|
||||
end
|
||||
Kemal.config.add_filter_handler(filter_handler)
|
||||
|
||||
post "/sensitive_post" do
|
||||
"sensitive"
|
||||
end
|
||||
|
||||
request = HTTP::Request.new("post", "/sensitive_post")
|
||||
client_response = call_request_on_app(request)
|
||||
client_response.status_code.should eq(200)
|
||||
client_response.body.should eq("")
|
||||
end
|
||||
end
|
37
spec/head_request_handler_spec.cr
Normal file
37
spec/head_request_handler_spec.cr
Normal file
|
@ -0,0 +1,37 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe "Kemal::HeadRequestHandler" do
|
||||
it "implicitly handles GET endpoints, with Content-Length header" do
|
||||
get "/" do
|
||||
"hello"
|
||||
end
|
||||
request = HTTP::Request.new("HEAD", "/")
|
||||
client_response = call_request_on_app(request)
|
||||
client_response.body.should eq("")
|
||||
client_response.headers["Content-Length"].should eq("5")
|
||||
end
|
||||
|
||||
it "prefers explicit HEAD endpoint if specified" do
|
||||
Kemal::RouteHandler::INSTANCE.add_route("HEAD", "/") { "hello" }
|
||||
get "/" do
|
||||
raise "shouldn't be called!"
|
||||
end
|
||||
request = HTTP::Request.new("HEAD", "/")
|
||||
client_response = call_request_on_app(request)
|
||||
client_response.body.should eq("")
|
||||
client_response.headers["Content-Length"].should eq("5")
|
||||
end
|
||||
|
||||
it "gives compressed Content-Length when gzip enabled" do
|
||||
gzip true
|
||||
get "/" do
|
||||
"hello"
|
||||
end
|
||||
headers = HTTP::Headers{"Accept-Encoding" => "gzip"}
|
||||
request = HTTP::Request.new("HEAD", "/", headers)
|
||||
client_response = call_request_on_app(request)
|
||||
client_response.body.should eq("")
|
||||
client_response.headers["Content-Encoding"].should eq("gzip")
|
||||
client_response.headers["Content-Length"].should eq("25")
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ describe "Macros" do
|
|||
it "adds a custom handler" do
|
||||
add_handler CustomTestHandler.new
|
||||
Kemal.config.setup
|
||||
Kemal.config.handlers.size.should eq 7
|
||||
Kemal.config.handlers.size.should eq 8
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -106,6 +106,7 @@ describe "Macros" do
|
|||
request = HTTP::Request.new("GET", "/")
|
||||
response = call_request_on_app(request)
|
||||
response.status_code.should eq(200)
|
||||
|
||||
response.headers["Content-Type"].should eq("application/octet-stream")
|
||||
response.headers["Content-Length"].should eq("18")
|
||||
end
|
||||
|
@ -150,7 +151,7 @@ describe "Macros" do
|
|||
it "adds HTTP::CompressHandler to handlers" do
|
||||
gzip true
|
||||
Kemal.config.setup
|
||||
Kemal.config.handlers[4].should be_a(HTTP::CompressHandler)
|
||||
Kemal.config.handlers[5].should be_a(HTTP::CompressHandler)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,11 +6,23 @@ describe "Kemal::InitHandler" do
|
|||
io = IO::Memory.new
|
||||
response = HTTP::Server::Response.new(io)
|
||||
context = HTTP::Server::Context.new(request, response)
|
||||
Kemal::InitHandler::INSTANCE.next = ->(_context : HTTP::Server::Context) {}
|
||||
Kemal::InitHandler::INSTANCE.next = ->(_context : HTTP::Server::Context) { }
|
||||
Kemal::InitHandler::INSTANCE.call(context)
|
||||
context.response.headers["Content-Type"].should eq "text/html"
|
||||
end
|
||||
|
||||
it "initializes context with Date header" do
|
||||
request = HTTP::Request.new("GET", "/")
|
||||
io = IO::Memory.new
|
||||
response = HTTP::Server::Response.new(io)
|
||||
context = HTTP::Server::Context.new(request, response)
|
||||
Kemal::InitHandler::INSTANCE.next = ->(_context : HTTP::Server::Context) { }
|
||||
Kemal::InitHandler::INSTANCE.call(context)
|
||||
date = context.response.headers["Date"]?.should_not be_nil
|
||||
date = HTTP.parse_time(date).should_not be_nil
|
||||
date.should be_close(Time.utc, 1.second)
|
||||
end
|
||||
|
||||
it "initializes context with X-Powered-By: Kemal" do
|
||||
request = HTTP::Request.new("GET", "/")
|
||||
io = IO::Memory.new
|
||||
|
|
29
spec/override_method_handler_spec.cr
Normal file
29
spec/override_method_handler_spec.cr
Normal file
|
@ -0,0 +1,29 @@
|
|||
require "./spec_helper"
|
||||
|
||||
describe "Kemal::OverrideMethodHandler" do
|
||||
it "does not override method without _method for POST requests" do
|
||||
request = HTTP::Request.new(
|
||||
"POST",
|
||||
"/",
|
||||
body: "_not_method=PATCH",
|
||||
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
)
|
||||
|
||||
context = create_request_and_return_io_and_context(Kemal::OverrideMethodHandler::INSTANCE, request)[1]
|
||||
|
||||
context.request.method.should eq "POST"
|
||||
end
|
||||
|
||||
it "overrides method with _method for POST requests" do
|
||||
request = HTTP::Request.new(
|
||||
"POST",
|
||||
"/",
|
||||
body: "_method=PATCH",
|
||||
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
)
|
||||
|
||||
context = create_request_and_return_io_and_context(Kemal::OverrideMethodHandler::INSTANCE, request)[1]
|
||||
|
||||
context.request.method.should eq "PATCH"
|
||||
end
|
||||
end
|
|
@ -26,7 +26,7 @@ describe "Run" do
|
|||
end
|
||||
|
||||
it "runs without a block being specified" do
|
||||
run(<<-CR).should eq "[test] Kemal is ready to lead at http://0.0.0.0:3000\ntrue\n"
|
||||
run(<<-CR).should contain "[test] Kemal is running in test mode."
|
||||
Kemal.config.env = "test"
|
||||
Kemal.run
|
||||
puts Kemal.config.running
|
||||
|
@ -34,12 +34,17 @@ describe "Run" do
|
|||
end
|
||||
|
||||
it "allows custom HTTP::Server bind" do
|
||||
run(<<-CR).should eq "[test] Kemal is ready to lead at http://127.0.0.1:3000, http://0.0.0.0:3001\n"
|
||||
run(<<-CR).should contain "[test] Kemal is running in test mode."
|
||||
Kemal.config.env = "test"
|
||||
Kemal.run do |config|
|
||||
server = config.server.not_nil!
|
||||
server.bind_tcp "127.0.0.1", 3000, reuse_port: true
|
||||
server.bind_tcp "0.0.0.0", 3001, reuse_port: true
|
||||
|
||||
{% if flag?(:windows) %}
|
||||
server.bind_tcp "127.0.0.1", 3000
|
||||
{% else %}
|
||||
server.bind_tcp "127.0.0.1", 3000, reuse_port: true
|
||||
server.bind_tcp "0.0.0.0", 3001, reuse_port: true
|
||||
{% end %}
|
||||
end
|
||||
CR
|
||||
end
|
||||
|
|
|
@ -85,6 +85,7 @@ end
|
|||
|
||||
Spec.after_each do
|
||||
Kemal.config.clear
|
||||
Kemal::FilterHandler::INSTANCE.tree = Radix::Tree(Array(Kemal::FilterHandler::FilterBlock)).new
|
||||
Kemal::RouteHandler::INSTANCE.routes = Radix::Tree(Route).new
|
||||
Kemal::RouteHandler::INSTANCE.cached_routes = Hash(String, Radix::Result(Route)).new
|
||||
Kemal::WebSocketHandler::INSTANCE.routes = Radix::Tree(WebSocket).new
|
||||
|
|
|
@ -141,11 +141,11 @@ describe Kemal::StaticFileHandler do
|
|||
end
|
||||
|
||||
it "should handle setting custom headers" do
|
||||
headers = Proc(HTTP::Server::Response, String, File::Info, Void).new do |response, path, stat|
|
||||
headers = Proc(HTTP::Server::Context, String, File::Info, Void).new do |env, path, stat|
|
||||
if path =~ /\.html$/
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
end
|
||||
response.headers.add("Content-Size", stat.size.to_s)
|
||||
env.response.headers.add("Content-Size", stat.size.to_s)
|
||||
end
|
||||
|
||||
static_headers(&headers)
|
||||
|
|
|
@ -38,7 +38,7 @@ describe "Kemal::WebSocketHandler" do
|
|||
|
||||
it "fetches named url parameters" do
|
||||
handler = Kemal::WebSocketHandler::INSTANCE
|
||||
ws "/:id" { |_, c| c.ws_route_lookup.params["id"] }
|
||||
ws "/:id" { |_, context| context.ws_route_lookup.params["id"] }
|
||||
headers = HTTP::Headers{
|
||||
"Upgrade" => "websocket",
|
||||
"Connection" => "Upgrade",
|
||||
|
|
12
src/kemal.cr
12
src/kemal.cr
|
@ -27,7 +27,7 @@ module Kemal
|
|||
#
|
||||
# To use custom command line arguments, set args to nil
|
||||
#
|
||||
def self.run(port : Int32? = nil, args = ARGV, trap_signal : Bool = true, &block)
|
||||
def self.run(port : Int32? = nil, args = ARGV, trap_signal : Bool = true, &)
|
||||
Kemal::CLI.new args
|
||||
config = Kemal.config
|
||||
config.setup
|
||||
|
@ -66,8 +66,12 @@ module Kemal
|
|||
end
|
||||
|
||||
def self.display_startup_message(config, server)
|
||||
addresses = server.addresses.join ", " { |address| "#{config.scheme}://#{address}" }
|
||||
log "[#{config.env}] #{config.app_name} is ready to lead at #{addresses}"
|
||||
if config.env != "test"
|
||||
addresses = server.addresses.join ", " { |address| "#{config.scheme}://#{address}" }
|
||||
log "[#{config.env}] #{config.app_name} is ready to lead at #{addresses}"
|
||||
else
|
||||
log "[#{config.env}] #{config.app_name} is running in test mode. Server not listening"
|
||||
end
|
||||
end
|
||||
|
||||
def self.stop
|
||||
|
@ -89,7 +93,7 @@ module Kemal
|
|||
end
|
||||
|
||||
private def self.setup_trap_signal
|
||||
Signal::INT.trap do
|
||||
Process.on_terminate do
|
||||
log "#{Kemal.config.app_name} is going to take a rest!" if Kemal.config.shutdown_message
|
||||
Kemal.stop
|
||||
exit
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module Kemal
|
||||
VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }}
|
||||
VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }}
|
||||
|
||||
# Stores all the configuration options for a Kemal application.
|
||||
# It's a singleton and you can access it like.
|
||||
|
@ -20,11 +20,11 @@ module Kemal
|
|||
@ssl : OpenSSL::SSL::Context::Server?
|
||||
{% end %}
|
||||
|
||||
property host_binding, ssl, port, env, public_folder, logging, running
|
||||
property app_name, host_binding, ssl, port, env, public_folder, logging, running
|
||||
property always_rescue, server : HTTP::Server?, extra_options, shutdown_message
|
||||
property serve_static : (Bool | Hash(String, Bool))
|
||||
property static_headers : (HTTP::Server::Response, String, File::Info -> Void)?
|
||||
property powered_by_header : Bool = true, app_name
|
||||
property static_headers : (HTTP::Server::Context, String, File::Info -> Void)?
|
||||
property? powered_by_header : Bool = true
|
||||
|
||||
def initialize
|
||||
@app_name = "Kemal"
|
||||
|
@ -103,6 +103,7 @@ module Kemal
|
|||
unless @default_handlers_setup && @router_included
|
||||
setup_init_handler
|
||||
setup_log_handler
|
||||
setup_head_request_handler
|
||||
setup_error_handler
|
||||
setup_static_file_handler
|
||||
setup_custom_handlers
|
||||
|
@ -129,6 +130,11 @@ module Kemal
|
|||
@handler_position += 1
|
||||
end
|
||||
|
||||
private def setup_head_request_handler
|
||||
HANDLERS.insert(@handler_position, Kemal::HeadRequestHandler::INSTANCE)
|
||||
@handler_position += 1
|
||||
end
|
||||
|
||||
private def setup_error_handler
|
||||
if @always_rescue
|
||||
@error_handler ||= Kemal::ExceptionHandler.new
|
||||
|
@ -153,13 +159,13 @@ module Kemal
|
|||
end
|
||||
|
||||
private def setup_filter_handlers
|
||||
FILTER_HANDLERS.each do |h|
|
||||
HANDLERS.insert(@handler_position, h)
|
||||
FILTER_HANDLERS.each do |handler|
|
||||
HANDLERS.insert(@handler_position, handler)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.config
|
||||
def self.config(&)
|
||||
yield Config::INSTANCE
|
||||
end
|
||||
|
||||
|
|
|
@ -9,16 +9,20 @@ class HTTP::Server
|
|||
STORE_MAPPINGS = [Nil, String, Int32, Int64, Float64, Bool]
|
||||
|
||||
macro finished
|
||||
alias StoreTypes = Union({{ *STORE_MAPPINGS }})
|
||||
alias StoreTypes = Union({{ STORE_MAPPINGS.splat }})
|
||||
@store = {} of String => StoreTypes
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= Kemal::ParamParser.new(@request, route_lookup.params)
|
||||
if ws_route_found?
|
||||
@params ||= Kemal::ParamParser.new(@request, ws_route_lookup.params)
|
||||
else
|
||||
@params ||= Kemal::ParamParser.new(@request, route_lookup.params)
|
||||
end
|
||||
end
|
||||
|
||||
def redirect(url : String, status_code : Int32 = 302, *, body : String? = nil, close : Bool = true)
|
||||
@response.headers.add "Location", url
|
||||
def redirect(url : String | URI, status_code : Int32 = 302, *, body : String? = nil, close : Bool = true)
|
||||
@response.headers.add "Location", url.to_s
|
||||
@response.status_code = status_code
|
||||
@response.print(body) if body
|
||||
@response.close if close
|
||||
|
|
|
@ -3,6 +3,7 @@ module Kemal
|
|||
class FilterHandler
|
||||
include HTTP::Handler
|
||||
INSTANCE = new
|
||||
property tree
|
||||
|
||||
# This middleware is lazily instantiated and added to the handlers as soon as a call to `after_X` or `before_X` is made.
|
||||
def initialize
|
||||
|
|
|
@ -11,19 +11,17 @@ module Kemal
|
|||
|
||||
macro only(paths, method = "GET")
|
||||
class_name = {{@type.name}}
|
||||
method_downcase = {{method.downcase}}
|
||||
class_name_method = "#{class_name}/#{method_downcase}"
|
||||
class_name_method = "#{class_name}/#{{{method}}}"
|
||||
({{paths}}).each do |path|
|
||||
@@only_routes_tree.add class_name_method + path, '/' + method_downcase + path
|
||||
@@only_routes_tree.add class_name_method + path, '/' + {{method}} + path
|
||||
end
|
||||
end
|
||||
|
||||
macro exclude(paths, method = "GET")
|
||||
class_name = {{@type.name}}
|
||||
method_downcase = {{method.downcase}}
|
||||
class_name_method = "#{class_name}/#{method_downcase}"
|
||||
class_name_method = "#{class_name}/#{{{method}}}"
|
||||
({{paths}}).each do |path|
|
||||
@@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path
|
||||
@@exclude_routes_tree.add class_name_method + path, '/' + {{method}} + path
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -74,7 +72,7 @@ module Kemal
|
|||
end
|
||||
|
||||
private def radix_path(method : String, path : String)
|
||||
"#{self.class}/#{method.downcase}#{path}"
|
||||
"#{self.class}/#{method}#{path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
60
src/kemal/head_request_handler.cr
Normal file
60
src/kemal/head_request_handler.cr
Normal file
|
@ -0,0 +1,60 @@
|
|||
require "http/server/handler"
|
||||
|
||||
module Kemal
|
||||
class HeadRequestHandler
|
||||
include HTTP::Handler
|
||||
|
||||
INSTANCE = new
|
||||
|
||||
private class NullIO < IO
|
||||
@original_output : IO
|
||||
@out_count : Int32
|
||||
@response : HTTP::Server::Response
|
||||
|
||||
def initialize(@response)
|
||||
@closed = false
|
||||
@original_output = @response.output
|
||||
@out_count = 0
|
||||
end
|
||||
|
||||
def read(slice : Bytes)
|
||||
raise NotImplementedError.new("read")
|
||||
end
|
||||
|
||||
def write(slice : Bytes) : Nil
|
||||
@out_count += slice.bytesize
|
||||
end
|
||||
|
||||
def close : Nil
|
||||
return if @closed
|
||||
@closed = true
|
||||
|
||||
# Matching HTTP::Server::Response#close behavior:
|
||||
# Conditionally determine based on status if the `content-length` header should be added automatically.
|
||||
# See https://tools.ietf.org/html/rfc7230#section-3.3.2.
|
||||
status = @response.status
|
||||
set_content_length = !(status.not_modified? || status.no_content? || status.informational?)
|
||||
|
||||
if !@response.headers.has_key?("Content-Length") && set_content_length
|
||||
@response.content_length = @out_count
|
||||
end
|
||||
|
||||
@original_output.close
|
||||
end
|
||||
|
||||
def closed? : Bool
|
||||
@closed
|
||||
end
|
||||
end
|
||||
|
||||
def call(context) : Nil
|
||||
if context.request.method == "HEAD"
|
||||
# Capture and count bytes of response body generated on HEAD requests without actually sending the body back.
|
||||
capture_io = NullIO.new(context.response)
|
||||
context.response.output = capture_io
|
||||
end
|
||||
|
||||
call_next(context)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,8 +13,9 @@ module Kemal::Exceptions
|
|||
end
|
||||
|
||||
class CustomException < Exception
|
||||
def initialize(context : HTTP::Server::Context)
|
||||
super "Rendered error with #{context.response.status_code}"
|
||||
def initialize(@context : HTTP::Server::Context, message : String? = nil)
|
||||
message ||= "Rendered error with #{context.response.status_code}"
|
||||
super message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %}
|
||||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 && !flag?(:without_zlib) %}
|
||||
require "compress/deflate"
|
||||
require "compress/gzip"
|
||||
{% end %}
|
||||
require "mime"
|
||||
|
||||
# Adds given `Kemal::Handler` to handlers chain.
|
||||
# There are 5 handlers by default and all the custom handlers
|
||||
# goes between the first 4 and the last `Kemal::RouteHandler`.
|
||||
# There are 6 handlers by default and all the custom handlers
|
||||
# goes between the first 5 and the last `Kemal::RouteHandler`.
|
||||
#
|
||||
# - `Kemal::InitHandler`
|
||||
# - `Kemal::LogHandler`
|
||||
# - `Kemal::HeadRequestHandler`
|
||||
# - `Kemal::ExceptionHandler`
|
||||
# - `Kemal::StaticFileHandler`
|
||||
# - Here goes custom handlers
|
||||
|
@ -48,13 +49,13 @@ end
|
|||
# This is used to replace the built-in `Kemal::LogHandler` with a custom logger.
|
||||
#
|
||||
# A custom logger must inherit from `Kemal::BaseLogHandler` and must implement
|
||||
# `call(env)`, `write(message)` methods.
|
||||
# `call(context)`, `write(message)` methods.
|
||||
#
|
||||
# ```
|
||||
# class MyCustomLogger < Kemal::BaseLogHandler
|
||||
# def call(env)
|
||||
# def call(context)
|
||||
# puts "I'm logging some custom stuff here."
|
||||
# call_next(env) # => This calls the next handler
|
||||
# call_next(context) # => This calls the next handler
|
||||
# end
|
||||
#
|
||||
# # This is used from `log` method.
|
||||
|
@ -71,7 +72,6 @@ end
|
|||
# ```
|
||||
def logger(logger : Kemal::BaseLogHandler)
|
||||
Kemal.config.logger = logger
|
||||
Kemal.config.add_handler logger
|
||||
end
|
||||
|
||||
# Enables / Disables static file serving.
|
||||
|
@ -134,40 +134,45 @@ def send_file(env : HTTP::Server::Context, path : String, mime_type : String? =
|
|||
filestat = File.info(file_path)
|
||||
attachment(env, filename, disposition)
|
||||
|
||||
Kemal.config.static_headers.try(&.call(env.response, file_path, filestat))
|
||||
Kemal.config.static_headers.try(&.call(env, file_path, filestat))
|
||||
|
||||
File.open(file_path) do |file|
|
||||
if env.request.method == "GET" && env.request.headers.has_key?("Range")
|
||||
next multipart(file, env)
|
||||
end
|
||||
|
||||
condition = config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && Kemal::Utils.zip_types(file_path)
|
||||
if condition && request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %}
|
||||
Compress::Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% else %}
|
||||
Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% end %}
|
||||
elsif condition && request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %}
|
||||
Compress::Deflate::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% else %}
|
||||
Flate::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% end %}
|
||||
else
|
||||
{% if flag?(:without_zlib) %}
|
||||
env.response.content_length = filesize
|
||||
IO.copy(file, env.response)
|
||||
end
|
||||
{% else %}
|
||||
condition = config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && Kemal::Utils.zip_types(file_path)
|
||||
if condition && request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %}
|
||||
Compress::Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% else %}
|
||||
Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% end %}
|
||||
elsif condition && request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %}
|
||||
Compress::Deflate::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% else %}
|
||||
Flate::Writer.open(env.response) do |deflate|
|
||||
IO.copy(file, deflate)
|
||||
end
|
||||
{% end %}
|
||||
else
|
||||
env.response.content_length = filesize
|
||||
IO.copy(file, env.response)
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
return
|
||||
end
|
||||
|
@ -245,13 +250,13 @@ end
|
|||
# Adds headers to `Kemal::StaticFileHandler`. This is especially useful for `CORS`.
|
||||
#
|
||||
# ```
|
||||
# static_headers do |response, filepath, filestat|
|
||||
# static_headers do |env, filepath, filestat|
|
||||
# if filepath =~ /\.html$/
|
||||
# response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
# env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
# end
|
||||
# response.headers.add("Content-Size", filestat.size.to_s)
|
||||
# env.response.headers.add("Content-Size", filestat.size.to_s)
|
||||
# end
|
||||
# ```
|
||||
def static_headers(&headers : HTTP::Server::Response, String, File::Info -> Void)
|
||||
def static_headers(&headers : HTTP::Server::Context, String, File::Info -> Void)
|
||||
Kemal.config.static_headers = headers
|
||||
end
|
||||
|
|
|
@ -26,7 +26,7 @@ def render_500(context, exception, verbosity)
|
|||
context.response.status_code = 500
|
||||
|
||||
template = if verbosity
|
||||
Kemal::ExceptionPage.for_runtime_exception(context, exception).to_s
|
||||
Kemal::ExceptionPage.new(context, exception).to_s
|
||||
else
|
||||
Kemal::ExceptionPage.for_production_exception
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require "http"
|
||||
|
||||
module Kemal
|
||||
# Initializes the context with default values, such as
|
||||
# *Content-Type* or *X-Powered-By* headers.
|
||||
|
@ -7,8 +9,9 @@ module Kemal
|
|||
INSTANCE = new
|
||||
|
||||
def call(context : HTTP::Server::Context)
|
||||
context.response.headers.add "X-Powered-By", "Kemal" if Kemal.config.powered_by_header
|
||||
context.response.headers.add "X-Powered-By", "Kemal" if Kemal.config.powered_by_header?
|
||||
context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type")
|
||||
context.response.headers.add "Date", HTTP.format_time(Time.utc)
|
||||
call_next context
|
||||
end
|
||||
end
|
||||
|
|
33
src/kemal/override_method_handler.cr
Normal file
33
src/kemal/override_method_handler.cr
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Kemal
|
||||
# Adds support for `_method` magic parameter to simulate PUT, PATCH, DELETE requests in an html form.
|
||||
#
|
||||
# This middleware is **not** in the default Kemal handlers. You need to explicitly add this to your handlers:
|
||||
#
|
||||
# ```ruby
|
||||
# add_handler Kemal::OverrideMethodHandler
|
||||
# ```
|
||||
#
|
||||
# **Important:** This middleware consumes `params.body` to read the `_method` magic parameter.
|
||||
class OverrideMethodHandler
|
||||
include HTTP::Handler
|
||||
INSTANCE = new
|
||||
|
||||
ALLOWED_METHODS = ["PUT", "PATCH", "DELETE"]
|
||||
OVERRIDE_METHOD = "POST"
|
||||
OVERRIDE_METHOD_PARAM_KEY = "_method"
|
||||
|
||||
def call(context)
|
||||
request = context.request
|
||||
if request.method == OVERRIDE_METHOD
|
||||
if context.params.body.has_key?(OVERRIDE_METHOD_PARAM_KEY) && override_method_valid?(context.params.body[OVERRIDE_METHOD_PARAM_KEY])
|
||||
request.method = context.params.body["_method"].upcase
|
||||
end
|
||||
end
|
||||
call_next(context)
|
||||
end
|
||||
|
||||
private def override_method_valid?(override_method : String)
|
||||
ALLOWED_METHODS.includes?(override_method.upcase)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,11 +17,9 @@ module Kemal
|
|||
process_request(context)
|
||||
end
|
||||
|
||||
# Adds a given route to routing tree. As an exception each `GET` route additionaly defines
|
||||
# a corresponding `HEAD` route.
|
||||
# Adds a given route to routing tree.
|
||||
def add_route(method : String, path : String, &handler : HTTP::Server::Context -> _)
|
||||
add_to_radix_tree method, path, Route.new(method, path, &handler)
|
||||
# add_to_radix_tree("HEAD", path, Route.new("HEAD", path) { }) if method == "GET"
|
||||
end
|
||||
|
||||
# Looks up the route from the Radix::Tree for the first time and caches to improve performance.
|
||||
|
@ -34,6 +32,11 @@ module Kemal
|
|||
|
||||
route = @routes.find(lookup_path)
|
||||
|
||||
if verb == "HEAD" && !route.found?
|
||||
# On HEAD requests, implicitly fallback to running the GET handler.
|
||||
route = @routes.find(radix_path("GET", path))
|
||||
end
|
||||
|
||||
if route.found?
|
||||
@cached_routes.clear if @cached_routes.size == CACHED_ROUTES_LIMIT
|
||||
@cached_routes[lookup_path] = route
|
||||
|
@ -57,7 +60,7 @@ module Kemal
|
|||
end
|
||||
|
||||
private def radix_path(method, path)
|
||||
'/' + method.downcase + path
|
||||
'/' + method + path
|
||||
end
|
||||
|
||||
private def add_to_radix_tree(method, path, route)
|
||||
|
|
|
@ -27,7 +27,7 @@ module Kemal
|
|||
return
|
||||
end
|
||||
|
||||
expanded_path = File.expand_path(request_path, "/")
|
||||
expanded_path = request_path
|
||||
is_dir_path = if original_path.ends_with?('/') && !expanded_path.ends_with? '/'
|
||||
expanded_path = expanded_path + '/'
|
||||
true
|
||||
|
@ -57,7 +57,7 @@ module Kemal
|
|||
end
|
||||
send_file(context, file_path)
|
||||
elsif config.is_a?(Hash) && config.fetch("dir_listing", false)
|
||||
context.response.content_type = "text/html"
|
||||
context.response.content_type = "text/html; charset=utf-8"
|
||||
directory_listing(context.response, request_path, file_path)
|
||||
else
|
||||
call_next(context)
|
||||
|
|
Loading…
Reference in a new issue