├── .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 | [![Build Status](https://travis-ci.org/benoist/carbon-crystal.svg?branch=master)](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 |

Hello from index <%= test %>

2 | -------------------------------------------------------------------------------- /example/src/views/application/new.html.ecr: -------------------------------------------------------------------------------- 1 |

Hello from b <%= test %>

2 | -------------------------------------------------------------------------------- /example/src/views/layouts/application.html.ecr: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test 4 | <%= javascript_include_tag "application.js" %> 5 | 6 | 7 | Hello layout! 8 | 9 |
<%= yield %>
10 | 11 | 12 | -------------------------------------------------------------------------------- /example/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoist/carbon-crystal/3ec4bf3845c544cb3ff7480e34a667b201a2d515/example/tmp/.gitkeep -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: carbon 2 | version: 0.2.0 3 | 4 | authors: 5 | - Benoist Claassen 6 | 7 | license: MIT 8 | 9 | -------------------------------------------------------------------------------- /spec/carbon_controller/redirect_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module CarbonControllerTest 4 | class RedirectTestController < CarbonController::Base 5 | def redirect_relative_path 6 | redirect_to "/foo" 7 | end 8 | 9 | def redirect_url 10 | redirect_to "http://www.example.com" 11 | end 12 | 13 | def redirect_url_with_status_hash 14 | redirect_to "http://www.example.com", {status: :permanent_redirect} 15 | end 16 | 17 | def redirect_to_url_with_unescaped_query_string 18 | redirect_to "http://example.com/query?status=new" 19 | end 20 | 21 | def redirect_to_url_with_complex_scheme 22 | redirect_to "x-test+scheme.complex:redirect" 23 | end 24 | 25 | def redirect_back 26 | redirect_to :back 27 | end 28 | 29 | def redirect_with_header_break 30 | redirect_to "/lol\r\nwat" 31 | end 32 | 33 | def redirect_with_null_bytes 34 | redirect_to "http://www.example.com\000/lol\r\nwat" 35 | end 36 | 37 | def redirect_nil 38 | redirect_to nil 39 | end 40 | 41 | def redirect_http_params 42 | redirect_to HTTP::Params.parse("foo=bar&foo=baz&baz=qux") 43 | end 44 | end 45 | end 46 | 47 | describe CarbonController::Redirect do 48 | context ".redirect_to" do 49 | it "redirects to a relative path" do 50 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 51 | request = CarbonDispatch::Request.new(http_request) 52 | response = CarbonDispatch::Response.new 53 | 54 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 55 | controller.redirect_relative_path 56 | 57 | response.status_code.should eq(302) 58 | response.location.should eq("http://test.host/foo") 59 | response.body.to_s.should eq("") 60 | end 61 | 62 | it "redirects to a relative path with non standard port" do 63 | http_request = MockRequest.new.get("/", {"HOST": "test.host:8888"}) 64 | request = CarbonDispatch::Request.new(http_request) 65 | response = CarbonDispatch::Response.new 66 | 67 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 68 | controller.redirect_relative_path 69 | 70 | response.status_code.should eq(302) 71 | response.location.should eq("http://test.host:8888/foo") 72 | response.body.to_s.should eq("") 73 | end 74 | 75 | it "redirects to a URL" do 76 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 77 | request = CarbonDispatch::Request.new(http_request) 78 | response = CarbonDispatch::Response.new 79 | 80 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 81 | controller.redirect_url 82 | 83 | response.status_code.should eq(302) 84 | response.location.should eq("http://www.example.com") 85 | response.body.to_s.should eq("") 86 | end 87 | 88 | it "redirects to a URL with a custom HTTP status hash" do 89 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 90 | request = CarbonDispatch::Request.new(http_request) 91 | response = CarbonDispatch::Response.new 92 | 93 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 94 | controller.redirect_url_with_status_hash 95 | 96 | response.status_code.should eq(308) 97 | response.location.should eq("http://www.example.com") 98 | response.body.to_s.should eq("") 99 | end 100 | 101 | it "redirects to a URL with unescaped query params" do 102 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 103 | request = CarbonDispatch::Request.new(http_request) 104 | response = CarbonDispatch::Response.new 105 | 106 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 107 | controller.redirect_to_url_with_unescaped_query_string 108 | 109 | response.status_code.should eq(302) 110 | response.location.should eq("http://example.com/query?status=new") 111 | response.body.to_s.should eq("") 112 | end 113 | 114 | it "redirects to a complex schema" do 115 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 116 | request = CarbonDispatch::Request.new(http_request) 117 | response = CarbonDispatch::Response.new 118 | 119 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 120 | controller.redirect_to_url_with_complex_scheme 121 | 122 | response.status_code.should eq(302) 123 | response.location.should eq("x-test+scheme.complex:redirect") 124 | response.body.to_s.should eq("") 125 | end 126 | 127 | it "redirects back based on the 'REFERER' HTTP header" do 128 | http_request = MockRequest.new.get("/", {"HOST": "test.host", "REFERER": "http://www.example.com/hello_world"}) 129 | request = CarbonDispatch::Request.new(http_request) 130 | response = CarbonDispatch::Response.new 131 | 132 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 133 | controller.redirect_back 134 | 135 | response.status_code.should eq(302) 136 | response.location.should eq("http://www.example.com/hello_world") 137 | response.body.to_s.should eq("") 138 | end 139 | 140 | it "redirects to a relative path, even when there are header and line breaks in the path" do 141 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 142 | request = CarbonDispatch::Request.new(http_request) 143 | response = CarbonDispatch::Response.new 144 | 145 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 146 | controller.redirect_with_header_break 147 | 148 | response.status_code.should eq(302) 149 | response.location.should eq("http://test.host/lolwat") 150 | response.body.to_s.should eq("") 151 | end 152 | 153 | it "redirects to a URL, even when there are null bytes in the URL" do 154 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 155 | request = CarbonDispatch::Request.new(http_request) 156 | response = CarbonDispatch::Response.new 157 | 158 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 159 | controller.redirect_with_null_bytes 160 | 161 | response.status_code.should eq(302) 162 | response.location.should eq("http://www.example.com/lolwat") 163 | response.body.to_s.should eq("") 164 | end 165 | 166 | it "raises RedirectBackError, when redirect back with blank 'REFERER' HTTP header" do 167 | http_request = MockRequest.new.get("/", {"HOST": "test.host", "REFERER": ""}) 168 | request = CarbonDispatch::Request.new(http_request) 169 | response = CarbonDispatch::Response.new 170 | 171 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 172 | 173 | expect_raises CarbonController::RedirectBackError do 174 | controller.redirect_back 175 | end 176 | end 177 | 178 | it "raises RedirectBackError, when redirect back with NO 'REFERER' HTTP header" do 179 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 180 | request = CarbonDispatch::Request.new(http_request) 181 | response = CarbonDispatch::Response.new 182 | 183 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 184 | 185 | expect_raises CarbonController::RedirectBackError do 186 | controller.redirect_back 187 | end 188 | end 189 | 190 | it "raises CarbonControllerError, when redirect with nil" do 191 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 192 | request = CarbonDispatch::Request.new(http_request) 193 | response = CarbonDispatch::Response.new 194 | 195 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 196 | 197 | expect_raises CarbonController::CarbonControllerError do 198 | controller.redirect_nil 199 | end 200 | end 201 | 202 | it "raises CarbonControllerError, when redirect with HTTP::Params" do 203 | http_request = MockRequest.new.get("/", {"HOST": "test.host"}) 204 | request = CarbonDispatch::Request.new(http_request) 205 | response = CarbonDispatch::Response.new 206 | 207 | controller = CarbonControllerTest::RedirectTestController.new(request, response) 208 | 209 | expect_raises CarbonController::CarbonControllerError do 210 | controller.redirect_http_params 211 | end 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /spec/carbon_dispatch/cookies_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | KEY_GENERATOR = CarbonSupport::CachingKeyGenerator.new(CarbonSupport::KeyGenerator.new("secret", 1)) 4 | 5 | def new_cookie_jar(headers = HTTP::Headers.new) 6 | cookies = CarbonDispatch::Cookies::CookieJar.new(KEY_GENERATOR) 7 | cookies.update(CarbonDispatch::Cookies::CookieJar.from_headers(headers)) 8 | cookies 9 | end 10 | 11 | def cookie_header(cookies) 12 | http_headers = HTTP::Headers.new 13 | cookies.write(http_headers) 14 | http_headers["Set-Cookie"] 15 | end 16 | 17 | module CarbonDispatch 18 | describe Cookies::CookieJar do 19 | it "sets a cookie" do 20 | cookies = new_cookie_jar 21 | cookies.set "user_name", "david" 22 | cookie_header(cookies).should eq "user_name=david; path=/" 23 | end 24 | 25 | it "reads a cookie" do 26 | cookies = new_cookie_jar 27 | cookies.set "user_name", "Jamie" 28 | cookies["user_name"].should eq "Jamie" 29 | end 30 | 31 | it "sets a permanent cookie" do 32 | cookies = new_cookie_jar 33 | cookies.set "user_name", "Jamie" 34 | cookies.permanent.set "user_name", "Jamie" 35 | cookie_header(cookies).should eq "user_name=Jamie; path=/; expires=#{HTTP.rfc1123_date(20.years.from_now)}" 36 | end 37 | 38 | it "reads a permanent cookie" do 39 | cookies = new_cookie_jar 40 | cookies.permanent.set "user_name", "Jamie" 41 | cookies.permanent["user_name"].should eq "Jamie" 42 | end 43 | 44 | it "sets a cookie with escapable characters" do 45 | cookies = new_cookie_jar 46 | cookies.set "that & guy", "foo & bar => baz" 47 | cookie_header(cookies).should eq "that%20%26%20guy=foo%20%26%20bar%20%3D%3E%20baz; path=/" 48 | end 49 | 50 | it "sets the cookie with expiration" do 51 | cookies = new_cookie_jar 52 | cookies.set "user_name", "david", expires: Time.new(2005, 10, 10, 5) 53 | cookie_header(cookies).should eq "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 GMT" 54 | end 55 | 56 | it "sets the cookie with http_only" do 57 | cookies = new_cookie_jar 58 | cookies.set "user_name", "david", http_only: true 59 | cookie_header(cookies).should eq "user_name=david; path=/; HttpOnly" 60 | end 61 | 62 | it "sets the cookie with secure if the jar is secure" do 63 | cookies = new_cookie_jar 64 | cookies.secure = true 65 | cookies.set "user_name", "david", secure: true 66 | cookie_header(cookies).should eq "user_name=david; path=/; Secure" 67 | end 68 | 69 | it "does not set the cookie with secure if the jar is insecure" do 70 | cookies = new_cookie_jar 71 | cookies.secure = false 72 | cookies.set "user_name", "david", secure: true 73 | cookie_header(cookies).should eq "" 74 | end 75 | 76 | it "sets the insecure cookie with if the jar is secure" do 77 | cookies = new_cookie_jar 78 | cookies.secure = true 79 | cookies.set "user_name", "david", secure: false 80 | cookie_header(cookies).should eq "user_name=david; path=/" 81 | end 82 | 83 | it "sets multiple cookies" do 84 | cookies = new_cookie_jar 85 | cookies.set "user_name", "david", expires: Time.new(2005, 10, 10, 5) 86 | cookies.set "login", "XJ-122" 87 | cookies.size.should eq 2 88 | cookie_header(cookies).should eq "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 GMT,login=XJ-122; path=/" 89 | end 90 | 91 | it "sets an encrypted cookie" do 92 | cookies = new_cookie_jar 93 | cookies.encrypted.set "user_name", "david" 94 | cookies.encrypted["user_name"].should eq "david" 95 | cookie_header(cookies).should_not eq "user_name=david; path=/" 96 | end 97 | 98 | it "gets an encrypted cookie" do 99 | cookies = new_cookie_jar 100 | cookie = HTTP::Cookie::Parser.parse_cookies("user_name=YVpKaXlJN29vZUlwUnNuR3JzOVFPdEFwazFGWWNrYlpIUzhqU21YWWJDbz0tLVAvUldZaFZCQklLOW44ZGJLMDAramc9PQ%3D%3D--cead74d6b7a64512a499fef31483fd21d9e89b85378a3eaa440c7ac7f9cd6b94; path=/").first 101 | cookies[cookie.name] = cookie 102 | cookies.encrypted["user_name"].should eq "david" 103 | end 104 | 105 | it "ignores tampered cookie signature" do 106 | cookies = new_cookie_jar 107 | cookie = HTTP::Cookie::Parser.parse_cookies("user_name=YVpKaXlJN29vZUlwUnNuR3JzOVFPdEFwazFGWWNrYlpIUzhqU21YWWJDbz0tLVAvUldZaFZCQklLOW44ZGJLMDAramc9PQ%3D%3D--tampered; path=/").first 108 | cookies[cookie.name] = cookie 109 | cookies.encrypted["user_name"].should eq "" 110 | end 111 | 112 | it "ignores tampered cookie value" do 113 | cookies = new_cookie_jar(HTTP::Headers{"Cookie" => "user_name=tampered%3D%3D--cead74d6b7a64512a499fef31483fd21d9e89b85378a3eaa440c7ac7f9cd6b94;"}) 114 | cookies.encrypted["user_name"].should eq "" 115 | end 116 | 117 | it "ignores unset encrypted cookies" do 118 | cookies = new_cookie_jar 119 | cookies.encrypted["invalid"].should eq nil 120 | end 121 | 122 | it "raises cookie overflow error" do 123 | cookies = new_cookie_jar 124 | expect_raises(Cookies::CookieOverflow) do 125 | cookies.encrypted["user_name"] = "long" * 2000 126 | end 127 | end 128 | 129 | it "deletes a cookie" do 130 | cookies = new_cookie_jar(HTTP::Headers{"Cookie" => "user_name=david"}) 131 | cookies["user_name"].should eq "david" 132 | cookies.delete "user_name" 133 | cookie_header(cookies).should eq "user_name=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT" 134 | end 135 | 136 | it "allow deleting a unexisting cookie" do 137 | cookies = new_cookie_jar 138 | cookies.delete "invalid" 139 | end 140 | 141 | it "returns true if the cookie is delete" do 142 | cookies = new_cookie_jar(HTTP::Headers{"Cookie" => "user_name=david"}) 143 | cookies.delete "user_name" 144 | cookies.deleted?("user_name").should eq true 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/carbon_dispatch/flash_spec.cr: -------------------------------------------------------------------------------- 1 | def flash 2 | json = <<-JSON 3 | { 4 | "discard": ["alert"], "flashes": {"notice": "success"} 5 | } 6 | JSON 7 | CarbonDispatch::Flash::FlashHash.from_session_value(json) 8 | end 9 | 10 | class CarbonDispatch::Flash 11 | describe FlashHash do 12 | it "loads from the session value and sweeps" do 13 | flash.@flashes.should eq({"notice" => "success"}) 14 | flash.@discard.should eq(Set(String).new(["notice"])) 15 | end 16 | 17 | it "returns the keys" do 18 | flash.keys.should eq ["notice"] 19 | end 20 | 21 | it "handles key?" do 22 | flash.has_key?("notice").should eq true 23 | flash.has_key?("invalid").should eq false 24 | end 25 | 26 | it "deletes a key" do 27 | flash = flash 28 | flash.delete("notice").should eq flash 29 | flash.has_key?("notice").should eq false 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/carbon_dispatch/request_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe CarbonDispatch::Request do 4 | context "#scheme" do 5 | it "contains the scheme information" do 6 | mock_http_request = MockRequest.new 7 | 8 | http_request = mock_http_request.get("/") 9 | request = CarbonDispatch::Request.new(http_request) 10 | request.scheme.should eq("http") 11 | 12 | http_request = mock_http_request.get("/", {"HTTPS": "on"}) 13 | request = CarbonDispatch::Request.new(http_request) 14 | request.scheme.should eq("https") 15 | 16 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com"}) 17 | request = CarbonDispatch::Request.new(http_request) 18 | request.scheme.should eq("http") 19 | 20 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:8080"}) 21 | request = CarbonDispatch::Request.new(http_request) 22 | request.scheme.should eq("http") 23 | 24 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com", "HTTPS": "on"}) 25 | request = CarbonDispatch::Request.new(http_request) 26 | request.scheme.should eq("https") 27 | 28 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:8443", "HTTPS": "on"}) 29 | request = CarbonDispatch::Request.new(http_request) 30 | request.scheme.should eq("https") 31 | end 32 | 33 | it "supports forwarded scheme information by proxies" do 34 | mock_http_request = MockRequest.new 35 | 36 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_SSL": "on"}) 37 | request = CarbonDispatch::Request.new(http_request) 38 | request.scheme.should eq("https") 39 | 40 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com", "HTTP_X_FORWARDED_SSL": "on"}) 41 | request = CarbonDispatch::Request.new(http_request) 42 | request.scheme.should eq("https") 43 | 44 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:8443", "HTTP_X_FORWARDED_SSL": "on"}) 45 | request = CarbonDispatch::Request.new(http_request) 46 | request.scheme.should eq("https") 47 | 48 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_SCHEME": "https"}) 49 | request = CarbonDispatch::Request.new(http_request) 50 | request.scheme.should eq("https") 51 | 52 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com", "HTTP_X_FORWARDED_SCHEME": "https"}) 53 | request = CarbonDispatch::Request.new(http_request) 54 | request.scheme.should eq("https") 55 | 56 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:8443", "HTTP_X_FORWARDED_SCHEME": "https"}) 57 | request = CarbonDispatch::Request.new(http_request) 58 | request.scheme.should eq("https") 59 | 60 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_PROTO": "https"}) 61 | request = CarbonDispatch::Request.new(http_request) 62 | request.scheme.should eq("https") 63 | 64 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_PROTO": "https, http, http"}) 65 | request = CarbonDispatch::Request.new(http_request) 66 | request.scheme.should eq("https") 67 | 68 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_PROTO": ["https", "http", "http"]}) 69 | request = CarbonDispatch::Request.new(http_request) 70 | request.scheme.should eq("https") 71 | end 72 | end 73 | 74 | context "#port" do 75 | it "contains the port information" do 76 | mock_http_request = MockRequest.new 77 | 78 | http_request = mock_http_request.get("/") 79 | request = CarbonDispatch::Request.new(http_request) 80 | request.port.should eq(80) 81 | 82 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:8080"}) 83 | request = CarbonDispatch::Request.new(http_request) 84 | request.port.should eq(8080) 85 | 86 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com"}) 87 | request = CarbonDispatch::Request.new(http_request) 88 | request.port.should eq(80) 89 | 90 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:9292"}) 91 | request = CarbonDispatch::Request.new(http_request) 92 | request.port.should eq(9292) 93 | 94 | http_request = mock_http_request.get("/", {"SERVER_NAME": "example.org", "SERVER_PORT": "9292"}) 95 | request = CarbonDispatch::Request.new(http_request) 96 | request.port.should eq(9292) 97 | 98 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:9292", "SERVER_NAME": "example.com", "SERVER_PORT": "9292"}) 99 | request = CarbonDispatch::Request.new(http_request) 100 | request.port.should eq(9292) 101 | end 102 | 103 | it "supports forwarded port information by proxies" do 104 | mock_http_request = MockRequest.new 105 | 106 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_PORT": "9292"}) 107 | request = CarbonDispatch::Request.new(http_request) 108 | request.port.should eq(9292) 109 | 110 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost", "HTTP_X_FORWARDED_PORT": "9292"}) 111 | request = CarbonDispatch::Request.new(http_request) 112 | request.port.should eq(9292) 113 | 114 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_PORT": "9292"}) 115 | request = CarbonDispatch::Request.new(http_request) 116 | request.port.should eq(92) 117 | 118 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "HTTP_X_FORWARDED_PORT": "9292"}) 119 | request = CarbonDispatch::Request.new(http_request) 120 | request.port.should eq(9292) 121 | 122 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com:9595", "HTTP_X_FORWARDED_PORT": "9292"}) 123 | request = CarbonDispatch::Request.new(http_request) 124 | request.port.should eq(9595) 125 | 126 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com"}) 127 | request = CarbonDispatch::Request.new(http_request) 128 | request.port.should eq(80) 129 | 130 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com:9292"}) 131 | request = CarbonDispatch::Request.new(http_request) 132 | request.port.should eq(9292) 133 | 134 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "SERVER_PORT": "80"}) 135 | request = CarbonDispatch::Request.new(http_request) 136 | request.port.should eq(80) 137 | 138 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com:9292", "SERVER_PORT": "80"}) 139 | request = CarbonDispatch::Request.new(http_request) 140 | request.port.should eq(9292) 141 | end 142 | 143 | it "derives the port information based on the scheme" do 144 | mock_http_request = MockRequest.new 145 | 146 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "HTTP_X_FORWARDED_SSL": "on"}) 147 | request = CarbonDispatch::Request.new(http_request) 148 | request.port.should eq(443) 149 | 150 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "HTTP_X_FORWARDED_SCHEME": "https"}) 151 | request = CarbonDispatch::Request.new(http_request) 152 | request.port.should eq(443) 153 | 154 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "HTTP_X_FORWARDED_PROTO": "https"}) 155 | request = CarbonDispatch::Request.new(http_request) 156 | request.port.should eq(443) 157 | 158 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_HOST": "example.com", "HTTP_X_FORWARDED_PROTO": "https, http, http"}) 159 | request = CarbonDispatch::Request.new(http_request) 160 | request.port.should eq(443) 161 | 162 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_SSL": "on", "SERVER_PORT": "80"}) 163 | request = CarbonDispatch::Request.new(http_request) 164 | request.port.should eq(443) 165 | 166 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_SCHEME": "https", "SERVER_PORT": "80"}) 167 | request = CarbonDispatch::Request.new(http_request) 168 | request.port.should eq(443) 169 | 170 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_PROTO": "https", "SERVER_PORT": "80"}) 171 | request = CarbonDispatch::Request.new(http_request) 172 | request.port.should eq(443) 173 | 174 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:92", "HTTP_X_FORWARDED_PROTO": "https, http, http", "SERVER_PORT": "80"}) 175 | request = CarbonDispatch::Request.new(http_request) 176 | request.port.should eq(443) 177 | end 178 | end 179 | 180 | context "#host_with_port" do 181 | it "returns the #host without the #port, with a #standard_port?" do 182 | mock_http_request = MockRequest.new 183 | 184 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost"}) 185 | request = CarbonDispatch::Request.new(http_request) 186 | request.host_with_port.should eq("localhost") 187 | 188 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:80"}) 189 | request = CarbonDispatch::Request.new(http_request) 190 | request.host_with_port.should eq("localhost") 191 | 192 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:443"}) 193 | request = CarbonDispatch::Request.new(http_request) 194 | request.host_with_port.should eq("localhost") 195 | 196 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost", "HTTP_X_FORWARDED_HOST": "example.com"}) 197 | request = CarbonDispatch::Request.new(http_request) 198 | request.host_with_port.should eq("example.com") 199 | 200 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost", "HTTP_X_FORWARDED_HOST": "example.com:80"}) 201 | request = CarbonDispatch::Request.new(http_request) 202 | request.host_with_port.should eq("example.com") 203 | 204 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost", "HTTP_X_FORWARDED_HOST": "example.com:443"}) 205 | request = CarbonDispatch::Request.new(http_request) 206 | request.host_with_port.should eq("example.com") 207 | 208 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:8080", "HTTP_X_FORWARDED_HOST": "example.com"}) 209 | request = CarbonDispatch::Request.new(http_request) 210 | request.host_with_port.should eq("example.com") 211 | end 212 | 213 | it "returns the #host with the #port, without a #standard_port?" do 214 | mock_http_request = MockRequest.new 215 | 216 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:9393"}) 217 | request = CarbonDispatch::Request.new(http_request) 218 | request.host_with_port.should eq("localhost:9393") 219 | 220 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost", "HTTP_X_FORWARDED_HOST": "example.com:9393"}) 221 | request = CarbonDispatch::Request.new(http_request) 222 | request.host_with_port.should eq("example.com:9393") 223 | 224 | http_request = mock_http_request.get("/", {"HTTP_HOST": "localhost:93", "HTTP_X_FORWARDED_HOST": "example.com:9393"}) 225 | request = CarbonDispatch::Request.new(http_request) 226 | request.host_with_port.should eq("example.com:9393") 227 | end 228 | end 229 | 230 | context "#host and #raw_host_with_port" do 231 | it "contains the host information" do 232 | mock_http_request = MockRequest.new 233 | 234 | http_request = mock_http_request.get("/", {"HOST": "localhost"}) 235 | request = CarbonDispatch::Request.new(http_request) 236 | request.host.should eq("localhost") 237 | request.raw_host_with_port.should eq("localhost") 238 | 239 | http_request = mock_http_request.get("/", {"HOST": "localhost:80"}) 240 | request = CarbonDispatch::Request.new(http_request) 241 | request.host.should eq("localhost") 242 | request.raw_host_with_port.should eq("localhost:80") 243 | 244 | http_request = mock_http_request.get("/", {"HOST": "localhost:94"}) 245 | request = CarbonDispatch::Request.new(http_request) 246 | request.host.should eq("localhost") 247 | request.raw_host_with_port.should eq("localhost:94") 248 | 249 | http_request = mock_http_request.get("/", {"HTTP_HOST": "example.com"}) 250 | request = CarbonDispatch::Request.new(http_request) 251 | request.host.should eq("example.com") 252 | request.raw_host_with_port.should eq("example.com") 253 | 254 | http_request = mock_http_request.get("/", {"HTTP_HOST": "example.com:80"}) 255 | request = CarbonDispatch::Request.new(http_request) 256 | request.host.should eq("example.com") 257 | request.raw_host_with_port.should eq("example.com:80") 258 | 259 | http_request = mock_http_request.get("/", {"HTTP_HOST": "example.com:94"}) 260 | request = CarbonDispatch::Request.new(http_request) 261 | request.host.should eq("example.com") 262 | request.raw_host_with_port.should eq("example.com:94") 263 | 264 | http_request = mock_http_request.get("/", {"SERVER_NAME": "example.com", "SERVER_PORT": "80"}) 265 | request = CarbonDispatch::Request.new(http_request) 266 | request.host.should eq("example.com") 267 | request.raw_host_with_port.should eq("example.com:80") 268 | 269 | http_request = mock_http_request.get("/", {"SERVER_NAME": "example.com", "SERVER_PORT": "94"}) 270 | request = CarbonDispatch::Request.new(http_request) 271 | request.host.should eq("example.com") 272 | request.raw_host_with_port.should eq("example.com:94") 273 | 274 | http_request = mock_http_request.get("/", {"HOST": "localhost", "HTTP_HOST": "example.com"}) 275 | request = CarbonDispatch::Request.new(http_request) 276 | request.host.should eq("localhost") 277 | request.raw_host_with_port.should eq("localhost") 278 | 279 | http_request = mock_http_request.get("/", {"HOST": "localhost", "SERVER_NAME": "example.com", "SERVER_PORT": "94"}) 280 | request = CarbonDispatch::Request.new(http_request) 281 | request.host.should eq("localhost") 282 | request.raw_host_with_port.should eq("localhost") 283 | 284 | http_request = mock_http_request.get("/", {"HOST": "localhost", "HTTP_HOST": "www.example.com:94", "SERVER_NAME": "example.com", "SERVER_PORT": "94"}) 285 | request = CarbonDispatch::Request.new(http_request) 286 | request.host.should eq("localhost") 287 | request.raw_host_with_port.should eq("localhost") 288 | 289 | http_request = mock_http_request.get("/", {"HTTP_HOST": "www.example.com:94", "SERVER_NAME": "example.com", "SERVER_PORT": "94"}) 290 | request = CarbonDispatch::Request.new(http_request) 291 | request.host.should eq("www.example.com") 292 | request.raw_host_with_port.should eq("www.example.com:94") 293 | end 294 | 295 | it "supports forwarded host information by proxies" do 296 | mock_http_request = MockRequest.new 297 | 298 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": "example.com"}) 299 | request = CarbonDispatch::Request.new(http_request) 300 | request.host.should eq("example.com") 301 | request.raw_host_with_port.should eq("example.com") 302 | 303 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": "example.com:80"}) 304 | request = CarbonDispatch::Request.new(http_request) 305 | request.host.should eq("example.com") 306 | request.raw_host_with_port.should eq("example.com:80") 307 | 308 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": "example.com:9494"}) 309 | request = CarbonDispatch::Request.new(http_request) 310 | request.host.should eq("example.com") 311 | request.raw_host_with_port.should eq("example.com:9494") 312 | 313 | http_request = mock_http_request.get("/", {"HOST": "localhost", "HTTP_X_FORWARDED_HOST": "example.com"}) 314 | request = CarbonDispatch::Request.new(http_request) 315 | request.host.should eq("example.com") 316 | request.raw_host_with_port.should eq("example.com") 317 | 318 | http_request = mock_http_request.get("/", {"HOST": "localhost:80", "HTTP_X_FORWARDED_HOST": "example.com:8080"}) 319 | request = CarbonDispatch::Request.new(http_request) 320 | request.host.should eq("example.com") 321 | request.raw_host_with_port.should eq("example.com:8080") 322 | 323 | http_request = mock_http_request.get("/", {"HOST": "localhost", "HTTP_HOST": "example.com:8080", "HTTP_X_FORWARDED_HOST": "example.com"}) 324 | request = CarbonDispatch::Request.new(http_request) 325 | request.host.should eq("example.com") 326 | request.raw_host_with_port.should eq("example.com") 327 | 328 | http_request = mock_http_request.get("/", {"HOST": "localhost", "HTTP_HOST": "example.com", "HTTP_X_FORWARDED_HOST": "example.com:8080"}) 329 | request = CarbonDispatch::Request.new(http_request) 330 | request.host.should eq("example.com") 331 | request.raw_host_with_port.should eq("example.com:8080") 332 | 333 | http_request = mock_http_request.get("/", {"SERVER_NAME": "example.com", "SERVER_PORT": "80", "HTTP_X_FORWARDED_HOST": "example.com:8080"}) 334 | request = CarbonDispatch::Request.new(http_request) 335 | request.host.should eq("example.com") 336 | request.raw_host_with_port.should eq("example.com:8080") 337 | 338 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": "example1.com, example2.com, example3.com"}) 339 | request = CarbonDispatch::Request.new(http_request) 340 | request.host.should eq("example3.com") 341 | request.raw_host_with_port.should eq("example3.com") 342 | 343 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": ["example1.com", "example2.com", "example3.com"]}) 344 | request = CarbonDispatch::Request.new(http_request) 345 | request.host.should eq("example3.com") 346 | request.raw_host_with_port.should eq("example3.com") 347 | 348 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": "example1.com:80, example2.com:8080, example3.com:9494"}) 349 | request = CarbonDispatch::Request.new(http_request) 350 | request.host.should eq("example3.com") 351 | request.raw_host_with_port.should eq("example3.com:9494") 352 | 353 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_HOST": ["example1.com:80", "example2.com:8080", "example3.com:9494"]}) 354 | request = CarbonDispatch::Request.new(http_request) 355 | request.host.should eq("example3.com") 356 | request.raw_host_with_port.should eq("example3.com:9494") 357 | end 358 | end 359 | 360 | context "#ip" do 361 | it "contains the IP information" do 362 | mock_http_request = MockRequest.new 363 | 364 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4"}) 365 | request = CarbonDispatch::Request.new(http_request) 366 | request.ip.should eq("1.2.3.4") 367 | 368 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4,3.4.5.6"}) 369 | request = CarbonDispatch::Request.new(http_request) 370 | request.ip.should eq("1.2.3.4") 371 | 372 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4, 3.4.5.6"}) 373 | request = CarbonDispatch::Request.new(http_request) 374 | request.ip.should eq("1.2.3.4") 375 | 376 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": ["1.2.3.4", "3.4.5.6"]}) 377 | request = CarbonDispatch::Request.new(http_request) 378 | request.ip.should eq("1.2.3.4") 379 | 380 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "fe80::202:b3ff:fe1e:8329"}) 381 | request = CarbonDispatch::Request.new(http_request) 382 | request.ip.should eq("fe80::202:b3ff:fe1e:8329") 383 | 384 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "2620:0:1c00:0:812c:9583:754b:ca11,fd5b:982e:9130:247f:0000:0000:0000:0000"}) 385 | request = CarbonDispatch::Request.new(http_request) 386 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 387 | 388 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "2620:0:1c00:0:812c:9583:754b:ca11, fd5b:982e:9130:247f:0000:0000:0000:0000"}) 389 | request = CarbonDispatch::Request.new(http_request) 390 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 391 | 392 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": ["2620:0:1c00:0:812c:9583:754b:ca11", "fd5b:982e:9130:247f:0000:0000:0000:0000"]}) 393 | request = CarbonDispatch::Request.new(http_request) 394 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 395 | end 396 | 397 | it "supports forwarded IP information by proxies" do 398 | mock_http_request = MockRequest.new 399 | 400 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4", "HTTP_X_FORWARDED_FOR": "3.4.5.6"}) 401 | request = CarbonDispatch::Request.new(http_request) 402 | request.ip.should eq("1.2.3.4") 403 | 404 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4", "HTTP_X_FORWARDED_FOR": "3.4.5.6,7.8.9.0"}) 405 | request = CarbonDispatch::Request.new(http_request) 406 | request.ip.should eq("1.2.3.4") 407 | 408 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "1.2.3.4", "HTTP_X_FORWARDED_FOR": ["3.4.5.6", "7.8.9.0"]}) 409 | request = CarbonDispatch::Request.new(http_request) 410 | request.ip.should eq("1.2.3.4") 411 | 412 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "3.4.5.6"}) 413 | request = CarbonDispatch::Request.new(http_request) 414 | request.ip.should eq("3.4.5.6") 415 | 416 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "3.4.5.6,7.8.9.0"}) 417 | request = CarbonDispatch::Request.new(http_request) 418 | request.ip.should eq("7.8.9.0") 419 | 420 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": ["3.4.5.6", "7.8.9.0"]}) 421 | request = CarbonDispatch::Request.new(http_request) 422 | request.ip.should eq("7.8.9.0") 423 | 424 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "fe80::202:b3ff:fe1e:8329"}) 425 | request = CarbonDispatch::Request.new(http_request) 426 | request.ip.should eq("fe80::202:b3ff:fe1e:8329") 427 | 428 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "2620:0:1c00:0:812c:9583:754b:ca11,fd5b:982e:9130:247f:0000:0000:0000:0000"}) 429 | request = CarbonDispatch::Request.new(http_request) 430 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 431 | 432 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": ["2620:0:1c00:0:812c:9583:754b:ca11", "fd5b:982e:9130:247f:0000:0000:0000:0000"]}) 433 | request = CarbonDispatch::Request.new(http_request) 434 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 435 | 436 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "unknown,3.4.5.6"}) 437 | request = CarbonDispatch::Request.new(http_request) 438 | request.ip.should eq("3.4.5.6") 439 | 440 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "other,unknown,3.4.5.6"}) 441 | request = CarbonDispatch::Request.new(http_request) 442 | request.ip.should eq("3.4.5.6") 443 | end 444 | 445 | it "ignores trusted IP addresses" do 446 | mock_http_request = MockRequest.new 447 | 448 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "127.0.0.1,3.4.5.6"}) 449 | request = CarbonDispatch::Request.new(http_request) 450 | request.ip.should eq("3.4.5.6") 451 | 452 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "192.168.0.1, 3.4.5.6"}) 453 | request = CarbonDispatch::Request.new(http_request) 454 | request.ip.should eq("3.4.5.6") 455 | 456 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": ["10.0.0.1", "3.4.5.6"]}) 457 | request = CarbonDispatch::Request.new(http_request) 458 | request.ip.should eq("3.4.5.6") 459 | 460 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": ["10.0.0.1", "10.0.0.1", "3.4.5.6"]}) 461 | request = CarbonDispatch::Request.new(http_request) 462 | request.ip.should eq("3.4.5.6") 463 | 464 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "::1,2620:0:1c00:0:812c:9583:754b:ca11"}) 465 | request = CarbonDispatch::Request.new(http_request) 466 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 467 | 468 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "2620:0:1c00:0:812c:9583:754b:ca11,::1"}) 469 | request = CarbonDispatch::Request.new(http_request) 470 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 471 | 472 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "fd5b:982e:9130:247f:0000:0000:0000:0000,2620:0:1c00:0:812c:9583:754b:ca11"}) 473 | request = CarbonDispatch::Request.new(http_request) 474 | request.ip.should eq("2620:0:1c00:0:812c:9583:754b:ca11") 475 | 476 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "127.0.0.1", "HTTP_X_FORWARDED_FOR": "3.4.5.6"}) 477 | request = CarbonDispatch::Request.new(http_request) 478 | request.ip.should eq("3.4.5.6") 479 | 480 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "127.0.0.1", "HTTP_X_FORWARDED_FOR": "10.0.0.1,3.4.5.6"}) 481 | request = CarbonDispatch::Request.new(http_request) 482 | request.ip.should eq("3.4.5.6") 483 | 484 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "unix", "HTTP_X_FORWARDED_FOR": "3.4.5.6"}) 485 | request = CarbonDispatch::Request.new(http_request) 486 | request.ip.should eq("3.4.5.6") 487 | 488 | http_request = mock_http_request.get("/", {"REMOTE_ADDR": "unix:/tmp/foo", "HTTP_X_FORWARDED_FOR": "3.4.5.6"}) 489 | request = CarbonDispatch::Request.new(http_request) 490 | request.ip.should eq("3.4.5.6") 491 | 492 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "unknown,192.168.0.1"}) 493 | request = CarbonDispatch::Request.new(http_request) 494 | request.ip.should eq("unknown") 495 | 496 | http_request = mock_http_request.get("/", {"HTTP_X_FORWARDED_FOR": "other,unknown,192.168.0.1"}) 497 | request = CarbonDispatch::Request.new(http_request) 498 | request.ip.should eq("unknown") 499 | end 500 | end 501 | 502 | context "#trusted_proxy?" do 503 | it "ignores local and trusted IP addresses" do 504 | mock_http_request = MockRequest.new 505 | http_request = mock_http_request.get("/", {"HOST": "host.example.org"}) 506 | request = CarbonDispatch::Request.new(http_request) 507 | 508 | request.trusted_proxy?("127.0.0.1").should eq(0) 509 | request.trusted_proxy?("127.0.0.1").should eq(0) 510 | request.trusted_proxy?("10.0.0.1").should eq(0) 511 | request.trusted_proxy?("172.16.0.1").should eq(0) 512 | request.trusted_proxy?("172.20.0.1").should eq(0) 513 | request.trusted_proxy?("172.30.0.1").should eq(0) 514 | request.trusted_proxy?("172.31.0.1").should eq(0) 515 | request.trusted_proxy?("192.168.0.1").should eq(0) 516 | request.trusted_proxy?("::1").should eq(0) 517 | request.trusted_proxy?("fd00::").should eq(0) 518 | request.trusted_proxy?("localhost").should eq(0) 519 | request.trusted_proxy?("unix").should eq(0) 520 | request.trusted_proxy?("unix:/tmp/sock").should eq(0) 521 | 522 | request.trusted_proxy?("unix.example.org").should eq(nil) 523 | request.trusted_proxy?("example.org\n127.0.0.1").should eq(nil) 524 | request.trusted_proxy?("127.0.0.1\nexample.org").should eq(nil) 525 | request.trusted_proxy?("11.0.0.1").should eq(nil) 526 | request.trusted_proxy?("172.15.0.1").should eq(nil) 527 | request.trusted_proxy?("172.32.0.1").should eq(nil) 528 | request.trusted_proxy?("2001:470:1f0b:18f8::1").should eq(nil) 529 | end 530 | end 531 | end 532 | -------------------------------------------------------------------------------- /spec/carbon_dispatch/route_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module CarbonDispatchTest 4 | extend self 5 | 6 | def route(controller, action, method, path, app = app) 7 | CarbonDispatch::Route.new(controller, action, method, path, app) 8 | end 9 | 10 | def app 11 | ->(request : CarbonDispatch::Request, response : CarbonDispatch::Response) {} 12 | end 13 | 14 | describe CarbonDispatch::Route do 15 | context "pattern" do 16 | it "matches normal string" do 17 | route("TestController", "index", ["GET"], "/").match("GET", "/").should be_truthy 18 | route("TestController", "index", ["GET"], "/foo").match("GET", "/foo").should be_truthy 19 | route("TestController", "index", ["GET"], "/foo").match("GET", "/").should be_falsey 20 | end 21 | 22 | it "matches with named_params" do 23 | route("TestController", "index", ["GET"], "/base/:id").match("GET", "/base/1").should be_truthy 24 | route("TestController", "index", ["GET"], "/base/:id").match("GET", "/base/1/").should be_truthy 25 | route("TestController", "index", ["GET"], "/base/:id").match("GET", "/base/1/foo").should be_falsey 26 | end 27 | 28 | it "matches with optional params" do 29 | route("TestController", "index", ["GET"], "/base(:option)").match("GET", "/base").should be_truthy 30 | route("TestController", "index", ["GET"], "/base(:option)").match("GET", "/base1").should be_truthy 31 | route("TestController", "index", ["GET"], "/base(:option)").match("GET", "/base/a").should be_falsey 32 | route("TestController", "index", ["GET"], "/base(/:option)").match("GET", "/base/1").should be_truthy 33 | route("TestController", "index", ["GET"], "/base(/:option/fixed)").match("GET", "/base/1").should be_falsey 34 | route("TestController", "index", ["GET"], "/base(/:option/fixed)").match("GET", "/base/12/fixed").should be_truthy 35 | end 36 | 37 | it "matches with slugged param" do 38 | route("TestController", "index", ["GET"], "/base/*slug").match("GET", "/base/foo").should be_truthy 39 | route("TestController", "index", ["GET"], "/base/*slug").match("GET", "/base/foo/bar").should be_truthy 40 | route("TestController", "index", ["GET"], "/base/*slug").match("GET", "/base/").should be_falsey 41 | end 42 | end 43 | 44 | context "method" do 45 | it "matches against the request method" do 46 | route("TestController", "index", ["GET"], "/").match("POST", "/").should be_falsey 47 | route("TestController", "index", ["POST"], "/").match("POST", "/").should be_truthy 48 | end 49 | 50 | it "matches againt any method in the list" do 51 | route("TestController", "index", ["GET", "POST"], "/").match("POST", "/").should be_truthy 52 | route("TestController", "index", ["GET", "POST"], "/").match("PATCH", "/").should be_falsey 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/carbon_dispatch/router_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module CarbonDispatchTest 4 | class BlogPostsController < CarbonController::Base 5 | def index 6 | end 7 | 8 | def show 9 | end 10 | 11 | def new 12 | end 13 | 14 | def create 15 | end 16 | 17 | def edit 18 | end 19 | 20 | def update 21 | end 22 | 23 | def destroy 24 | end 25 | end 26 | 27 | class TestController < CarbonController::Base 28 | def index 29 | end 30 | 31 | def new 32 | end 33 | 34 | def create 35 | end 36 | 37 | def update 38 | end 39 | 40 | def destroy 41 | end 42 | end 43 | 44 | router = CarbonDispatch::Router.new 45 | 46 | router.draw do 47 | get "/", controller: "test", action: "index" 48 | get "/new", controller: "test", action: "new" 49 | post "/", controller: "test", action: "create" 50 | put "/:id", controller: "test", action: "update" 51 | patch "/:id", controller: "test", action: "update" 52 | delete "/:id", controller: "test", action: "destroy" 53 | 54 | resources :blog_posts 55 | 56 | resources :test, only: [:index] 57 | resources :blog_posts, except: [:new, :create, :show, :edit, :update, :destroy] 58 | end 59 | 60 | describe CarbonDispatch::Router do 61 | it "creates routes" do 62 | router.routes.should be_a(Array(CarbonDispatch::Route)) 63 | end 64 | 65 | it "adds routes" do 66 | routes = router.routes 67 | routes[0].should eq CarbonDispatch::Route.create("test", "index", ["GET"], "/") 68 | routes[1].should eq CarbonDispatch::Route.create("test", "new", ["GET"], "/new") 69 | routes[2].should eq CarbonDispatch::Route.create("test", "create", ["POST"], "/") 70 | routes[3].should eq CarbonDispatch::Route.create("test", "update", ["PUT"], "/:id") 71 | routes[4].should eq CarbonDispatch::Route.create("test", "update", ["PATCH"], "/:id") 72 | routes[5].should eq CarbonDispatch::Route.create("test", "destroy", ["DELETE"], "/:id") 73 | end 74 | 75 | it "adds resources" do 76 | routes = router.routes 77 | routes[6].should eq CarbonDispatch::Route.create("blog_posts", "index", ["GET"], "/blog_posts") 78 | routes[7].should eq CarbonDispatch::Route.create("blog_posts", "new", ["GET"], "/blog_posts/new") 79 | routes[8].should eq CarbonDispatch::Route.create("blog_posts", "create", ["POST"], "/blog_posts") 80 | routes[9].should eq CarbonDispatch::Route.create("blog_posts", "show", ["GET"], "/blog_posts/:id") 81 | routes[10].should eq CarbonDispatch::Route.create("blog_posts", "edit", ["GET"], "/blog_posts/:id/edit") 82 | routes[11].should eq CarbonDispatch::Route.create("blog_posts", "update", ["PATCH", "PUT"], "/blog_posts/:id") 83 | routes[12].should eq CarbonDispatch::Route.create("blog_posts", "destroy", ["DELETE"], "/blog_posts/:id") 84 | end 85 | 86 | it "adds resources with only" do 87 | routes = router.routes 88 | routes[13].should eq CarbonDispatch::Route.create("test", "index", ["GET"], "/test") 89 | end 90 | 91 | it "adds resources with except" do 92 | routes = router.routes 93 | routes[14].should eq CarbonDispatch::Route.create("blog_posts", "index", ["GET"], "/blog_posts") 94 | routes.size.should eq 15 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/carbon_support/callbacks_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module CarbonSupportTest 4 | class Base 5 | include CarbonSupport::Callbacks 6 | getter :callstack 7 | 8 | def initialize 9 | @callstack = [] of String 10 | end 11 | 12 | define_callbacks :test 13 | 14 | set_callback :test, :before, :inherited_before 15 | 16 | def inherited_before 17 | callstack << "inherited before" 18 | end 19 | end 20 | 21 | class NormalCallbackTest < Base 22 | set_callback :test, :before, :before1 23 | set_callback :test, :around, :test_around 24 | set_callback :test, :before, :before2 25 | set_callback :test, :after, :after1 26 | set_callback :test, :around, :test_around2 27 | set_callback :test, :before, :before3 28 | set_callback :test, :after, :after2 29 | 30 | def test 31 | run_callbacks :test do 32 | callstack << "test" 33 | "result" 34 | end 35 | end 36 | 37 | def test_around 38 | callstack << "around1a" 39 | yield 40 | callstack << "around1b" 41 | end 42 | 43 | def test_around2 44 | callstack << "around2a" 45 | yield 46 | callstack << "around2b" 47 | end 48 | 49 | def before1 50 | callstack << "before 1" 51 | end 52 | 53 | def before2 54 | callstack << "before 2" 55 | end 56 | 57 | def before3 58 | callstack << "before 3" 59 | end 60 | 61 | def after1 62 | callstack << "after 1" 63 | end 64 | 65 | def after2 66 | callstack << "after 2" 67 | end 68 | end 69 | 70 | class InheritedCallbackTest < NormalCallbackTest 71 | end 72 | 73 | class HaltingCallbackTest 74 | include CarbonSupport::Callbacks 75 | getter :callstack 76 | 77 | def initialize 78 | @callstack = [] of String 79 | end 80 | 81 | class CallStackTerminator 82 | def terminate?(target, result) 83 | target.callstack.size == 1 if target.is_a?(HaltingCallbackTest) 84 | end 85 | end 86 | 87 | define_callbacks(:test) 88 | define_callbacks(:test_with_terminator, CallbackChain::Options.new( 89 | terminator: CallStackTerminator.new, 90 | skip_after_callbacks_if_terminated: true)) 91 | set_callback :test, :before, :before1 92 | set_callback :test, :before, :before2 93 | set_callback :test_with_terminator, :before, :before1 94 | set_callback :test_with_terminator, :before, :before2 95 | 96 | def halted_callback_hook(filter) 97 | callstack << "halted #{filter}" 98 | end 99 | 100 | def test 101 | run_callbacks :test do 102 | callstack << "test" 103 | "result" 104 | end 105 | end 106 | 107 | def test_with_terminator 108 | run_callbacks :test_with_terminator do 109 | callstack << "test" 110 | "result" 111 | end 112 | end 113 | 114 | def before1 115 | callstack << "before 1" 116 | end 117 | 118 | def before2 119 | callstack << "before 2" 120 | false 121 | end 122 | end 123 | 124 | class OtherTerminatorTest < Base 125 | class DelegateTerminator 126 | def terminate?(target, result) 127 | target.terminate? if target.is_a?(OtherTerminatorTest) 128 | end 129 | end 130 | 131 | def terminate? 132 | true 133 | end 134 | 135 | define_callbacks(:test_with_terminator, CallbackChain::Options.new( 136 | terminator: DelegateTerminator.new, 137 | skip_after_callbacks_if_terminated: true)) 138 | set_callback :test_with_terminator, :before, :before1 139 | set_callback :test_with_terminator, :before, :before2 140 | 141 | def test_with_terminator 142 | run_callbacks :test_with_terminator do 143 | callstack << "test" 144 | "result" 145 | end 146 | end 147 | 148 | def halted_callback_hook(filter) 149 | callstack << "halted #{filter}" 150 | end 151 | 152 | def before1 153 | callstack << "before 1" 154 | end 155 | 156 | def before2 157 | callstack << "before 2" 158 | false 159 | end 160 | end 161 | 162 | describe CarbonSupport::Callbacks do 163 | it "normal callbacks" do 164 | object = NormalCallbackTest.new 165 | result = object.test 166 | object.callstack.should eq [ 167 | "inherited before", 168 | "before 1", 169 | "around1a", 170 | "before 2", 171 | "around2a", 172 | "before 3", 173 | "test", 174 | "after 2", 175 | "around2b", 176 | "after 1", 177 | "around1b", 178 | ] 179 | result.should eq "result" 180 | end 181 | 182 | it "inherited callbacks" do 183 | object = InheritedCallbackTest.new 184 | result = object.test 185 | object.callstack.should eq [ 186 | "inherited before", 187 | "before 1", 188 | "around1a", 189 | "before 2", 190 | "around2a", 191 | "before 3", 192 | "test", 193 | "after 2", 194 | "around2b", 195 | "after 1", 196 | "around1b", 197 | ] 198 | result.should eq "result" 199 | end 200 | 201 | it "halting callbacks" do 202 | object = HaltingCallbackTest.new 203 | result = object.test 204 | object.callstack.should eq [ 205 | "before 1", 206 | "before 2", 207 | "halted before2", 208 | ] 209 | result.should eq false 210 | end 211 | 212 | it "halting callbacks" do 213 | object = HaltingCallbackTest.new 214 | result = object.test_with_terminator 215 | object.callstack.should eq [ 216 | "before 1", 217 | "halted before1", 218 | ] 219 | result.should eq false 220 | end 221 | 222 | it "other terminator callbacks" do 223 | object = OtherTerminatorTest.new 224 | result = object.test_with_terminator 225 | object.callstack.should eq [ 226 | "before 1", 227 | "halted before1", 228 | ] 229 | result.should eq false 230 | end 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /spec/carbon_support/core_ext/string_ext_spec.cr: -------------------------------------------------------------------------------- 1 | module StringExtTest 2 | class NewObject 3 | def to_s 4 | "other" 5 | end 6 | end 7 | 8 | describe "OutputSafetyTest" do 9 | string = "hello" 10 | object = NewObject.new 11 | 12 | it "should be unsafe by default" do 13 | string.html_safe?.should eq false 14 | end 15 | 16 | it "should mark a string safe" do 17 | safe_string = string.html_safe 18 | safe_string.html_safe?.should eq true 19 | end 20 | 21 | it "returns the string after marking safe " do 22 | string.html_safe.should eq string 23 | end 24 | 25 | it "should be safe for numbers" do 26 | 5.html_safe?.should eq true 27 | end 28 | 29 | it "should be safe for floats" do 30 | 5.7.html_safe?.should eq true 31 | end 32 | 33 | it "should be unsafe for objects" do 34 | object.html_safe?.should eq false 35 | end 36 | 37 | it "returns a safe string when adding an object to a safe string" do 38 | safe_string = string.html_safe 39 | safe_string += object.to_s 40 | 41 | safe_string.should eq "helloother" 42 | safe_string.html_safe?.should eq true 43 | end 44 | 45 | it "returns a safe string when adding a safe string to another safe string " do 46 | other_string = "other".html_safe 47 | safe_string = string.html_safe 48 | combination = other_string + safe_string 49 | 50 | combination.should eq "otherhello" 51 | combination.html_safe?.should eq true 52 | end 53 | 54 | it "escapes it and returns a safe string when adding an unsafe string to a safe string" do 55 | other_string = "other".html_safe 56 | combination = other_string + "" 57 | other_combination = string + "" 58 | 59 | combination.should eq "other<foo>" 60 | other_combination.should eq "hello" 61 | 62 | combination.html_safe?.should eq true 63 | other_combination.html_safe?.should eq false 64 | end 65 | 66 | it "Concatting safe onto unsafe yields unsafe" do 67 | other_string = "other" 68 | 69 | string = string.html_safe 70 | other_string += string.to_s 71 | other_string.html_safe?.should eq false 72 | end 73 | 74 | it "Concatting unsafe onto safe yields escaped safe" do 75 | other_string = "other".html_safe 76 | string = other_string.concat("") 77 | string.should eq "other<foo>" 78 | string.html_safe?.should eq true 79 | end 80 | 81 | it "Concatting safe onto safe yields safe" do 82 | other_string = "other".html_safe 83 | string = string.html_safe 84 | 85 | other_string.concat(string) 86 | other_string.html_safe?.should eq true 87 | end 88 | 89 | it "Concatting safe onto unsafe with << yields unsafe" do 90 | other_string = "other" 91 | string = string.html_safe 92 | 93 | other_string += string.to_s 94 | other_string.html_safe?.should eq false 95 | end 96 | 97 | it "Concatting unsafe onto safe with << yields escaped safe" do 98 | other_string = "other".html_safe 99 | string = other_string + "" 100 | string.should eq "other<foo>" 101 | string.html_safe?.should eq true 102 | end 103 | 104 | it "Concatting safe onto safe with << yields safe" do 105 | other_string = "other".html_safe 106 | string = string.html_safe 107 | 108 | other_string += string 109 | other_string.html_safe?.should eq true 110 | end 111 | 112 | it "Concatting safe onto unsafe with % yields unsafe" do 113 | other_string = "other%s" 114 | string = string.html_safe 115 | 116 | other_string = other_string % string 117 | other_string.html_safe?.should eq false 118 | end 119 | 120 | it "Concatting unsafe onto safe with % yields escaped safe" do 121 | other_string = "other%s".html_safe 122 | string = other_string % "" 123 | 124 | string.should eq "other<foo>" 125 | string.html_safe?.should eq true 126 | end 127 | 128 | it "Concatting safe onto safe with % yields safe" do 129 | other_string = "other%s".html_safe 130 | string = string.html_safe 131 | 132 | other_string = other_string % string 133 | other_string.html_safe?.should eq true 134 | end 135 | 136 | it "Concatting with % doesn't modify a string" do 137 | other_string = ["

", "", "

"] 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 "

", tag("p", {:ignored => nil}) 46 | end 47 | 48 | it "tag_options_accepts_false_option" do 49 | assert_equal "

", tag("p", {:value => false}) 50 | end 51 | 52 | it "tag_options_accepts_blank_option" do 53 | assert_equal "

", tag("p", {:included => ""}) 54 | end 55 | 56 | # it "tag_options_converts_boolean_option" do 57 | # assert_dom_equal "

", 58 | # tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true) 59 | # end 60 | 61 | it "content_tag" do 62 | assert_equal "Create", content_tag("a", "Create", {"href" => "create"}) 63 | content_tag("a", "Create", {"href" => "create"}).html_safe?.should eq true 64 | assert_equal content_tag("a", "Create", {"href" => "create"}), 65 | content_tag("a", "Create", {:href => "create"}) 66 | 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 "
Hello world!
", buffer 75 | # end 76 | 77 | # it "content_tag_with_block_in_erb_containing_non_displayed_erb" do 78 | # buffer = render_erb("<%= content_tag(:p) do %><% 1 %><% end %>") 79 | # assert_dom_equal "

", buffer 80 | # end 81 | 82 | # it "content_tag_with_block_and_options_in_erb" do 83 | # buffer = render_erb("<%= content_tag(:div, :class => "green") do %>Hello world!<% end %>") 84 | # assert_dom_equal %(
Hello world!
), buffer 85 | # end 86 | 87 | # it "content_tag_with_block_and_options_out_of_erb" do 88 | # assert_dom_equal %(
Hello world!
), content_tag(:div, :class => "green") { "Hello world!" } 89 | # end 90 | 91 | it "content_tag_with_block_and_options_outside_out_of_erb" do 92 | assert_equal content_tag("a", "Create", {:href => "create"}), 93 | content_tag("a", {"href" => "create"}) { "Create" } 94 | end 95 | 96 | it "content_tag_with_block_and_non_string_outside_out_of_erb" do 97 | content_tag("p") { 3.times { "do_something" } }.should eq content_tag("p") 98 | end 99 | 100 | # it "content_tag_nested_in_content_tag_out_of_erb" do 101 | # assert_equal content_tag("p", content_tag("b", "Hello")), 102 | # content_tag("p") { content_tag("b", "Hello") }, 103 | # output_buffer 104 | # end 105 | 106 | # it "content_tag_nested_in_content_tag_in_erb" do 107 | # assert_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("") 146 | end 147 | 148 | it "cdata_section_with_string_conversion" do 149 | assert_equal "", cdata_section(nil) 150 | end 151 | 152 | it "cdata_section_splitted" do 153 | assert_equal "world]]>", cdata_section("hello]]>world") 154 | assert_equal "world]]]]>again]]>", cdata_section("hello]]>world]]>again") 155 | end 156 | 157 | it "escape_once" do 158 | assert_equal "1 < 2 & 3", TestView.new.escape_once("1 < 2 & 3") 159 | assert_equal " ' ' λ λ " ' < > ", TestView.new.escape_once(" ' ' λ λ \" ' < > ") 160 | end 161 | 162 | it "tag_honors_html_safe_for_param_values" do 163 | ["1&2", "1 < 2", "“test“"].each do |escaped| 164 | assert_equal %(), tag("a", {:href => escaped.html_safe}) 165 | end 166 | end 167 | 168 | it "tag_honors_html_safe_with_escaped_array_class" do 169 | str = tag("p", {:class => ["song>", "play>".html_safe]}) 170 | assert_equal "

\" />", 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 | Carbon Controller: Exception caught 6 | 40 | 41 | 42 |

43 |

<%= exception.class.to_s %>

44 |
45 | 46 |
47 |

<%= exception.message %>

48 | <% exception.backtrace.each do |line| %> 49 |
<%= line %>
50 | <% end %> 51 |
52 | 53 |
54 |

Request

55 |

Path

56 | 57 |

<%= request.path %>

58 |

Headers

59 | 60 | <% request.headers.each do |key, value| %> 61 | 62 | 63 | 64 | 65 | <% end %> 66 |
<%= key %><%= value %>
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/carbon_dispatch/request.cr: -------------------------------------------------------------------------------- 1 | require "./request/session" 2 | 3 | class CarbonDispatch::Request 4 | getter :request, :path_params 5 | 6 | delegate :path, @request 7 | delegate :method, @request 8 | delegate :headers, @request 9 | delegate :body, @request 10 | 11 | def initialize(@request) 12 | @path_params = Hash(String, String?).new 13 | end 14 | 15 | def ssl? 16 | scheme == "https" 17 | end 18 | 19 | def protocol 20 | @protocol ||= ssl? ? "https://" : "http://" 21 | end 22 | 23 | def scheme(default = true) 24 | if headers["HTTPS"]? == "on" 25 | "https" 26 | elsif headers["HTTP_X_FORWARDED_SSL"]? == "on" 27 | "https" 28 | elsif headers["HTTP_X_FORWARDED_SCHEME"]? 29 | headers["HTTP_X_FORWARDED_SCHEME"]? 30 | elsif headers["HTTP_X_FORWARDED_PROTO"]? 31 | forwarded_protocol = headers["HTTP_X_FORWARDED_PROTO"]? 32 | forwarded_protocol.to_s.split(",").first 33 | else 34 | "http" if default 35 | end 36 | end 37 | 38 | def port 39 | port_from_host = raw_host_with_port.to_s.match(/:(\d+)$/) { |md| md[1]? } 40 | 41 | port = begin 42 | if scheme(false) 43 | standard_port 44 | elsif port_from_host 45 | port_from_host 46 | elsif headers["HTTP_X_FORWARDED_PORT"]? 47 | headers["HTTP_X_FORWARDED_PORT"]? 48 | elsif headers["SERVER_PORT"]? 49 | headers["SERVER_PORT"]? 50 | else 51 | standard_port 52 | end 53 | end 54 | 55 | port.to_i if port 56 | end 57 | 58 | STANDARD_PORTS_FOR_SCHEME = {"http": 80, "https": 443} 59 | 60 | def standard_port 61 | STANDARD_PORTS_FOR_SCHEME[scheme]? || 80 62 | end 63 | 64 | def standard_port? 65 | STANDARD_PORTS_FOR_SCHEME.values.includes?(port) 66 | end 67 | 68 | def host_with_port 69 | if standard_port? 70 | host 71 | else 72 | "#{host}:#{port}" 73 | end 74 | end 75 | 76 | def host 77 | raw_host_with_port.sub(/:\d+$/, "") 78 | end 79 | 80 | def raw_host_with_port 81 | forwarded_host = headers["HTTP_X_FORWARDED_HOST"]? 82 | 83 | if forwarded_host 84 | forwarded_host.to_s.split(/,\s?/).last 85 | else 86 | server_name_or_addr = headers["SERVER_NAME"]? || headers["SERVER_ADDR"]? 87 | 88 | headers["HOST"]? || headers["HTTP_HOST"]? || "#{server_name_or_addr}:#{headers["SERVER_PORT"]?}" 89 | end 90 | end 91 | 92 | def params 93 | @params ||= request_params.merge(path_params.merge(query_params)) 94 | end 95 | 96 | def query_params 97 | query_params = @query_params 98 | return query_params if query_params 99 | 100 | query_params = Hash(String, String?).new 101 | 102 | HTTP::Params.parse(@request.query.to_s) do |key, value| 103 | query_params[key] = value 104 | end 105 | @query_params = query_params 106 | end 107 | 108 | def request_params 109 | request_params = @request_params 110 | return request_params if request_params 111 | 112 | request_params = Hash(String, String?).new 113 | 114 | HTTP::Params.parse(@request.body.to_s) do |key, value| 115 | request_params[key] = value 116 | end 117 | @request_params = request_params 118 | end 119 | 120 | def path_params=(params : Hash(String, String?)) 121 | @path_params = params 122 | end 123 | 124 | def ip 125 | remote_addrs = split_ip_addresses(headers["REMOTE_ADDR"]?) 126 | remote_addrs = reject_trusted_ip_addresses(remote_addrs) 127 | 128 | return remote_addrs.first if !remote_addrs.empty? 129 | 130 | forwarded_ips = split_ip_addresses(headers["HTTP_X_FORWARDED_FOR"]?) 131 | forwarded_ips = reject_trusted_ip_addresses(forwarded_ips) 132 | 133 | forwarded_ips.last? 134 | end 135 | 136 | def trusted_proxy?(ip) 137 | ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i 138 | end 139 | 140 | private def split_ip_addresses(ip_addresses) 141 | if ip_addresses 142 | ip_addresses.strip.split(/[,\s]+/) 143 | else 144 | [] of String 145 | end 146 | end 147 | 148 | private def reject_trusted_ip_addresses(ip_addresses) 149 | ip_addresses.reject { |ip| trusted_proxy?(ip) } 150 | end 151 | 152 | private def uri 153 | (@uri ||= URI.parse(@request.resource)).not_nil! 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /src/carbon_dispatch/request/session.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Request 3 | class Session 4 | def initialize(@cookie_jar) 5 | @session = get_cookie 6 | @session["_id"] ||= SecureRandom.uuid 7 | end 8 | 9 | def id 10 | @session["_id"] 11 | end 12 | 13 | def destroy 14 | @session.clear 15 | end 16 | 17 | def [](key) 18 | @session[key] 19 | end 20 | 21 | def has_key?(key) 22 | @session.has_key?(key) 23 | end 24 | 25 | def keys 26 | @session.keys 27 | end 28 | 29 | def values 30 | @session.values 31 | end 32 | 33 | def []=(key, value) 34 | @session[key] = value.to_s 35 | end 36 | 37 | def to_hash 38 | @session 39 | end 40 | 41 | def update(hash : Hash(String, String)) 42 | @session.update(hash) 43 | end 44 | 45 | def delete(key) 46 | @session.delete(key) if has_key?(key) 47 | end 48 | 49 | def fetch(key, default = nil) 50 | @session.fetch(key, default) 51 | end 52 | 53 | def empty? 54 | @session.empty? 55 | end 56 | 57 | def set_cookie 58 | @cookie_jar.encrypted["_session"] = @session.to_json 59 | end 60 | 61 | def get_cookie 62 | Hash(String, String).from_json(@cookie_jar.encrypted["_session"] || "{}") 63 | rescue e : JSON::ParseException 64 | Hash(String, String).new 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/carbon_dispatch/response.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Response 3 | delegate :headers, @response 4 | delegate "status_code=", @response 5 | delegate "status_code", @response 6 | delegate "close", @response 7 | 8 | property :path 9 | 10 | def initialize 11 | io = MemoryIO.new 12 | @response = HTTP::Server::Response.new(io) 13 | @rendered = false 14 | @path = "" 15 | @body = "" 16 | @callbacks = [] of -> 17 | end 18 | 19 | def initialize(@response : HTTP::Server::Response) 20 | @rendered = false 21 | @path = "" 22 | @body = "" 23 | @callbacks = [] of -> 24 | end 25 | 26 | def finish 27 | @callbacks.map { |callback| callback.call } 28 | end 29 | 30 | def register_callback(&block) 31 | @callbacks << block 32 | end 33 | 34 | def is_path? 35 | @path && File.exists?(@path) 36 | end 37 | 38 | def location=(location) 39 | @response.headers["Location"] = location 40 | end 41 | 42 | def location 43 | @response.headers["Location"] 44 | end 45 | 46 | def body 47 | @body 48 | end 49 | 50 | def body=(@body : String) 51 | @rendered = true 52 | @response.write(@body.to_slice) 53 | end 54 | 55 | def rendered? 56 | @rendered 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/carbon_dispatch/route.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Route 3 | macro create(controller, action, methods, pattern) 4 | CarbonDispatch::Route.new "{{controller.id.camelcase}}Controller", 5 | {{action}}, 6 | {{methods}}, 7 | {{pattern}}, 8 | ->(request : CarbonDispatch::Request, response : CarbonDispatch::Response) { 9 | {{controller.id.camelcase}}Controller.action({{action}}, request, response) 10 | } 11 | end 12 | 13 | getter controller, action, methods, pattern 14 | 15 | def initialize(@controller, @action, @methods : Array(String), path, @block) 16 | @params = [] of String 17 | lparen = path.split(/(\()/) 18 | rparen = lparen.flat_map { |word| word.split(/(\))/) } 19 | params = rparen.flat_map { |word| word.split(/(:\w+)/) } 20 | slugged = params.flat_map { |word| word.split(/(\*\w+)/) } 21 | pattern = slugged.map do |word| 22 | word.gsub(/\(/) { "(?:" } 23 | .gsub(/\)/) { "){0,1}" } 24 | .gsub(/:(\w+)/) { @params << $1; "(?<#{$1}>[^/]+)" } 25 | .gsub(/\*(\w+)/) { @params << $1; "(?<#{$1}>.+)" } 26 | end.join 27 | 28 | @pattern = Regex.new("^#{pattern}$") 29 | end 30 | 31 | def match(method, path) 32 | return false unless @methods.includes?(method) 33 | 34 | path = normalize_path(path) 35 | 36 | match = path.to_s.match(@pattern) 37 | 38 | if match 39 | @params.reduce({} of String => String?) { |hash, param| hash[param] = match[param]?; hash } 40 | else 41 | false 42 | end 43 | end 44 | 45 | def normalize_path(path) 46 | path = "/#{path}" 47 | path = path.squeeze("/") 48 | path = path.sub(%r{/+\Z}, "") 49 | path = path.gsub(/(%[a-f0-9]{2})/) { $1.upcase } 50 | path = "/" if path == "" 51 | path 52 | end 53 | 54 | def call(request : CarbonDispatch::Request, response : CarbonDispatch::Response) 55 | @block.call(request, response) 56 | end 57 | 58 | def ==(other) 59 | @controller == other.controller && @action == other.action && @methods == other.methods && @pattern == other.pattern 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/carbon_dispatch/router.cr: -------------------------------------------------------------------------------- 1 | module CarbonDispatch 2 | class Router 3 | include Middleware 4 | 5 | getter :routes 6 | 7 | def initialize(@routes = [] of Route) 8 | end 9 | 10 | def call(request, response) 11 | action = nil 12 | routes.each do |route| 13 | match = route.match(request.method, request.path) 14 | if match.is_a?(Hash(String, String?)) 15 | request.path_params = match 16 | action = route 17 | break 18 | end 19 | end 20 | 21 | if action 22 | action.call(request, response) 23 | else 24 | response.status_code = 404 25 | response.body = "Not Found" 26 | end 27 | end 28 | 29 | def draw 30 | with self yield 31 | end 32 | 33 | macro action(methods, path, controller = nil, action = nil) 34 | routes << CarbonDispatch::Route.create({{controller}}, {{action}}, {{methods}}, {{path}}) 35 | end 36 | 37 | macro get(path, controller = nil, action = nil) 38 | action(["GET"], {{path}}, {{controller}}, {{action}}) 39 | end 40 | 41 | macro post(path, controller = nil, action = nil) 42 | action(["POST"], {{path}}, {{controller}}, {{action}}) 43 | end 44 | 45 | macro put(path, controller = nil, action = nil) 46 | action(["PUT"], {{path}}, {{controller}}, {{action}}) 47 | end 48 | 49 | macro patch(path, controller = nil, action = nil) 50 | action(["PATCH"], {{path}}, {{controller}}, {{action}}) 51 | end 52 | 53 | macro delete(path, controller = nil, action = nil) 54 | action(["DELETE"], {{path}}, {{controller}}, {{action}}) 55 | end 56 | 57 | macro resources(resource, only = nil, except = nil) 58 | {% methods = [:index, :new, :create, :show, :edit, :update, :destroy] %} 59 | 60 | {% if only %} 61 | {% for action in only %} 62 | resource_action({{resource}}, {{action}}) 63 | {% end %} 64 | {% elsif except %} 65 | {% for action in methods %} 66 | {% should_include = true %} 67 | {% for except_action in except %} 68 | {% if except_action == action %} 69 | {% should_include = false %} 70 | {% end %} 71 | {% end %} 72 | {% if should_include %} 73 | resource_action({{resource}}, {{action}}) 74 | {% end %} 75 | {% end %} 76 | {% else %} 77 | {% for action in methods %} 78 | resource_action({{resource}}, {{action}}) 79 | {% end %} 80 | {% end %} 81 | end 82 | 83 | macro resource_action(resource, action) 84 | {% if action == :index %} 85 | get("/{{resource.id}}", {{resource.id}}, "index") 86 | {% elsif action == :show %} 87 | get("/{{resource.id}}/:id", {{resource.id}}, "show") 88 | {% elsif action == :new %} 89 | get("/{{resource.id}}/new", {{resource.id}}, "new") 90 | {% elsif action == :edit %} 91 | get("/{{resource.id}}/:id/edit", {{resource.id}}, "edit") 92 | {% elsif action == :create %} 93 | post("/{{resource.id}}", {{resource.id}}, "create") 94 | {% elsif action == :update %} 95 | action(["PATCH", "PUT"], "/{{resource.id}}/:id", {{resource.id}}, "update") 96 | {% elsif action == :destroy %} 97 | delete("/{{resource.id}}/:id", {{resource.id}}, "destroy") 98 | {% end %} 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /src/carbon_support/callbacks.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport::Callbacks 2 | end 3 | 4 | require "./callbacks/environment" 5 | require "./callbacks/chain" 6 | require "./callbacks/callback" 7 | require "./callbacks/sequence" 8 | 9 | module CarbonSupport::Callbacks 10 | module DescendantMethods 11 | macro included 12 | def load_callbacks 13 | super 14 | end 15 | end 16 | end 17 | 18 | macro included 19 | macro inherited 20 | include DescendantMethods 21 | end 22 | 23 | def load_callbacks 24 | @callbacks ||= Hash(String, CallbackChain).new 25 | end 26 | end 27 | 28 | macro define_callbacks(name, options = CallbackChain::Options.new) 29 | def load_callbacks 30 | previous_def.tap do |callbacks| 31 | callbacks["{{name.id}}"] = CallbackChain.new("{{name.id}}", {{options.id}}) 32 | end 33 | end 34 | end 35 | 36 | macro set_callback(name, type, filter, options = Callback::Options.new) 37 | def load_callbacks 38 | previous_def.tap do |callbacks| 39 | chain = callbacks["{{name.id}}"] 40 | {% if type == :around %} 41 | around = ->(block : -> ) { !!{{filter.id}}(&block) } 42 | callback = Callback::Around.new("{{filter.id}}", around, {{options.id}}, chain.options) 43 | {% elsif type == :before %} 44 | before = ->(block : ->) { !!{{filter.id}} } 45 | callback = Callback::Before.new("{{filter.id}}", before, {{options.id}}, chain.options) 46 | {% elsif type == :after %} 47 | after = ->(block : ->) { !!{{filter.id}} } 48 | callback = Callback::After.new("{{filter.id}}", after, {{options.id}}, chain.options) 49 | {% end %} 50 | chain.append(callback) 51 | end 52 | end 53 | end 54 | 55 | def run_callbacks(name, &block : -> _) 56 | chain = load_callbacks[name.to_s] 57 | runner = chain.compile 58 | e = Environment.new(self, false, nil, &block) 59 | runner.call(e) 60 | e.value 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/carbon_support/callbacks/callback.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport::Callbacks 2 | abstract class Callback 3 | class Options 4 | end 5 | 6 | getter :name, :kind, :block 7 | 8 | def duplicates?(other) 9 | name == other.name && kind == other.kind 10 | end 11 | 12 | class Before < Callback 13 | def initialize(@name, @block, @callback_options : Callback::Options, @chain_options : CallbackChain::Options) 14 | @kind = :before 15 | end 16 | 17 | def apply(sequence) 18 | sequence.before(self) 19 | end 20 | 21 | def call(env : Environment) 22 | terminator = @chain_options.terminator 23 | target = env.target 24 | 25 | if !env.halted 26 | result = @block.call ->{} 27 | env.halted = terminator.terminate?(env.target, result) 28 | target.halted_callback_hook(@name) if env.halted && target.responds_to?(:halted_callback_hook) 29 | end 30 | env 31 | end 32 | end 33 | 34 | class After < Callback 35 | def initialize(@name, @block, @callback_options : Callback::Options, @chain_options : CallbackChain::Options) 36 | @kind = :after 37 | end 38 | 39 | def call(env : Environment) 40 | terminator = @chain_options.terminator 41 | if !env.halted || !@chain_options.skip_after_callbacks_if_terminated 42 | result = @block.call ->{} 43 | env.halted = terminator.terminate?(env.target, result) 44 | end 45 | env 46 | end 47 | 48 | def apply(sequence) 49 | sequence.after(self) 50 | end 51 | end 52 | 53 | class Around < Callback 54 | def initialize(@name, @block, @callback_options : Callback::Options, @chain_options : CallbackChain::Options) 55 | @kind = :after 56 | end 57 | 58 | def apply(sequence) 59 | sequence.around(self) 60 | end 61 | 62 | def call(env : Environment, block : -> _) 63 | if !env.halted 64 | @block.call(block) 65 | end 66 | env 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /src/carbon_support/callbacks/chain.cr: -------------------------------------------------------------------------------- 1 | class CarbonSupport::Callbacks::CallbackChain 2 | class Options 3 | class BoolTerminator 4 | def terminate?(target, result) 5 | !result 6 | end 7 | end 8 | 9 | getter :terminator, :if, :unless, :skip_after_callbacks_if_terminated 10 | 11 | def initialize(terminator = nil, @if = nil, @unless = nil, @skip_after_callbacks_if_terminated = false) 12 | if terminator 13 | @terminator = terminator 14 | else 15 | @terminator = BoolTerminator.new 16 | end 17 | end 18 | end 19 | 20 | getter :name, :options 21 | 22 | def initialize(@name, @options = Options.new) 23 | @chain = [] of Callback 24 | end 25 | 26 | def append(callback : Callback) 27 | @chain.push(callback) 28 | end 29 | 30 | def compile 31 | final_sequence = CallbackSequence.new ->(environment : Environment) do 32 | block = environment.run_block 33 | environment.value = !environment.halted && (!block || block.call) 34 | environment 35 | end 36 | @callbacks ||= @chain.reverse.reduce(final_sequence) do |callback_sequence, callback| 37 | callback.apply callback_sequence 38 | end 39 | end 40 | 41 | def append(*callbacks) 42 | callbacks.each { |c| append_one(c) } 43 | end 44 | 45 | def prepend(*callbacks) 46 | callbacks.each { |c| prepend_one(c) } 47 | end 48 | 49 | private def append_one(callback) 50 | @callbacks = nil 51 | remove_duplicates(callback) 52 | @chain.push(callback) 53 | end 54 | 55 | private def prepend_one(callback) 56 | @callbacks = nil 57 | remove_duplicates(callback) 58 | @chain.unshift(callback) 59 | end 60 | 61 | private def remove_duplicates(callback) 62 | @callbacks = nil 63 | @chain.reject! { |c| callback.duplicates?(c) } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /src/carbon_support/callbacks/environment.cr: -------------------------------------------------------------------------------- 1 | class CarbonSupport::Callbacks::Environment 2 | property :target, :halted, :value, :run_block 3 | 4 | def initialize(@target, @halted, @value, &block : -> _) 5 | @run_block = block 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/carbon_support/callbacks/sequence.cr: -------------------------------------------------------------------------------- 1 | class CarbonSupport::Callbacks::CallbackSequence 2 | def initialize(@block : Environment -> _) 3 | @before = [] of Callback::Before 4 | @after = [] of Callback::After 5 | end 6 | 7 | def before(callback) 8 | @before.unshift(callback) 9 | self 10 | end 11 | 12 | def after(callback) 13 | @after.push(callback) 14 | self 15 | end 16 | 17 | def around(callback) 18 | CallbackSequence.new ->(environment : Environment) do 19 | proc = ->{ self.call(environment) } 20 | callback.call(environment, proc) 21 | end 22 | end 23 | 24 | def call(environment) 25 | @before.each { |b| b.call(environment) } 26 | value = @block.call(environment) 27 | @after.each { |b| b.call(environment) } 28 | value 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/carbon_support/core_ext/object/blank.cr: -------------------------------------------------------------------------------- 1 | class Object 2 | def blank? 3 | if self.responds_to?(:empty?) 4 | !!self.empty? 5 | else 6 | !self 7 | end 8 | end 9 | 10 | def present? 11 | !blank? 12 | end 13 | 14 | def presence 15 | if present? 16 | self 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/carbon_support/core_ext/string/output_safety.cr: -------------------------------------------------------------------------------- 1 | require "ecr" 2 | 3 | module ECR 4 | module Util 5 | HTML_ESCAPE = {"&" => "&", ">" => ">", "<" => "<", "\"" => """, "'" => "'"} 6 | HTML_ESCAPE_REGEXP = /[&"'><]/ 7 | HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ 8 | 9 | def self.html_escape(s) 10 | unwrapped_html_escape(s).html_safe 11 | end 12 | 13 | def self.unwrapped_html_escape(s) # :nodoc: 14 | if s.html_safe? 15 | s 16 | else 17 | s.to_s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) 18 | end 19 | end 20 | 21 | def self.html_escape_once(s) 22 | result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) 23 | s.html_safe? ? result.html_safe : result 24 | end 25 | end 26 | end 27 | 28 | class Object 29 | def html_safe? 30 | false 31 | end 32 | end 33 | 34 | struct Number 35 | def html_safe? 36 | true 37 | end 38 | end 39 | 40 | module CarbonSupport 41 | class SafeBuffer 42 | include Comparable(String) 43 | 44 | def ==(other) 45 | @string == other.to_s 46 | end 47 | 48 | def initialize(@string) 49 | @html_safe = true 50 | @string 51 | end 52 | 53 | def concat(value) 54 | @string += html_escape_interpolated_argument(value) 55 | self 56 | end 57 | 58 | def +(value) 59 | concat(value) 60 | end 61 | 62 | def %(args) 63 | case args 64 | when Hash 65 | escaped_args = Hash[args.map { |k, arg| [k, html_escape_interpolated_argument(arg)] }] 66 | when Array 67 | escaped_args = args.map { |arg| html_escape_interpolated_argument(arg) } 68 | else 69 | escaped_args = [args].map { |arg| html_escape_interpolated_argument(arg) } 70 | end 71 | 72 | self.class.new(@string % escaped_args) 73 | end 74 | 75 | def html_safe? 76 | !!@html_safe 77 | end 78 | 79 | def to_s(*args) 80 | @string.to_s(*args) 81 | end 82 | 83 | def inspect(*args) 84 | @string.inspect(*args) 85 | end 86 | 87 | def html_safe 88 | self 89 | end 90 | 91 | private def html_escape_interpolated_argument(arg) 92 | (!html_safe? || arg.html_safe?) ? arg.to_s : arg.to_s.gsub(ECR::Util::HTML_ESCAPE_REGEXP, ECR::Util::HTML_ESCAPE) 93 | end 94 | end 95 | end 96 | 97 | class String 98 | def html_safe 99 | CarbonSupport::SafeBuffer.new(self) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /src/carbon_support/key_generator.cr: -------------------------------------------------------------------------------- 1 | require "openssl/pkcs5" 2 | 3 | module CarbonSupport 4 | class KeyGenerator 5 | def initialize(@secret, @iterations = 2**16) 6 | end 7 | 8 | def generate_key(salt, key_size = 64) 9 | OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) 10 | end 11 | end 12 | 13 | class CachingKeyGenerator 14 | def initialize(@key_generator) 15 | @cache_keys = {} of String => Slice(UInt8) 16 | end 17 | 18 | def generate_key(salt, key_size = 64) 19 | @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/carbon_support/log_subscriber.cr: -------------------------------------------------------------------------------- 1 | require "./subscriber" 2 | 3 | module CarbonSupport 4 | class LogSubscriber < Subscriber 5 | # Embed in a String to clear all previous ANSI sequences. 6 | CLEAR = "\e[0m" 7 | BOLD = "\e[1m" 8 | 9 | # Colors 10 | BLACK = "\e[30m" 11 | RED = "\e[31m" 12 | GREEN = "\e[32m" 13 | YELLOW = "\e[33m" 14 | BLUE = "\e[34m" 15 | MAGENTA = "\e[35m" 16 | CYAN = "\e[36m" 17 | WHITE = "\e[37m" 18 | 19 | def self.logger 20 | @@logger ||= Carbon.logger 21 | end 22 | 23 | def self.logger=(logger) 24 | @@logger = logger 25 | end 26 | 27 | def self.log_subscribers 28 | subscribers 29 | end 30 | 31 | def logger 32 | LogSubscriber.logger 33 | end 34 | 35 | def start(name, id, payload) 36 | super if logger 37 | end 38 | 39 | def finish(name, id, payload) 40 | super if logger 41 | rescue e : Exception 42 | logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" 43 | end 44 | 45 | {% for level in ["info", "debug", "warn", "error", "fatal", "unknown"] %} 46 | protected def {{level.id}} 47 | logger.{{level.id}}(yield) if logger 48 | end 49 | 50 | protected def {{level.id}}(progname = nil) 51 | logger.{{level.id}}(progname) if logger 52 | end 53 | {% end %} 54 | 55 | # Set color by using a symbol or one of the defined constants. If a third 56 | # option is set to +true+, it also adds bold to the string. This is based 57 | # on the Highline implementation and will automatically append CLEAR to the 58 | # end of the returned String. 59 | macro color(text, color, bold = false) 60 | color = {{@type}}::{{color.id.upcase}} 61 | bold = {{bold.id}} ? BOLD : "" 62 | "#{bold}#{color}{{text.id}}#{CLEAR}" 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /src/carbon_support/message_encryptor.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "openssl/cipher" 3 | 4 | module CarbonSupport 5 | class MessageEncryptor 6 | class InvalidMessage < Exception 7 | end 8 | 9 | def initialize(@secret, @cipher = "aes-256-cbc", @digest = :sha1, @sign_secret = nil) 10 | @verifier = MessageVerifier.new(@sign_secret || @secret, digest: @digest) 11 | end 12 | 13 | # Encrypt and sign a message. We need to sign the message in order to avoid 14 | # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. 15 | def encrypt_and_sign(value : Slice(UInt8)) : String 16 | verifier.generate(_encrypt(value)) 17 | end 18 | 19 | def encrypt_and_sign(value : String) : String 20 | encrypt_and_sign(value.to_slice) 21 | end 22 | 23 | # Decrypt and verify a message. We need to verify the message in order to 24 | # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. 25 | def decrypt_and_verify(value : String) : Slice(UInt8) 26 | _decrypt(verifier.verify(value)) 27 | end 28 | 29 | private def _encrypt(value) 30 | cipher = new_cipher 31 | cipher.encrypt 32 | cipher.key = @secret 33 | 34 | # Rely on OpenSSL for the initialization vector 35 | iv = cipher.random_iv 36 | 37 | encrypted_data = MemoryIO.new 38 | encrypted_data.write(cipher.update(value)) 39 | encrypted_data.write(cipher.final) 40 | 41 | "#{::Base64.strict_encode encrypted_data.to_slice}--#{::Base64.strict_encode iv}" 42 | end 43 | 44 | private def _decrypt(encrypted_message) 45 | cipher = new_cipher 46 | encrypted_data, iv = encrypted_message.split("--").map { |v| ::Base64.decode(v) } 47 | 48 | cipher.decrypt 49 | cipher.key = @secret 50 | cipher.iv = iv 51 | 52 | decrypted_data = MemoryIO.new 53 | decrypted_data.write cipher.update(encrypted_data) 54 | decrypted_data.write cipher.final 55 | decrypted_data.to_slice 56 | rescue OpenSSL::Cipher::Error 57 | raise InvalidMessage.new 58 | end 59 | 60 | private def new_cipher 61 | OpenSSL::Cipher.new(@cipher) 62 | end 63 | 64 | private def verifier 65 | @verifier 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/carbon_support/message_verifier.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "openssl/hmac" 3 | require "crypto/subtle" 4 | 5 | module CarbonSupport 6 | class MessageVerifier 7 | class InvalidSignature < Exception 8 | end 9 | 10 | def initialize(@secret, @digest = :sha1) 11 | end 12 | 13 | def valid_message?(signed_message) 14 | splitted = signed_message.to_s.split("--", 2) 15 | return if splitted.size < 2 16 | data, digest = splitted 17 | data.size > 0 && digest.size > 0 && Crypto::Subtle.constant_time_compare(digest.bytes, generate_digest(data).bytes) == 1 18 | end 19 | 20 | def verified(signed_message : String) 21 | if valid_message?(signed_message) 22 | begin 23 | data = signed_message.split("--")[0] 24 | String.new(decode(data)) 25 | rescue argument_error : ArgumentError 26 | return if argument_error.message =~ %r{invalid base64} 27 | raise argument_error 28 | end 29 | end 30 | end 31 | 32 | def verify(signed_message) : String 33 | verified(signed_message) || raise(InvalidSignature.new) 34 | end 35 | 36 | def generate(value : String) 37 | data = encode(value) 38 | "#{data}--#{generate_digest(data)}" 39 | end 40 | 41 | private def encode(data) 42 | ::Base64.strict_encode(data) 43 | end 44 | 45 | private def decode(data) 46 | ::Base64.decode(data) 47 | end 48 | 49 | private def generate_digest(data) 50 | OpenSSL::HMAC.hexdigest(@digest, @secret, data) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/carbon_support/notifications.cr: -------------------------------------------------------------------------------- 1 | require "./notifications/payload" 2 | require "./notifications/fanout" 3 | require "./notifications/event" 4 | require "./notifications/instrumenter" 5 | require "./subscriber" 6 | 7 | module CarbonSupport 8 | module Notifications 9 | def self.notifier=(notifier) 10 | @@notifier = notifier 11 | end 12 | 13 | def self.notifier 14 | @@notifier ||= Fanout.new 15 | end 16 | 17 | def self.publish(name, started, finish, id, payload) 18 | notifier.publish(name, started, finish, id, payload) 19 | end 20 | 21 | def self.instrument(name, payload = Payload.new) 22 | if notifier.listening?(name) 23 | instrumenter.instrument(name, payload) { yield payload } 24 | else 25 | yield payload 26 | end 27 | end 28 | 29 | def self.instrument(name, payload = Payload.new) 30 | if notifier.listening?(name) 31 | instrumenter.instrument(name, payload) { } 32 | end 33 | end 34 | 35 | def self.subscribe(pattern, subscriber : Subscriber) 36 | notifier.subscribe(pattern, subscriber) 37 | end 38 | 39 | def self.subscribe(pattern, callback : CarbonSupport::Notifications::Event ->) 40 | notifier.subscribe(pattern, callback) 41 | end 42 | 43 | def self.subscribe(pattern, &block) 44 | notifier.subscribe(pattern, block) 45 | end 46 | 47 | def self.subscribed(callback, name, &block) 48 | subscriber = subscribe(name, callback) 49 | yield 50 | ensure 51 | unsubscribe(subscriber) 52 | end 53 | 54 | def self.unsubscribe(subscriber_or_name) 55 | notifier.unsubscribe(subscriber_or_name) 56 | end 57 | 58 | def self.instrumenter 59 | InstrumentationRegistry::INSTANCE.instrumenter_for(notifier) 60 | end 61 | 62 | class InstrumentationRegistry # :nodoc: 63 | INSTANCE = new 64 | 65 | def initialize 66 | @registry = Hash(Fanout, Instrumenter).new 67 | end 68 | 69 | def instrumenter_for(notifier) 70 | @registry[notifier] ||= Instrumenter.new(notifier) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /src/carbon_support/notifications/event.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport 2 | module Notifications 3 | class Event 4 | property :name 5 | property :start 6 | property :end 7 | property :transaction_id 8 | property :children 9 | property :payload 10 | property :object 11 | 12 | def initialize(name : String, start : Time, ending : Time, transaction_id : String, payload : Payload) 13 | @name = name 14 | @payload = payload 15 | @start = start 16 | @transaction_id = transaction_id 17 | @end = ending 18 | @children = [] of Event 19 | @duration = nil 20 | end 21 | 22 | def duration 23 | start = @start || Time.now 24 | finish = @finish || Time.now 25 | 26 | finish - start 27 | end 28 | 29 | def <<(event : Event) 30 | @children << event 31 | end 32 | 33 | def parent_of?(event : Event) 34 | @children.include? event 35 | end 36 | 37 | def duration_text 38 | minutes = duration.total_minutes 39 | return "#{minutes.round(2)}m" if minutes >= 1 40 | 41 | seconds = duration.total_seconds 42 | return "#{seconds.round(2)}s" if seconds >= 1 43 | 44 | millis = duration.total_milliseconds 45 | return "#{millis.round(2)}ms" if millis >= 1 46 | 47 | "#{(millis * 1000).round(2)}µs" 48 | end 49 | 50 | def ==(other) 51 | name == other.name && 52 | payload == other.payload && 53 | start == other.start && 54 | self.end == other.end && 55 | transaction_id == other.transaction_id 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/carbon_support/notifications/fanout.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport 2 | module Notifications 3 | class Fanout 4 | def initialize 5 | @subscribers = [] of (Subscribers::Evented | Subscribers::Timed) 6 | @listeners_for = {} of String => Array(Subscribers::Evented | Subscribers::Timed) 7 | super 8 | end 9 | 10 | def subscribe(pattern, subscriber : Subscriber) 11 | subscriber = Subscribers.new pattern, subscriber 12 | @subscribers << subscriber 13 | @listeners_for.clear 14 | subscriber 15 | end 16 | 17 | def subscribe(pattern = nil, &block : CarbonSupport::Notifications::Event ->) 18 | subscribe(pattern, block) 19 | end 20 | 21 | def subscribe(pattern, block : CarbonSupport::Notifications::Event ->) 22 | subscriber = Subscribers.new pattern, block 23 | @subscribers << subscriber 24 | @listeners_for.clear 25 | subscriber 26 | end 27 | 28 | def unsubscribe(subscriber_or_name) 29 | case subscriber_or_name 30 | when String 31 | @subscribers.reject! { |s| s.matches?(subscriber_or_name) } 32 | else 33 | @subscribers.delete(subscriber_or_name) 34 | end 35 | 36 | @listeners_for.clear 37 | end 38 | 39 | def start(name, id, payload) 40 | listeners_for(name).each { |s| s.start(name, id, payload) } 41 | end 42 | 43 | def finish(name, id, payload) 44 | listeners_for(name).each { |s| s.finish(name, id, payload) } 45 | end 46 | 47 | def publish(name, started, finish, id, payload) 48 | listeners_for(name).each { |s| s.publish(name, started, finish, id, payload) } 49 | end 50 | 51 | def listeners_for(name) 52 | @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } 53 | end 54 | 55 | def listening?(name) 56 | listeners_for(name).any? 57 | end 58 | 59 | # This is a sync queue, so there is no waiting. 60 | def wait 61 | end 62 | 63 | module Subscribers # :nodoc: 64 | def self.new(pattern, listener) 65 | if listener.responds_to?(:start) && listener.responds_to?(:finish) 66 | Evented.new pattern, listener 67 | elsif listener.responds_to?(:call) 68 | Timed.new pattern, listener 69 | else 70 | raise "Invalid listener" 71 | end 72 | end 73 | 74 | class Evented # :nodoc: 75 | def self.timestack 76 | @@timestack ||= Hash(Fiber, Array(Time)).new { |h, k| h[k] = [] of Time } 77 | end 78 | 79 | def initialize(pattern, delegate) 80 | @pattern = pattern 81 | @delegate = delegate 82 | @can_publish = delegate.responds_to?(:publish) 83 | end 84 | 85 | def publish(name, started, finish, id, payload) 86 | delegate = @delegate 87 | delegate.publish(name, started, finish, id, payload) if delegate.responds_to?(:publish) 88 | end 89 | 90 | def start(name, id, payload) 91 | delegate = @delegate 92 | delegate.start name, id, payload if delegate.responds_to?(:start) 93 | end 94 | 95 | def finish(name, id, payload) 96 | delegate = @delegate 97 | delegate.finish name, id, payload if delegate.responds_to?(:finish) 98 | end 99 | 100 | def subscribed_to?(name) 101 | @pattern === name || @pattern == nil 102 | end 103 | 104 | def matches?(name) 105 | @pattern && @pattern === name 106 | end 107 | end 108 | 109 | class Timed < Evented 110 | def publish(name, started, finish, id, payload) 111 | @delegate.call CarbonSupport::Notifications::Event.new(name, started, Time.now, id, payload) 112 | end 113 | 114 | def start(name, id, payload) 115 | self.class.timestack[Fiber.current].push Time.now 116 | end 117 | 118 | def finish(name, id, payload) 119 | started = self.class.timestack[Fiber.current].pop 120 | @delegate.call CarbonSupport::Notifications::Event.new(name, started, Time.now, id, payload) 121 | end 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /src/carbon_support/notifications/instrumenter.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport 2 | module Notifications 3 | class Instrumenter 4 | getter :id 5 | 6 | def initialize(notifier) 7 | @id = unique_id 8 | @notifier = notifier 9 | end 10 | 11 | # Instrument the given block by measuring the time taken to execute it 12 | # and publish it. Notice that events get sent even if an error occurs 13 | # in the passed-in block. 14 | def instrument(name, payload = Payload.new) 15 | start name, payload 16 | begin 17 | yield payload 18 | rescue e : Exception 19 | payload.exception = [e.class.name, e.message] 20 | raise e 21 | ensure 22 | finish name, payload 23 | end 24 | end 25 | 26 | # Send a start notification with +name+ and +payload+. 27 | def start(name, payload) 28 | @notifier.start name, @id, payload 29 | end 30 | 31 | # Send a finish notification with +name+ and +payload+. 32 | def finish(name, payload) 33 | @notifier.finish name, @id, payload 34 | end 35 | 36 | private def unique_id 37 | SecureRandom.hex(10) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/carbon_support/notifications/payload.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport 2 | module Notifications 3 | class Payload 4 | property :exception 5 | property :message 6 | 7 | macro define_property(name) 8 | class {{@type}} 9 | def {{name.id}} 10 | @{{name.id}} 11 | end 12 | def {{name.id}}=(value) 13 | @{{name.id}} = value 14 | end 15 | end 16 | end 17 | 18 | def ==(other : Payload) 19 | exception == other.exception && 20 | message == other.message 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/carbon_support/notifier.cr: -------------------------------------------------------------------------------- 1 | # require "./notifications/event" 2 | # require "./subscriber" 3 | # 4 | # module CarbonSupport 5 | # class Notifier 6 | # getter subscribers 7 | # INSTANCE = new 8 | # 9 | # def self.instrumenter 10 | # instance 11 | # end 12 | # 13 | # def self.instance 14 | # INSTANCE 15 | # end 16 | # 17 | # def initialize 18 | # @subscribers = Set(Subscriber).new 19 | # end 20 | # 21 | # def subscribe(subscriber) 22 | # @subscribers << subscriber 23 | # end 24 | # 25 | # def unsubscribe(subscriber) 26 | # @subscribers.try &.delete(subscriber) 27 | # end 28 | # 29 | # def start(event) 30 | # @subscribers.try &.each &.receive_start event 31 | # end 32 | # 33 | # def finish(event) 34 | # @subscribers.try &.each &.receive_finish event 35 | # end 36 | # 37 | # def instrument(event) 38 | # instrument(event) {} 39 | # end 40 | # 41 | # def instrument(event, &block : CarbonSupport::Notifications::Event -> Void) 42 | # @subscribers.try &.each &.receive_start event 43 | # 44 | # block.call(event) 45 | # 46 | # @subscribers.try &.each &.receive_finish event 47 | # end 48 | # end 49 | # end 50 | -------------------------------------------------------------------------------- /src/carbon_support/subscriber.cr: -------------------------------------------------------------------------------- 1 | module CarbonSupport 2 | class Subscriber 3 | macro attach_to(namespace, subscriber = {{@type}}.new, notifier = CarbonSupport::Notifications) 4 | @@namespace = {{namespace}} 5 | @@subscriber = {{subscriber}} 6 | @@notifier = {{notifier}} 7 | 8 | {% for event in @type.methods %} 9 | {% if {{event.visibility}} == :public %} 10 | add_event_subscriber({{event.name}}) 11 | {% end %} 12 | {% end %} 13 | end 14 | 15 | # Adds event subscribers for all new methods added to the class. 16 | def self.method_added(event) 17 | # Only public methods are added as subscribers, and only if a notifier 18 | # has been set up. This means that subscribers will only be set up for 19 | # classes that call #attach_to. 20 | if public_method_defined?(event) && notifier 21 | add_event_subscriber(event) 22 | end 23 | end 24 | 25 | macro add_event_subscriber(event) 26 | unless ["start", "finish"].includes?("{{event}}") 27 | pattern = "{{event}}.#{@@namespace}" 28 | 29 | # don't add multiple subscribers (eg. if methods are redefined) 30 | patterns = @@subscriber.patterns.not_nil! 31 | unless patterns.includes?(pattern) 32 | patterns << pattern 33 | @@subscriber.callers[pattern] = ->(e : CarbonSupport::Notifications::Event) { @@subscriber.{{event}}(e) } 34 | @@notifier.subscribe(pattern, @@subscriber) 35 | end 36 | end 37 | end 38 | 39 | getter :patterns # :nodoc: 40 | 41 | 42 | def initialize 43 | @queue_key = [self.class.name, object_id].join "-" 44 | @patterns = [] of String 45 | end 46 | 47 | def callers 48 | @callers ||= {} of String => CarbonSupport::Notifications::Event -> 49 | end 50 | 51 | def start(name, id, payload) 52 | e = CarbonSupport::Notifications::Event.new(name, Time.now, Time.now, id, payload) 53 | 54 | if event_stack.any? 55 | parent = event_stack.last 56 | parent << e 57 | end 58 | 59 | event_stack.push e 60 | end 61 | 62 | def finish(name, id, payload) 63 | finished = Time.now 64 | event = event_stack.pop 65 | event.end = finished 66 | event.payload = payload 67 | 68 | callers[name].call(event) if callers.has_key?(name) 69 | end 70 | 71 | def call(event : CarbonSupport::Notifications::Event) 72 | raise "subscribers cannot respond to all messages" 73 | end 74 | 75 | private def event_stack 76 | @event_stack ||= [] of CarbonSupport::Notifications::Event 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /src/carbon_view/base.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | class Base 3 | end 4 | end 5 | 6 | require "./buffers" 7 | require "./helpers" 8 | require "./context" 9 | require "./view" 10 | require "./layout" 11 | require "./partial" 12 | 13 | module CarbonView 14 | macro load_views(view_dir, processor = "carbon_view/process") 15 | \{{run({{processor}}, {{ view_dir }}) }} 16 | end 17 | 18 | class Base 19 | include Context 20 | include Helpers 21 | 22 | @@views = {} of String => View.class 23 | @@layouts = {} of String => Layout.class 24 | 25 | def self.views 26 | @@views 27 | end 28 | 29 | def self.layouts 30 | @@layouts 31 | end 32 | 33 | def initialize(@controller = nil) 34 | end 35 | 36 | macro method_missing(name) 37 | {% if name.is_a?(StringLiteral) %} 38 | controller = @controller 39 | controller.try(&.{{name.id}}) if controller.responds_to?(:{{name.id}}) 40 | {% end %} 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/carbon_view/buffers.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | class OutputBuffer < CarbonSupport::SafeBuffer 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /src/carbon_view/context.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | module Context 3 | property :output_buffer 4 | property :view_flow 5 | 6 | def _prepare_context 7 | @view_flow = OutputFlow.new 8 | @output_buffer = nil 9 | @virtual_path = nil 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/carbon_view/helpers.cr: -------------------------------------------------------------------------------- 1 | require "./helpers/**" 2 | 3 | module CarbonView 4 | module Helpers 5 | include TagHelper 6 | include AssetTagHelper 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/carbon_view/helpers/asset_tag_helper.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | module Helpers 3 | module AssetTagHelper 4 | def javascript_include_tag(*arg) 5 | end 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/carbon_view/helpers/capture_helper.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | module Helpers 3 | module CaptureHelper 4 | def capture(&block : -> _) 5 | value = block.call 6 | ECR::Util.html_escape value if value.is_a?(String) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/carbon_view/helpers/output_safety_helper.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | module Helpers 3 | module OutputSafetyHelper 4 | def raw(stringish) 5 | stringish.to_s.html_safe 6 | end 7 | 8 | def safe_join(array, sep = " ") 9 | sep = ECR::Util.unwrapped_html_escape(sep) 10 | 11 | array.flatten.map! { |i| ECR::Util.unwrapped_html_escape(i) }.join(sep).html_safe 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/carbon_view/helpers/tag_helper.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | module Helpers 3 | module TagHelper 4 | include CaptureHelper 5 | include OutputSafetyHelper 6 | 7 | BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer 8 | autoplay controls loop selected hidden scoped async 9 | defer reversed ismap seamless muted required 10 | autofocus novalidate formnovalidate open pubdate 11 | itemscope allowfullscreen default inert sortable 12 | truespeed typemustmatch).to_set 13 | 14 | # BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map { |attribute| attribute }) 15 | 16 | TAG_PREFIXES = ["aria", "data", :aria, :data].to_set 17 | 18 | PRE_CONTENT_STRINGS = { 19 | "textarea" => "\n", 20 | } 21 | 22 | def tag(name, options = nil, open = false, escape = true) 23 | "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe 24 | end 25 | 26 | def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true) 27 | content_tag_string(name, content_or_options_with_block, options, escape) 28 | end 29 | 30 | def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block : -> _) 31 | options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) 32 | content_tag_string(name, capture(&block), options, escape) 33 | end 34 | 35 | def cdata_section(content) 36 | splitted = content.to_s.gsub(/\]\]\>/, "]]]]>") 37 | "".html_safe 38 | end 39 | 40 | def escape_once(html) 41 | ECR::Util.html_escape_once(html) 42 | end 43 | 44 | private def content_tag_string(name, content, options, escape = true) 45 | tag_options = tag_options(options, escape) if options 46 | content = ECR::Util.unwrapped_html_escape(content) if escape 47 | "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_s]?}#{content}".html_safe 48 | end 49 | 50 | private def tag_options(options, escape = true) 51 | return if options.blank? 52 | attrs = [] of String 53 | options.each do |key, value| 54 | if TAG_PREFIXES.includes?(key) && value.is_a?(Hash) 55 | value.each do |k, v| 56 | attrs << prefix_tag_option(key, k, v, escape) 57 | end 58 | elsif BOOLEAN_ATTRIBUTES.includes?(key) 59 | attrs << boolean_tag_option(key) if value 60 | elsif !value.nil? 61 | attrs << tag_option(key, value, escape) 62 | end 63 | end 64 | " #{attrs.join(" ")}" unless attrs.empty? 65 | end 66 | 67 | private def prefix_tag_option(prefix, key, value, escape) 68 | key = "#{prefix}-#{key.to_s.dasherize}" 69 | unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) 70 | value = value.to_json 71 | end 72 | tag_option(key, value, escape) 73 | end 74 | 75 | private def boolean_tag_option(key) 76 | %(#{key}="#{key}") 77 | end 78 | 79 | private def tag_option(key, value, escape) 80 | if value.is_a?(Array) 81 | value = escape ? safe_join(value, " ") : value.flatten.join(" ") 82 | else 83 | value = escape ? ECR::Util.unwrapped_html_escape(value) : value 84 | end 85 | %(#{key}="#{value}") 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /src/carbon_view/layout.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | class Layout < View 3 | def render(view) 4 | String.build do |io| 5 | to_s io do 6 | view.render 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/carbon_view/partial.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | class Partial < Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /src/carbon_view/process.cr: -------------------------------------------------------------------------------- 1 | require "ecr" 2 | view_dir = ARGV[0] 3 | 4 | output = [] of String 5 | 6 | Dir.cd(view_dir) do 7 | Dir["**/*.ecr"].each do |f| 8 | file_name = File.basename(f, ".ecr") 9 | 10 | namespaces = File.dirname(f).split("/").map(&.camelcase).join("::") 11 | view_name = File.basename(file_name, File.extname(file_name)).camelcase 12 | 13 | if view_name.starts_with?("_") 14 | view_type = "Partial" 15 | else 16 | if namespaces.starts_with?("Layouts") 17 | view_type = "Layout" 18 | else 19 | view_type = "View" 20 | end 21 | end 22 | 23 | output << %{ 24 | class CarbonViews::#{namespaces}::#{view_name} < CarbonView::#{view_type} 25 | def to_s(__io__) 26 | #{ECR.process_file(f, "__io__")} 27 | end 28 | end 29 | } 30 | 31 | if view_type == "Layout" 32 | output << %(CarbonView::Base.layouts["#{namespaces}::#{view_name}"] = CarbonViews::#{namespaces}::#{view_name}) 33 | else 34 | output << %(CarbonView::Base.views["#{namespaces}::#{view_name}"] = CarbonViews::#{namespaces}::#{view_name}) 35 | end 36 | end 37 | end 38 | 39 | puts output.join("\n") 40 | -------------------------------------------------------------------------------- /src/carbon_view/view.cr: -------------------------------------------------------------------------------- 1 | module CarbonView 2 | class View < Base 3 | def render 4 | to_s 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/command.cr: -------------------------------------------------------------------------------- 1 | require "option_parser" 2 | require "ecr" 3 | require "ecr/macros" 4 | require "colorize" 5 | require "./carbon/version" 6 | require "./command/*" 7 | 8 | module Carbon 9 | class Command 10 | USAGE = <<-USAGE 11 | Usage: carbon [command] [arguments] 12 | 13 | Command: 14 | app generate new crystal project 15 | --help, -h show this help 16 | --version, -v show version 17 | USAGE 18 | 19 | def self.run(options) 20 | self.new(options).run 21 | end 22 | 23 | def initialize(@options) 24 | end 25 | 26 | private getter options 27 | 28 | def run 29 | command = options.first? 30 | 31 | if command 32 | case 33 | when "app".starts_with?(command) 34 | options.shift 35 | app 36 | when "--help" == command, "-h" == command 37 | puts USAGE 38 | exit 39 | when "--version" == command, "-v" == command 40 | puts "Carbon #{Carbon::VERSION}" 41 | exit 42 | end 43 | else 44 | puts USAGE 45 | exit 46 | end 47 | end 48 | 49 | def app 50 | NewApp.run(options) 51 | end 52 | end 53 | end 54 | 55 | Carbon::Command.run(ARGV) 56 | -------------------------------------------------------------------------------- /src/command/generator.cr: -------------------------------------------------------------------------------- 1 | require "./generators/view" 2 | 3 | module Carbon 4 | class Generator 5 | macro inherited 6 | @@views = [] of Carbon::Generator::View.class 7 | @@empty_files = [] of String 8 | end 9 | 10 | def self.register_template(view) 11 | views << view 12 | end 13 | 14 | def self.empty_file(path) 15 | empty_files << path 16 | end 17 | 18 | def self.views 19 | @@views 20 | end 21 | 22 | def self.empty_files 23 | @@empty_files 24 | end 25 | 26 | macro template(name, template_path, full_path) 27 | class {{name.id}} < Carbon::Generator::View 28 | ECR.def_to_s "{{TEMPLATE_DIR.id}}/{{template_path.id}}" 29 | def full_path 30 | "#{config.dir}/#{{{full_path}}}" 31 | end 32 | end 33 | 34 | {{@type}}.register_template({{name.id}}) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/command/generators/view.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | class Generator 3 | abstract class View 4 | getter config 5 | 6 | def initialize(@config) 7 | end 8 | 9 | def render 10 | Dir.mkdir_p(File.dirname(full_path)) 11 | File.write(full_path, to_s) 12 | puts log_message unless config.silent 13 | end 14 | 15 | def log_message 16 | " #{"create".colorize(:light_green)} #{full_path}" 17 | end 18 | 19 | def module_name 20 | config.name.split("-").map(&.camelcase).join("::") 21 | end 22 | 23 | abstract def full_path 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/command/helper.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | class Command 3 | module Helper 4 | WHICH_GIT_COMMAND = "which git >/dev/null" 5 | 6 | def self.fetch_author 7 | return "[your-name-here]" unless system(WHICH_GIT_COMMAND) 8 | `git config --get user.name`.strip 9 | end 10 | 11 | def self.fetch_email 12 | return "[your-email-here]" unless system(WHICH_GIT_COMMAND) 13 | `git config --get user.email`.strip 14 | end 15 | 16 | def self.fetch_github_name 17 | default = "[your-github-name]" 18 | return default unless system(WHICH_GIT_COMMAND) 19 | github_user = `git config --get github.user`.strip 20 | github_user.empty? ? default : github_user 21 | end 22 | 23 | def self.fetch_required_parameter(opts, args, name) 24 | if args.empty? 25 | puts "#{name} is missing" 26 | puts opts 27 | exit 1 28 | end 29 | args.shift 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/command/new_app.cr: -------------------------------------------------------------------------------- 1 | module Carbon 2 | class Command 3 | class NewApp 4 | def self.run(args) 5 | config = Config.new 6 | 7 | OptionParser.parse(args) do |opts| 8 | opts.banner = %{USAGE: carbon app NAME [DIR] 9 | NAME - name of project to be generated, 10 | eg: example 11 | DIR - directory where project will be generated, 12 | default: NAME, eg: ./custom/path/example 13 | } 14 | 15 | opts.on("--help", "Shows this message") do 16 | puts opts 17 | exit 18 | end 19 | 20 | opts.unknown_args do |args, after_dash| 21 | config.name = Helper.fetch_required_parameter(opts, args, "NAME") 22 | config.dir = args.empty? ? config.name : args.shift 23 | end 24 | end 25 | 26 | NewAppGenerator.new(config).run 27 | end 28 | 29 | class Config 30 | property :name 31 | property :dir 32 | property :app_name 33 | property :silent 34 | 35 | def initialize( 36 | @name = "none", 37 | @dir = "none", 38 | @silent = false) 39 | end 40 | 41 | def app_name 42 | name.camelcase 43 | end 44 | end 45 | 46 | class NewAppGenerator < Generator 47 | TEMPLATE_DIR = "#{__DIR__}/new_app/template" 48 | 49 | def initialize(@config) 50 | end 51 | 52 | def run 53 | self.class.views.each do |view| 54 | view.new(@config).render 55 | end 56 | self.class.empty_files.each do |file| 57 | full_path = "#{@config.dir}/#{file}" 58 | Dir.mkdir_p(File.dirname(full_path)) 59 | File.write(full_path, to_s) 60 | puts " #{"create".colorize(:light_green)} #{full_path}" unless @config.silent 61 | end 62 | end 63 | 64 | template GitignoreView, "gitignore.ecr", ".gitignore" 65 | template ServerView, "server.cr.ecr", "server.cr" 66 | template ApplicationControllerView, "application_controller.cr.ecr", "src/controllers/application_controller.cr" 67 | template WelcomeView, "welcome.html.ecr", "src/views/application/welcome.html.ecr" 68 | template ViewLayoutView, "application.html.ecr", "src/views/layouts/application.html.ecr" 69 | template ApplicationView, "application.cr.ecr", "config/application.cr" 70 | template EnvironmentView, "environment.cr.ecr", "config/environment.cr" 71 | template RoutesView, "routes.cr.ecr", "config/routes.cr" 72 | template RobotsView, "robots.txt.ecr", "public/robots.txt" 73 | template ShardView, "shard.yml.ecr", "shard.yml" 74 | template GuardfileView, "guardfile.ecr", "Guardfile" 75 | 76 | empty_file "public/favicon.ico" 77 | empty_file "log/.gitkeep" 78 | empty_file "tmp/.gitkeep" 79 | empty_file "lib/.gitkeep" 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /src/command/new_app/template/application.cr.ecr: -------------------------------------------------------------------------------- 1 | require "carbon/all" 2 | 3 | module <%= config.app_name %>App 4 | class Application < Carbon::Application 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /src/command/new_app/template/application.html.ecr: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= config.app_name %> 4 | 5 | 6 | <%= "<\%= include \%\>" %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/command/new_app/template/application_controller.cr.ecr: -------------------------------------------------------------------------------- 1 | class ApplicationController < CarbonController::Base 2 | def welcome 3 | render_template "welcome" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /src/command/new_app/template/environment.cr.ecr: -------------------------------------------------------------------------------- 1 | # Load the Carbon application. 2 | require "./application" 3 | 4 | Carbon.root = File.expand_path("../", File.dirname(__FILE__)) 5 | 6 | require "../src/**" 7 | 8 | require "./routes" 9 | 10 | # Initialize the Carbon application. 11 | Carbon.application.initialize! 12 | -------------------------------------------------------------------------------- /src/command/new_app/template/gitignore.ecr: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | 6 | /shard.lock 7 | /log 8 | /tmp 9 | -------------------------------------------------------------------------------- /src/command/new_app/template/guardfile.ecr: -------------------------------------------------------------------------------- 1 | guard 'process', :name => 'Spec', :command => 'crystal spec' do 2 | watch(/spec\/(.*).e?cr$/) 3 | watch(/src\/(.*).e?cr$/) 4 | end 5 | 6 | guard 'process', :name => 'Build', :command => 'crystal build server.cr' do 7 | watch(/src\/(.*).e?cr$/) 8 | end 9 | 10 | guard 'process', :name => 'Server', :command => './server' do 11 | watch('server') 12 | end 13 | -------------------------------------------------------------------------------- /src/command/new_app/template/robots.txt.ecr: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/command/new_app/template/routes.cr.ecr: -------------------------------------------------------------------------------- 1 | class <%= config.app_name %>App::Router < CarbonDispatch::Router 2 | get "/", controller: "application", action: "welcome" 3 | end 4 | -------------------------------------------------------------------------------- /src/command/new_app/template/server.cr.ecr: -------------------------------------------------------------------------------- 1 | require "./config/environment" 2 | 3 | Carbon.application.run 4 | -------------------------------------------------------------------------------- /src/command/new_app/template/shard.yml.ecr: -------------------------------------------------------------------------------- 1 | name: <%= config.app_name %> 2 | version: 0.1.0 3 | 4 | dependencies: 5 | carbon: 6 | github: benoist/carbon-crystal 7 | branch: master 8 | -------------------------------------------------------------------------------- /src/command/new_app/template/welcome.html.ecr: -------------------------------------------------------------------------------- 1 |

You are now using the Carbon Crystal framework

2 | -------------------------------------------------------------------------------- /src/lib/file_string.cr: -------------------------------------------------------------------------------- 1 | class FileString 2 | def initialize(@string) 3 | end 4 | 5 | def join(*other) 6 | FileString.new(File.join(@string.to_s, *other.map(&.to_s))) 7 | end 8 | 9 | def to_s 10 | @string 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/lib/http_util.cr: -------------------------------------------------------------------------------- 1 | class HTTPUtil 2 | HTTP_STATUS_CODES = { 3 | 100 => "Continue", 4 | 101 => "Switching Protocols", 5 | 102 => "Processing", 6 | 200 => "OK", 7 | 201 => "Created", 8 | 202 => "Accepted", 9 | 203 => "Non-Authoritative Information", 10 | 204 => "No Content", 11 | 205 => "Reset Content", 12 | 206 => "Partial Content", 13 | 207 => "Multi-Status", 14 | 208 => "Already Reported", 15 | 226 => "IM Used", 16 | 300 => "Multiple Choices", 17 | 301 => "Moved Permanently", 18 | 302 => "Found", 19 | 303 => "See Other", 20 | 304 => "Not Modified", 21 | 305 => "Use Proxy", 22 | 307 => "Temporary Redirect", 23 | 308 => "Permanent Redirect", 24 | 400 => "Bad Request", 25 | 401 => "Unauthorized", 26 | 402 => "Payment Required", 27 | 403 => "Forbidden", 28 | 404 => "Not Found", 29 | 405 => "Method Not Allowed", 30 | 406 => "Not Acceptable", 31 | 407 => "Proxy Authentication Required", 32 | 408 => "Request Timeout", 33 | 409 => "Conflict", 34 | 410 => "Gone", 35 | 411 => "Length Required", 36 | 412 => "Precondition Failed", 37 | 413 => "Payload Too Large", 38 | 414 => "URI Too Long", 39 | 415 => "Unsupported Media Type", 40 | 416 => "Range Not Satisfiable", 41 | 417 => "Expectation Failed", 42 | 421 => "Misdirected Request", 43 | 422 => "Unprocessable Entity", 44 | 423 => "Locked", 45 | 424 => "Failed Dependency", 46 | 426 => "Upgrade Required", 47 | 428 => "Precondition Required", 48 | 429 => "Too Many Requests", 49 | 431 => "Request Header Fields Too Large", 50 | 500 => "Internal Server Error", 51 | 501 => "Not Implemented", 52 | 502 => "Bad Gateway", 53 | 503 => "Service Unavailable", 54 | 504 => "Gateway Timeout", 55 | 505 => "HTTP Version Not Supported", 56 | 506 => "Variant Also Negotiates", 57 | 507 => "Insufficient Storage", 58 | 508 => "Loop Detected", 59 | 510 => "Not Extended", 60 | 511 => "Network Authentication Required", 61 | } 62 | 63 | STRING_TO_STATUS_CODE = HTTP_STATUS_CODES.each_with_object(Hash(String, Int32).new) do |hash, code, message| 64 | hash[message.downcase.gsub(/\s|-|'/, '_').to_s] = code 65 | end 66 | 67 | def self.status_code(status) 68 | if status.is_a?(Symbol) || status.is_a?(String) 69 | STRING_TO_STATUS_CODE[status.to_s]? || 500 70 | else 71 | status.to_i 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /src/shard.yml: -------------------------------------------------------------------------------- 1 | name: carbon 2 | version: 0.2.0 3 | 4 | authors: 5 | - Benoist Claassen 6 | 7 | dependencies: 8 | duktape: 9 | github: jessedoyle/duktape.cr 10 | version: ~> 0.6.2 11 | 12 | license: MIT 13 | 14 | --------------------------------------------------------------------------------