nyara 0.0.1.pre

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