blix-rest 0.8.2 → 0.11.1

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.
@@ -48,32 +48,4 @@ module Blix::Rest
48
48
 
49
49
  end
50
50
 
51
- # implement cache as a simple ruby hash
52
- class MemoryCache < Cache
53
-
54
- def cache
55
- @cache ||= {}
56
- end
57
-
58
- def get(key)
59
- cache[key]
60
- end
61
-
62
- def set(key, data)
63
- cache[key] = data
64
- end
65
-
66
- def clear
67
- cache.clear
68
- end
69
-
70
- def key?(key)
71
- cache.key?(key)
72
- end
73
-
74
- def delete(key)
75
- cache.delete(key)
76
- end
77
-
78
- end
79
- end
51
+ end
@@ -82,7 +82,7 @@ module Blix::Rest
82
82
 
83
83
  # ovverride the path method to return the internal path.
84
84
  def path
85
- p = req.path
85
+ p = CGI.unescape(req.path)
86
86
  p = '/' + p if p[0, 1] != '/' # ensure a leading slash on path
87
87
  idx = RequestMapper.path_root_length
88
88
  if idx > 0
@@ -241,6 +241,35 @@ module Blix::Rest
241
241
  [login, password]
242
242
  end
243
243
 
244
+ def throttle_basic_auth(realm=nil, options={})
245
+ login, password = get_basic_auth(realm)
246
+ # check that the waiting time has expired
247
+ info = self.class.basic_store.get_hash(login) || {}
248
+ fail_count = info['fail_count'].to_i
249
+ if fail_count > 3
250
+ now = Time.now
251
+ fail_time = info['fail_time']
252
+ delta = 60 # one minute
253
+ delta = 60*10 if fail_count > 10 # 10 minutes
254
+ delta = 60*60*24 if fail_count > 100 # one day
255
+ if (fail_time + delta) > now
256
+ raise Blix::Rest::AuthorizationError.new("try again after #{(fail_time + delta)}", realm)
257
+ end
258
+ end
259
+ if yield(login,password)
260
+ # auth success - set error count to 0
261
+ info['fail_count'] = 0
262
+ self.class.basic_store.store_hash(login, info)
263
+ true
264
+ else
265
+ # auth failure - increment error count / set error time
266
+ info['fail_count'] = fail_count + 1
267
+ info['fail_time'] = Time.now
268
+ self.class.basic_store.store_hash(login, info)
269
+ raise Blix::Rest::AuthorizationError.new("invalid login or password", realm)
270
+ end
271
+ end
272
+
244
273
  # set the cors headers
245
274
  def set_accept_cors(opts={})
246
275
  origin = opts[:origin] || env['HTTP_ORIGIN'] || '*'
@@ -371,6 +400,57 @@ module Blix::Rest
371
400
  value
372
401
  end
373
402
 
403
+ # only allow so many exceptions in a given time.
404
+ # the delay applies to
405
+ # - 3x failure
406
+ # - 10x failure
407
+ # - 100x failure
408
+
409
+ # options:
410
+ # :prefix # the prefix to use in the cache
411
+ # :cache # a cache object ( server_cache )
412
+ # :times # array of delays in seconds to apply default: [60, 600, 86400]
413
+ #
414
+ def rate_limit(name, options = {})
415
+ # check that the waiting time has expired
416
+ cache = options[:cache] || server_cache()
417
+ prefix = options[:prefix] || 'rlimit'
418
+ key = "#{prefix}|#{name}"
419
+ info = cache.get(key) || {}
420
+ fail_count = info['fc'].to_i
421
+ times = options[:times] || []
422
+ if fail_count > 2
423
+ now = Time.now
424
+ fail_time = info['ft']
425
+ delta = times[0] || 60 # one minute
426
+ delta = times[1] || 60 * 10 if fail_count > 10 # 10 minutes
427
+ delta = times[2] || 60 * 60 * 24 if fail_count > 100 # one day
428
+ ltime = fail_time + delta
429
+ if ltime > now
430
+ raise RateError, ltime.to_s
431
+ end
432
+ end
433
+ exception = nil
434
+ result = begin
435
+ yield(key)
436
+ rescue Exception => e
437
+ exception = e
438
+ nil
439
+ end
440
+ if exception
441
+ # auth failure - increment error count / set error time
442
+ info['fc'] = fail_count + 1
443
+ info['ft'] = Time.now
444
+ cache.set(key, info)
445
+ raise exception
446
+ else
447
+ # auth success - set error count to 0
448
+ info['fc'] = 0
449
+ cache.set(key, info)
450
+ result
451
+ end
452
+ end
453
+
374
454
  # manage session handling --------------------------------------------------
375
455
  # setup the session and retrieve the session_id
376
456
  # this id can be used to retrieve and data associated
@@ -671,6 +751,17 @@ module Blix::Rest
671
751
  end
672
752
  end
673
753
 
754
+ def allow_methods(*methods)
755
+ out = String.new
756
+ methods.each do |method|
757
+ method = method.to_s.upcase
758
+ next if (HTTP_VERBS + ['ALL']).include?(method)
759
+ out << "def #{method.downcase}(*a, &b);route '#{method}', *a, &b;end\n"
760
+ end
761
+ puts out if $DEBUG || $VERBOSE
762
+ eval out unless out.empty?
763
+ end
764
+
674
765
  end
675
766
 
676
767
  end
@@ -0,0 +1,30 @@
1
+ module Blix::Rest
2
+ # implement cache as a simple ruby hash
3
+ class MemoryCache < Cache
4
+
5
+ def cache
6
+ @cache ||= {}
7
+ end
8
+
9
+ def get(key)
10
+ cache[key]
11
+ end
12
+
13
+ def set(key, data)
14
+ cache[key] = data
15
+ end
16
+
17
+ def clear
18
+ cache.clear
19
+ end
20
+
21
+ def key?(key)
22
+ cache.key?(key)
23
+ end
24
+
25
+ def delete(key)
26
+ cache.delete(key)
27
+ end
28
+
29
+ end
30
+ end
@@ -14,8 +14,9 @@ module Blix::Rest
14
14
  PATH_SEP = '/'
15
15
  STAR_PLACEHOLDER = '*'
16
16
 
17
- # the
18
-
17
+ # the TableNode class is used to build a tree structure to
18
+ # represent the routes.
19
+ #
19
20
  class TableNode
20
21
 
21
22
  attr_accessor :blk
@@ -27,19 +28,7 @@ module Blix::Rest
27
28
 
28
29
  def initialize(name)
29
30
  @children = {}
30
- if name[0, 1] == ':'
31
- @parameter = name[1..-1].to_sym
32
- @value = WILD_PLACEHOLDER
33
- elsif name[0, 1] == '*'
34
- @parameter = if name[1..-1].empty?
35
- :wildpath
36
- else
37
- name[1..-1].to_sym
38
- end
39
- @value = STAR_PLACEHOLDER
40
- else
41
- @value = name
42
- end
31
+ @value, @parameter = parse_name(name)
43
32
  @extract_format = true
44
33
  end
45
34
 
@@ -51,6 +40,19 @@ module Blix::Rest
51
40
  @children[k] = v
52
41
  end
53
42
 
43
+ private
44
+
45
+ def parse_name(name)
46
+ case name[0]
47
+ when ':'
48
+ [WILD_PLACEHOLDER, name[1..].to_sym]
49
+ when '*'
50
+ [STAR_PLACEHOLDER, name[1..].empty? ? :wildpath : name[1..].to_sym]
51
+ else
52
+ [name, nil]
53
+ end
54
+ end
55
+
54
56
  end
55
57
 
56
58
  class << self
@@ -58,18 +60,20 @@ module Blix::Rest
58
60
  # the root always starts with '/' and finishes with '/'
59
61
  def set_path_root(root)
60
62
  root = root.to_s
61
- root = '/' + root if root[0, 1] != '/'
62
- root += '/' if root[-1, 1] != '/'
63
+ root = '/' + root unless root.start_with?('/')
64
+ root += '/' unless root.end_with?('/')
63
65
  @path_root = root
64
66
  @path_root_length = @path_root.length - 1
65
67
  end
66
68
 
69
+ # if the path_root has not been set then return '/'
67
70
  def path_root
68
71
  @path_root || '/'
69
72
  end
70
73
 
74
+ # return 0 if the path_root has not been set
71
75
  def path_root_length
72
- @path_root_length.to_i
76
+ @path_root_length || 0
73
77
  end
74
78
 
75
79
  def full_path(path)
@@ -91,7 +95,7 @@ module Blix::Rest
91
95
 
92
96
  def table
93
97
  # compile the table in one thread only.
94
- @table ||= @@mutex.synchronize{@table ||= compile}
98
+ @table || @@mutex.synchronize{@table ||= compile}
95
99
  end
96
100
 
97
101
  def dump
@@ -317,31 +321,33 @@ module Blix::Rest
317
321
  list.sort! { |a, b| a[0] <=> b[0] }
318
322
  str = String.new
319
323
  list.each do |route|
320
- #pairs = route[1]
321
- (HTTP_VERBS + ['ALL']).each do |verb|
322
- if route[1].key? verb
323
- str << verb << "\t" << route[0] << route[1][verb] << "\n"
324
- end
324
+ pairs = route[1].to_a.sort{|a,b| a[0]<=>b[0]}
325
+ pairs.each do |pair|
326
+ str << pair[0] << "\t" << route[0] << "\t" << pair[1] << "\n"
325
327
  end
326
328
  str << "\n"
327
329
  end
328
330
  str
329
331
  end
330
-
331
332
  end
332
333
 
333
334
  end # RequestMapper
334
335
 
335
- def self.set_path_root(*args)
336
- RequestMapper.set_path_root(*args)
337
- end
336
+ class << self
338
337
 
339
- def self.path_root
340
- RequestMapper.path_root
341
- end
342
338
 
343
- def self.full_path(path)
344
- RequestMapper.full_path(path)
339
+ def set_path_root(*args)
340
+ RequestMapper.set_path_root(*args)
341
+ end
342
+
343
+ def path_root
344
+ RequestMapper.path_root
345
+ end
346
+
347
+ def full_path(path)
348
+ RequestMapper.full_path(path)
349
+ end
350
+
345
351
  end
346
352
 
347
353
  RequestMapper.set_path_root(ENV['BLIX_REST_ROOT']) if ENV['BLIX_REST_ROOT']
@@ -1,6 +1,10 @@
1
1
  # pass a response object to the controller to set
2
2
  # header status and content.
3
3
 
4
+ unless defined?(Rack::Headers)
5
+ class Rack::Headers < Hash; end
6
+ end
7
+
4
8
  module Blix::Rest
5
9
 
6
10
 
@@ -22,5 +26,11 @@ module Blix::Rest
22
26
  @headers.merge!(headers) if headers
23
27
  end
24
28
 
29
+ def set_options(options={})
30
+ @status = options[:status].to_i if options[:status]
31
+ @headers.merge!(options[:headers]) if options[:headers]
32
+ @headers['content-type'] = options[:content_type] if options[:content_type]
33
+ end
34
+
25
35
  end
26
36
  end
@@ -3,18 +3,11 @@
3
3
  module Blix::Rest
4
4
  class Server
5
5
 
6
- def initialize(opts = {})
6
+ def initialize(options = {})
7
7
  @_parsers = {}
8
8
  @_mime_types = {}
9
-
10
- # register the default parsers and any passed in as options.
11
-
12
- register_parser('html', HtmlFormatParser.new)
13
- register_parser('json', JsonFormatParser.new)
14
- register_parser('xml', XmlFormatParser.new)
15
- register_parser('raw', RawFormatParser.new)
16
- extract_parsers_from_options(opts)
17
- @_options = opts
9
+ @_options = options
10
+ setup_parsers(options)
18
11
  end
19
12
 
20
13
  # options passed to the server
@@ -22,7 +15,6 @@ module Blix::Rest
22
15
  @_options
23
16
  end
24
17
 
25
-
26
18
  # the object serving as a cache
27
19
  def _cache
28
20
  @_cache ||= begin
@@ -36,16 +28,15 @@ module Blix::Rest
36
28
  end
37
29
  end
38
30
 
39
- def extract_parsers_from_options(opts)
40
- opts.each do |k, v|
41
- next unless k =~ /^(\w*)_parser&/
31
+ def extract_parsers_from_options(options)
32
+ options.each do |k, v|
33
+ next unless k =~ /^(\w*)_parser$/
42
34
 
43
35
  format = Regexp.last_match(1)
44
36
  parser = v
45
37
  register_parser(format, parser)
46
38
  end
47
39
  end
48
-
49
40
  def set_custom_headers(format, headers)
50
41
  parser = get_parser(format)
51
42
  raise "parser not found for custom headers format=>#{format}" unless parser
@@ -61,7 +52,7 @@ module Blix::Rest
61
52
  end
62
53
 
63
54
  def register_parser(format, parser)
64
- raise "#{k} must be an object with parent class Blix::Rest::FormatParser" unless parser.is_a?(FormatParser)
55
+ raise "#{format} must be an object with parent class Blix::Rest::FormatParser" unless parser.is_a?(FormatParser)
65
56
 
66
57
  parser._format = format
67
58
  @_parsers[format.to_s.downcase] = parser
@@ -71,7 +62,7 @@ module Blix::Rest
71
62
  # retrieve parameters from the http request
72
63
  def retrieve_params(env)
73
64
  post_params = {}
74
- body = ''
65
+ body = ''
75
66
  params = env['params'] || {}
76
67
  params.merge!(::Rack::Utils.parse_nested_query(env['QUERY_STRING']))
77
68
 
@@ -85,19 +76,15 @@ module Blix::Rest
85
76
  post_params = {}
86
77
  else
87
78
  begin
88
- post_params = case (env['CONTENT_TYPE'])
79
+ post_params = case env['CONTENT_TYPE']
89
80
  when URL_ENCODED
90
81
  ::Rack::Utils.parse_nested_query(body)
91
- when JSON_ENCODED then
82
+ when JSON_ENCODED
92
83
  json = MultiJson.load(body)
93
- if json.is_a?(Hash)
94
- json
95
- else
96
- { '_json' => json }
97
- end
84
+ json.is_a?(Hash) ? json : { '_json' => json }
98
85
  else
99
86
  {}
100
- end
87
+ end
101
88
  rescue StandardError => e
102
89
  raise BadRequestError, "Invalid parameters: #{e.class}"
103
90
  end
@@ -124,13 +111,22 @@ module Blix::Rest
124
111
  case mime
125
112
  when 'application/json' then :json
126
113
  when 'text/html' then :html
127
- when 'application/xml' then :xml
114
+ when 'application/xml' then :xml
128
115
  when 'application/xhtml+xml' then :xhtml
129
116
  when '*/*' then :*
130
117
  end
131
118
  end
132
119
 
133
- # attempt to handle mjltiple accept formats here..
120
+ def setup_parsers(options)
121
+ # Register default parsers
122
+ register_parser('html', HtmlFormatParser.new)
123
+ register_parser('json', JsonFormatParser.new)
124
+ register_parser('xml', XmlFormatParser.new)
125
+ register_parser('raw', RawFormatParser.new)
126
+ extract_parsers_from_options(options)
127
+ end
128
+
129
+ # attempt to handle multiple accept formats here..
134
130
  # mime can include '.../*' and '*/*'
135
131
  # FIXME
136
132
  def get_format_new(env, options)
@@ -156,22 +152,22 @@ module Blix::Rest
156
152
 
157
153
  # the call function is the interface with the rack framework
158
154
  def call(env)
155
+ t1 = Time.now
159
156
  req = Rack::Request.new(env)
160
157
 
161
- verb = env['REQUEST_METHOD']
162
- path = req.path
163
- path = CGI.unescape(path).gsub('+',' ') unless _options[:unescape] == false
158
+ verb = env['REQUEST_METHOD']
159
+ path = req.path
160
+ path = CGI.unescape(path) unless _options[:unescape] == false
164
161
 
165
162
  blk, path_params, options, is_wild = RequestMapper.match(verb, path)
166
163
 
167
164
  match_all = RequestMapper.match('ALL', path) unless blk && !is_wild
168
- blk, path_params, options = match_all if match_all && match_all[0] # override
169
-
165
+ blk, path_params, options = match_all if match_all && match_all[0]
170
166
 
171
167
  default_format = options && options[:default] && options[:default].to_sym
172
168
  force_format = options && options[:force] && options[:force].to_sym
173
- do_cache = options && options[:cache] && !Blix::Rest.cache_disabled
174
- clear_cache = options && options[:cache_reset]
169
+ do_cache = options && options[:cache] && !Blix::Rest.cache_disabled
170
+ clear_cache = options && options[:cache_reset]
175
171
 
176
172
  query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
177
173
 
@@ -189,54 +185,59 @@ module Blix::Rest
189
185
 
190
186
  parser._options = options
191
187
 
192
- # check for cached response end return with cached response if found.
193
- #
194
- if do_cache && ( response = _cache["#{verb}|#{format}|#{path}"] )
188
+ # check for cached response and return with cached response if found.
189
+ if do_cache && (response = _cache["#{verb}|#{format}|#{path}"])
195
190
  return [response.status, response.headers.merge('x-blix-cache' => 'cached'), response.content]
196
191
  end
197
192
 
198
193
  response = Response.new
199
194
 
200
195
  if blk
201
-
202
196
  begin
203
197
  params = env['params']
204
198
  context = Context.new(path_params, params, req, format, response, verb, self)
205
- value = blk.call(context)
199
+ value, response_options = blk.call(context)
206
200
  rescue ServiceError => e
207
- set_default_headers(parser,response)
201
+ set_default_headers(parser, response)
208
202
  response.set(e.status, parser.format_error(e.message), e.headers)
203
+ logger << e.message if $VERBOSE
209
204
  rescue RawResponse => e
210
205
  value = e.content
211
- value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
212
- response.status = e.status if e.status
206
+ value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
207
+ response.status = e.status if e.status
213
208
  response.content = value
214
209
  response.headers.merge!(e.headers) if e.headers
215
210
  rescue AuthorizationError => e
216
- set_default_headers(parser,response)
211
+ set_default_headers(parser, response)
217
212
  response.set(401, parser.format_error(e.message), AUTH_HEADER => "#{e.type} realm=\"#{e.realm}\", charset=\"UTF-8\"")
218
213
  rescue Exception => e
219
- set_default_headers(parser,response)
214
+ set_default_headers(parser, response)
220
215
  response.set(500, parser.format_error('internal error'))
221
- ::Blix::Rest.logger << "----------------------------\n#{$!}\n----------------------------"
222
- ::Blix::Rest.logger << "----------------------------\n#{$@}\n----------------------------"
216
+ logger << e.message
217
+ logger << e.backtrace.join("\n")
223
218
  else # no error
224
- set_default_headers(parser,response)
225
- parser.format_response(value, response)
219
+ response_options ||= {}
220
+ response.set_options(response_options)
221
+ if response_options[:raw]
222
+ response.content = value
223
+ else
224
+ set_default_headers(parser, response)
225
+ parser.format_response(value, response)
226
+ end
226
227
  # cache response if requested
227
228
  _cache.clear if clear_cache
228
229
  _cache["#{verb}|#{format}|#{path}"] = response if do_cache
229
230
  end
230
-
231
231
  else
232
- set_default_headers(parser,response)
232
+ set_default_headers(parser, response)
233
233
  response.set(404, parser.format_error('Invalid Url'))
234
234
  end
235
235
 
236
+ logger << "#{verb} #{path} total time=#{((Time.now - t1) * 1000).round(2)}ms"
236
237
  [response.status.to_i, response.headers, response.content]
237
238
  end
238
239
 
239
- def set_default_headers(parser,response)
240
+ def set_default_headers(parser, response)
240
241
  if parser.__custom_headers
241
242
  response.headers.merge! parser.__custom_headers
242
243
  else
@@ -244,5 +245,10 @@ module Blix::Rest
244
245
  end
245
246
  end
246
247
 
248
+ private
249
+
250
+ def logger
251
+ Blix::Rest.logger
252
+ end
247
253
  end
248
254
  end
@@ -26,6 +26,11 @@ module Blix::Rest
26
26
  self.class.get_session_manager
27
27
  end
28
28
 
29
+ def session_skip_update
30
+ @__session_id = nil
31
+ end
32
+
33
+
29
34
  def session_name
30
35
  self.class.get_session_name
31
36
  end
@@ -67,9 +72,9 @@ module Blix::Rest
67
72
  end
68
73
  end
69
74
 
70
- if opts[:csrf]
71
- if env["HTTP_X_CSRF_TOKEN"] != csrf_token
72
- send_error("invalid csrf token")
75
+ if opts[:csrf] && (ENV['RACK_ENV']!='test')
76
+ if env["HTTP_X_CSRF_TOKEN"] != csrf_token
77
+ send_error("error [0100]")
73
78
  end
74
79
  end
75
80
 
@@ -1,5 +1,5 @@
1
1
  module Blix
2
2
  module Rest
3
- VERSION = "0.8.2"
3
+ VERSION = "0.11.1"
4
4
  end
5
5
  end
data/lib/blix/rest.rb CHANGED
@@ -33,6 +33,10 @@ module Blix
33
33
  @_environment ||= ENV['RACK_ENV'] || 'development'
34
34
  end
35
35
 
36
+ def self.init
37
+ RequestMapper.table # compile the table
38
+ end
39
+
36
40
  def self.environment?(val)
37
41
  environment == val.to_s
38
42
  end
@@ -96,9 +100,14 @@ module Blix
96
100
 
97
101
  def initialize(message, status = nil, headers = nil)
98
102
  super(message || "")
99
- @status = status || 406
103
+ @status = (status || 406).to_i
100
104
  @headers = headers
101
105
  end
106
+
107
+ def to_i
108
+ @status
109
+ end
110
+
102
111
  end
103
112
 
104
113
  class RawResponse < StandardError
@@ -122,6 +131,8 @@ module Blix
122
131
  @type = type || 'Basic'
123
132
  end
124
133
  end
134
+
135
+ class RateError < StandardError; end
125
136
  end
126
137
  end
127
138
 
@@ -149,6 +160,7 @@ require 'blix/rest/response'
149
160
  require 'blix/rest/format_parser'
150
161
  require 'blix/rest/request_mapper'
151
162
  require 'blix/rest/cache'
163
+ require 'blix/rest/memory_cache'
152
164
  require 'blix/rest/server'
153
165
  require 'blix/rest/route'
154
166
  require 'blix/rest/controller'
@@ -7,6 +7,45 @@ module Blix
7
7
  Dir.glob("#{path}/*.rb").each {|file| require File.expand_path(file)[0..-4] }
8
8
  end
9
9
 
10
+ def self.klass_to_name(klass)
11
+ return unless klass
12
+ klass = klass.to_s.split('::')[-1]
13
+ klass.gsub(/([a-z]+)([A-Z]+)/){"#{$1}_#{$2}"}.downcase
14
+ end
15
+
16
+ def self.name_to_klass(name)
17
+ construct_klass(name)
18
+ end
19
+
20
+ # construct a klass from a name
21
+ def self.construct_klass(name)
22
+ name && name.to_s.downcase.split('_').map(&:capitalize).join
23
+ end
24
+
25
+ def self.construct_singular(name)
26
+ name && name.to_s.sub(/s$/,'')
27
+ end
28
+
29
+ def self.construct_plural(name)
30
+ name && name.plural
31
+ end
32
+
33
+ def self.camelcase(str)
34
+ downcase_front(construct_klass(str))
35
+ end
36
+
37
+ def self.downcase_front(str)
38
+ str[0, 1].downcase + str[1..-1]
39
+ end
40
+
41
+ def self.underscore(str)
42
+ str.gsub(/::/, '/')
43
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
44
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
45
+ .tr('-', '_')
46
+ .downcase
47
+ end
48
+
10
49
 
11
50
 
12
51
  # filter the hash using the supplied filter