midori.rb 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +9 -0
- data/LICENSE +21 -0
- data/ext/midori/extconf.rb +4 -0
- data/ext/midori/websocket.c +32 -0
- data/lib/midori/api.rb +426 -0
- data/lib/midori/api_engine.rb +109 -0
- data/lib/midori/clean_room.rb +24 -0
- data/lib/midori/configure.rb +12 -0
- data/lib/midori/connection.rb +59 -0
- data/lib/midori/const.rb +118 -0
- data/lib/midori/core_ext/configurable.rb +33 -0
- data/lib/midori/core_ext/define_class.rb +29 -0
- data/lib/midori/core_ext/proc.rb +13 -0
- data/lib/midori/core_ext/string.rb +29 -0
- data/lib/midori/env.rb +18 -0
- data/lib/midori/eventsource.rb +20 -0
- data/lib/midori/exception.rb +22 -0
- data/lib/midori/logger.rb +15 -0
- data/lib/midori/middleware.rb +31 -0
- data/lib/midori/request.rb +115 -0
- data/lib/midori/response.rb +34 -0
- data/lib/midori/route.rb +20 -0
- data/lib/midori/runner.rb +63 -0
- data/lib/midori/sandbox.rb +46 -0
- data/lib/midori/server.rb +106 -0
- data/lib/midori/version.rb +5 -0
- data/lib/midori/websocket.rb +105 -0
- data/lib/midori.rb +37 -0
- data/midori.sublime-project +16 -0
- data/tutorial/README.md +11 -0
- data/tutorial/SUMMARY.md +28 -0
- data/tutorial/advanced/custom_extensions.md +0 -0
- data/tutorial/advanced/deploying_for_production.md +0 -0
- data/tutorial/advanced/error_handling.md +0 -0
- data/tutorial/advanced/rerl.md +0 -0
- data/tutorial/advanced/scaling_project.md +0 -0
- data/tutorial/advanced/unit_testing.md +0 -0
- data/tutorial/essentials/extensions.md +31 -0
- data/tutorial/essentials/getting_started.md +27 -0
- data/tutorial/essentials/installation.md +93 -0
- data/tutorial/essentials/middlewares.md +88 -0
- data/tutorial/essentials/request_handling.md +74 -0
- data/tutorial/essentials/routing.md +136 -0
- data/tutorial/essentials/runner.md +59 -0
- data/tutorial/meta/comparison_with_other_frameworks.md +0 -0
- data/tutorial/meta/join_the_midori_community.md +0 -0
- data/tutorial/meta/next_steps.md +0 -0
- metadata +155 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
##
|
2
|
+
# States of a connection
|
3
|
+
class Midori::Connection
|
4
|
+
include Midori::Server
|
5
|
+
|
6
|
+
# @!attribute data
|
7
|
+
# @return [String] string buffer of data to send
|
8
|
+
attr_accessor :data
|
9
|
+
|
10
|
+
# Init Connection with socket
|
11
|
+
# @param [IO] socket raw socket
|
12
|
+
def initialize(socket)
|
13
|
+
@registered = false
|
14
|
+
@socket = socket
|
15
|
+
@monitor = nil
|
16
|
+
@close_flag = false
|
17
|
+
@data = ''
|
18
|
+
listen(socket)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Register events of connection
|
22
|
+
# @param [IO] socket raw socket
|
23
|
+
def listen(socket)
|
24
|
+
EventLoop.register(socket, :rw) do |monitor|
|
25
|
+
@monitor = monitor
|
26
|
+
if monitor.readable?
|
27
|
+
receive_data(monitor)
|
28
|
+
end
|
29
|
+
if monitor.writable?
|
30
|
+
if !@data == ''
|
31
|
+
# :nocov:
|
32
|
+
# Leave for corner cases
|
33
|
+
monitor.io.write_nonblock(@data)
|
34
|
+
@data = ''
|
35
|
+
# :nocov:
|
36
|
+
elsif @close_flag
|
37
|
+
close_connection
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Send message to client
|
44
|
+
# @param [String] data data to send
|
45
|
+
def send_data(data)
|
46
|
+
@monitor.writable? ? @socket.write_nonblock(data) : @data << data
|
47
|
+
end
|
48
|
+
|
49
|
+
# Close the connection
|
50
|
+
def close_connection
|
51
|
+
EventLoop.deregister @socket
|
52
|
+
@socket.close
|
53
|
+
end
|
54
|
+
|
55
|
+
# Close the connection after writing
|
56
|
+
def close_connection_after_writing
|
57
|
+
@close_flag = true
|
58
|
+
end
|
59
|
+
end
|
data/lib/midori/const.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
##
|
2
|
+
# Module for store Midori Const
|
3
|
+
module Midori::Const
|
4
|
+
# Hash table for converting numbers to HTTP/1.1 status code line
|
5
|
+
STATUS_CODE = {
|
6
|
+
100 => '100 Continue',
|
7
|
+
101 => '101 Switching Protocols',
|
8
|
+
102 => '102 Processing', # RFC 2518
|
9
|
+
200 => '200 OK',
|
10
|
+
201 => '201 Created',
|
11
|
+
202 => '202 Accepted',
|
12
|
+
203 => '203 Non-Authoritative Information',
|
13
|
+
204 => '204 No Content',
|
14
|
+
205 => '205 Reset Content',
|
15
|
+
206 => '206 Partial Content', # RFC 7233
|
16
|
+
207 => '207 Multi-Status', # RFC 4918
|
17
|
+
208 => '208 Already Reported', # RFC 5842
|
18
|
+
226 => '226 IM Used', # RFC 3229
|
19
|
+
300 => '300 Multiple Choices',
|
20
|
+
301 => '301 Moved Permanently',
|
21
|
+
302 => '302 Found',
|
22
|
+
303 => '303 See Other',
|
23
|
+
304 => '304 Not Modified', # RFC 7232
|
24
|
+
305 => '305 Use Proxy',
|
25
|
+
307 => '307 Temporary Redirect',
|
26
|
+
308 => '308 Permanent Redirect', # RFC 7538
|
27
|
+
400 => '400 Bad Request',
|
28
|
+
401 => '401 Unauthorized', # RFC 7235
|
29
|
+
402 => '402 Payment Required',
|
30
|
+
403 => '403 Forbidden',
|
31
|
+
404 => '404 Not Found',
|
32
|
+
405 => '405 Method Not Allowed',
|
33
|
+
406 => '406 Not Acceptable',
|
34
|
+
407 => '407 Proxy Authentication Required', # RFC 7235
|
35
|
+
408 => '408 Request Time-out',
|
36
|
+
409 => '409 Conflict',
|
37
|
+
410 => '410 Gone',
|
38
|
+
411 => '411 Length Required',
|
39
|
+
412 => '412 Precondition Failed', # RFC 7232
|
40
|
+
413 => '413 Payload Too Large', # RFC 7231
|
41
|
+
414 => '414 URI Too Long', # RFC 7231
|
42
|
+
415 => '415 Unsupported Media Type',
|
43
|
+
416 => '416 Range Not Satisfiable',
|
44
|
+
417 => '417 Expectation Failed', # RFC 2324
|
45
|
+
418 => '418 I\'m a teaport', # RFC 2324
|
46
|
+
421 => '421 Misdirected Request', # RFC 7540
|
47
|
+
422 => '422 Unprocessable Entity', # RFC 4918
|
48
|
+
423 => '423 Locked', # RFC 4918
|
49
|
+
424 => '424 Failed Dependency', # RFC 4918
|
50
|
+
426 => '426 Upgrade Required',
|
51
|
+
428 => '428 Precondition Required', # RFC 6585
|
52
|
+
429 => '429 Too Many Requests', # RFC 6585
|
53
|
+
431 => '431 Request Header Fields Too Large', # RFC 6585
|
54
|
+
451 => '451 Unavailable For Legal Reasons',
|
55
|
+
500 => '500 Internal Server Error',
|
56
|
+
501 => '501 Not Implemented',
|
57
|
+
502 => '502 Bad Gateway',
|
58
|
+
503 => '503 Service Unavailable',
|
59
|
+
504 => '504 Gateway Time-out',
|
60
|
+
505 => '505 HTTP Version not supported',
|
61
|
+
506 => '506 Variant Also Negotiates', # RFC 2295
|
62
|
+
507 => '507 Insufficient Storage', # RFC 4918
|
63
|
+
508 => '508 Loop Detected', # RFC 5842
|
64
|
+
510 => '510 Not Extended', # RFC 2774
|
65
|
+
511 => '511 Network Authentication Required', # RFC 6585
|
66
|
+
}
|
67
|
+
STATUS_CODE.default = '500 Internal Server Error'
|
68
|
+
STATUS_CODE.freeze
|
69
|
+
|
70
|
+
# Default header for Basic HTTP response
|
71
|
+
DEFAULT_HEADER = {
|
72
|
+
'Server' => "Midori/#{Midori::VERSION}"
|
73
|
+
}
|
74
|
+
|
75
|
+
# Default header for EventSource response
|
76
|
+
EVENTSOURCE_HEADER = {
|
77
|
+
'Content-Type' => 'text-event-stream',
|
78
|
+
'Cache-Control' => 'no-cache',
|
79
|
+
'Connection' => 'keep-alive'
|
80
|
+
}
|
81
|
+
|
82
|
+
# Default header for WebSocket response
|
83
|
+
WEBSOCKET_HEADER = {
|
84
|
+
'Upgrade' => 'websocket',
|
85
|
+
'Connection' => 'Upgrade'
|
86
|
+
}
|
87
|
+
|
88
|
+
# Array of Route Methods
|
89
|
+
ROUTE_METHODS = %i(
|
90
|
+
DELETE
|
91
|
+
GET
|
92
|
+
HEAD
|
93
|
+
POST
|
94
|
+
PUT
|
95
|
+
CONNECT
|
96
|
+
OPTIONS
|
97
|
+
TRACE
|
98
|
+
COPY
|
99
|
+
LOCK
|
100
|
+
MKCOL
|
101
|
+
MOVE
|
102
|
+
PROPFIND
|
103
|
+
PROPPATCH
|
104
|
+
UNLOCK
|
105
|
+
REPORT
|
106
|
+
MKACTIVITY
|
107
|
+
CHECKOUT
|
108
|
+
MERGE
|
109
|
+
M-SEARCH
|
110
|
+
NOTIFY
|
111
|
+
SUBSCRIBE
|
112
|
+
UNSUBSCRIBE
|
113
|
+
PATCH
|
114
|
+
PURGE
|
115
|
+
WEBSOCKET
|
116
|
+
EVENTSOURCE
|
117
|
+
)
|
118
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
##
|
2
|
+
# Modified from Sinatra
|
3
|
+
# Provide flexible configuration for Midori Server
|
4
|
+
module Configurable
|
5
|
+
# Sets an option to the given value.
|
6
|
+
# @param [Symbol] option the name of config
|
7
|
+
# @param [Object] value value to the name
|
8
|
+
# @param [Boolean] read_only Generate option= method or not
|
9
|
+
# @return [nil] nil
|
10
|
+
def set(option, value = (not_set = true), read_only = false)
|
11
|
+
raise ArgumentError if not_set
|
12
|
+
|
13
|
+
setter = proc { |val| set option, val }
|
14
|
+
getter = proc { value }
|
15
|
+
|
16
|
+
define_singleton("#{option}=", setter) unless read_only
|
17
|
+
define_singleton(option, getter)
|
18
|
+
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Dynamically defines a method on settings.
|
24
|
+
# @param [String] name method name
|
25
|
+
# @param [Proc] content method content
|
26
|
+
# @return [nil] nil
|
27
|
+
private def define_singleton(name, content = Proc.new)
|
28
|
+
singleton_class.class_eval do
|
29
|
+
undef_method(name) if method_defined? name
|
30
|
+
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
##
|
2
|
+
# Meta-programming Kernel for Syntactic Sugars
|
3
|
+
module Kernel
|
4
|
+
# This method is implemented to dynamically generate class with given name and template.
|
5
|
+
# Referenced from {Ruby China}[https://ruby-china.org/topics/17382]
|
6
|
+
# @param [String] name name of class
|
7
|
+
# @param [Class] ancestor class to be inherited
|
8
|
+
# @yield inner block to be inserted into class
|
9
|
+
# @return [Class] the class defined
|
10
|
+
def define_class(name, ancestor = Object)
|
11
|
+
Object.const_set(name, Class.new(ancestor))
|
12
|
+
Object.const_get(name).class_eval(&Proc.new) if block_given?
|
13
|
+
Object.const_get(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Define a batch of error handler with given name
|
17
|
+
# @param [Array<Symbol>] args names to be defined
|
18
|
+
# @return [nil] nil
|
19
|
+
# @example
|
20
|
+
# define_error(:foo_error, :bar_error)
|
21
|
+
# => nil, FooError < StandardError and BarError < StandardError would be defined
|
22
|
+
def define_error(*args)
|
23
|
+
args.each do |arg|
|
24
|
+
class_name = arg.to_s.split('_').collect(&:capitalize).join
|
25
|
+
define_class(class_name, StandardError)
|
26
|
+
end
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
##
|
2
|
+
# Meta-programming Proc for Syntactic Sugars
|
3
|
+
class Proc
|
4
|
+
# Convert [Proc] to [Lambda]
|
5
|
+
# @param [Object] instance the context
|
6
|
+
# @return [Lambda] Lambda converted
|
7
|
+
# @note Converting [Proc] to [Lambda] may have incorrect behaviours on corner cases.
|
8
|
+
# @note See {Ruby Language Issues}[https://bugs.ruby-lang.org/issues/7314] for more details.
|
9
|
+
def to_lambda(instance = Object.new)
|
10
|
+
instance.define_singleton_method(:_, &self)
|
11
|
+
instance.method(:_).to_proc
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
##
|
2
|
+
# Meta-programming String for Syntactic Sugars
|
3
|
+
class String
|
4
|
+
# @param [Integer] color_code ANSI color code
|
5
|
+
# @return [String] colored string
|
6
|
+
def colorize(color_code)
|
7
|
+
"\e[#{color_code}m#{self}\e[0m"
|
8
|
+
end
|
9
|
+
|
10
|
+
# color the string with red color
|
11
|
+
def red
|
12
|
+
colorize(31)
|
13
|
+
end
|
14
|
+
|
15
|
+
# color the string with green color
|
16
|
+
def green
|
17
|
+
colorize(32)
|
18
|
+
end
|
19
|
+
|
20
|
+
# color the string with yellow color
|
21
|
+
def yellow
|
22
|
+
colorize(33)
|
23
|
+
end
|
24
|
+
|
25
|
+
# color the string with blue color
|
26
|
+
def blue
|
27
|
+
colorize(34)
|
28
|
+
end
|
29
|
+
end
|
data/lib/midori/env.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Midori
|
2
|
+
# @return [String] midori environment
|
3
|
+
def self.env
|
4
|
+
ENV['MIDORI_ENV'] || 'development'
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class String
|
9
|
+
# @return [TrueClass | FalseClass] if string is equal to production
|
10
|
+
def production?
|
11
|
+
self == 'production'
|
12
|
+
end
|
13
|
+
|
14
|
+
#@return [TrueClass | FalseClass] if string is equal to development
|
15
|
+
def development?
|
16
|
+
self == 'development'
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
##
|
2
|
+
# This class provides methods for EventSource connection instance.
|
3
|
+
# @attr [Midori::Connection] connection the connection instance of EventMachine
|
4
|
+
class Midori::EventSource
|
5
|
+
attr_accessor :connection
|
6
|
+
|
7
|
+
# Init a EventSource instance with a connection
|
8
|
+
# @param [Midori::Connection] connection the connection instance of EventMachine
|
9
|
+
def initialize(connection)
|
10
|
+
@connection = connection
|
11
|
+
end
|
12
|
+
|
13
|
+
# Send data and close the connection
|
14
|
+
# @param [String] data data to be sent
|
15
|
+
def send(data)
|
16
|
+
raise Midori::Exception::EventSourceTypeError unless data.is_a? String
|
17
|
+
@connection.send_data(data.split("\n").map {|str| "data: #{str}\n"}.join + "\n")
|
18
|
+
@connection.close_connection_after_writing
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
##
|
2
|
+
# This module store errors to be handled inside Midori
|
3
|
+
module Midori::Exception
|
4
|
+
# No route matched
|
5
|
+
class NotFound < StandardError; end
|
6
|
+
# Internal Error
|
7
|
+
class InternalError < StandardError; end
|
8
|
+
# Midori doesn't support continuous frame of WebSockets yet
|
9
|
+
class ContinuousFrame < StandardError; end
|
10
|
+
# WebSocket OpCode not defined in RFC standards
|
11
|
+
class OpCodeError < StandardError; end
|
12
|
+
# Websocket request not masked
|
13
|
+
class NotMasked < StandardError; end
|
14
|
+
# Websocket frame has ended
|
15
|
+
class FrameEnd < StandardError; end
|
16
|
+
# Websocket Ping Pong size too large
|
17
|
+
class PingPongSizeTooLarge < StandardError; end
|
18
|
+
# Not sending String in EventSource
|
19
|
+
class EventSourceTypeError < StandardError; end
|
20
|
+
# Insert a not middleware class to middleware list
|
21
|
+
class MiddlewareError < StandardError; end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Midori
|
2
|
+
class << self
|
3
|
+
# Return current logger midori is using
|
4
|
+
# @return [Logger] the current logger midori is using
|
5
|
+
def logger
|
6
|
+
@logger ||= ::Logger.new(STDOUT)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Return midori's logger
|
10
|
+
# @param [Logger] logger set midori logger
|
11
|
+
def logger=(logger)
|
12
|
+
@logger = logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
##
|
2
|
+
# Ancestor of all middlewares
|
3
|
+
class Midori::Middleware
|
4
|
+
# Init a middleware
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
# run before processing a request
|
9
|
+
# @param [Midori::Request] request raw request
|
10
|
+
# @return [Midori::Request] request to be further processed
|
11
|
+
def before(request)
|
12
|
+
request
|
13
|
+
end
|
14
|
+
|
15
|
+
# run after processing a request
|
16
|
+
# @param [Midori::Request] _request raw request
|
17
|
+
# @param [Midori::Response] response raw response
|
18
|
+
# @return [Midori::Response] response to be further processed
|
19
|
+
def after(_request, response)
|
20
|
+
response
|
21
|
+
end
|
22
|
+
|
23
|
+
# Dynamically generate a method to use inside router
|
24
|
+
# @param [Symbol] name name of the method
|
25
|
+
# @yield the block to run
|
26
|
+
def self.helper(name, &block)
|
27
|
+
Midori::CleanRoom.class_exec do
|
28
|
+
define_method(name, &block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
##
|
2
|
+
# Request class for midori
|
3
|
+
# @attr [String] ip client ip address
|
4
|
+
# @attr [Integer] port client port
|
5
|
+
# @attr [String] protocol protocol version of HTTP request
|
6
|
+
# @attr [Symbol] method HTTP method
|
7
|
+
# @attr [String] path request path
|
8
|
+
# @attr [Hash] query_param parameter parsed from query string
|
9
|
+
# @attr [String | nil] query_string request query string
|
10
|
+
# @attr [Hash] header request header
|
11
|
+
# @attr [String] body request body
|
12
|
+
# @attr [Hash] cookie cookie hash coming from request
|
13
|
+
# @attr [Boolean] parsed whether the request header parsed
|
14
|
+
# @attr [Boolean] body_parsed whether the request body parsed
|
15
|
+
# @attr [Hash] params params in the url
|
16
|
+
class Midori::Request
|
17
|
+
attr_accessor :ip, :port,
|
18
|
+
:protocol, :method, :path, :query_params, :query_string,
|
19
|
+
:header, :body, :parsed, :body_parsed, :params, :cookie
|
20
|
+
|
21
|
+
# Init Request
|
22
|
+
def initialize
|
23
|
+
@header = {}
|
24
|
+
@parsed = false
|
25
|
+
@body_parsed = false
|
26
|
+
@is_websocket = false
|
27
|
+
@is_eventsource = false
|
28
|
+
@parser = Http::Parser.new
|
29
|
+
@params = {}
|
30
|
+
@query_params = Hash.new(Array.new)
|
31
|
+
@cookie = {}
|
32
|
+
@body = ''
|
33
|
+
@parser.on_headers_complete = proc do
|
34
|
+
@protocol = @parser.http_version
|
35
|
+
@method = @parser.http_method
|
36
|
+
@path = @parser.request_url
|
37
|
+
@header = @parser.headers
|
38
|
+
|
39
|
+
@query_string = @path.match(/\?(.*?)$/)
|
40
|
+
unless @query_string.nil?
|
41
|
+
@query_string = @query_string[1]
|
42
|
+
@query_params = CGI::parse(@query_string)
|
43
|
+
end
|
44
|
+
|
45
|
+
@cookie = CGI::Cookie.parse(@header['Cookie']) unless @header['Cookie'].nil?
|
46
|
+
@path.gsub!(/\?(.*?)$/, '')
|
47
|
+
@method = @method.to_sym
|
48
|
+
@parsed = true
|
49
|
+
:stop
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Init an request with String data
|
54
|
+
# @param [String] data
|
55
|
+
# @return [nil] nil
|
56
|
+
def parse(data)
|
57
|
+
# Call parser if header not parsed
|
58
|
+
if @parsed
|
59
|
+
@body += data
|
60
|
+
else
|
61
|
+
offset = @parser << data
|
62
|
+
@body += data[offset..-1] if @parsed
|
63
|
+
end
|
64
|
+
|
65
|
+
# Set body parsed if body reaches content length
|
66
|
+
if @parsed && (@header['Content-Length'].to_i || 0) <= @body.bytesize
|
67
|
+
@body_parsed = true
|
68
|
+
pre_proceed
|
69
|
+
end
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Preproceed the request after parsed
|
74
|
+
# @return [nil] nil
|
75
|
+
def pre_proceed
|
76
|
+
# Deal with WebSocket
|
77
|
+
if @header['Upgrade'] == 'websocket' && @header['Connection'] == 'Upgrade'
|
78
|
+
@method = :WEBSOCKET
|
79
|
+
@is_websocket = true
|
80
|
+
end
|
81
|
+
|
82
|
+
# Deal with EventSource
|
83
|
+
if @header['Accept'] == 'text/event-stream'
|
84
|
+
@method = :EVENTSOURCE
|
85
|
+
@is_eventsource = true
|
86
|
+
end
|
87
|
+
|
88
|
+
@method = @method.to_sym
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# Syntactic sugar for whether a request header is parsed
|
93
|
+
# @return [Boolean] parsed or not
|
94
|
+
def parsed?
|
95
|
+
@parsed
|
96
|
+
end
|
97
|
+
|
98
|
+
# Syntactic sugar for whether a request body is parsed
|
99
|
+
# @return [Boolean] parsed or not
|
100
|
+
def body_parsed?
|
101
|
+
@body_parsed
|
102
|
+
end
|
103
|
+
|
104
|
+
# Syntactic sugar for whether a request is a websocket request
|
105
|
+
# @return [Boolean] websocket or not
|
106
|
+
def websocket?
|
107
|
+
@is_websocket
|
108
|
+
end
|
109
|
+
|
110
|
+
# Syntactic sugar for whether a request is an eventsource request
|
111
|
+
# @return [Boolean] eventsource or not
|
112
|
+
def eventsource?
|
113
|
+
@is_eventsource
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
##
|
2
|
+
# Class for midori response
|
3
|
+
# @attr [String] status HTTP response status
|
4
|
+
# @attr [Hash] header HTTP response header
|
5
|
+
# @attr [String] body HTTP response body
|
6
|
+
class Midori::Response
|
7
|
+
attr_accessor :status, :header, :body
|
8
|
+
|
9
|
+
# @param [Hash] options HTTP response
|
10
|
+
# @option options [Integer] code HTTP response code
|
11
|
+
# @option options [Hash] header HTTP response header
|
12
|
+
# @option options [String] body HTTP response body
|
13
|
+
# Init a Response
|
14
|
+
def initialize(options = {})
|
15
|
+
code = options[:status] || 200
|
16
|
+
@status = Midori::Const::STATUS_CODE[code]
|
17
|
+
@header = options[:header] || Midori::Const::DEFAULT_HEADER.clone
|
18
|
+
@body = options[:body] || ''
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generate header string from hash
|
22
|
+
# @return [String] generated string
|
23
|
+
def generate_header
|
24
|
+
@header.map do |key, value|
|
25
|
+
"#{key}: #{value}\r\n"
|
26
|
+
end.join
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert response to raw string
|
30
|
+
# @return [String] generated string
|
31
|
+
def to_s
|
32
|
+
"HTTP/1.1 #{@status}\r\n#{generate_header}\r\n#{@body}"
|
33
|
+
end
|
34
|
+
end
|
data/lib/midori/route.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
##
|
2
|
+
# Class for Midori route
|
3
|
+
# @attr [String] method HTTP method
|
4
|
+
# @attr [Regexp] path regex to match
|
5
|
+
# @attr [Proc] function what to do after matched
|
6
|
+
# @attr [Array<Class>] middlewares middlewares used in the route
|
7
|
+
class Midori::Route
|
8
|
+
attr_accessor :method, :path, :function, :middlewares
|
9
|
+
|
10
|
+
# Define a route
|
11
|
+
# @param [String] method HTTP method
|
12
|
+
# @param [Regexp] path regex to match
|
13
|
+
# @param [Proc] function what to do after matched
|
14
|
+
def initialize(method, path, function)
|
15
|
+
@method = method
|
16
|
+
@path = path
|
17
|
+
@function = function
|
18
|
+
@middlewares = []
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
##
|
2
|
+
# Abstract runner class to control instance of Midori Server
|
3
|
+
# @attr [String] bind the address to bind
|
4
|
+
# @attr [Integer] port the port to bind
|
5
|
+
# @attr [Logger] logger midori logger
|
6
|
+
class Midori::Runner
|
7
|
+
attr_reader :bind, :port, :logger
|
8
|
+
|
9
|
+
# Define status of a runner
|
10
|
+
# @param [Class] api inherited from [Midori::API]
|
11
|
+
# @param [Class] configure inherited from [Midori::Configure]
|
12
|
+
def initialize(api, configure = Midori::Configure)
|
13
|
+
@logger = configure.logger
|
14
|
+
Midori.logger = configure.logger
|
15
|
+
@bind = configure.bind
|
16
|
+
@port = configure.port
|
17
|
+
@api = (api.is_a?Midori::APIEngine) ? api : Midori::APIEngine.new(api, configure.route_type)
|
18
|
+
@before = configure.before
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get Midori server whether running
|
22
|
+
# @return [Boolean] [true] running
|
23
|
+
# @return [Boolean] [false] not running
|
24
|
+
def running?
|
25
|
+
!!@server
|
26
|
+
end
|
27
|
+
|
28
|
+
# Start the Midori server
|
29
|
+
# @note This is an async method, but no callback
|
30
|
+
def start
|
31
|
+
return false if running? || EventLoop.running?
|
32
|
+
@logger.info "Midori #{Midori::VERSION} is now running on #{bind}:#{port}".blue
|
33
|
+
@server = TCPServer.new(@bind, @port)
|
34
|
+
EventLoop.register(@server, :r) do |monitor|
|
35
|
+
socket = monitor.io.accept_nonblock
|
36
|
+
connection = Midori::Connection.new(socket)
|
37
|
+
connection.server_initialize(@api, @logger)
|
38
|
+
end
|
39
|
+
async_fiber(Fiber.new do
|
40
|
+
@before.call
|
41
|
+
end)
|
42
|
+
EventLoop.start
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# Stop the Midori server
|
47
|
+
# @note This is an async method, but no callback
|
48
|
+
# @return [Boolean] [true] stop successfully
|
49
|
+
# @return [Boolean] [false] nothing to stop
|
50
|
+
def stop
|
51
|
+
if running?
|
52
|
+
@logger.info 'Stopping Midori'.blue
|
53
|
+
EventLoop.deregister @server
|
54
|
+
@server.close
|
55
|
+
@server = nil
|
56
|
+
EventLoop.stop
|
57
|
+
true
|
58
|
+
else
|
59
|
+
@logger.error 'Midori Server has NOT been started'.red
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
##
|
2
|
+
# Sandbox for global error capture
|
3
|
+
class Midori::Sandbox
|
4
|
+
class << self
|
5
|
+
def class_initialize
|
6
|
+
@handlers = Hash.new
|
7
|
+
@handlers[Midori::Exception::InternalError] = proc {|e| Midori::Response.new(status: 500, body: "#{e.inspect} #{e.backtrace}")}
|
8
|
+
@handlers[Midori::Exception::NotFound] = proc {|_e| Midori::Response.new(status: 404, body: '404 Not Found')}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a rule to Sandbox
|
12
|
+
# @param [Class] class_name the class to capture
|
13
|
+
# @param [Proc] block what to do when captured
|
14
|
+
# @return [nil] nil
|
15
|
+
def add_rule(class_name, block)
|
16
|
+
@handlers[class_name] = block
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# Detect what to run with given error
|
21
|
+
# @param [StandardError] error the error captured
|
22
|
+
# @return [nil] nil
|
23
|
+
def capture(error)
|
24
|
+
if @handlers[error.class].nil?
|
25
|
+
@handlers[Midori::Exception::InternalError].call(error)
|
26
|
+
else
|
27
|
+
@handlers[error.class].call(error)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Run sandbox inside given clean room
|
32
|
+
# @param [Midori::CleanRoom] clean_room Clean room to run
|
33
|
+
# @param [Proc] function the block to run
|
34
|
+
# @return [nil] nil
|
35
|
+
def run(clean_room, function, *args)
|
36
|
+
begin
|
37
|
+
function.to_lambda(clean_room).call(*args)
|
38
|
+
rescue StandardError => e
|
39
|
+
capture(e)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private_class_method :class_initialize
|
45
|
+
class_initialize
|
46
|
+
end
|