├── .gitignore ├── .travis.yml ├── Guardfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── bin ├── .keep └── shards ├── example ├── config │ ├── .gitkeep │ ├── application.cr │ ├── environment.cr │ ├── routes.cr │ └── secrets.yml ├── log │ └── .gitkeep ├── public │ ├── .gitkeep │ ├── favicon.ico │ └── robots.txt ├── server.cr ├── src │ ├── .gitkeep │ ├── assets │ │ └── javascripts │ │ │ └── application.js │ ├── controllers │ │ ├── application_controller.cr │ │ ├── redirections_controller.cr │ │ └── welcome_controller.cr │ └── views │ │ ├── application │ │ ├── index.html.ecr │ │ └── new.html.ecr │ │ └── layouts │ │ └── application.html.ecr └── tmp │ └── .gitkeep ├── shard.yml ├── spec ├── carbon_controller │ └── redirect_spec.cr ├── carbon_dispatch │ ├── cookies_spec.cr │ ├── flash_spec.cr │ ├── request_spec.cr │ ├── route_spec.cr │ └── router_spec.cr ├── carbon_support │ ├── callbacks_spec.cr │ ├── core_ext │ │ └── string_ext_spec.cr │ ├── key_generator_spec.cr │ ├── log_subscriber_spec.cr │ ├── message_encryptor_spec.cr │ ├── message_verifier_spec.cr │ ├── notifications │ │ ├── evented_and_timed_spec.cr │ │ └── instrumenter_spec.cr │ └── notifications_spec.cr ├── carbon_view │ ├── base_spec.cr │ └── helpers │ │ └── tag_helper_spec.cr ├── lib │ ├── file_string_spec.cr │ └── http_util_spec.cr ├── spec_helper.cr └── support │ └── mock_request.cr └── src ├── LICENSE ├── all.cr ├── carbon.cr ├── carbon ├── application.cr └── version.cr ├── carbon_controller ├── abstract.cr ├── abstract │ └── callbacks.cr ├── base.cr ├── implicit_render.cr ├── log_subscriber.cr ├── metal.cr └── metal │ ├── cookies.cr │ ├── exceptions.cr │ ├── flash.cr │ ├── head.cr │ ├── instrumentation.cr │ ├── redirect.cr │ └── session.cr ├── carbon_dispatch ├── body_proxy.cr ├── environment.cr ├── handler.cr ├── middleware.cr ├── middleware │ ├── cookies.cr │ ├── flash.cr │ ├── head.cr │ ├── logger.cr │ ├── request_id.cr │ ├── runtime.cr │ ├── sendfile.cr │ ├── session.cr │ ├── show_exceptions.cr │ ├── stack.cr │ ├── static.cr │ └── templates │ │ └── exception.html.ecr ├── request.cr ├── request │ └── session.cr ├── response.cr ├── route.cr └── router.cr ├── carbon_support ├── callbacks.cr ├── callbacks │ ├── callback.cr │ ├── chain.cr │ ├── environment.cr │ └── sequence.cr ├── core_ext │ ├── object │ │ └── blank.cr │ └── string │ │ └── output_safety.cr ├── key_generator.cr ├── log_subscriber.cr ├── message_encryptor.cr ├── message_verifier.cr ├── notifications.cr ├── notifications │ ├── event.cr │ ├── fanout.cr │ ├── instrumenter.cr │ └── payload.cr ├── notifier.cr └── subscriber.cr ├── carbon_view ├── base.cr ├── buffers.cr ├── context.cr ├── helpers.cr ├── helpers │ ├── asset_tag_helper.cr │ ├── capture_helper.cr │ ├── output_safety_helper.cr │ └── tag_helper.cr ├── layout.cr ├── partial.cr ├── process.cr └── view.cr ├── command.cr ├── command ├── generator.cr ├── generators │ └── view.cr ├── helper.cr ├── new_app.cr └── new_app │ └── template │ ├── application.cr.ecr │ ├── application.html.ecr │ ├── application_controller.cr.ecr │ ├── environment.cr.ecr │ ├── gitignore.ecr │ ├── guardfile.ecr │ ├── robots.txt.ecr │ ├── routes.cr.ecr │ ├── server.cr.ecr │ ├── shard.yml.ecr │ └── welcome.html.ecr ├── lib ├── file_string.cr └── http_util.cr └── shard.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | .crystal/ 4 | /.shards/ 5 | example/server 6 | / 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | /worksheet.cr 11 | carbon 12 | .idea/ 13 | Gemfile 14 | Gemfile.lock 15 | middlewares.txt 16 | test.log 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | crystal: 3 | - latest 4 | - nightly 5 | matrix: 6 | allow_failures: 7 | - crystal: nightly 8 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'process', :name => 'Spec', :command => 'crystal spec' do 2 | watch(/spec\/(.*).cr$/) 3 | watch(/src\/(.*).cr$/) 4 | watch(/example\/(.*).cr$/) 5 | end 6 | 7 | guard 'process', :name => 'Build', :command => 'crystal build server.cr', dir: "example" do 8 | watch(/src\/(.*).cr$/) 9 | watch(/example\/(.*).cr$/) 10 | end 11 | 12 | guard 'process', :name => 'Server', :command => './server', dir: "example" do 13 | watch('example/server') 14 | end 15 | 16 | guard 'process', :name => 'Worksheet', :command => 'crystal run worksheet.cr' do 17 | watch(/src\/(.*).cr$/) 18 | watch('worksheet.cr') 19 | end 20 | 21 | guard 'process', :name => 'Format', :command => 'crystal tool format' do 22 | watch(/(.*).cr$/) 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Benoist Claassen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #ifndef CRYSTAL_BIN 2 | # CRYSTAL_BIN := $(shell which crystal) 3 | #endif 4 | CRYSTAL_BIN := $(shell which crystal) 5 | 6 | VERSION := $(shell cat VERSION) 7 | OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') 8 | ARCH := $(shell uname -m) 9 | 10 | ifeq ($(OS),linux) 11 | CRFLAGS := --link-flags "-static -L/opt/crystal/embedded/lib" 12 | endif 13 | 14 | ifeq ($(OS),darwin) 15 | CRFLAGS := --link-flags "-L." 16 | endif 17 | 18 | # Builds an unoptimized binary. 19 | all: 20 | $(CRYSTAL_BIN) build -o bin/carbon src/command.cr 21 | 22 | # Builds an optimized static binary ready for distribution. 23 | # 24 | # On OS X the binary is only partially static (it depends on the system's 25 | # dylib), but libyaml should be bundled, unless the linker can find 26 | # libyaml.dylib 27 | release: 28 | if [ "$(OS)" = "darwin" ] ; then \ 29 | cp /usr/local/lib/libyaml.a . ;\ 30 | chmod 644 libyaml.a ;\ 31 | export LIBRARY_PATH= ;\ 32 | fi 33 | $(CRYSTAL_BIN) build --release -o bin/carbon src/command.cr $(CRFLAGS) 34 | gzip -c bin/carbon > carbon-$(VERSION)_$(OS)_$(ARCH).gz 35 | 36 | clean: 37 | rm -rf .crystal bin/carbon 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # carbon 2 | 3 | Carbon Crystal 4 | A framework with Rails in mind. 5 | 6 | ## Status 7 | 8 | [](https://travis-ci.org/benoist/carbon-crystal) 9 | Only works on latest master. To use locally build it from source or on osx use brew install crystal-lang --HEAD. 10 | 11 | Right now it's still alpha stage. I am testing this in production on a small project, but I wouldn't recommend to do it unless you really want to :) 12 | 13 | ## Release goal 14 | 15 | For the first release I'm aiming towards a 15 min blog post screencast. 16 | 17 | ## TODO 18 | 19 | - [X] Notifications (ActiveSupport like) 20 | - [ ] Middleware 21 | - [X] Send file 22 | - [X] Static File 23 | - [X] Runtime 24 | - [X] RequestId 25 | - [X] Logger 26 | - [X] RemoteIP 27 | - [X] Exceptions 28 | - [X] ParamsParser 29 | - [X] Head 30 | - [X] Cookies 31 | - [X] Sessions Cookie Store 32 | - [X] Flash 33 | - [ ] ConditionalGet 34 | - [ ] ETag 35 | - [X] Resourceful routing 36 | - [X] Action filters 37 | - [ ] Conditional 38 | - [X] Halting 39 | - [ ] Generators 40 | - [ ] Asset pipeline 41 | - [ ] View helpers 42 | - [X] Write specs 43 | 44 | ## Contributing 45 | 46 | 1. Fork it ( https://github.com/[your-github-name]/carbon/fork ) 47 | 2. Create your feature branch (git checkout -b my-new-feature) 48 | 3. Commit your changes (git commit -am 'Add some feature') 49 | 4. Push to the branch (git push origin my-new-feature) 50 | 5. Create a new Pull Request 51 | 52 | ## Contributors 53 | 54 | - [benoist](https://github.com/benoist]) Benoist Claassen - creator, maintainer 55 | - [JanDintel](https://github.com/JanDintel]) JanDintel - contributor 56 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /bin/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/bin/.keep -------------------------------------------------------------------------------- /bin/shards: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/bin/shards -------------------------------------------------------------------------------- /example/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/config/.gitkeep -------------------------------------------------------------------------------- /example/config/application.cr: -------------------------------------------------------------------------------- 1 | require "../../src/all" 2 | 3 | module ExampleApp 4 | class Application < Carbon::Application 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/config/environment.cr: -------------------------------------------------------------------------------- 1 | # Load the Carbon application. 2 | require "./application" 3 | 4 | Carbon.root = File.expand_path("../", File.dirname(__FILE__)) 5 | CarbonView.load_views "src/views", "../../src/carbon_view/process" 6 | 7 | require "../src/**" 8 | 9 | require "./routes" 10 | 11 | # Initialize the Carbon application. 12 | Carbon.application.initialize! 13 | -------------------------------------------------------------------------------- /example/config/routes.cr: -------------------------------------------------------------------------------- 1 | Carbon.application.routes.draw do 2 | get "/redirect_to_new", controller: "redirections", action: "to_new" 3 | get "/redirect_to_google", controller: "redirections", action: "to_google" 4 | 5 | get "/new", controller: "welcome", action: "index" 6 | get "/", controller: "application", action: "index" 7 | end 8 | -------------------------------------------------------------------------------- /example/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 90697dff34d283cb16c73f9372764d9c0e2937f84156bd5afcc0d9ce973e6e6fa766ca2913b0c08aeed9601a3bfa7fa4d25ccfc627692173a59ae4c6b2ec0652 15 | 16 | test: 17 | secret_key_base: 816e4ff543c8c231994cfdf5c86fe14550f29eddf49f9f38e18a42f864818080899ef1eb3aa4acfa943252b95f592c867bfdede4aa219c0d9ab63a7b89443d62 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /example/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/log/.gitkeep -------------------------------------------------------------------------------- /example/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/public/.gitkeep -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /example/server.cr: -------------------------------------------------------------------------------- 1 | require "./config/environment" 2 | 3 | Carbon.application.run 4 | -------------------------------------------------------------------------------- /example/src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/src/.gitkeep -------------------------------------------------------------------------------- /example/src/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | console.log("hello from crystal"); 2 | -------------------------------------------------------------------------------- /example/src/controllers/application_controller.cr: -------------------------------------------------------------------------------- 1 | class ApplicationController < CarbonController::Base 2 | layout "application" 3 | 4 | before_action :before 5 | around_action :around 6 | after_action :after2 7 | 8 | def index 9 | @test = session.to_hash.to_json 10 | 11 | render_template "index" 12 | end 13 | 14 | def new 15 | render_json cookies.cookies 16 | end 17 | 18 | def redirect_to_new 19 | redirect_to "/new" 20 | end 21 | 22 | def redirect_to_google 23 | redirect_to "http://www.google.com" 24 | end 25 | 26 | private def before 27 | Carbon.logger.debug "Before action" 28 | end 29 | 30 | private def after2 31 | Carbon.logger.debug "After action" 32 | end 33 | 34 | private def around 35 | Carbon.logger.debug "start" 36 | yield 37 | Carbon.logger.debug "finish" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /example/src/controllers/redirections_controller.cr: -------------------------------------------------------------------------------- 1 | class RedirectionsController < ApplicationController 2 | before_action :redirect 3 | 4 | def to_new 5 | redirect_to "/new" 6 | end 7 | 8 | def to_google 9 | redirect_to "http://www.google.com" 10 | end 11 | 12 | def redirect 13 | Carbon.logger.debug "redirection" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/src/controllers/welcome_controller.cr: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | def index 3 | render_json [""] 4 | end 5 | 6 | def test 7 | render_json [""] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /example/src/views/application/index.html.ecr: -------------------------------------------------------------------------------- 1 |
", "", ""]
138 | _ = "%s %s %s".html_safe % other_string
139 |
140 | other_string.should eq ["
", "", ""]
141 | end
142 |
143 | # it "Concatting a fixnum to safe always yields safe" do
144 | # string = string.html_safe
145 | # string = string.concat(13)
146 | # string.should eq "hello".concat(13)
147 | # session[:]tring.html_safe?.should eq true
148 | # end
149 |
150 | # it "emits normal string yaml" do
151 | # "foo".html_safe.to_yaml(:foo => 1).should eq "foo".to_yaml
152 | # end
153 |
154 | # it "call to_param returns a normal string" do
155 | # string = string.html_safe
156 | # string.html_safe?.should eq true
157 | # string.to_param.html_safe?.should eq false
158 | # end
159 |
160 | it "ERB::Util.html_escape should escape unsafe characters" do
161 | string = "<>&\"'"
162 | expected = "<>&"'"
163 | ECR::Util.html_escape(string).should eq expected
164 | end
165 |
166 | it "ECR::Util.html_escape should correctly handle invalid UTF-8 strings" do
167 | string = "\251 <"
168 | expected = "© <"
169 | ECR::Util.html_escape(string).should eq expected
170 | end
171 |
172 | it "ECR::Util.html_escape should not escape safe strings" do
173 | safe_string = "hello".html_safe
174 | ECR::Util.html_escape(safe_string).should eq "hello"
175 | end
176 |
177 | it "ECR::Util.html_escape_once only escapes once" do
178 | string = "1 < 2 & 3"
179 | escaped_string = "1 < 2 & 3"
180 |
181 | ECR::Util.html_escape_once(string).should eq escaped_string
182 | ECR::Util.html_escape_once(escaped_string).should eq escaped_string
183 | end
184 |
185 | it "ECR::Util.html_escape_once should correctly handle invalid UTF-8 strings" do
186 | string = "\251 <"
187 | expected = "© <"
188 | ECR::Util.html_escape_once(string).should eq expected
189 | end
190 | end
191 | end
192 |
--------------------------------------------------------------------------------
/spec/carbon_support/key_generator_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | module CarbonSupportTest
4 | describe CarbonSupport::KeyGenerator do
5 | secret = SecureRandom.hex(64)
6 | generator = CarbonSupport::KeyGenerator.new(secret, iterations: 2)
7 |
8 | it "Generating a key of the default length" do
9 | derived_key = generator.generate_key("some_salt")
10 | derived_key.should be_a(Slice(UInt8))
11 | derived_key.size.should eq 64
12 | end
13 |
14 | it "Generating a key of an alternative length" do
15 | derived_key = generator.generate_key("some_salt", 32)
16 | derived_key.should be_a(Slice(UInt8))
17 | derived_key.size.should eq 32
18 | end
19 | end
20 |
21 | describe CarbonSupport::KeyGenerator do
22 | secret = SecureRandom.hex(64)
23 | generator = CarbonSupport::KeyGenerator.new(secret, iterations: 2)
24 | caching_generator = CarbonSupport::CachingKeyGenerator.new(generator)
25 |
26 | it "Generating a cached key for same salt and key size" do
27 | derived_key = caching_generator.generate_key("some_salt", 32)
28 | cached_key = caching_generator.generate_key("some_salt", 32)
29 |
30 | cached_key.should eq derived_key
31 | end
32 |
33 | it "Does not cache key for different salt" do
34 | derived_key = caching_generator.generate_key("some_salt", 32)
35 | different_salt_key = caching_generator.generate_key("other_salt", 32)
36 |
37 | derived_key.should_not eq different_salt_key
38 | end
39 |
40 | it "Does not cache key for different length" do
41 | derived_key = caching_generator.generate_key("some_salt", 32)
42 | different_length_key = caching_generator.generate_key("some_salt", 64)
43 |
44 | derived_key.should_not eq different_length_key
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/carbon_support/log_subscriber_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | module CarbonSupportTest
4 | class MyLogSubscriber < CarbonSupport::LogSubscriber
5 | getter :event
6 |
7 | def some_event(event)
8 | @event = event
9 | info event.name
10 | end
11 |
12 | def foo(event)
13 | debug "debug"
14 | info { "info" }
15 | warn "warn"
16 | end
17 |
18 | def bar(event)
19 | info "#{color("cool", :red)}, #{color("isn't it?", :blue, true)}"
20 | end
21 |
22 | def puke(event)
23 | raise "puke"
24 | end
25 | end
26 |
27 | describe CarbonSupport::LogSubscriber do
28 | it "logs with colors" do
29 | IO.pipe do |r, w|
30 | CarbonSupport::LogSubscriber.logger = Logger.new(w).tap do |logger|
31 | logger.level = Logger::Severity::DEBUG
32 | logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io|
33 | io << message
34 | end
35 | end
36 | log_subscriber = MyLogSubscriber.new
37 | log_subscriber.bar(nil)
38 |
39 | r.gets.should eq "\e[31mcool\e[0m, \e[1m\e[34misn't it?\e[0m\n"
40 | end
41 | end
42 |
43 | it "loggs" do
44 | IO.pipe do |r, w|
45 | CarbonSupport::LogSubscriber.logger = Logger.new(w).tap do |logger|
46 | logger.level = Logger::Severity::DEBUG
47 | logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io|
48 | io << message
49 | end
50 | end
51 | log_subscriber = MyLogSubscriber.new
52 | log_subscriber.foo(nil)
53 |
54 | r.gets.should eq "debug\n"
55 | r.gets.should eq "info\n"
56 | r.gets.should eq "warn\n"
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/carbon_support/message_encryptor_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | macro assert_not_decrypted(value)
4 | expect_raises(CarbonSupport::MessageEncryptor::InvalidMessage) do
5 | encryptor.decrypt_and_verify(verifier.generate({{value.id}}))
6 | end
7 | end
8 |
9 | macro assert_not_verified(value)
10 | expect_raises(CarbonSupport::MessageVerifier::InvalidSignature) do
11 | encryptor.decrypt_and_verify({{value.id}})
12 | end
13 | end
14 |
15 | module CarbonSupportTest
16 | secret = SecureRandom.hex(64)
17 | verifier = CarbonSupport::MessageVerifier.new(secret)
18 | encryptor = CarbonSupport::MessageEncryptor.new(secret)
19 | data_hash = {"some" => "data", "now" => Time.new(2010, 1, 1).to_s}.to_json
20 |
21 | describe CarbonSupport::MessageEncryptor do
22 | it "encrypting_twice_yields_differing_cipher_text" do
23 | first_message = encryptor.encrypt_and_sign(data_hash).split("--").first
24 | second_message = encryptor.encrypt_and_sign(data_hash).split("--").first
25 | first_message.should_not eq second_message
26 | end
27 |
28 | it "messing_with_either_encrypted_values_causes_failure" do
29 | text, iv = verifier.verify(encryptor.encrypt_and_sign(data_hash)).split("--")
30 | assert_not_decrypted([iv, text].join "--")
31 | end
32 |
33 | it "messing_with_verified_values_causes_failures" do
34 | text, iv = encryptor.encrypt_and_sign(data_hash).split("--")
35 | assert_not_verified([iv, text].join "--")
36 | end
37 |
38 | it "signed_round_tripping" do
39 | message = encryptor.encrypt_and_sign(data_hash)
40 | String.new(encryptor.decrypt_and_verify(message)).should eq data_hash
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/carbon_support/message_verifier_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | module CarbonSupportTest
4 | verifier = CarbonSupport::MessageVerifier.new("Hey, I'm a secret!")
5 | data_hash = {"some" => "data", "now" => Time.new(2010, 1, 1).epoch}.to_json
6 |
7 | describe CarbonSupport::MessageVerifier do
8 | it "valid_message" do
9 | data, hash = verifier.generate(data_hash).split("--")
10 | verifier.valid_message?(nil).should be_falsey
11 | verifier.valid_message?("").should be_falsey
12 | verifier.valid_message?("#{data.reverse}--#{hash}").should be_falsey
13 | verifier.valid_message?("#{data}--#{hash.reverse}").should be_falsey
14 | verifier.valid_message?("purejunk").should be_falsey
15 | end
16 |
17 | it "simple_round_tripping" do
18 | message = verifier.generate(data_hash)
19 | verifier.verified(message).should eq data_hash
20 | verifier.verify(message).should eq data_hash
21 | end
22 |
23 | it "verified_returns_false_on_invalid_message" do
24 | verifier.verified("purejunk").should be_falsey
25 | end
26 |
27 | it "verify_exception_on_invalid_message" do
28 | expect_raises(CarbonSupport::MessageVerifier::InvalidSignature) do
29 | verifier.verify("purejunk")
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/carbon_support/notifications/evented_and_timed_spec.cr:
--------------------------------------------------------------------------------
1 | require "../../spec_helper"
2 |
3 | module CarbonSupportTest
4 | include CarbonSupport::Notifications
5 |
6 | class Listener < CarbonSupport::Subscriber
7 | getter :events
8 |
9 | def initialize
10 | @events = [] of Event
11 | end
12 |
13 | def start(name, id, payload)
14 | @events << Event.new("start.#{name}", Time.epoch(0), Time.epoch(0), id, payload)
15 | end
16 |
17 | def finish(name, id, payload)
18 | @events << Event.new("finish.#{name}", Time.epoch(0), Time.epoch(0), id, payload)
19 | end
20 |
21 | def call(name, start, finish, id, payload)
22 | @events << Event.new("call.#{name}", start, finish, id, payload)
23 | end
24 | end
25 |
26 | class ListenerWithTimedSupport < Listener
27 | def call(name, start, finish, id, payload)
28 | @events << Event.new("call.#{name}", start, finish, id, payload)
29 | end
30 | end
31 |
32 | it "listens for evented events" do
33 | notifier = Fanout.new
34 | listener = Listener.new
35 | notifier.subscribe "hi", listener
36 | notifier.start "hi", "1", Payload.new
37 | notifier.start "hi", "2", Payload.new
38 | notifier.finish "hi", "2", Payload.new
39 | notifier.finish "hi", "1", Payload.new
40 |
41 | listener.events.size.should eq(4)
42 | listener.events.should eq [
43 | Event.new("start.hi", Time.epoch(0), Time.epoch(0), "1", Payload.new),
44 | Event.new("start.hi", Time.epoch(0), Time.epoch(0), "2", Payload.new),
45 | Event.new("finish.hi", Time.epoch(0), Time.epoch(0), "2", Payload.new),
46 | Event.new("finish.hi", Time.epoch(0), Time.epoch(0), "1", Payload.new),
47 | ]
48 | end
49 |
50 | it "handles no events" do
51 | notifier = Fanout.new
52 | listener = Listener.new
53 | notifier.subscribe "hi", listener
54 | notifier.start "world", "1", Payload.new
55 | listener.events.size.should eq 0
56 | end
57 |
58 | it "handles priority" do
59 | notifier = Fanout.new
60 | listener = ListenerWithTimedSupport.new
61 | notifier.subscribe "hi", listener
62 |
63 | notifier.start "hi", "1", Payload.new
64 | notifier.finish "hi", "1", Payload.new
65 |
66 | listener.events.should eq [
67 | Event.new("start.hi", Time.epoch(0), Time.epoch(0), "1", Payload.new),
68 | Event.new("finish.hi", Time.epoch(0), Time.epoch(0), "1", Payload.new),
69 | ]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/carbon_support/notifications/instrumenter_spec.cr:
--------------------------------------------------------------------------------
1 | require "../../spec_helper"
2 |
3 | module CarbonSupportTest
4 | include CarbonSupport::Notifications
5 |
6 | class TestNotifier
7 | getter :starts, :finishes
8 |
9 | def initialize
10 | @starts = [] of {String, String, CarbonSupport::Notifications::Payload}
11 | @finishes = [] of {String, String, CarbonSupport::Notifications::Payload}
12 | end
13 |
14 | def start(*args)
15 | @starts << args
16 | end
17 |
18 | def finish(*args)
19 | @finishes << args
20 | end
21 | end
22 |
23 | describe Instrumenter do
24 | it "calls the block" do
25 | notifier = TestNotifier.new
26 | instrumenter = Instrumenter.new notifier
27 | payload = Payload.new
28 |
29 | called = false
30 | instrumenter.instrument("foo", payload) {
31 | called = true
32 | }
33 |
34 | called.should be_truthy
35 | end
36 |
37 | it "yield the payload" do
38 | notifier = TestNotifier.new
39 | instrumenter = Instrumenter.new notifier
40 | instrumenter.instrument("awesome") { |p| p.message = "test" }.should eq "test"
41 | notifier.finishes.size.should eq 1
42 | name, _, payload = notifier.finishes.first
43 | name.should eq "awesome"
44 | payload.message.should eq "test"
45 | end
46 |
47 | it "tests start" do
48 | notifier = TestNotifier.new
49 | instrumenter = Instrumenter.new notifier
50 | payload = Payload.new
51 |
52 | instrumenter.start("foo", payload)
53 |
54 | notifier.starts.should eq [{"foo", instrumenter.id, payload}]
55 | notifier.finishes.empty?.should be_truthy
56 | end
57 |
58 | it "tests finish" do
59 | notifier = TestNotifier.new
60 | instrumenter = Instrumenter.new notifier
61 | payload = Payload.new
62 | instrumenter.finish("foo", payload)
63 | notifier.finishes.should eq [{"foo", instrumenter.id, payload}]
64 | notifier.starts.empty?.should be_truthy
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/carbon_support/notifications_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | module CarbonSupportTest
4 | describe CarbonSupport::Notifications do
5 | it "tests subscribed" do
6 | name = "foo"
7 | name2 = name * 2
8 | expected = [name, name]
9 |
10 | events = [] of String
11 | callback = ->(event : CarbonSupport::Notifications::Event) { events << event.name }
12 | CarbonSupport::Notifications.subscribed(callback, name) do
13 | CarbonSupport::Notifications.instrument(name)
14 | CarbonSupport::Notifications.instrument(name2)
15 | CarbonSupport::Notifications.instrument(name)
16 | end
17 | events.should eq expected
18 |
19 | CarbonSupport::Notifications.instrument(name)
20 | events.should eq expected
21 | end
22 |
23 | it "removes a subscription when unsubscribing" do
24 | notifier = CarbonSupport::Notifications::Fanout.new
25 | events = [] of String
26 | subscription = notifier.subscribe do |event|
27 | events << event.name
28 | end
29 | notifier.publish "name", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
30 | notifier.wait
31 | events.should eq ["name"]
32 | notifier.unsubscribe(subscription)
33 | notifier.publish "name", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
34 | notifier.wait
35 | events.should eq ["name"]
36 | end
37 |
38 | it "removes a subscription when unsubscribing with name" do
39 | notifier = CarbonSupport::Notifications::Fanout.new
40 | named_events = [] of String
41 | subscription = notifier.subscribe "named.subscription" do |event|
42 | named_events << event.name
43 | end
44 | notifier.publish "named.subscription", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
45 | notifier.wait
46 | named_events.should eq ["named.subscription"]
47 | notifier.unsubscribe("named.subscription")
48 | notifier.publish "named.subscription", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
49 | notifier.wait
50 | named_events.should eq ["named.subscription"]
51 | end
52 |
53 | it "leaves the other subscriptions when unsubscribing by name " do
54 | notifier = CarbonSupport::Notifications::Fanout.new
55 | events = [] of String
56 | named_events = [] of String
57 | subscription = notifier.subscribe "named.subscription" do |event|
58 | named_events << event.name
59 | end
60 | subscription = notifier.subscribe do |event|
61 | events << event.name
62 | end
63 | notifier.publish "named.subscription", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
64 | notifier.wait
65 | events.should eq ["named.subscription"]
66 | notifier.unsubscribe("named.subscription")
67 | notifier.publish "named.subscription", Time.now, Time.now, "id", CarbonSupport::Notifications::Payload.new
68 | notifier.wait
69 | events.should eq ["named.subscription", "named.subscription"]
70 | end
71 |
72 | it "returns the block result" do
73 | CarbonSupport::Notifications.instrument("name") { 1 + 1 }.should eq 2
74 | end
75 |
76 | it "exposes an id method" do
77 | CarbonSupport::Notifications.instrumenter.id.size.should eq 20
78 | end
79 |
80 | it "allows nested events" do
81 | CarbonSupport::Notifications.notifier = CarbonSupport::Notifications::Fanout.new
82 | events = [] of String
83 | CarbonSupport::Notifications.notifier.subscribe do |event|
84 | events << event.name
85 | end
86 |
87 | CarbonSupport::Notifications.instrument("outer") do
88 | CarbonSupport::Notifications.instrument("inner") do
89 | 1 + 1
90 | end
91 | events.size.should eq 1
92 | events.first.should eq "inner"
93 | end
94 |
95 | events.size.should eq 2
96 | events.last.should eq "outer"
97 | end
98 |
99 | it "publishes when exceptions are raised" do
100 | CarbonSupport::Notifications.notifier = CarbonSupport::Notifications::Fanout.new
101 | events = [] of String
102 | CarbonSupport::Notifications.notifier.subscribe do |event|
103 | events << event.name
104 | end
105 |
106 | begin
107 | CarbonSupport::Notifications.instrument("raises") do
108 | raise "FAIL"
109 | end
110 | rescue e : Exception
111 | e.message.should eq "FAIL"
112 | end
113 |
114 | events.size.should eq 1
115 | end
116 |
117 | it "publishes when instrumented without a block" do
118 | CarbonSupport::Notifications.notifier = CarbonSupport::Notifications::Fanout.new
119 | events = [] of String
120 | CarbonSupport::Notifications.notifier.subscribe do |event|
121 | events << event.name
122 | end
123 |
124 | CarbonSupport::Notifications.instrument("no block")
125 |
126 | events.size.should eq 1
127 | events.first.should eq "no block"
128 | end
129 |
130 | it "publishes events with details" do
131 | CarbonSupport::Notifications.notifier = CarbonSupport::Notifications::Fanout.new
132 | events = [] of CarbonSupport::Notifications::Event
133 | CarbonSupport::Notifications.notifier.subscribe do |event|
134 | events << event
135 | end
136 |
137 | CarbonSupport::Notifications.instrument("outer", CarbonSupport::Notifications::Payload.new.tap { |p| p.message = "test" }) do
138 | CarbonSupport::Notifications.instrument("inner")
139 | end
140 |
141 | events.first.name.should eq "inner"
142 | events.last.payload.message.should eq "test"
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/spec/carbon_view/base_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | module CarbonViewTest
4 | class TestController < CarbonController::Base
5 | def index
6 | @missing_method = "not missing"
7 | end
8 | end
9 |
10 | describe CarbonView::Base do
11 | it "delegates missing methods to the controller instance variables" do
12 | # controller = TestController.action("index", "request", "response")
13 | # controller.index
14 | # CarbonView::Base.new(controller).missing_method.should eq("not missing")
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/carbon_view/helpers/tag_helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "../../spec_helper"
2 |
3 | def assert_equal(first, other)
4 | first.to_s.should eq other.to_s
5 | end
6 |
7 | def assert_match(regex, str)
8 | !!(regex =~ str)
9 | end
10 |
11 | class TestView < CarbonView::Base
12 | end
13 |
14 | module CarbonViewTest
15 | def self.tag(*args)
16 | TestView.new.tag(*args)
17 | end
18 |
19 | def self.content_tag(*args)
20 | TestView.new.content_tag(*args)
21 | end
22 |
23 | def self.content_tag(*args, &block : -> _)
24 | TestView.new.content_tag(*args, &block)
25 | end
26 |
27 | def self.cdata_section(*args)
28 | TestView.new.cdata_section(*args)
29 | end
30 |
31 | describe CarbonView::Helpers::TagHelper do
32 | it "returns a tag" do
33 | tag("br").should eq "
"
34 | tag(:br, {:clear => "left"}).should eq "
"
35 | tag("br", nil, true).should eq "
"
36 | end
37 |
38 | it "tag_options" do
39 | str = tag("p", {"class" => "show", :class => "elsewhere"})
40 | assert_match(/class="show"/, str)
41 | assert_match(/class="elsewhere"/, str)
42 | end
43 |
44 | it "tag_options_rejects_nil_option" do
45 | assert_equal "
<script>evil_js</script>
", 67 | content_tag(:p, "") 68 | assert_equal "", 69 | content_tag(:p, "", nil, false) 70 | end 71 | 72 | # it "content_tag_with_block_in_erb" do 73 | # buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>") 74 | # assert_dom_equal "\n Hello\n
", view.render("test/content_tag_nested_in_content_tag") 108 | # end 109 | 110 | it "content_tag_with_escaped_array_class" do 111 | str = content_tag("p", "limelight", {:class => ["song", "play>"]}) 112 | assert_equal "limelight
", str 113 | 114 | str = content_tag("p", "limelight", {:class => ["song", "play"]}) 115 | assert_equal "limelight
", str 116 | 117 | str = content_tag("p", "limelight", {:class => ["song", ["play"]]}) 118 | assert_equal "limelight
", str 119 | end 120 | 121 | it "content_tag_with_unescaped_array_class" do 122 | str = content_tag("p", "limelight", {:class => ["song", "play>"]}, false) 123 | assert_equal "\">limelight
", str 124 | 125 | str = content_tag("p", "limelight", {:class => ["song", ["play>"]]}, false) 126 | assert_equal "\">limelight
", str 127 | end 128 | 129 | it "content_tag_with_empty_array_class" do 130 | str = content_tag("p", "limelight", {:class => [] of String}) 131 | assert_equal "limelight
", str 132 | end 133 | 134 | it "content_tag_with_unescaped_empty_array_class" do 135 | str = content_tag("p", "limelight", {:class => [] of String}, false) 136 | assert_equal "limelight
", str 137 | end 138 | 139 | # it "content_tag_with_data_attributes" do 140 | # assert_dom_equal "limelight
", 141 | # content_tag("p", "limelight", data: { number: 1, string: "hello", string_with_quotes: "double"quote"party"" }) 142 | # end 143 | 144 | it "cdata_section" do 145 | assert_equal "]]>", cdata_section("\" />", str 171 | 172 | str = tag("p", {:class => ["song>".html_safe, "play>"]}) 173 | assert_equal "
play>\" />", str 174 | end 175 | 176 | it "skip_invalid_escaped_attributes" do 177 | ["&1;", "dfa3;", "& #123;"].each do |escaped| 178 | assert_equal %(), tag("a", {:href => escaped}) 179 | end 180 | end 181 | 182 | it "disable_escaping" do 183 | assert_equal "", tag("a", {:href => "&"}, false, false) 184 | end 185 | 186 | # it "data_attributes" do 187 | # ["data", :data].each { |data| 188 | # assert_dom_equal "", 189 | # tag("a", { data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: "double"quote"party"" } }) 190 | # } 191 | # end 192 | # 193 | # it "aria_attributes" do 194 | # ["aria", :aria].each { |aria| 195 | # assert_dom_equal "", 196 | # tag("a", { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: "double"quote"party"" } }) 197 | # } 198 | # end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /spec/lib/file_string_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe FileString do 4 | context "#join" do 5 | it "joins the paths" do 6 | FileString.new("some/").join("/path").to_s.should eq "some/path" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/lib/http_util_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe HTTPUtil do 4 | context ".status_code" do 5 | it "returns the HTTP status code for the given status Symbol or String" do 6 | status_code = HTTPUtil.status_code(:ok) 7 | status_code.should eq(200) 8 | 9 | status_code = HTTPUtil.status_code("ok") 10 | status_code.should eq(200) 11 | 12 | status_code = HTTPUtil.status_code(:unprocessable_entity) 13 | status_code.should eq(422) 14 | 15 | status_code = HTTPUtil.status_code("unprocessable_entity") 16 | status_code.should eq(422) 17 | 18 | status_code = HTTPUtil.status_code(:non_authoritative_information) 19 | status_code.should eq(203) 20 | 21 | status_code = HTTPUtil.status_code("non_authoritative_information") 22 | status_code.should eq(203) 23 | end 24 | 25 | it "returns the given status as Int32, when the given status is NOT a Symbol or String" do 26 | status_code = HTTPUtil.status_code(201) 27 | status_code.should eq(201) 28 | 29 | status_code = HTTPUtil.status_code(502.32) 30 | status_code.should eq(502) 31 | end 32 | 33 | it "returns the HTTP status code 500, when the given status is NOT existing" do 34 | status_code = HTTPUtil.status_code(:not_ok) 35 | status_code.should eq(500) 36 | 37 | status_code = HTTPUtil.status_code("not_ok") 38 | status_code.should eq(500) 39 | 40 | status_code = HTTPUtil.status_code(:coffeepot) 41 | status_code.should eq(500) 42 | 43 | status_code = HTTPUtil.status_code("coffeepot") 44 | status_code.should eq(500) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/all" 3 | require "./support/*" 4 | -------------------------------------------------------------------------------- /spec/support/mock_request.cr: -------------------------------------------------------------------------------- 1 | class MockRequest 2 | def initialize 3 | end 4 | 5 | def get(path : String, headers : HTTP::Headers) 6 | HTTP::Request.new("GET", path, headers) 7 | end 8 | 9 | def get(path : String, headers : Hash(String, String | Array(String)) = Hash(String, String | Array(String)).new) 10 | http_headers = HTTP::Headers.new 11 | 12 | headers.each do |key, value| 13 | http_headers.add(key, value) 14 | end 15 | 16 | HTTP::Request.new("GET", path, http_headers) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Benoist Claassen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/all.cr: -------------------------------------------------------------------------------- 1 | require "./lib/**" 2 | require "http" 3 | require "json" 4 | require "yaml" 5 | require "ecr" 6 | require "logger" 7 | require "benchmark" 8 | require "secure_random" 9 | require "ecr/macros" 10 | require "./carbon" 11 | require "./carbon_support/core_ext/**" 12 | require "./carbon/application" 13 | require "./carbon/version" 14 | require "./carbon_support/*" 15 | require "./carbon_view/base" 16 | require "./carbon_controller/*" 17 | require "./carbon_dispatch/*" 18 | require "./carbon_dispatch/middleware" 19 | -------------------------------------------------------------------------------- /src/carbon.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | struct Environment 3 | def initialize(@env) 4 | end 5 | 6 | {% for env in ["development", "test", "production"] %} 7 | def {{env.id}}? 8 | @env == {{env}} 9 | end 10 | {% end %} 11 | 12 | def to_s(io : IO) 13 | @env.to_s(io) 14 | end 15 | end 16 | 17 | def self.application=(app) 18 | @@application = app 19 | end 20 | 21 | def self.application 22 | @@application || raise "Application not created" 23 | end 24 | 25 | def self.key_generator=(key_generator) 26 | @@key_generator = key_generator 27 | end 28 | 29 | def self.key_generator 30 | @@key_generator || raise "Key generator not defined" 31 | end 32 | 33 | def self.root=(root) 34 | @@root = FileString.new(root) 35 | end 36 | 37 | def self.root 38 | @@root || raise "Root is not defined" 39 | end 40 | 41 | def self.logger 42 | @@logger ||= Logger.new(STDOUT).tap do |logger| 43 | logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io| 44 | io << message 45 | end 46 | logger.level = log_level 47 | end 48 | end 49 | 50 | def self.log_level 51 | @@log_level ||= env.development? ? Logger::DEBUG : Logger::INFO 52 | end 53 | 54 | def self.log_level=(level : Logger::Severity) 55 | @@log_level = level 56 | puts "LogLevel set to: #{level}" 57 | logger.level = level 58 | end 59 | 60 | def self.log_level=(level : String) 61 | self.log_level = case level.upcase 62 | when "ERROR" 63 | Logger::ERROR 64 | when "INFO" 65 | Logger::INFO 66 | else 67 | Logger::DEBUG 68 | end 69 | end 70 | 71 | def self.logger=(logger) 72 | @@logger = logger 73 | end 74 | 75 | def self.env 76 | @@env = Environment.new(ENV["CARBON_ENV"]? || "development") 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /src/carbon/application.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | class Application 3 | macro inherited 4 | Carbon.application = {{@type}}.new 5 | end 6 | 7 | macro config 8 | Carbon.application 9 | end 10 | 11 | macro views(*views) 12 | {% for view in views %} 13 | @actions[{{action}}.to_s] = ->{{action.id}} 14 | {% end %} 15 | end 16 | 17 | def initialize! 18 | set_key_generator 19 | set_default_middleware 20 | app = middleware.build 21 | @handler = CarbonDispatch::Handler.new(app) 22 | end 23 | 24 | def routes 25 | @router ||= CarbonDispatch::Router.new 26 | end 27 | 28 | def router=(router) 29 | @router = router 30 | end 31 | 32 | def router 33 | @router || raise "No routes set up" 34 | end 35 | 36 | def middleware 37 | @middleware ||= CarbonDispatch::MiddlewareStack::INSTANCE 38 | end 39 | 40 | def run 41 | server = create_server(ENV["BIND"]? || "::1", 3000) 42 | server.listen 43 | end 44 | 45 | private def create_server(ip, port) 46 | handler = @handler 47 | raise "Application not initialized!" unless handler 48 | Carbon.logger.info "Carbon #{Carbon::VERSION} application starting in #{Carbon.env} on http://#{ip}:#{port}" 49 | 50 | HTTP::Server.new(ip, port, [handler]) 51 | end 52 | 53 | private def set_key_generator 54 | secrets = YAML.parse(File.read(Carbon.root.join("config/secrets.yml").to_s).to_s)[Carbon.env.to_s.downcase] 55 | secret_key_base = ENV["SECRET_KEY_BASE"]? || secrets["secret_key_base"].to_s 56 | Carbon.key_generator = CarbonSupport::CachingKeyGenerator.new(CarbonSupport::KeyGenerator.new(secret_key_base, 1000)) 57 | end 58 | 59 | private def set_default_middleware 60 | middleware.use CarbonDispatch::Sendfile.new "X-Accel-Redirect" 61 | middleware.use CarbonDispatch::Static.new 62 | middleware.use CarbonDispatch::Runtime.new 63 | middleware.use CarbonDispatch::RequestId.new 64 | middleware.use CarbonDispatch::Logger.new 65 | middleware.use CarbonDispatch::ShowExceptions.new 66 | middleware.use CarbonDispatch::Head.new 67 | middleware.use CarbonDispatch::Cookies.new 68 | middleware.use CarbonDispatch::Session.new 69 | middleware.use CarbonDispatch::Flash.new 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/carbon/version.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /src/carbon_controller/abstract.cr: -------------------------------------------------------------------------------- 1 | require "./abstract/*" 2 | 3 | module CarbonController 4 | class Abstract 5 | def process_action(name, block) 6 | block.call(self) 7 | end 8 | 9 | def process(name, block) 10 | @_action_name = name 11 | @_response_body = nil 12 | process_action(name, block) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/carbon_controller/abstract/callbacks.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | module Callbacks 3 | class ResponseTerminator 4 | def terminate?(target, result) 5 | target.response.rendered? if target.is_a?(CarbonController::Base) 6 | end 7 | end 8 | 9 | macro included 10 | include CarbonSupport::Callbacks 11 | define_callbacks(:process_action, CallbackChain::Options.new( 12 | terminator: ResponseTerminator.new, 13 | skip_after_callbacks_if_terminated: true) 14 | ) 15 | end 16 | 17 | def process_action(name, block) 18 | run_callbacks(:process_action) do 19 | super 20 | true 21 | end 22 | end 23 | 24 | macro before_action(name) 25 | set_callback :process_action, :before, {{name}} 26 | end 27 | 28 | macro after_action(name) 29 | set_callback :process_action, :after, {{name}} 30 | end 31 | 32 | macro around_action(name) 33 | set_callback :process_action, :around, {{name}} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/carbon_controller/base.cr: -------------------------------------------------------------------------------- 1 | require "./metal" 2 | 3 | module CarbonController 4 | class Base < Metal 5 | delegate :params, :request 6 | 7 | def self.layout(layout = nil) 8 | @@layout ||= layout 9 | end 10 | 11 | include Head 12 | include Redirect 13 | include Session 14 | include Cookies 15 | include Flash 16 | include CarbonController::Callbacks 17 | include Instrumentation 18 | 19 | def request 20 | @_request 21 | end 22 | 23 | def response : CarbonDispatch::Response 24 | @_response 25 | end 26 | 27 | macro render_template(template, status = 200) 28 | layout = CarbonView::Base.layouts["Layouts::{{ @type.id.gsub(/Controller\+?/, "") }}"].new(controller = self) 29 | view = CarbonViews::{{ @type.id.gsub(/Controller\+?/, "") }}::{{template.camelcase.id}}.new(controller = self) 30 | 31 | response.status_code = status 32 | response.body = layout.render(view) 33 | response.headers["Content-Type"] = "text/html" 34 | end 35 | 36 | def render_text(text, status = 200) 37 | response.status_code = status 38 | response.body = text 39 | response.headers["Content-Type"] = "text/plain" 40 | end 41 | 42 | def render_json(object, status = 200) 43 | response.status_code = status 44 | response.body = object.to_json 45 | response.headers["Content-Type"] = "application/json" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/carbon_controller/implicit_render.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | module ImplicitRender 3 | def process_action(name, block) 4 | super 5 | unless response.body 6 | render_template name 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/carbon_controller/log_subscriber.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | class LogSubscriber < CarbonSupport::LogSubscriber 3 | def start_processing(event) 4 | payload = event.payload 5 | 6 | info "Processing by #{payload.controller}##{payload.action} as HTML" 7 | info " Parameters: #{payload.params}" 8 | end 9 | 10 | def process_action(event) 11 | info do 12 | payload = event.payload 13 | 14 | status = payload.status 15 | if status.nil? && payload.exception 16 | status = 500 17 | end 18 | "Completed #{status} in #{event.duration_text}" 19 | end 20 | end 21 | 22 | def halted_callback(event) 23 | info { "Filter chain halted as :#{event.payload.filter} rendered or redirected" } 24 | end 25 | 26 | attach_to :carbon_controller 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/carbon_controller/metal.cr: -------------------------------------------------------------------------------- 1 | require "./metal/*" 2 | 3 | module CarbonController 4 | class Metal < Abstract 5 | macro action(name, request, response) 6 | proc = ->(controller : {{@type}}) { controller.{{name.id}} } 7 | 8 | {{@type}}.new({{request}}, {{response}}).dispatch(:{{name.id}}, proc) 9 | end 10 | 11 | def initialize(request, response) 12 | @_headers = {"Content-Type" => "text/html"} 13 | @_status = 200 14 | @_request = request 15 | @_response = response 16 | @_routes = nil 17 | super() 18 | end 19 | 20 | def dispatch(action, block) 21 | process(action, block) 22 | nil 23 | end 24 | 25 | def request 26 | @_request 27 | end 28 | 29 | def response 30 | @_response 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/cookies.cr: -------------------------------------------------------------------------------- 1 | module CarbonController # :nodoc: 2 | module Cookies 3 | private def cookies 4 | request.cookie_jar 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/exceptions.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | class CarbonControllerError < Exception 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/flash.cr: -------------------------------------------------------------------------------- 1 | module CarbonController # :nodoc: 2 | module Flash 3 | private def flash 4 | request.flash 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/head.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | module Head 3 | def head(status, location : String? = nil, content_type : String? = nil) 4 | response.status_code = status 5 | response.location = location if location 6 | response.headers["Content-Type"] = content_type if content_type 7 | 8 | if include_content?(status) 9 | response.headers["Content-Type"] = content_type if content_type # || (Mime[formats.first] if formats) 10 | else 11 | response.headers.delete("Content-Type") 12 | response.headers.delete("Content-Length") 13 | end 14 | 15 | true 16 | end 17 | 18 | private def include_content?(status) 19 | case status 20 | when 100..199 21 | false 22 | when 204, 205, 304 23 | false 24 | else 25 | true 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/instrumentation.cr: -------------------------------------------------------------------------------- 1 | {% for key in [:view_runtime, :status, :controller, :action, :params, :format, :method, :path, :filter] %} 2 | CarbonSupport::Notifications::Payload.define_property({{key}}) 3 | {% end %} 4 | 5 | module CarbonController 6 | module Instrumentation 7 | def process_action(name, block) 8 | raw_payload = CarbonSupport::Notifications::Payload.new 9 | 10 | raw_payload.controller = self.class.to_s 11 | raw_payload.action = @_action_name 12 | raw_payload.method = request.method 13 | raw_payload.path = (request.path rescue "unknown") 14 | raw_payload.params = request.params 15 | 16 | CarbonSupport::Notifications.instrument("start_processing.carbon_controller", raw_payload) 17 | 18 | CarbonSupport::Notifications.instrument("process_action.carbon_controller", raw_payload) do |payload| 19 | begin 20 | result = super 21 | payload.status = response.status_code 22 | result 23 | ensure 24 | append_info_to_payload(payload) 25 | end 26 | end 27 | end 28 | 29 | def halted_callback_hook(filter) 30 | raw_payload = CarbonSupport::Notifications::Payload.new 31 | raw_payload.filter = filter 32 | CarbonSupport::Notifications.instrument("halted_callback.carbon_controller", raw_payload) 33 | end 34 | 35 | def render(*args) 36 | @view_runtime = cleanup_view_runtime do 37 | Benchmark.realtime { @render_output = super } 38 | end 39 | @render_output 40 | end 41 | 42 | private def view_runtime 43 | @view_runtime 44 | end 45 | 46 | def cleanup_view_runtime 47 | yield 48 | end 49 | 50 | protected def append_info_to_payload(payload) 51 | payload.view_runtime = view_runtime 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/redirect.cr: -------------------------------------------------------------------------------- 1 | module CarbonController 2 | class RedirectBackError < Exception 3 | DEFAULT_MESSAGE = "No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully." 4 | 5 | def initialize(message = nil) 6 | super(message || DEFAULT_MESSAGE) 7 | end 8 | end 9 | 10 | module Redirect 11 | def redirect_to(options = Hash(Symbol, Symbol | String).new, response_status : Hash(Symbol, Symbol) = Hash(Symbol, Symbol).new) 12 | raise CarbonControllerError.new("Cannot redirect to nil!") if options.nil? 13 | raise CarbonControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(HTTP::Params) 14 | 15 | response.status_code = _extract_redirect_to_status(options, response_status) 16 | response.location = _compute_redirect_to_location(request, options) 17 | end 18 | 19 | private def _compute_redirect_to_location(request, options) # :nodoc: 20 | # First case: 21 | # 22 | # The scheme name consist of a letter followed by any combination of 23 | # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") 24 | # characters; and is terminated by a colon (":"). 25 | # See http://tools.ietf.org/html/rfc3986#section-3.1 26 | # The protocol relative scheme starts with a double slash "//". 27 | case options 28 | when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i 29 | options 30 | when String 31 | request.protocol + request.host_with_port + options 32 | when :back 33 | referer = request.headers["Referer"]? 34 | 35 | if referer && referer != "" 36 | referer 37 | else 38 | raise RedirectBackError.new 39 | end 40 | else 41 | # TODO: Use this once #url_for is implemented 42 | # url_for(options) 43 | end.to_s.delete("\0\r\n") 44 | end 45 | 46 | private def _extract_redirect_to_status(options, response_status) 47 | if options.is_a?(Hash) && options.has_key?(:status) 48 | HTTPUtil.status_code(options.delete(:status)) 49 | elsif response_status.has_key?(:status) 50 | HTTPUtil.status_code(response_status[:status]) 51 | else 52 | 302 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/carbon_controller/metal/session.cr: -------------------------------------------------------------------------------- 1 | module CarbonController # :nodoc: 2 | module Session 3 | def process_action(name, block) 4 | super 5 | @_request.session.set_cookie 6 | end 7 | 8 | def session 9 | @_request.session.not_nil! 10 | end 11 | 12 | def reset_session 13 | session.destroy 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/carbon_dispatch/body_proxy.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class BodyProxy 3 | getter :body 4 | 5 | def initialize(@body) 6 | end 7 | 8 | def initialize(@body, &@block) 9 | end 10 | 11 | def path=(path) 12 | @path 13 | end 14 | 15 | def path 16 | @path.to_s 17 | end 18 | 19 | def is_path? 20 | @path && File.exists?(@path) 21 | end 22 | 23 | def to_s 24 | @body.to_s 25 | end 26 | 27 | def present? 28 | !@body.nil? && @body != "" 29 | end 30 | 31 | def close 32 | block = @block 33 | block.call if block 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/carbon_dispatch/environment.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Environment 3 | getter :request 4 | getter :errors 5 | property :request_id 6 | property :ip 7 | 8 | @request : ::HTTP::Request 9 | 10 | def initialize(@request) 11 | @errors = STDOUT 12 | @ip = "127.0.0.1" # TODO: fetch real IP 13 | end 14 | 15 | def [](value) 16 | @request.headers[value] 17 | end 18 | 19 | def []?(value) 20 | @request.headers[value]? 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/carbon_dispatch/handler.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Handler < HTTP::Handler 3 | def initialize(@app : Middleware) 4 | end 5 | 6 | def call(context : HTTP::Server::Context) 7 | # env = CarbonDispatch::Environment.new(request) 8 | request = Request.new(context.request) 9 | response = Response.new(context.response) 10 | @app.call(request, response) 11 | response.finish 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware.cr: -------------------------------------------------------------------------------- 1 | require "./middleware/stack" 2 | require "./middleware/*" 3 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/cookies.cr: -------------------------------------------------------------------------------- 1 | require "http" 2 | 3 | module CarbonDispatch 4 | class Request 5 | def cookie_jar 6 | @cookies ||= CarbonDispatch::Cookies::CookieJar.build(self, Carbon.key_generator) 7 | end 8 | end 9 | 10 | class Cookies 11 | include Middleware 12 | 13 | def call(request, response) 14 | app.call(request, response) 15 | request.cookie_jar.write(response.headers) 16 | end 17 | 18 | # Cookies can typically store 4096 bytes. 19 | MAX_COOKIE_SIZE = 4096 20 | 21 | # Raised when storing more than 4K of session data. 22 | class CookieOverflow < Exception 23 | end 24 | 25 | module ChainedCookieJars 26 | def permanent 27 | @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) 28 | end 29 | 30 | def encrypted 31 | @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) 32 | end 33 | end 34 | 35 | class CookieJar 36 | include Enumerable(String) 37 | include ChainedCookieJars 38 | 39 | def self.from_headers(headers) 40 | cookies = {} of String => HTTP::Cookie 41 | if values = headers.get?("Cookie") 42 | values.each do |header| 43 | HTTP::Cookie::Parser.parse_cookies(header) do |cookie| 44 | cookies[cookie.name] = cookie 45 | end 46 | end 47 | end 48 | cookies 49 | end 50 | 51 | def self.build(request, key_generator = CarbonSupport::KeyGenerator.new("secret")) 52 | headers = request.headers 53 | host = request.host 54 | secure = request.ssl? 55 | 56 | new(key_generator, host, secure).tap do |jar| 57 | jar.update(from_headers(headers)) 58 | end 59 | end 60 | 61 | getter :cookies 62 | property :host 63 | property :secure 64 | 65 | def initialize(@key_generator, @host = nil, @secure = nil) 66 | @cookies = {} of String => String 67 | @set_cookies = {} of String => HTTP::Cookie 68 | @delete_cookies = {} of String => HTTP::Cookie 69 | end 70 | 71 | def update(cookies) 72 | cookies.each do |name, cookie| 73 | @cookies[name] = cookie.value 74 | end 75 | end 76 | 77 | def each(&block : T -> _) 78 | @cookies.values.each do |cookie| 79 | yield cookie 80 | end 81 | end 82 | 83 | def each 84 | @cookies.each_value 85 | end 86 | 87 | def [](name) 88 | get(name) 89 | end 90 | 91 | def get(name) 92 | @cookies[name]? 93 | end 94 | 95 | def set(name : String, value : String, path : String = "/", 96 | expires : Time? = nil, domain : String? = nil, 97 | secure : Bool = false, http_only : Bool = false, 98 | extension : String? = nil) 99 | if @cookies[name]? != value || expires 100 | @cookies[name] = value 101 | @set_cookies[name] = HTTP::Cookie.new(name, value, path, expires, domain, secure, http_only, extension) 102 | @delete_cookies.delete(name) if @delete_cookies.has_key?(name) 103 | end 104 | end 105 | 106 | def delete(name : String, path = "/", domain : String? = nil) 107 | return unless @cookies.has_key?(name) 108 | 109 | value = @cookies.delete(name) 110 | @delete_cookies[name] = HTTP::Cookie.new(name, "", path, Time.epoch(0), domain) 111 | value 112 | end 113 | 114 | def deleted?(name) 115 | @delete_cookies.has_key?(name) 116 | end 117 | 118 | def []=(name, value) 119 | set(name, value) 120 | end 121 | 122 | def []=(name, cookie : HTTP::Cookie) 123 | @cookies[name] = cookie.value 124 | @set_cookies[name] = cookie 125 | end 126 | 127 | def write(headers) 128 | cookies = [] of String 129 | @set_cookies.each { |name, cookie| cookies << cookie.to_set_cookie_header if write_cookie?(cookie) } 130 | @delete_cookies.each { |name, cookie| cookies << cookie.to_set_cookie_header } 131 | headers.add("Set-Cookie", cookies) 132 | end 133 | 134 | def write_cookie?(cookie) 135 | @secure || !cookie.secure 136 | end 137 | end 138 | 139 | class JsonSerializer # :nodoc: 140 | def self.load(value) 141 | JSON.parse(value) 142 | end 143 | 144 | def self.dump(value) 145 | value.to_json 146 | end 147 | end 148 | 149 | module SerializedCookieJars 150 | protected def serialize(name, value) 151 | serializer.dump(value) 152 | end 153 | 154 | protected def deserialize(name, value) 155 | serializer.load(value)["value"].to_s 156 | end 157 | 158 | protected def serializer 159 | JsonSerializer 160 | end 161 | 162 | protected def digest 163 | :sha256 164 | end 165 | end 166 | 167 | class PermanentCookieJar # :nodoc: 168 | include ChainedCookieJars 169 | 170 | def initialize(parent_jar : CookieJar, key_generator, options = {} of String => String) 171 | @parent_jar = parent_jar 172 | @key_generator = key_generator 173 | @options = options 174 | end 175 | 176 | def [](name) 177 | get(name) 178 | end 179 | 180 | def get(name) 181 | @parent_jar.get(name) 182 | end 183 | 184 | def []=(name, value) 185 | set(name, value) 186 | end 187 | 188 | def set(name : String, value : String, path : String = "/", domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil) 189 | cookie = HTTP::Cookie.new(name, value, path, 20.years.from_now, domain, secure, http_only, extension) 190 | @parent_jar[name] = cookie 191 | end 192 | end 193 | 194 | class EncryptedCookieJar # :nodoc: 195 | include ChainedCookieJars 196 | include SerializedCookieJars 197 | 198 | def initialize(parent_jar, key_generator, options = {} of String => String) 199 | @parent_jar = parent_jar 200 | @options = options 201 | secret = key_generator.generate_key("encrypted_cookie_salt") 202 | sign_secret = key_generator.generate_key("encrypted_signed_cookie_salt") 203 | @encryptor = CarbonSupport::MessageEncryptor.new(secret, sign_secret: sign_secret, digest: digest) 204 | end 205 | 206 | def [](name) 207 | get(name) 208 | end 209 | 210 | def []=(name, value) 211 | set(name, value) 212 | end 213 | 214 | def get(name) 215 | if value = @parent_jar.get(name) 216 | deserialize name, decrypt_and_verify(value) 217 | end 218 | end 219 | 220 | def set(name : String, value : String, path : String = "/", expires : Time? = nil, domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil) 221 | cookie = HTTP::Cookie.new(name, value, path, expires, domain, secure, http_only, extension) 222 | 223 | cookie.value = @encryptor.encrypt_and_sign(serialize(name, {"value": cookie.value})) 224 | 225 | raise CookieOverflow.new if cookie.value.bytesize > MAX_COOKIE_SIZE 226 | @parent_jar[name] = cookie 227 | end 228 | 229 | private def decrypt_and_verify(encrypted_message) 230 | String.new(@encryptor.decrypt_and_verify(encrypted_message)) 231 | rescue e : CarbonSupport::MessageVerifier::InvalidSignature | CarbonSupport::MessageEncryptor::InvalidMessage 232 | "{\"value\":\"\"}" 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/flash.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch # :nodoc: 2 | class Request 3 | def flash 4 | @flash_hash ||= Flash::FlashHash.from_session_value(session.fetch("_flash", "{}")) 5 | end 6 | end 7 | 8 | class Flash 9 | include Middleware 10 | 11 | class FlashNow 12 | property :flash 13 | 14 | def initialize(flash) 15 | @flash = flash 16 | end 17 | 18 | def []=(key, value) 19 | @flash[key] = value 20 | @flash.discard(key) 21 | value 22 | end 23 | 24 | def [](k) 25 | @flash[k.to_s] 26 | end 27 | 28 | # Convenience accessor for flash.now["alert"]=. 29 | def alert=(message) 30 | self["alert"] = message 31 | end 32 | 33 | # Convenience accessor for flash.now["notice"]=. 34 | def notice=(message) 35 | self["notice"] = message 36 | end 37 | end 38 | 39 | class FlashHash 40 | JSON.mapping({ 41 | flashes: Hash(String, String), 42 | discard: Set(String), 43 | }) 44 | 45 | def self.from_session_value(json) 46 | from_json(json).tap(&.sweep) 47 | rescue e : JSON::ParseException 48 | new 49 | end 50 | 51 | def initialize 52 | @flashes = Hash(String, String).new 53 | @discard = Set(String).new 54 | end 55 | 56 | def discard=(value : Array(String)) 57 | @discard = value.to_set 58 | end 59 | 60 | def []=(key, value) 61 | discard.delete key 62 | @flashes[key] = value 63 | end 64 | 65 | def [](key) 66 | @flashes[key]? 67 | end 68 | 69 | def update(hash : Hash(String, String)) # :nodoc: 70 | @discard.subtract hash.keys 71 | @flashes.update hash 72 | self 73 | end 74 | 75 | def keys 76 | @flashes.keys 77 | end 78 | 79 | def has_key?(key) 80 | @flashes.has_key?(key) 81 | end 82 | 83 | def delete(key) 84 | @discard.delete key 85 | @flashes.delete key 86 | self 87 | end 88 | 89 | def to_hash 90 | @flashes.dup 91 | end 92 | 93 | def empty? 94 | @flashes.empty? 95 | end 96 | 97 | def clear 98 | @discard.clear 99 | @flashes.clear 100 | end 101 | 102 | def now 103 | @now ||= FlashNow.new(self) 104 | end 105 | 106 | def keep(key = nil) 107 | key = key.to_s if key 108 | @discard.subtract key 109 | key ? self[key] : self 110 | end 111 | 112 | def discard(key = nil) 113 | keys = key ? [key] : self.keys 114 | @discard.merge keys 115 | key ? self[key] : self 116 | end 117 | 118 | def sweep 119 | @discard.each { |k| @flashes.delete k } 120 | @discard.clear 121 | @discard.merge @flashes.keys 122 | end 123 | 124 | def alert 125 | self["alert"] 126 | end 127 | 128 | def alert=(message) 129 | self["alert"] = message 130 | end 131 | 132 | def notice 133 | self["notice"] 134 | end 135 | 136 | def notice=(message) 137 | self["notice"] = message 138 | end 139 | 140 | def to_session 141 | {"flashes": @flashes, "discard": @discard}.to_json 142 | end 143 | end 144 | 145 | def call(request : Request, response) 146 | app.call(request, response) 147 | ensure 148 | session = request.session 149 | flash = request.flash.not_nil! 150 | 151 | session["_flash"] = flash.to_session 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/head.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Head 3 | include Middleware 4 | 5 | def call(request, response) 6 | app.call(request, response) 7 | 8 | if request.method == "HEAD" 9 | response.close 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/logger.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Logger < CarbonSupport::LogSubscriber 3 | include Middleware 4 | 5 | def call(request, response) 6 | @start = Time.now 7 | 8 | if Carbon.env.development? 9 | logger.debug "" 10 | logger.debug "" 11 | end 12 | 13 | instrumenter = CarbonSupport::Notifications.instrumenter 14 | instrumenter.start "request.action_dispatch", CarbonSupport::Notifications::Payload.new 15 | logger.info { started_request_message(request) } 16 | app.call(request, response) 17 | 18 | response.register_callback { instrument_finish(request) } 19 | rescue e : Exception 20 | instrument_finish(request) 21 | raise e 22 | end 23 | 24 | # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 25 | def started_request_message(request) 26 | "Started %s \"%s\" for %s at %s" % [ 27 | request.method, 28 | request.path, 29 | request.ip, 30 | Time.now.to_s, 31 | ] 32 | end 33 | 34 | def instrument_finish(request) 35 | instrumenter = CarbonSupport::Notifications.instrumenter 36 | instrumenter.finish "request.action_dispatch", CarbonSupport::Notifications::Payload.new 37 | end 38 | 39 | def logger 40 | Carbon.logger 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/request_id.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class RequestId 3 | include Middleware 4 | 5 | def call(request, response) 6 | request_id = external_request_id(request) || internal_request_id 7 | app.call(request, response) 8 | response.headers["X-Request-Id"] = request_id.to_s 9 | end 10 | 11 | private def external_request_id(request) 12 | if request_id = request.headers["HTTP_X_REQUEST_ID"]? 13 | request_id.gsub(/[^\w\-]/, "")[0, 255] 14 | end 15 | end 16 | 17 | private def internal_request_id 18 | SecureRandom.uuid 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/runtime.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Runtime 3 | include Middleware 4 | 5 | def initialize(name = nil) 6 | super() 7 | header_name = "X-Runtime" 8 | header_name += "-#{name}" if name 9 | @header_name = header_name 10 | end 11 | 12 | FORMAT_STRING = "%0.6f" 13 | 14 | def call(request, response) 15 | start_time = Time.now 16 | app.call(request, response) 17 | request_time = Time.now - start_time 18 | 19 | if !response.headers.has_key?(@header_name) 20 | response.headers[@header_name] = FORMAT_STRING % request_time.to_f 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/sendfile.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Sendfile 3 | include Middleware 4 | 5 | def initialize(@variation) 6 | super() 7 | end 8 | 9 | def call(request, response) 10 | app.call(request, response) 11 | status = response.status_code 12 | headers = response.headers 13 | 14 | if response.is_path? 15 | case type = variation(request) 16 | when "X-Accel-Redirect" 17 | path = File.expand_path(response.path) 18 | if url = map_accel_path(request, path) 19 | headers["Content-Length"] = "0" 20 | headers[type] = url 21 | else 22 | Carbon.logger.error "X-Accel-Mapping header missing" 23 | end 24 | when "X-Sendfile", "X-Lighttpd-Send-File" 25 | path = File.expand_path(response.path) 26 | headers["Content-Length"] = "0" 27 | headers[type] = path 28 | else 29 | Carbon.logger.error "Unknown x-sendfile variation: '#{type}'.\n" 30 | end 31 | end 32 | end 33 | 34 | def variation(request) 35 | @variation || request.headers["HTTP_X_SENDFILE_TYPE"] 36 | end 37 | 38 | def map_accel_path(request, path) 39 | if mapping = request.headers["HTTP_X_ACCEL_MAPPING"] 40 | internal, external = mapping.split('=', 2).map { |p| p.strip } 41 | path.gsub(/^#{internal}/i, external) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/session.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Request 3 | def session 4 | @session ||= Request::Session.new(cookie_jar) 5 | end 6 | end 7 | 8 | class Session 9 | include Middleware 10 | 11 | def call(request, response) 12 | app.call(request, response) 13 | request.session.set_cookie 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/show_exceptions.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class ShowExceptions 3 | class ExceptionApp 4 | ECR.def_to_s __DIR__ + "/templates/exception.html.ecr" 5 | 6 | def call(exception, request, response) 7 | @exception = exception 8 | @request = request 9 | @response = response 10 | response.body = to_s 11 | end 12 | 13 | def exception 14 | @exception || raise(ArgumentError.new("No Exception given")) 15 | end 16 | 17 | def request 18 | @request || raise(ArgumentError.new("No request given")) 19 | end 20 | 21 | def response 22 | @response || raise(ArgumentError.new("No response given")) 23 | end 24 | end 25 | 26 | include Middleware 27 | 28 | def initialize(exception_app = nil) 29 | super() 30 | @reference = SecureRandom.uuid 31 | @exception_app = exception_app || ExceptionApp.new 32 | end 33 | 34 | def call(request, response) 35 | begin 36 | app.call(request, response) 37 | rescue e : Exception 38 | log "Reference: #{@reference}" 39 | log e.message 40 | if Carbon.env.development? 41 | render_exception(e, request, response) 42 | else 43 | render_fallback(response) 44 | end 45 | end 46 | end 47 | 48 | def render_exception(exception, request, response) 49 | begin 50 | @exception_app.call(exception, request, response) 51 | rescue e : Exception 52 | log e.message 53 | render_fallback(response) 54 | end 55 | end 56 | 57 | def render_fallback(response) 58 | response.status_code = 500 59 | response.headers["Content-Type"] = "text/plain" 60 | response.body = "500 Internal server error. Reference #{@reference}" 61 | end 62 | 63 | def log(message) 64 | Carbon.logger.error(message.to_s) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/stack.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | module Middleware 3 | def call(request : Request, response : Response) 4 | app.call(request, response) 5 | end 6 | 7 | def build(app) 8 | @app = app 9 | self 10 | end 11 | 12 | def app 13 | @app || raise "App not defined: #{self.class}" 14 | end 15 | end 16 | 17 | class MiddlewareStack 18 | INSTANCE = new 19 | 20 | def self.instance 21 | INSTANCE 22 | end 23 | 24 | def initialize 25 | @middleware = [] of CarbonDispatch::Middleware 26 | end 27 | 28 | def use(middleware : CarbonDispatch::Middleware) 29 | @middleware << middleware 30 | end 31 | 32 | def build 33 | app = Carbon.application.router 34 | @middleware.reverse.each do |middleware| 35 | app = middleware.build(app) 36 | end 37 | app 38 | end 39 | 40 | def to_s(io : IO) 41 | msg = super 42 | @middleware.each do |mdware| 43 | msg += "use #{mdware}\n" 44 | end 45 | io << msg 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/static.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Static 3 | include Middleware 4 | 5 | def call(request, response) 6 | case request.method 7 | when "GET", "HEAD" 8 | file_path = Carbon.root.join("public", request.path).to_s 9 | if File.file?(file_path) 10 | response.status_code = 200 11 | response.headers["Content-Type"] = mime_type(file_path) 12 | response.body = File.read(file_path) 13 | 14 | return 15 | end 16 | end 17 | 18 | app.call(request, response) 19 | end 20 | 21 | private def mime_type(path) 22 | case File.extname(path) 23 | when ".txt" then "text/plain" 24 | when ".htm", ".html" then "text/html" 25 | when ".css" then "text/css" 26 | when ".js" then "application/javascript" 27 | else "application/octet-stream" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /src/carbon_dispatch/middleware/templates/exception.html.ecr: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |<%= request.path %>
58 |<%= key %> | 63 |<%= value %> | 64 |