blix-rest 0.1.30 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'thread'
2
3
 
3
4
  module Blix::Rest
4
5
  class RequestMapperError < StandardError; end
@@ -7,6 +8,8 @@ module Blix::Rest
7
8
  # these routes and return an associated block and parameters.
8
9
  class RequestMapper
9
10
 
11
+ @@mutex = Mutex.new
12
+
10
13
  WILD_PLACEHOLDER = '/'
11
14
  PATH_SEP = '/'
12
15
  STAR_PLACEHOLDER = '*'
@@ -52,6 +55,7 @@ module Blix::Rest
52
55
 
53
56
  class << self
54
57
 
58
+ # the root always starts with '/' and finishes with '/'
55
59
  def set_path_root(root)
56
60
  root = root.to_s
57
61
  root = '/' + root if root[0, 1] != '/'
@@ -64,19 +68,30 @@ module Blix::Rest
64
68
  @path_root || '/'
65
69
  end
66
70
 
67
- attr_reader :path_root_length
71
+ def path_root_length
72
+ @path_root_length.to_i
73
+ end
68
74
 
69
75
  def full_path(path)
70
76
  path = path[1..-1] if path[0, 1] == '/'
71
77
  path_root + path
72
78
  end
73
79
 
80
+ # ensure that absolute path is the full path
81
+ def ensure_full_path(path)
82
+ if path[0, 1] == '/' && (path_root_length>0) && path[0, path_root_length] != path_root[0, path_root_length]
83
+ path = path_root + path[1..-1]
84
+ end
85
+ path
86
+ end
87
+
74
88
  def locations
75
89
  @locations ||= Hash.new { |h, k| h[k] = [] }
76
90
  end
77
91
 
78
92
  def table
79
- @table ||= compile
93
+ # compile the table in one thread only.
94
+ @table ||= @@mutex.synchronize{@table ||= compile}
80
95
  end
81
96
 
82
97
  def dump
@@ -147,6 +162,7 @@ module Blix::Rest
147
162
  path = path[1..-1] if path[0, 1] == PATH_SEP
148
163
  RequestMapper.locations[verb] << [verb, path, opts, blk]
149
164
  @table = nil # force recompile
165
+ true
150
166
  end
151
167
 
152
168
  # match a given path to declared route.
@@ -212,12 +228,12 @@ module Blix::Rest
212
228
  # .. if there is a wildpath foloowing then fine ..
213
229
  # .. otherwise an error !
214
230
 
215
- if idx == limit # the last section of request
231
+ if idx == limit # the last section of path
216
232
  if current.blk
217
233
  return [current.blk, parameters, current.opts]
218
234
  elsif (havewild = current[STAR_PLACEHOLDER])
219
235
  parameters[havewild.parameter.to_s] = '/'
220
- return [havewild.blk, parameters, havewild.opts]
236
+ return [havewild.blk, parameters, havewild.opts, true]
221
237
  else
222
238
  return [nil, {}, nil]
223
239
  end
@@ -250,7 +266,7 @@ module Blix::Rest
250
266
  return [current.blk, parameters, current.opts]
251
267
  elsif (havewild = current[STAR_PLACEHOLDER])
252
268
  parameters[havewild.parameter.to_s] = '/'
253
- return [havewild.blk, parameters, havewild.opts]
269
+ return [havewild.blk, parameters, havewild.opts, true]
254
270
  else
255
271
  return [nil, {}, nil]
256
272
  end
@@ -268,7 +284,7 @@ module Blix::Rest
268
284
  parameters['format'] = wildformat[1..-1].to_sym
269
285
  end
270
286
  parameters[current.parameter.to_s] = wildpath
271
- return [current.blk, parameters, current.opts]
287
+ return [current.blk, parameters, current.opts, true]
272
288
  else
273
289
  return [nil, {}, nil]
274
290
  end
@@ -12,13 +12,13 @@ module Blix::Rest
12
12
 
13
13
  def initialize
14
14
  @status = 200
15
- @headers = {}
15
+ @headers = Rack::Headers.new
16
16
  @content = nil
17
17
  end
18
18
 
19
19
  def set(status,content=nil,headers=nil)
20
20
  @status = status if status
21
- @content = String.new(content) if content
21
+ @content = [String.new(content)] if content
22
22
  @headers.merge!(headers) if headers
23
23
  end
24
24
 
@@ -0,0 +1,33 @@
1
+ module Blix::Rest
2
+
3
+ # class used to represent a route and to allow manipulation of the
4
+ # options and path of the route before registrATION
5
+
6
+ class Route
7
+
8
+ attr_reader :verb, :path, :opts
9
+ alias_method :method, :verb
10
+ alias_method :options, :opts
11
+
12
+ def initialize(verb, path, opts)
13
+ @verb = verb.dup
14
+ @path = path
15
+ @opts = opts
16
+ end
17
+
18
+ def default_option(key, val)
19
+ opts[key.to_sym] = val unless opts.key?(key) || opts.key?(key.to_sym)
20
+ end
21
+
22
+ def path_prefix(prefix)
23
+ prefix = prefix.to_s
24
+ prefix = '/' + prefix unless prefix[0] == '/'
25
+ path.replace( prefix + path) unless path.start_with?(prefix)
26
+ end
27
+
28
+ def path=(val)
29
+ path.replace(val.to_s)
30
+ end
31
+
32
+ end
33
+ end
@@ -17,8 +17,23 @@ module Blix::Rest
17
17
  @_options = opts
18
18
  end
19
19
 
20
+ # options passed to the server
21
+ def _options
22
+ @_options
23
+ end
24
+
25
+
26
+ # the object serving as a cache
20
27
  def _cache
21
- @_cache ||= {}
28
+ @_cache ||= begin
29
+ obj = _options[:cache] || _options['cache']
30
+ if obj
31
+ raise "cache must be a subclass of Blix::Rest::Cache" unless obj.is_a?(Cache)
32
+ obj
33
+ else
34
+ MemoryCache.new
35
+ end
36
+ end
22
37
  end
23
38
 
24
39
  def extract_parsers_from_options(opts)
@@ -34,7 +49,6 @@ module Blix::Rest
34
49
  def set_custom_headers(format, headers)
35
50
  parser = get_parser(format)
36
51
  raise "parser not found for custom headers format=>#{format}" unless parser
37
-
38
52
  parser.__custom_headers = headers
39
53
  end
40
54
 
@@ -54,6 +68,7 @@ module Blix::Rest
54
68
  parser._types.each { |t| @_mime_types[t.downcase] = parser } # register each of the mime types
55
69
  end
56
70
 
71
+ # retrieve parameters from the http request
57
72
  def retrieve_params(env)
58
73
  post_params = {}
59
74
  body = ''
@@ -104,6 +119,7 @@ module Blix::Rest
104
119
  end
105
120
  end
106
121
 
122
+ # determine standard format from http mime type
107
123
  def get_format_from_mime(mime)
108
124
  case mime
109
125
  when 'application/json' then :json
@@ -138,20 +154,24 @@ module Blix::Rest
138
154
  parser
139
155
  end
140
156
 
157
+ # the call function is the interface with the rack framework
141
158
  def call(env)
142
159
  req = Rack::Request.new(env)
143
160
 
144
- verb = env['REQUEST_METHOD']
145
- path = req.path
161
+ verb = env['REQUEST_METHOD']
162
+ path = req.path
163
+ path = CGI.unescape(path).gsub('+',' ') unless _options[:unescape] == false
146
164
 
147
- blk, path_params, options = RequestMapper.match(verb, path)
165
+ blk, path_params, options, is_wild = RequestMapper.match(verb, path)
166
+
167
+ match_all = RequestMapper.match('ALL', path) unless blk && !is_wild
168
+ blk, path_params, options = match_all if match_all && match_all[0] # override
148
169
 
149
- blk, path_params, options = RequestMapper.match('ALL', path) unless blk
150
170
 
151
171
  default_format = options && options[:default] && options[:default].to_sym
152
172
  force_format = options && options[:force] && options[:force].to_sym
153
- do_cache = options && options[:cache]
154
- clear_cache = options && options[:cache_reset]
173
+ do_cache = options && options[:cache] && !Blix::Rest.cache_disabled
174
+ clear_cache = options && options[:cache_reset]
155
175
 
156
176
  query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
157
177
 
@@ -159,39 +179,49 @@ module Blix::Rest
159
179
 
160
180
  parser = get_parser(force_format || format)
161
181
 
162
- return [406, {}, ["Invalid Format: #{format}"]] unless parser
182
+ unless parser
183
+ if blk
184
+ return [406, {}, ["Invalid Format: #{format}"]]
185
+ else
186
+ return [404, {}, ["Invalid Url"]]
187
+ end
188
+ end
163
189
 
164
190
  parser._options = options
165
191
 
166
192
  # check for cached response end return with cached response if found.
167
193
  #
168
- if do_cache && _cache["#{verb}|#{format}|#{path}"]
169
- response = _cache["#{verb}|#{format}|#{path}"]
170
- return [response.status, response.headers.merge('X-Blix-Cache' => 'cached'), [response.content]]
194
+ if do_cache && ( response = _cache["#{verb}|#{format}|#{path}"] )
195
+ return [response.status, response.headers.merge('x-blix-cache' => 'cached'), response.content]
171
196
  end
172
197
 
173
198
  response = Response.new
174
199
 
175
- if parser.__custom_headers
176
- response.headers.merge! parser.__custom_headers
177
- else
178
- parser.set_default_headers(response.headers)
179
- end
180
-
181
200
  if blk
182
201
 
183
202
  begin
184
203
  params = env['params']
185
- value = blk.call(path_params, params, req, format, response, @_options)
204
+ context = Context.new(path_params, params, req, format, response, verb, self)
205
+ value = blk.call(context)
186
206
  rescue ServiceError => e
207
+ set_default_headers(parser,response)
187
208
  response.set(e.status, parser.format_error(e.message), e.headers)
209
+ rescue RawResponse => e
210
+ 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
213
+ response.content = value
214
+ response.headers.merge!(e.headers) if e.headers
188
215
  rescue AuthorizationError => e
216
+ set_default_headers(parser,response)
189
217
  response.set(401, parser.format_error(e.message), AUTH_HEADER => "#{e.type} realm=\"#{e.realm}\", charset=\"UTF-8\"")
190
218
  rescue Exception => e
219
+ set_default_headers(parser,response)
191
220
  response.set(500, parser.format_error('internal error'))
192
221
  ::Blix::Rest.logger << "----------------------------\n#{$!}\n----------------------------"
193
222
  ::Blix::Rest.logger << "----------------------------\n#{$@}\n----------------------------"
194
223
  else # no error
224
+ set_default_headers(parser,response)
195
225
  parser.format_response(value, response)
196
226
  # cache response if requested
197
227
  _cache.clear if clear_cache
@@ -199,9 +229,19 @@ module Blix::Rest
199
229
  end
200
230
 
201
231
  else
232
+ set_default_headers(parser,response)
202
233
  response.set(404, parser.format_error('Invalid Url'))
203
234
  end
204
- [response.status, response.headers, [response.content]]
235
+
236
+ [response.status.to_i, response.headers, response.content]
237
+ end
238
+
239
+ def set_default_headers(parser,response)
240
+ if parser.__custom_headers
241
+ response.headers.merge! parser.__custom_headers
242
+ else
243
+ parser.set_default_headers(response.headers)
244
+ end
205
245
  end
206
246
 
207
247
  end
@@ -0,0 +1,138 @@
1
+ module Blix::Rest
2
+
3
+ module Session
4
+ #----------------------------------------------------------------------------
5
+ #
6
+ # manage the session and authorization
7
+ #
8
+ #----------------------------------------------------------------------------
9
+
10
+ DAY = 24 * 60 * 60
11
+ MIN = 60
12
+ SESSION_NAME = 'blix'
13
+
14
+ SESSION_OPTS = {
15
+ #:secure=>true,
16
+ :http => false,
17
+ :samesite => :lax,
18
+ :path => Blix::Rest.full_path('/'),
19
+ :expire_secs => 30 * MIN, # 30 mins
20
+ :cleanup_every_secs => 5 * 60 # 5 minutes
21
+ #:max_age => nil # session cookie
22
+ }.freeze
23
+
24
+
25
+ def session_manager
26
+ self.class.get_session_manager
27
+ end
28
+
29
+ def session_name
30
+ self.class.get_session_name
31
+ end
32
+
33
+ def session_opts
34
+ self.class.get_session_opts
35
+ end
36
+
37
+ def session
38
+ @__session
39
+ end
40
+
41
+ def csrf_token
42
+ @__session['csrf'] ||= SecureRandom.hex(32)
43
+ end
44
+
45
+ def reset_session
46
+ raise 'login_session missing' unless @__session && @__session_id
47
+ session_manager.delete_session(@__session_id)
48
+ @__session_id = refresh_session_id(session_name, session_opts)
49
+ @__session['csrf'] = SecureRandom.hex(32)
50
+ session_manager.store_session(@__session_id, @__session)
51
+ end
52
+
53
+ # get a session id and use this to retrieve the session information - if any.
54
+ #
55
+ def session_before(opts)
56
+ @__session = {}
57
+
58
+ # do not set session on pages. that will be cached.
59
+ unless opts[:nosession] || opts[:cache]
60
+ @__session_id = get_session_id(session_name, session_opts)
61
+ @__session =
62
+ begin
63
+ session_manager.get_session(@__session_id)
64
+ rescue SessionExpiredError
65
+ @__session_id = refresh_session_id(session_name, session_opts)
66
+ session_manager.get_session(@__session_id)
67
+ end
68
+ end
69
+
70
+ if opts[:csrf]
71
+ if env["HTTP_X_CSRF_TOKEN"] != csrf_token
72
+ send_error("invalid csrf token")
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ # save the session hash before we go.
79
+ def session_after
80
+ session_manager.store_session(@__session_id, @__session) if @__session_id
81
+ end
82
+
83
+ private
84
+
85
+ def self.included(mod)
86
+ mod.extend Session::ClassMethods
87
+ end
88
+
89
+ module ClassMethods
90
+
91
+ def session_name(name)
92
+ @_sess_name = name.to_s
93
+ end
94
+
95
+ def session_opts(opts)
96
+ @_sess_custom_opts = opts
97
+ end
98
+
99
+ def session_manager(man)
100
+ raise ArgumentError, "incompatible session store" unless man.respond_to?(:get_session)
101
+ raise ArgumentError, "incompatible session store" unless man.respond_to?(:store_session)
102
+ raise ArgumentError, "incompatible session store" unless man.respond_to?(:delete_session)
103
+ @_sess_manager= man
104
+ end
105
+
106
+ def get_session_name
107
+ @_sess_name ||= begin
108
+ if superclass.respond_to?(:get_session_name)
109
+ superclass.get_session_name
110
+ else
111
+ SESSION_NAME
112
+ end
113
+ end
114
+ end
115
+
116
+ def get_session_opts
117
+ @_session_opts ||= begin
118
+ if superclass.respond_to?(:get_session_opts)
119
+ superclass.get_session_opts.merge(@_sess_custom_opts || {})
120
+ else
121
+ SESSION_OPTS.merge(@_sess_custom_opts || {})
122
+ end
123
+ end
124
+ end
125
+
126
+ def get_session_manager
127
+ @_sess_manager ||= begin
128
+ if superclass.respond_to?(:get_session_manager)
129
+ superclass.get_session_manager
130
+ else
131
+ Blix::RedisStore.new(get_session_opts)
132
+ end
133
+ end
134
+ end
135
+
136
+ end # ClassMethods
137
+ end # Session
138
+ end # Blix::Rest
@@ -1,5 +1,5 @@
1
1
  module Blix
2
2
  module Rest
3
- VERSION = "0.1.30"
3
+ VERSION = "0.8.2"
4
4
  end
5
5
  end
data/lib/blix/rest.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'base64'
2
2
  require 'logger'
3
3
  require 'time'
4
+ require 'cgi'
5
+ require 'rack'
4
6
 
5
7
  module Blix
6
8
  module Rest
@@ -8,14 +10,14 @@ module Blix
8
10
  # EXPIRED_TOKEN_MESSAGE = 'token expired'
9
11
  # INVALID_TOKEN_MESSAGE = 'invalid token'
10
12
 
11
- CONTENT_TYPE = 'Content-Type'.freeze
13
+ CONTENT_TYPE = 'content-type'.freeze
12
14
  CONTENT_TYPE_JSON = 'application/json'.freeze
13
15
  CONTENT_TYPE_HTML = 'text/html; charset=utf-8'.freeze
14
16
  CONTENT_TYPE_XML = 'application/xml'.freeze
15
- AUTH_HEADER = 'WWW-Authenticate'.freeze
16
- CACHE_CONTROL = 'Cache-Control'.freeze
17
+ AUTH_HEADER = 'www-authenticate'.freeze
18
+ CACHE_CONTROL = 'cache-control'.freeze
17
19
  CACHE_NO_STORE = 'no-store'.freeze
18
- PRAGMA = 'Pragma'.freeze
20
+ PRAGMA = 'pragma'.freeze
19
21
  NO_CACHE = 'no-cache'.freeze
20
22
  URL_ENCODED = %r{^application/x-www-form-urlencoded}.freeze
21
23
  JSON_ENCODED = %r{^application/json}.freeze # NOTE: "text/json" and "text/javascript" are deprecated forms
@@ -43,6 +45,14 @@ module Blix
43
45
  @_logger = val
44
46
  end
45
47
 
48
+ def self.cache_disabled
49
+ @_cache_disabled
50
+ end
51
+
52
+ def self.disable_cache
53
+ @_cache_disabled = true
54
+ end
55
+
46
56
  def self.logger
47
57
  @_logger ||= begin
48
58
  l = Logger.new(STDOUT)
@@ -67,13 +77,10 @@ module Blix
67
77
  end
68
78
 
69
79
  # interpret payload string as json
70
- class RawJsonString
71
- def initialize(str)
72
- @str = str
73
- end
80
+ class RawJsonString < String
74
81
 
75
82
  def as_json(*_a)
76
- @str
83
+ self
77
84
  end
78
85
 
79
86
  def to_json(*a)
@@ -94,6 +101,19 @@ module Blix
94
101
  end
95
102
  end
96
103
 
104
+ class RawResponse < StandardError
105
+ attr_reader :status
106
+ attr_reader :headers
107
+ attr_reader :content
108
+
109
+ def initialize(message, status = nil, headers = nil)
110
+ super(message || "")
111
+ @status = status
112
+ @headers = headers
113
+ @content = message
114
+ end
115
+ end
116
+
97
117
  class AuthorizationError < StandardError
98
118
  attr_reader :realm, :type
99
119
  def initialize(message=nil, realm=nil, type=nil)
@@ -128,8 +148,9 @@ require 'rack'
128
148
  require 'blix/rest/response'
129
149
  require 'blix/rest/format_parser'
130
150
  require 'blix/rest/request_mapper'
151
+ require 'blix/rest/cache'
131
152
  require 'blix/rest/server'
132
- # require 'blix/rest/provider'
153
+ require 'blix/rest/route'
133
154
  require 'blix/rest/controller'
134
155
  # require 'blix/rest/provider_controller'
135
156
 
@@ -36,7 +36,7 @@ module Blix
36
36
  def store_data(id, data)
37
37
  params = {}
38
38
  params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
39
- redis.set(_key(id), data, params)
39
+ redis.set(_key(id), data, **params)
40
40
  data
41
41
  end
42
42
 
@@ -89,7 +89,7 @@ module Blix
89
89
  params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
90
90
  hash ||= {}
91
91
  hash['_last_access'] = Time.now
92
- redis.set(_key(id), _encode(hash), params)
92
+ redis.set(_key(id), _encode(hash), **params)
93
93
  hash
94
94
  end
95
95
 
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.1.30
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clive Andrews
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-30 00:00:00.000000000 Z
11
+ date: 2023-09-24 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: '0'
47
+ version: 3.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: '0'
54
+ version: 3.0.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: |-
70
84
  Rack based framework focused on building JSON REST web services.
71
85
  Concentrates on making the basics very easy to write and fast to execute.
@@ -81,6 +95,7 @@ files:
81
95
  - LICENSE
82
96
  - README.md
83
97
  - lib/blix/rest.rb
98
+ - lib/blix/rest/cache.rb
84
99
  - lib/blix/rest/controller.rb
85
100
  - lib/blix/rest/cucumber.rb
86
101
  - lib/blix/rest/cucumber/hooks.rb
@@ -90,10 +105,13 @@ files:
90
105
  - lib/blix/rest/format.rb
91
106
  - lib/blix/rest/format_parser.rb
92
107
  - lib/blix/rest/handlebars_assets_fix.rb
108
+ - lib/blix/rest/redis_cache.rb
93
109
  - lib/blix/rest/request_mapper.rb
94
110
  - lib/blix/rest/resource_cache.rb
95
111
  - lib/blix/rest/response.rb
112
+ - lib/blix/rest/route.rb
96
113
  - lib/blix/rest/server.rb
114
+ - lib/blix/rest/session.rb
97
115
  - lib/blix/rest/string_hash.rb
98
116
  - lib/blix/rest/version.rb
99
117
  - lib/blix/utils.rb
@@ -119,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
137
  - !ruby/object:Gem::Version
120
138
  version: '0'
121
139
  requirements: []
122
- rubygems_version: 3.1.4
140
+ rubygems_version: 3.0.9
123
141
  signing_key:
124
142
  specification_version: 4
125
143
  summary: Framework for HTTP REST services