midori.rb 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|