blix-rest 0.9.3 → 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
@@ -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
@@ -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,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) 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.9.3"
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
@@ -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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blix-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clive Andrews
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-17 00:00:00.000000000 Z
11
+ date: 2025-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 3.0.0
47
+ version: 2.0.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 3.0.0
54
+ version: 2.0.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -97,14 +97,10 @@ files:
97
97
  - lib/blix/rest.rb
98
98
  - lib/blix/rest/cache.rb
99
99
  - lib/blix/rest/controller.rb
100
- - lib/blix/rest/cucumber.rb
101
- - lib/blix/rest/cucumber/hooks.rb
102
- - lib/blix/rest/cucumber/request_steps.rb
103
- - lib/blix/rest/cucumber/resource_steps.rb
104
- - lib/blix/rest/cucumber/world.rb
105
100
  - lib/blix/rest/format.rb
106
101
  - lib/blix/rest/format_parser.rb
107
102
  - lib/blix/rest/handlebars_assets_fix.rb
103
+ - lib/blix/rest/memory_cache.rb
108
104
  - lib/blix/rest/redis_cache.rb
109
105
  - lib/blix/rest/request_mapper.rb
110
106
  - lib/blix/rest/resource_cache.rb