nyara 0.0.1.pre
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/example/design.rb +62 -0
- data/example/fib.rb +15 -0
- data/example/hello.rb +5 -0
- data/example/stream.rb +10 -0
- data/ext/accept.c +133 -0
- data/ext/event.c +89 -0
- data/ext/extconf.rb +34 -0
- data/ext/hashes.c +130 -0
- data/ext/http-parser/AUTHORS +41 -0
- data/ext/http-parser/CONTRIBUTIONS +4 -0
- data/ext/http-parser/LICENSE-MIT +23 -0
- data/ext/http-parser/contrib/parsertrace.c +156 -0
- data/ext/http-parser/contrib/url_parser.c +44 -0
- data/ext/http-parser/http_parser.c +2175 -0
- data/ext/http-parser/http_parser.h +304 -0
- data/ext/http-parser/test.c +3425 -0
- data/ext/http_parser.c +1 -0
- data/ext/inc/epoll.h +60 -0
- data/ext/inc/kqueue.h +77 -0
- data/ext/inc/status_codes.inc +64 -0
- data/ext/inc/str_intern.h +66 -0
- data/ext/inc/version.inc +1 -0
- data/ext/mime.c +107 -0
- data/ext/multipart-parser-c/README.md +18 -0
- data/ext/multipart-parser-c/multipart_parser.c +309 -0
- data/ext/multipart-parser-c/multipart_parser.h +48 -0
- data/ext/multipart_parser.c +1 -0
- data/ext/nyara.c +56 -0
- data/ext/nyara.h +59 -0
- data/ext/request.c +474 -0
- data/ext/route.cc +325 -0
- data/ext/url_encoded.c +304 -0
- data/hello.rb +5 -0
- data/lib/nyara/config.rb +64 -0
- data/lib/nyara/config_hash.rb +51 -0
- data/lib/nyara/controller.rb +336 -0
- data/lib/nyara/cookie.rb +31 -0
- data/lib/nyara/cpu_counter.rb +65 -0
- data/lib/nyara/header_hash.rb +18 -0
- data/lib/nyara/mime_types.rb +612 -0
- data/lib/nyara/nyara.rb +82 -0
- data/lib/nyara/param_hash.rb +5 -0
- data/lib/nyara/request.rb +144 -0
- data/lib/nyara/route.rb +138 -0
- data/lib/nyara/route_entry.rb +43 -0
- data/lib/nyara/session.rb +104 -0
- data/lib/nyara/view.rb +317 -0
- data/lib/nyara.rb +25 -0
- data/nyara.gemspec +20 -0
- data/rakefile +91 -0
- data/readme.md +35 -0
- data/spec/ext_mime_match_spec.rb +27 -0
- data/spec/ext_parse_accept_value_spec.rb +29 -0
- data/spec/ext_parse_spec.rb +138 -0
- data/spec/ext_route_spec.rb +70 -0
- data/spec/hashes_spec.rb +71 -0
- data/spec/path_helper_spec.rb +77 -0
- data/spec/request_delegate_spec.rb +67 -0
- data/spec/request_spec.rb +56 -0
- data/spec/route_entry_spec.rb +12 -0
- data/spec/route_spec.rb +84 -0
- data/spec/session_spec.rb +66 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/view_spec.rb +87 -0
- data/tools/bench-cookie.rb +22 -0
- metadata +111 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
# coding: binary
|
2
|
+
|
3
|
+
module Nyara
|
4
|
+
# request and handler
|
5
|
+
class Request
|
6
|
+
# c-ext: http_method, scope, path, matched_accept, header
|
7
|
+
# status, response_content_type, response_header, response_header_extra_lines
|
8
|
+
# todo: body, move all underline methods into Ext
|
9
|
+
|
10
|
+
class << self
|
11
|
+
undef new
|
12
|
+
end
|
13
|
+
|
14
|
+
# method predicates
|
15
|
+
%w[get post put delete options patch].each do |m|
|
16
|
+
eval <<-RUBY
|
17
|
+
def #{m}?
|
18
|
+
http_method == "#{m.upcase}"
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
|
23
|
+
# header delegates
|
24
|
+
%w[content_length content_type referrer user_agent].each do |m|
|
25
|
+
eval <<-RUBY
|
26
|
+
def #{m}
|
27
|
+
header["#{m.split('_').map(&:capitalize).join '-'}"]
|
28
|
+
end
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
|
32
|
+
def scheme
|
33
|
+
@scheme ||= begin
|
34
|
+
h = header
|
35
|
+
if h['X-Forwarded-Ssl'] == 'on'
|
36
|
+
'https'
|
37
|
+
elsif s = h['X-Forwarded-Scheme']
|
38
|
+
s
|
39
|
+
elsif s = h['X-Forwarded-Proto']
|
40
|
+
s.split(',')[0]
|
41
|
+
else
|
42
|
+
'http'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ssl?
|
48
|
+
scheme == 'https'
|
49
|
+
end
|
50
|
+
|
51
|
+
def domain
|
52
|
+
@domain ||= begin
|
53
|
+
r = header['Host']
|
54
|
+
if r
|
55
|
+
r.split(':', 2).first
|
56
|
+
else
|
57
|
+
''
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def port
|
63
|
+
@port ||= begin
|
64
|
+
r = header['Host']
|
65
|
+
if r
|
66
|
+
r = r.split(':', 2).last
|
67
|
+
end
|
68
|
+
r ? r.to_i : 80 # or server running port?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def host
|
73
|
+
header['Host']
|
74
|
+
end
|
75
|
+
|
76
|
+
def xhr?
|
77
|
+
header["Requested-With"] == "XMLHttpRequest"
|
78
|
+
end
|
79
|
+
|
80
|
+
def accept
|
81
|
+
@accept ||= Ext.parse_accept_value header['Accept']
|
82
|
+
end
|
83
|
+
|
84
|
+
def accept_language
|
85
|
+
@accept_language ||= Ext.parse_accept_value header['Accept-Language']
|
86
|
+
end
|
87
|
+
|
88
|
+
def accept_charset
|
89
|
+
@accept_charset ||= Ext.parse_accept_value header['Accept-Charset']
|
90
|
+
end
|
91
|
+
|
92
|
+
def accept_encoding
|
93
|
+
@accept_encoding ||= Ext.parse_accept_value header['Accept-Encoding']
|
94
|
+
end
|
95
|
+
|
96
|
+
FORM_METHODS = %w[
|
97
|
+
POST
|
98
|
+
PUT
|
99
|
+
DELETE
|
100
|
+
PATCH
|
101
|
+
]
|
102
|
+
|
103
|
+
FORM_MEDIA_TYPES = %w[
|
104
|
+
application/x-www-form-urlencoded
|
105
|
+
multipart/form-data
|
106
|
+
]
|
107
|
+
|
108
|
+
def form?
|
109
|
+
if type = header['Content-Type']
|
110
|
+
FORM_METHODS.include?(http_method) and
|
111
|
+
FORM_MEDIA_TYPES.include?(type)
|
112
|
+
else
|
113
|
+
post?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def param
|
118
|
+
@param ||= begin
|
119
|
+
query_param = Ext.request_param self
|
120
|
+
if form?
|
121
|
+
Ext.parse_param query_param, body
|
122
|
+
end
|
123
|
+
query_param
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def cookie
|
128
|
+
@cookie ||= Cookie.decode header
|
129
|
+
end
|
130
|
+
|
131
|
+
def session
|
132
|
+
@session ||= Session.decode cookie
|
133
|
+
end
|
134
|
+
|
135
|
+
# todo serialize the changed cookie
|
136
|
+
|
137
|
+
# todo rename and move it into Ext
|
138
|
+
def not_found
|
139
|
+
puts "not found"
|
140
|
+
Ext.send_data self, "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
141
|
+
Ext.close self
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/nyara/route.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
module Nyara
|
2
|
+
# provide route preprocessing utils
|
3
|
+
module Route; end
|
4
|
+
class << Route
|
5
|
+
# note that controller may be not defined yet
|
6
|
+
def register_controller scope, controller
|
7
|
+
unless scope.is_a?(String)
|
8
|
+
raise ArgumentError, "route prefix should be a string"
|
9
|
+
end
|
10
|
+
scope = scope.dup.freeze
|
11
|
+
(@controllers ||= {})[scope] = controller
|
12
|
+
end
|
13
|
+
|
14
|
+
def compile
|
15
|
+
global_path_templates = {} # "name#id" => path
|
16
|
+
@path_templates = {} # klass => {any_id => path}
|
17
|
+
|
18
|
+
a = @controllers.map do |scope, c|
|
19
|
+
if c.is_a?(String)
|
20
|
+
c = name2const c
|
21
|
+
end
|
22
|
+
name = c.controller_name || const2name(c)
|
23
|
+
raise "#{c.inspect} is not a Nyara::Controller" unless Controller > c
|
24
|
+
|
25
|
+
if @path_templates[c]
|
26
|
+
raise "controller #{c.inspect} was already mapped"
|
27
|
+
end
|
28
|
+
|
29
|
+
route_entries = c.preprocess_actions
|
30
|
+
@path_templates[c] = {}
|
31
|
+
route_entries.each do |e|
|
32
|
+
id = e.id.to_s
|
33
|
+
path = File.join scope, e.path
|
34
|
+
global_path_templates[name + id] = path
|
35
|
+
@path_templates[c][id] = path
|
36
|
+
end
|
37
|
+
|
38
|
+
[scope, c, route_entries]
|
39
|
+
end
|
40
|
+
|
41
|
+
@path_templates.keys.each do |c|
|
42
|
+
@path_templates[c] = global_path_templates.merge @path_templates[c]
|
43
|
+
end
|
44
|
+
|
45
|
+
Ext.clear_route
|
46
|
+
process(a).each do |entry|
|
47
|
+
entry.validate
|
48
|
+
Ext.register_route entry
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def path_template klass, id
|
53
|
+
@path_templates[klass][id]
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
# gc mark fail if wrong order?
|
58
|
+
Ext.clear_route
|
59
|
+
@controllers = {}
|
60
|
+
@path_templates = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# private
|
64
|
+
|
65
|
+
def const2name c
|
66
|
+
name = c.to_s.sub /Controller$/, ''
|
67
|
+
name.gsub!(/(?<!\b)[A-Z]/){|s| "_#{s.downcase}" }
|
68
|
+
name.gsub!(/[A-Z]/, &:downcase)
|
69
|
+
name
|
70
|
+
end
|
71
|
+
|
72
|
+
def name2const name
|
73
|
+
name = name.gsub /(?<=\b|_)[a-z]/, &:upcase
|
74
|
+
name.gsub! '_', ''
|
75
|
+
name << 'Controller'
|
76
|
+
Module.const_get name
|
77
|
+
end
|
78
|
+
|
79
|
+
def process preprocessed
|
80
|
+
entries = []
|
81
|
+
preprocessed.each do |(scope, controller, route_entries)|
|
82
|
+
route_entries.each do |e|
|
83
|
+
e = e.dup # in case there is controller used in more than 1 maps
|
84
|
+
path = scope.sub /\/?$/, e.path
|
85
|
+
if path.empty?
|
86
|
+
path = '/'
|
87
|
+
end
|
88
|
+
e.prefix, suffix = analyse_path path
|
89
|
+
e.suffix, e.conv = compile_re suffix
|
90
|
+
e.scope = scope
|
91
|
+
e.controller = controller
|
92
|
+
entries << e
|
93
|
+
end
|
94
|
+
end
|
95
|
+
entries.sort_by! &:prefix
|
96
|
+
entries.reverse!
|
97
|
+
entries
|
98
|
+
end
|
99
|
+
|
100
|
+
# returns [str_re, conv]
|
101
|
+
def compile_re suffix
|
102
|
+
return ['', []] unless suffix
|
103
|
+
conv = []
|
104
|
+
re_segs = suffix.split(/(?<=%[dfsux])|(?=%[dfsux])/).map do |s|
|
105
|
+
case s
|
106
|
+
when '%d'
|
107
|
+
conv << :to_i
|
108
|
+
'(-?[0-9]+)'
|
109
|
+
when '%f'
|
110
|
+
conv << :to_f
|
111
|
+
# just copied from scanf
|
112
|
+
'([-+]?(?:0[xX](?:\.\h+|\h+(?:\.\h*)?)[pP][-+]\d+|\d+(?![\d.])|\d*\.\d*(?:[eE][-+]?\d+)?))'
|
113
|
+
when '%u'
|
114
|
+
conv << :to_i
|
115
|
+
'([0-9]+)'
|
116
|
+
when '%x'
|
117
|
+
conv << :hex
|
118
|
+
'(\h+)'
|
119
|
+
when '%s'
|
120
|
+
conv << :to_s
|
121
|
+
'([^/]+)'
|
122
|
+
else
|
123
|
+
Regexp.quote s
|
124
|
+
end
|
125
|
+
end
|
126
|
+
["^#{re_segs.join}$", conv]
|
127
|
+
end
|
128
|
+
|
129
|
+
# split the path into parts
|
130
|
+
def analyse_path path
|
131
|
+
raise 'path must contain no new line' if path.index "\n"
|
132
|
+
raise 'path must start with /' unless path.start_with? '/'
|
133
|
+
path = path.sub(/\/$/, '') if path != '/'
|
134
|
+
|
135
|
+
path.split(/(?=%[dfsux])/, 2)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Nyara
|
2
|
+
class RouteEntry
|
3
|
+
REQUIRED_ATTRS = [:http_method, :scope, :prefix, :suffix, :controller, :id, :conv]
|
4
|
+
attr_accessor *REQUIRED_ATTRS
|
5
|
+
|
6
|
+
# optional
|
7
|
+
attr_accessor :accept_exts, :accept_mimes
|
8
|
+
|
9
|
+
# tmp
|
10
|
+
attr_accessor :path, :blk
|
11
|
+
|
12
|
+
def initialize &p
|
13
|
+
instance_eval &p if p
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_accept_exts a
|
17
|
+
@accept_exts = {}
|
18
|
+
@accept_mimes = []
|
19
|
+
if a
|
20
|
+
a.each do |e|
|
21
|
+
e = e.to_s.dup.freeze
|
22
|
+
@accept_exts[e] = true
|
23
|
+
if MIME_TYPES[e]
|
24
|
+
v1, v2 = MIME_TYPES[e].split('/')
|
25
|
+
raise "bad mime type: #{MIME_TYPES[e].inspect}" if v1.nil? or v2.nil?
|
26
|
+
@accept_mimes << [v1, v2, e]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@accept_mimes = nil if @accept_mimes.empty?
|
31
|
+
@accept_exts = nil if @accept_exts.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate
|
35
|
+
REQUIRED_ATTRS.each do |attr|
|
36
|
+
unless instance_variable_get("@#{attr}")
|
37
|
+
raise ArgumentError, "missing #{attr}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
raise ArgumentError, "id must be symbol" unless id.is_a?(Symbol)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Nyara
|
2
|
+
# cookie based
|
3
|
+
# usually it's no need to call cache or database data a "session"
|
4
|
+
module Session
|
5
|
+
extend self
|
6
|
+
|
7
|
+
CIPHER_BLOCK_SIZE = 256/8
|
8
|
+
|
9
|
+
# session is by default DSA + SHA2/SHA1 signed, sub config options are:
|
10
|
+
#
|
11
|
+
# - name (session entry name in cookie, default is 'spare_me_plz')
|
12
|
+
# - key (DSA private key string, in der or pem format, use random if not given)
|
13
|
+
# - cipher_key (if exist, use aes-256-cbc to cipher the "sig&json", the first 256bit is sliced for iv)
|
14
|
+
# (it's no need to set cipher_key if using https)
|
15
|
+
|
16
|
+
# init from config
|
17
|
+
def init
|
18
|
+
c = Config['session'] || {}
|
19
|
+
@name = (c['name'] || 'spare_me_plz').to_s
|
20
|
+
|
21
|
+
if c['key']
|
22
|
+
@dsa = OpenSSL::PKey::DSA.new c['key']
|
23
|
+
else
|
24
|
+
@dsa = OpenSSL::PKey::DSA.generate 256
|
25
|
+
end
|
26
|
+
|
27
|
+
# DSA can sign on any digest since 1.0.0
|
28
|
+
@dss = OpenSSL::VERSION >= '1.0.0' ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::DSS1
|
29
|
+
|
30
|
+
@cipher_key = pad_256_bit c['cipher_key']
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :name
|
34
|
+
|
35
|
+
def encode h, cookie
|
36
|
+
str = h.to_json
|
37
|
+
sig = @dsa.syssign @dss.digest str
|
38
|
+
str = "#{Base64.urlsafe_encode64 sig}&#{str}"
|
39
|
+
cookie[@name] = @cipher_key ? cipher(str) : str
|
40
|
+
end
|
41
|
+
|
42
|
+
def decode cookie
|
43
|
+
str = cookie[@name].to_s
|
44
|
+
return empty_hash if str.empty?
|
45
|
+
|
46
|
+
str = decipher(str) if @cipher_key
|
47
|
+
sig, str = str.split '&', 2
|
48
|
+
return empty_hash unless str
|
49
|
+
|
50
|
+
begin
|
51
|
+
sig = Base64.urlsafe_decode64 sig
|
52
|
+
verified = @dsa.sysverify @dss.digest(str), sig
|
53
|
+
if verified
|
54
|
+
h = JSON.parse str, create_additions: false, object_class: ParamHash
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
return empty_hash unless h
|
58
|
+
end
|
59
|
+
|
60
|
+
if h.is_a?(ParamHash)
|
61
|
+
h
|
62
|
+
else
|
63
|
+
empty_hash
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# private
|
68
|
+
|
69
|
+
def cipher str
|
70
|
+
iv = rand(36**CIPHER_BLOCK_SIZE).to_s(36).ljust CIPHER_BLOCK_SIZE
|
71
|
+
c = new_cipher true, iv
|
72
|
+
Base64.urlsafe_encode64(iv.dup << c.update(str) << c.final)
|
73
|
+
end
|
74
|
+
|
75
|
+
def decipher str
|
76
|
+
str = Base64.urlsafe_decode64 str
|
77
|
+
iv = str.byteslice 0...CIPHER_BLOCK_SIZE
|
78
|
+
str = str.byteslice CIPHER_BLOCK_SIZE..-1
|
79
|
+
return '' if !str or str.empty?
|
80
|
+
c = new_cipher false, iv
|
81
|
+
c.update(str) << c.final rescue ''
|
82
|
+
end
|
83
|
+
|
84
|
+
def pad_256_bit s
|
85
|
+
s = s.to_s
|
86
|
+
return nil if s.empty?
|
87
|
+
len = CIPHER_BLOCK_SIZE
|
88
|
+
s[0...len].ljust len, '*'
|
89
|
+
end
|
90
|
+
|
91
|
+
def empty_hash
|
92
|
+
# todo invoke hook?
|
93
|
+
ParamHash.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def new_cipher encrypt, iv
|
97
|
+
c = OpenSSL::Cipher.new 'aes-256-cbc'
|
98
|
+
encrypt ? c.encrypt : c.decrypt
|
99
|
+
c.key = @cipher_key
|
100
|
+
c.iv = iv
|
101
|
+
c
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|