nyara 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|