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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/example/design.rb +62 -0
  3. data/example/fib.rb +15 -0
  4. data/example/hello.rb +5 -0
  5. data/example/stream.rb +10 -0
  6. data/ext/accept.c +133 -0
  7. data/ext/event.c +89 -0
  8. data/ext/extconf.rb +34 -0
  9. data/ext/hashes.c +130 -0
  10. data/ext/http-parser/AUTHORS +41 -0
  11. data/ext/http-parser/CONTRIBUTIONS +4 -0
  12. data/ext/http-parser/LICENSE-MIT +23 -0
  13. data/ext/http-parser/contrib/parsertrace.c +156 -0
  14. data/ext/http-parser/contrib/url_parser.c +44 -0
  15. data/ext/http-parser/http_parser.c +2175 -0
  16. data/ext/http-parser/http_parser.h +304 -0
  17. data/ext/http-parser/test.c +3425 -0
  18. data/ext/http_parser.c +1 -0
  19. data/ext/inc/epoll.h +60 -0
  20. data/ext/inc/kqueue.h +77 -0
  21. data/ext/inc/status_codes.inc +64 -0
  22. data/ext/inc/str_intern.h +66 -0
  23. data/ext/inc/version.inc +1 -0
  24. data/ext/mime.c +107 -0
  25. data/ext/multipart-parser-c/README.md +18 -0
  26. data/ext/multipart-parser-c/multipart_parser.c +309 -0
  27. data/ext/multipart-parser-c/multipart_parser.h +48 -0
  28. data/ext/multipart_parser.c +1 -0
  29. data/ext/nyara.c +56 -0
  30. data/ext/nyara.h +59 -0
  31. data/ext/request.c +474 -0
  32. data/ext/route.cc +325 -0
  33. data/ext/url_encoded.c +304 -0
  34. data/hello.rb +5 -0
  35. data/lib/nyara/config.rb +64 -0
  36. data/lib/nyara/config_hash.rb +51 -0
  37. data/lib/nyara/controller.rb +336 -0
  38. data/lib/nyara/cookie.rb +31 -0
  39. data/lib/nyara/cpu_counter.rb +65 -0
  40. data/lib/nyara/header_hash.rb +18 -0
  41. data/lib/nyara/mime_types.rb +612 -0
  42. data/lib/nyara/nyara.rb +82 -0
  43. data/lib/nyara/param_hash.rb +5 -0
  44. data/lib/nyara/request.rb +144 -0
  45. data/lib/nyara/route.rb +138 -0
  46. data/lib/nyara/route_entry.rb +43 -0
  47. data/lib/nyara/session.rb +104 -0
  48. data/lib/nyara/view.rb +317 -0
  49. data/lib/nyara.rb +25 -0
  50. data/nyara.gemspec +20 -0
  51. data/rakefile +91 -0
  52. data/readme.md +35 -0
  53. data/spec/ext_mime_match_spec.rb +27 -0
  54. data/spec/ext_parse_accept_value_spec.rb +29 -0
  55. data/spec/ext_parse_spec.rb +138 -0
  56. data/spec/ext_route_spec.rb +70 -0
  57. data/spec/hashes_spec.rb +71 -0
  58. data/spec/path_helper_spec.rb +77 -0
  59. data/spec/request_delegate_spec.rb +67 -0
  60. data/spec/request_spec.rb +56 -0
  61. data/spec/route_entry_spec.rb +12 -0
  62. data/spec/route_spec.rb +84 -0
  63. data/spec/session_spec.rb +66 -0
  64. data/spec/spec_helper.rb +52 -0
  65. data/spec/view_spec.rb +87 -0
  66. data/tools/bench-cookie.rb +22 -0
  67. 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
@@ -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