blix-rest 0.9.3 → 0.11.2

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
@@ -29,7 +29,8 @@ module Blix::Rest
29
29
  :format,
30
30
  :response,
31
31
  :method,
32
- :server
32
+ :server,
33
+ :response_options # can be set during a call.
33
34
  )
34
35
 
35
36
  class Controller
@@ -76,11 +77,16 @@ module Blix::Rest
76
77
  end
77
78
 
78
79
  def body
79
- @_body ||= env['rack.input'].read
80
- # env['rack.input'].rewindreq.POST #env["body"]
80
+ @_body ||= begin
81
+ if env['rack.input']
82
+ env['rack.input'].read
83
+ else
84
+ ''
85
+ end
86
+ end
81
87
  end
82
88
 
83
- # ovverride the path method to return the internal path.
89
+ # override the path method to return the internal path.
84
90
  def path
85
91
  p = CGI.unescape(req.path)
86
92
  p = '/' + p if p[0, 1] != '/' # ensure a leading slash on path
@@ -241,6 +247,35 @@ module Blix::Rest
241
247
  [login, password]
242
248
  end
243
249
 
250
+ def throttle_basic_auth(realm=nil, options={})
251
+ login, password = get_basic_auth(realm)
252
+ # check that the waiting time has expired
253
+ info = self.class.basic_store.get_hash(login) || {}
254
+ fail_count = info['fail_count'].to_i
255
+ if fail_count > 3
256
+ now = Time.now
257
+ fail_time = info['fail_time']
258
+ delta = 60 # one minute
259
+ delta = 60*10 if fail_count > 10 # 10 minutes
260
+ delta = 60*60*24 if fail_count > 100 # one day
261
+ if (fail_time + delta) > now
262
+ raise Blix::Rest::AuthorizationError.new("try again after #{(fail_time + delta)}", realm)
263
+ end
264
+ end
265
+ if yield(login,password)
266
+ # auth success - set error count to 0
267
+ info['fail_count'] = 0
268
+ self.class.basic_store.store_hash(login, info)
269
+ true
270
+ else
271
+ # auth failure - increment error count / set error time
272
+ info['fail_count'] = fail_count + 1
273
+ info['fail_time'] = Time.now
274
+ self.class.basic_store.store_hash(login, info)
275
+ raise Blix::Rest::AuthorizationError.new("invalid login or password", realm)
276
+ end
277
+ end
278
+
244
279
  # set the cors headers
245
280
  def set_accept_cors(opts={})
246
281
  origin = opts[:origin] || env['HTTP_ORIGIN'] || '*'
@@ -269,6 +304,11 @@ module Blix::Rest
269
304
  @_response.status = value.to_i
270
305
  end
271
306
 
307
+ def set_options(value)
308
+ @_context.response_options ||= {}
309
+ @_context.response_options.merge!(value)
310
+ end
311
+
272
312
  def add_headers(headers)
273
313
  @_response.headers.merge!(headers.map{|k,v| [k.to_s.downcase,v]}.to_h)
274
314
  end
@@ -371,6 +411,57 @@ module Blix::Rest
371
411
  value
372
412
  end
373
413
 
414
+ # only allow so many exceptions in a given time.
415
+ # the delay applies to
416
+ # - 3x failure
417
+ # - 10x failure
418
+ # - 100x failure
419
+
420
+ # options:
421
+ # :prefix # the prefix to use in the cache
422
+ # :cache # a cache object ( server_cache )
423
+ # :times # array of delays in seconds to apply default: [60, 600, 86400]
424
+ #
425
+ def rate_limit(name, options = {})
426
+ # check that the waiting time has expired
427
+ cache = options[:cache] || server_cache()
428
+ prefix = options[:prefix] || 'rlimit'
429
+ key = "#{prefix}|#{name}"
430
+ info = cache.get(key) || {}
431
+ fail_count = info['fc'].to_i
432
+ times = options[:times] || []
433
+ if fail_count > 2
434
+ now = Time.now
435
+ fail_time = info['ft']
436
+ delta = times[0] || 60 # one minute
437
+ delta = times[1] || 60 * 10 if fail_count > 10 # 10 minutes
438
+ delta = times[2] || 60 * 60 * 24 if fail_count > 100 # one day
439
+ ltime = fail_time + delta
440
+ if ltime > now
441
+ raise RateError, ltime.to_s
442
+ end
443
+ end
444
+ exception = nil
445
+ result = begin
446
+ yield(key)
447
+ rescue Exception => e
448
+ exception = e
449
+ nil
450
+ end
451
+ if exception
452
+ # auth failure - increment error count / set error time
453
+ info['fc'] = fail_count + 1
454
+ info['ft'] = Time.now
455
+ cache.set(key, info)
456
+ raise exception
457
+ else
458
+ # auth success - set error count to 0
459
+ info['fc'] = 0
460
+ cache.set(key, info)
461
+ result
462
+ end
463
+ end
464
+
374
465
  # manage session handling --------------------------------------------------
375
466
  # setup the session and retrieve the session_id
376
467
  # this id can be used to retrieve and data associated
@@ -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
@@ -325,21 +329,25 @@ module Blix::Rest
325
329
  end
326
330
  str
327
331
  end
328
-
329
332
  end
330
333
 
331
334
  end # RequestMapper
332
335
 
333
- def self.set_path_root(*args)
334
- RequestMapper.set_path_root(*args)
335
- end
336
+ class << self
336
337
 
337
- def self.path_root
338
- RequestMapper.path_root
339
- end
340
338
 
341
- def self.full_path(path)
342
- 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
+
343
351
  end
344
352
 
345
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,28 +152,25 @@ 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) 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
- 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]
175
-
176
- query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
177
-
178
- format = query_format || path_params[:format] || get_format_new(env, options) || default_format || :json
179
-
180
- parser = get_parser(force_format || format)
168
+ force_format = options && options[:force] && options[:force].to_sym
169
+ do_cache = options && options[:cache] && !Blix::Rest.cache_disabled
170
+ clear_cache = options && options[:cache_reset]
171
+ query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
172
+ format = query_format || path_params[:format] || get_format_new(env, options) || default_format || :json
173
+ parser = get_parser(force_format || format)
181
174
 
182
175
  unless parser
183
176
  if blk
@@ -189,54 +182,59 @@ module Blix::Rest
189
182
 
190
183
  parser._options = options
191
184
 
192
- # check for cached response end return with cached response if found.
193
- #
194
- if do_cache && ( response = _cache["#{verb}|#{format}|#{path}"] )
185
+ # check for cached response and return with cached response if found.
186
+ if do_cache && (response = _cache["#{verb}|#{format}|#{path}"])
195
187
  return [response.status, response.headers.merge('x-blix-cache' => 'cached'), response.content]
196
188
  end
197
189
 
198
190
  response = Response.new
199
191
 
200
192
  if blk
201
-
202
193
  begin
203
194
  params = env['params']
204
195
  context = Context.new(path_params, params, req, format, response, verb, self)
205
- value = blk.call(context)
196
+ value = blk.call(context)
197
+ response_options = context.response_options || {}
206
198
  rescue ServiceError => e
207
- set_default_headers(parser,response)
199
+ set_default_headers(parser, response)
208
200
  response.set(e.status, parser.format_error(e.message), e.headers)
201
+ logger << e.message if $VERBOSE
209
202
  rescue RawResponse => e
210
203
  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
204
+ value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
205
+ response.status = e.status if e.status
213
206
  response.content = value
214
207
  response.headers.merge!(e.headers) if e.headers
215
208
  rescue AuthorizationError => e
216
- set_default_headers(parser,response)
209
+ set_default_headers(parser, response)
217
210
  response.set(401, parser.format_error(e.message), AUTH_HEADER => "#{e.type} realm=\"#{e.realm}\", charset=\"UTF-8\"")
218
211
  rescue Exception => e
219
- set_default_headers(parser,response)
212
+ set_default_headers(parser, response)
220
213
  response.set(500, parser.format_error('internal error'))
221
- ::Blix::Rest.logger << "----------------------------\n#{$!}\n----------------------------"
222
- ::Blix::Rest.logger << "----------------------------\n#{$@}\n----------------------------"
214
+ logger << e.message
215
+ logger << e.backtrace.join("\n")
223
216
  else # no error
224
- set_default_headers(parser,response)
225
- parser.format_response(value, response)
217
+ response.set_options(response_options)
218
+ if response_options[:raw]
219
+ response.content = value
220
+ else
221
+ set_default_headers(parser, response)
222
+ parser.format_response(value, response)
223
+ end
226
224
  # cache response if requested
227
225
  _cache.clear if clear_cache
228
226
  _cache["#{verb}|#{format}|#{path}"] = response if do_cache
229
227
  end
230
-
231
228
  else
232
- set_default_headers(parser,response)
229
+ set_default_headers(parser, response)
233
230
  response.set(404, parser.format_error('Invalid Url'))
234
231
  end
235
232
 
233
+ logger << "#{verb} #{path} total time=#{((Time.now - t1) * 1000).round(2)}ms" if $VERBOSE
236
234
  [response.status.to_i, response.headers, response.content]
237
235
  end
238
236
 
239
- def set_default_headers(parser,response)
237
+ def set_default_headers(parser, response)
240
238
  if parser.__custom_headers
241
239
  response.headers.merge! parser.__custom_headers
242
240
  else
@@ -244,5 +242,10 @@ module Blix::Rest
244
242
  end
245
243
  end
246
244
 
245
+ private
246
+
247
+ def logger
248
+ Blix::Rest.logger
249
+ end
247
250
  end
248
251
  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.9.3"
3
+ VERSION = "0.11.2"
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
@@ -127,6 +131,8 @@ module Blix
127
131
  @type = type || 'Basic'
128
132
  end
129
133
  end
134
+
135
+ class RateError < StandardError; end
130
136
  end
131
137
  end
132
138
 
@@ -154,6 +160,7 @@ require 'blix/rest/response'
154
160
  require 'blix/rest/format_parser'
155
161
  require 'blix/rest/request_mapper'
156
162
  require 'blix/rest/cache'
163
+ require 'blix/rest/memory_cache'
157
164
  require 'blix/rest/server'
158
165
  require 'blix/rest/route'
159
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