cannon 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +5 -0
  6. data/README +3 -0
  7. data/Rakefile +20 -0
  8. data/bin/bundler +16 -0
  9. data/bin/coderay +16 -0
  10. data/bin/htmldiff +16 -0
  11. data/bin/ldiff +16 -0
  12. data/bin/mustache +16 -0
  13. data/bin/pry +16 -0
  14. data/bin/rspec +16 -0
  15. data/cannon.gemspec +29 -0
  16. data/lib/cannon.rb +16 -0
  17. data/lib/cannon/app.rb +196 -0
  18. data/lib/cannon/concerns/path_cache.rb +48 -0
  19. data/lib/cannon/concerns/signature.rb +14 -0
  20. data/lib/cannon/config.rb +58 -0
  21. data/lib/cannon/cookie_jar.rb +99 -0
  22. data/lib/cannon/handler.rb +25 -0
  23. data/lib/cannon/middleware.rb +47 -0
  24. data/lib/cannon/middleware/content_type.rb +15 -0
  25. data/lib/cannon/middleware/cookies.rb +18 -0
  26. data/lib/cannon/middleware/files.rb +33 -0
  27. data/lib/cannon/middleware/flush_and_benchmark.rb +20 -0
  28. data/lib/cannon/middleware/request_logger.rb +13 -0
  29. data/lib/cannon/middleware/router.rb +19 -0
  30. data/lib/cannon/request.rb +36 -0
  31. data/lib/cannon/response.rb +165 -0
  32. data/lib/cannon/route.rb +80 -0
  33. data/lib/cannon/route_action.rb +92 -0
  34. data/lib/cannon/version.rb +3 -0
  35. data/lib/cannon/views.rb +28 -0
  36. data/spec/app_spec.rb +20 -0
  37. data/spec/config_spec.rb +41 -0
  38. data/spec/environments_spec.rb +68 -0
  39. data/spec/features/action_types_spec.rb +154 -0
  40. data/spec/features/cookies_spec.rb +62 -0
  41. data/spec/features/files_spec.rb +17 -0
  42. data/spec/features/method_types_spec.rb +104 -0
  43. data/spec/features/requests_spec.rb +59 -0
  44. data/spec/features/views_spec.rb +31 -0
  45. data/spec/fixtures/public/background.jpg +0 -0
  46. data/spec/fixtures/views/render_test.html +1 -0
  47. data/spec/fixtures/views/test.html +1 -0
  48. data/spec/spec_helper.rb +98 -0
  49. data/spec/support/cannon_test.rb +108 -0
  50. metadata +219 -0
@@ -0,0 +1,14 @@
1
+ require 'openssl'
2
+
3
+ module Signature
4
+ class CookieSecretNotSet < StandardError; end
5
+
6
+ def signature(value)
7
+ raise CookieSecretNotSet, 'Set config.cookies.secret to use signed cookies' if Cannon.config.cookies.secret.nil?
8
+ OpenSSL::HMAC.hexdigest(digest, Cannon.config.cookies.secret, value)
9
+ end
10
+
11
+ def digest
12
+ @digest ||= OpenSSL::Digest.new('sha1')
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ module Cannon
2
+ class UnknownLogLevel < StandardError; end
3
+
4
+ class Config
5
+ attr_accessor :middleware, :public_path, :view_path, :reload_on_request, :benchmark_requests, :port, :ip_address
6
+ attr_reader :logger, :log_level
7
+
8
+ DEFAULT_MIDDLEWARE = %w{RequestLogger Files Cookies Router ContentType}
9
+
10
+ LOG_LEVELS = {
11
+ unknown: Logger::UNKNOWN,
12
+ fatal: Logger::FATAL,
13
+ error: Logger::ERROR,
14
+ warn: Logger::WARN,
15
+ info: Logger::INFO,
16
+ debug: Logger::DEBUG,
17
+ }
18
+
19
+ def initialize
20
+ self.ip_address = '127.0.0.1'
21
+ self.port = 5030
22
+ self.middleware = DEFAULT_MIDDLEWARE
23
+ self.public_path = 'public'
24
+ self.view_path = 'views'
25
+ self.reload_on_request = false
26
+ self.benchmark_requests = true
27
+ @log_level = :info
28
+ self.logger = Logger.new(STDOUT)
29
+ end
30
+
31
+ def logger=(value)
32
+ @logger = value
33
+ logger.datetime_format = '%Y-%m-%d %H:%M:%S'
34
+ logger.formatter = proc do |severity, datetime, progname, msg|
35
+ "#{msg}\n"
36
+ end
37
+ self.log_level = log_level
38
+ end
39
+
40
+ def log_level=(value)
41
+ raise UnknownLogLevel unless LOG_LEVELS.keys.include? value.to_sym
42
+ @log_level = value
43
+ logger.level = LOG_LEVELS[value.to_sym]
44
+ end
45
+
46
+ def cookies
47
+ @cookies ||= Cookies.new
48
+ end
49
+
50
+ class Cookies
51
+ attr_accessor :secret
52
+
53
+ def initialize
54
+ self.secret = nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,99 @@
1
+ require 'msgpack'
2
+
3
+ module Cannon
4
+ class CookieJar
5
+ include Signature
6
+
7
+ class EndOfString < Exception; end
8
+
9
+ def initialize(http_cookie: nil, cookies: nil, signed: false)
10
+ @http_cookie = http_cookie
11
+ @cookies = cookies
12
+ @signed = signed
13
+
14
+ self.define_singleton_method(:signed) do
15
+ @signed_cookies ||= CookieJar.new(cookies: cookies_with_signatures, signed: true)
16
+ end if !@signed
17
+ end
18
+
19
+ def [](cookie_name)
20
+ cookie = cookies[cookie_name]
21
+ if cookie
22
+ @signed ? verified_signature(cookie_name, cookie) : cookie['value']
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def verified_signature(name, cookie)
31
+ return cookie['value'] if cookie['verified']
32
+
33
+ if cookie['signature'] == signature(cookie['value'])
34
+ cookie['verified'] = true
35
+ cookie['value']
36
+ else
37
+ cookies.delete(name)
38
+ nil
39
+ end
40
+ end
41
+
42
+ def cookies_with_signatures
43
+ cookies.select { |k, v| v.include? 'signature' }
44
+ end
45
+
46
+ def cookies
47
+ @cookies ||= parse_cookies
48
+ end
49
+
50
+ def parse_cookies
51
+ cookies = {}
52
+ return cookies if @http_cookie.nil? || @http_cookie == ''
53
+
54
+ begin
55
+ pos = 0
56
+ loop do
57
+ pos = read_whitespace(@http_cookie, pos)
58
+ name, pos = read_cookie_name(@http_cookie, pos)
59
+ value, pos = read_cookie_value(@http_cookie, pos)
60
+ begin
61
+ cookies[name.to_sym] = MessagePack.unpack(value)
62
+ rescue StandardError; end
63
+ end
64
+ rescue EndOfString
65
+ end
66
+
67
+ cookies
68
+ end
69
+
70
+ def read_whitespace(cookie, pos)
71
+ raise EndOfString if cookie[pos] == nil
72
+ pos = pos + 1 while cookie[pos] == ' ' && pos < cookie.length
73
+ pos
74
+ end
75
+
76
+ def read_cookie_name(cookie, pos)
77
+ start_pos = pos
78
+ pos = pos + 1 while !['=', nil].include?(cookie[pos])
79
+ return cookie[start_pos..(pos - 1)], pos + 1
80
+ end
81
+
82
+ def read_cookie_value(cookie, pos)
83
+ in_quotes = false
84
+ pos = pos + 1 and in_quotes = true if cookie[pos] == '"'
85
+ start_pos = pos
86
+
87
+ if in_quotes
88
+ pos = pos + 1 while pos < cookie.length && !(cookie[pos] == '"' && cookie[pos - 1] != '\\')
89
+ value = cookie[start_pos..(pos - 1)].gsub("\\\"", '"')
90
+ pos = pos + 1 while ![';', nil].include?(cookie[pos])
91
+ else
92
+ pos = pos + 1 while ![';', nil].include?(cookie[pos])
93
+ value = cookie[start_pos..(pos - 1)]
94
+ end
95
+
96
+ return value, pos + 1
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,25 @@
1
+ module Cannon
2
+ class Handler < EventMachine::Connection
3
+ include EventMachine::HttpServer
4
+
5
+ def app
6
+ # magically defined by Cannon::App
7
+ self.class.app
8
+ end
9
+
10
+ def process_http_request
11
+ request = Request.new(self, app)
12
+ response = Response.new(self, app)
13
+
14
+ app.reload_environment if app.config.reload_on_request
15
+
16
+ app.middleware_runner.run(request, response) if middleware?
17
+ end
18
+
19
+ private
20
+
21
+ def middleware?
22
+ app.config.middleware.size > 0
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ require 'cannon/middleware/flush_and_benchmark'
2
+ require 'cannon/middleware/request_logger'
3
+ require 'cannon/middleware/files'
4
+ require 'cannon/middleware/router'
5
+ require 'cannon/middleware/content_type'
6
+ require 'cannon/middleware/cookies'
7
+
8
+ module Cannon
9
+ class MiddlewareRunner
10
+ include EventMachine::Deferrable
11
+
12
+ def initialize(ware, callback:, app:)
13
+ @app = app
14
+ @ware, @callback = instantiate(ware), callback
15
+ end
16
+
17
+ def run(request, response)
18
+ next_proc = -> do
19
+ setup_callback
20
+ self.succeed(request, response)
21
+ end
22
+
23
+ result = @ware.run(request, response, next_proc)
24
+ end
25
+
26
+ private
27
+
28
+ def setup_callback
29
+ set_deferred_status nil
30
+ callback do |request, response|
31
+ @callback.run(request, response) unless @callback.nil?
32
+ end
33
+ end
34
+
35
+ def instantiate(ware)
36
+ if ware.is_a?(String)
37
+ begin
38
+ Object.const_get(ware).new(@app)
39
+ rescue NameError
40
+ Object.const_get("Cannon::Middleware::#{ware}").new(@app)
41
+ end
42
+ else
43
+ ware.new(@app)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Cannon
2
+ module Middleware
3
+ class ContentType
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def run(request, response, next_proc)
9
+ return next_proc.call unless response.headers['Content-Type'].nil?
10
+ response.headers['Content-Type'] = 'text/plain; charset=us-ascii'
11
+ next_proc.call
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module Cannon
2
+ module Middleware
3
+ class Cookies
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def run(request, response, next_proc)
10
+ request.define_singleton_method(:cookies) do
11
+ @cookie_jar ||= CookieJar.new(http_cookie: request.http_cookie)
12
+ end
13
+
14
+ next_proc.call
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ module Cannon
2
+ module Middleware
3
+ class Files
4
+ include PathCache
5
+
6
+ def initialize(app)
7
+ @app = app
8
+
9
+ self.cache = :files
10
+ self.base_path = build_base_path
11
+ end
12
+
13
+ def run(request, response, next_proc)
14
+ reload_cache if outdated_cache?
15
+
16
+ if path_array.include? request.path
17
+ file, content_type = *file_and_content_type("#{base_path}#{request.path}")
18
+ response.header('Content-Type', content_type)
19
+ response.send(file)
20
+ response.flush
21
+ else
22
+ next_proc.call
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def build_base_path
29
+ @app.config.public_path =~ /^\// ? @app.config.public_path : "#{Cannon.root}/#{@app.config.public_path}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ module Cannon
2
+ module Middleware
3
+ class FlushAndBenchmark
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def run(request, response, next_proc)
9
+ response.flush unless response.flushed?
10
+ Cannon.logger.debug "Response took #{time_ago_in_ms(request.start_time)}ms" if @app.config.benchmark_requests
11
+ end
12
+
13
+ private
14
+
15
+ def time_ago_in_ms(time_ago)
16
+ Time.at((Time.now - time_ago)).strftime('%6N').to_i/1000.0
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Cannon
2
+ module Middleware
3
+ class RequestLogger
4
+ def initialize(app)
5
+ end
6
+
7
+ def run(request, response, next_proc)
8
+ Cannon.logger.info "#{request.method} #{request.path}"
9
+ next_proc.call
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Cannon
2
+ module Middleware
3
+ class Router
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def run(request, response, next_proc)
9
+ matched_route = @app.routes.find { |route| route.matches? request }
10
+ if matched_route.nil?
11
+ response.not_found
12
+ next_proc.call
13
+ else
14
+ matched_route.handle(request, response, next_proc)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ require 'cgi'
2
+
3
+ module Cannon
4
+ class Request
5
+ attr_accessor :protocol, :method, :http_cookie, :content_type, :path, :uri, :query_string, :post_content, :headers,
6
+ :start_time
7
+
8
+ def initialize(http_server, app)
9
+ self.protocol = http_server.instance_variable_get('@http_protocol')
10
+ self.method = http_server.instance_variable_get('@http_request_method')
11
+ self.http_cookie = http_server.instance_variable_get('@http_cookie')
12
+ self.content_type = http_server.instance_variable_get('@http_content_type')
13
+ self.path = http_server.instance_variable_get('@http_path_info')
14
+ self.uri = http_server.instance_variable_get('@http_request_uri')
15
+ self.query_string = http_server.instance_variable_get('@http_query_string')
16
+ self.post_content = http_server.instance_variable_get('@http_post_content')
17
+ self.headers = http_server.instance_variable_get('@http_headers')
18
+ self.start_time = Time.now
19
+ end
20
+
21
+ def params
22
+ @params ||= parse_params
23
+ end
24
+
25
+ private
26
+
27
+ def parse_params
28
+ case method.downcase
29
+ when 'get'
30
+ Hash[CGI::parse(query_string || '').map { |(k, v)| [k.to_sym, v.last] }]
31
+ else
32
+ Hash[CGI::parse(post_content || '').map { |(k, v)| [k.to_sym, v.count > 1 ? v : v.first] }]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,165 @@
1
+ require 'msgpack'
2
+
3
+ module Cannon
4
+ class Response
5
+ extend Forwardable
6
+ include Views
7
+ include Signature
8
+
9
+ attr_reader :delegated_response, :headers
10
+ attr_accessor :status
11
+
12
+ delegate :content => :delegated_response
13
+ delegate :content= => :delegated_response
14
+
15
+ HTTP_STATUS = {
16
+ continue: 100,
17
+ switching_protocols: 101,
18
+ ok: 200,
19
+ created: 201,
20
+ accepted: 202,
21
+ non_authoritative_information: 203,
22
+ no_content: 204,
23
+ reset_content: 205,
24
+ partial_content: 206,
25
+ multiple_choices: 300,
26
+ moved_permanently: 301,
27
+ found: 302,
28
+ see_other: 303,
29
+ not_modified: 304,
30
+ use_proxy: 305,
31
+ temporary_redirect: 307,
32
+ bad_request: 400,
33
+ unauthorized: 401,
34
+ payment_required: 402,
35
+ forbidden: 403,
36
+ not_found: 404,
37
+ method_not_allowed: 405,
38
+ not_acceptable: 406,
39
+ proxy_authentication_required: 407,
40
+ request_timeout: 408,
41
+ conflict: 409,
42
+ gone: 410,
43
+ length_required: 411,
44
+ precondition_failed: 412,
45
+ request_entity_too_large: 413,
46
+ request_uri_too_long: 414,
47
+ unsupported_media_type: 415,
48
+ requested_range_not_satisfied: 416,
49
+ expectation_failed: 417,
50
+ internal_server_error: 500,
51
+ not_implemented: 501,
52
+ bad_gateway: 502,
53
+ service_unavailable: 503,
54
+ gateway_timeout: 504,
55
+ http_version_not_supported: 505,
56
+ }
57
+
58
+ def initialize(http_server, app)
59
+ @app = app
60
+ @delegated_response = EventMachine::DelegatedHttpResponse.new(http_server)
61
+ @flushed = false
62
+ @headers = {}
63
+ @cookies = {}
64
+
65
+ initialize_views
66
+
67
+ self.status = :ok
68
+ end
69
+
70
+ def flushed?
71
+ @flushed
72
+ end
73
+
74
+ def send(content, status: self.status)
75
+ self.content ||= ''
76
+ self.status = status
77
+ delegated_response.status = converted_status(status)
78
+ delegated_response.content += content
79
+ end
80
+
81
+ def flush
82
+ unless flushed?
83
+ set_cookie_headers
84
+ delegated_response.headers = self.headers
85
+ delegated_response.send_response
86
+ @flushed = true
87
+ end
88
+ end
89
+
90
+ def header(key, value)
91
+ headers[key] = value
92
+ end
93
+
94
+ def location_header(location)
95
+ header('Location', location)
96
+ end
97
+
98
+ def permanent_redirect(location)
99
+ location_header(location)
100
+ self.status = :moved_permanently
101
+ flush
102
+ end
103
+
104
+ def temporary_redirect(location)
105
+ location_header(location)
106
+ self.status = :found
107
+ flush
108
+ end
109
+
110
+ def not_found
111
+ send('Not Found', status: :not_found)
112
+ end
113
+
114
+ def internal_server_error(title:, content:)
115
+ html = "<html><head><title>Internal Server Error: #{title}</title></head><body><h1>#{title}</h1><p>#{content}</p></body></html>"
116
+ header('Content-Type', 'text/html')
117
+ send(html, status: :internal_server_error)
118
+ end
119
+
120
+ def cookie(cookie, value:, expires: nil, httponly: nil, signed: false)
121
+ cookie_options = {:value => value}
122
+ cookie_options[:expires] = expires unless expires.nil?
123
+ cookie_options[:httponly] = httponly unless httponly.nil?
124
+ cookie_options[:signed] = signed
125
+ @cookies[cookie] = cookie_options
126
+ end
127
+
128
+ private
129
+
130
+ def set_cookie_headers
131
+ cookie_headers = (headers['Set-Cookie'] = [])
132
+ @cookies.each do |cookie, cookie_options|
133
+ cookie_headers << build_cookie_value(cookie, cookie_options)
134
+ end
135
+ end
136
+
137
+ def build_cookie_value(name, options)
138
+ cookie = "#{name}=#{cookie_value(options[:value], signed: options[:signed])}"
139
+ cookie << "; Expires=#{options[:expires].httpdate}" if options.include?(:expires)
140
+ cookie << '; HttpOnly' if options[:httponly] == true
141
+ cookie
142
+ end
143
+
144
+ def converted_status(status)
145
+ if status.is_a?(Symbol)
146
+ HTTP_STATUS[status] || status.to_s
147
+ elsif status.is_a?(Fixnum)
148
+ status
149
+ else
150
+ status.to_s
151
+ end
152
+ end
153
+
154
+ def cookie_value(value, signed:)
155
+ cookie_hash = {'value' => value}
156
+ cookie_hash['signature'] = signature(value) if signed
157
+ escape_cookie_value(cookie_hash.to_msgpack)
158
+ end
159
+
160
+ def escape_cookie_value(value)
161
+ return value unless value.match(/([\x00-\x20\x7F",;\\])/)
162
+ "\"#{value.gsub(/([\\"])/, "\\\\\\1")}\""
163
+ end
164
+ end
165
+ end