blix-rest 0.1.30 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,7 @@
4
4
  class RestWorld
5
5
  # the entry point to the rack application to be tested
6
6
  def self.app
7
- @_app ||= Rack::Builder.parse_file('config.ru').first
7
+ @_app ||= Rack::Builder.parse_file('config.ru')
8
8
  end
9
9
 
10
10
  # a dummy request to sent to the server
@@ -16,15 +16,16 @@ class RestWorld
16
16
  class Response
17
17
  def initialize(resp)
18
18
  @resp = resp
19
- if @resp.header['Content-Type'] == 'application/json'
19
+ content_type = @resp.headers['Content-Type'] || @resp.headers['content-type']
20
+ if content_type == 'application/json'
20
21
  begin
21
- @h = MultiJson.load(@resp.body) || {}
22
+ @h = MultiJson.load(body) || {}
22
23
  rescue Exception => e
23
- puts 'INVALID RESPONSE BODY=>' + @resp.body
24
+ log 'INVALID RESPONSE BODY=>' + body
24
25
  raise
25
26
  end
26
27
  else
27
- @h = { 'html' => @resp.body }
28
+ @h = { 'html' => body }
28
29
  end
29
30
 
30
31
  # get_ids_from_hash
@@ -35,7 +36,7 @@ class RestWorld
35
36
  end
36
37
 
37
38
  def body
38
- @resp.body
39
+ [@resp.body].flatten.join('')
39
40
  end
40
41
 
41
42
  def data
@@ -51,7 +52,7 @@ class RestWorld
51
52
  end
52
53
 
53
54
  def header
54
- @resp.header || {}
55
+ @resp.headers || {}
55
56
  end
56
57
 
57
58
  def content_type
@@ -93,10 +94,10 @@ class RestWorld
93
94
  end
94
95
 
95
96
  def explain
96
- puts "request ==> #{@_verb} #{@_request}"
97
- puts "cookies ==> #{cookies.join('; ')}" if cookies.length > 0
98
- puts "body ==> #{@_body}" if @_body
99
- puts "response ==> #{@_response.inspect}"
97
+ log "request ==> #{@_verb} #{@_request}"
98
+ log "cookies ==> #{cookies.join('; ')}" if cookies.length > 0
99
+ log "body ==> #{@_body}" if @_body
100
+ log "response ==> #{@_response.inspect}"
100
101
  end
101
102
 
102
103
  def before_parse_path(path); end
@@ -217,7 +218,7 @@ class RestWorld
217
218
  @_response = Response.new(raw_response)
218
219
  # add cookies to the cookie jar.
219
220
  #unless @_current_user=="guest"
220
- if cookie = @_response.header["Set-Cookie"]
221
+ if cookie = @_response.header["Set-Cookie"] || @_response.header["set-cookie"]
221
222
  parts = cookie.split(';')
222
223
  cookies << parts[0].strip
223
224
  end
@@ -72,16 +72,16 @@ module Blix::Rest
72
72
  def format_response(value, response)
73
73
  if value.is_a?(RawJsonString)
74
74
  response.content = if _options[:nodata]
75
- value.to_s
75
+ [value.to_s]
76
76
  else
77
- "{\"data\":#{value}}"
77
+ ["{\"data\":#{value}}"]
78
78
  end
79
79
  else
80
80
  begin
81
81
  response.content = if _options[:nodata]
82
- MultiJson.dump(value)
82
+ [MultiJson.dump(value)]
83
83
  else
84
- MultiJson.dump('data' => value)
84
+ [MultiJson.dump('data' => value)]
85
85
  end
86
86
  rescue Exception => e
87
87
  ::Blix::Rest.logger << e.to_s
@@ -107,7 +107,8 @@ module Blix::Rest
107
107
  end
108
108
 
109
109
  def format_response(value, response)
110
- response.content = value.to_s
110
+ value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
111
+ response.content = value
111
112
  end
112
113
 
113
114
  end
@@ -121,8 +122,8 @@ module Blix::Rest
121
122
 
122
123
  def set_default_headers(headers)
123
124
  headers[CACHE_CONTROL] = CACHE_NO_STORE
124
- headers[PRAGMA] = NO_CACHE
125
- headers[CONTENT_TYPE] = CONTENT_TYPE_XML
125
+ headers[PRAGMA] = NO_CACHE
126
+ headers[CONTENT_TYPE] = CONTENT_TYPE_XML
126
127
  end
127
128
 
128
129
  def format_error(message)
@@ -130,7 +131,7 @@ module Blix::Rest
130
131
  end
131
132
 
132
133
  def format_response(value, response)
133
- response.content = value.to_s # FIXME
134
+ response.content = [value.to_s]
134
135
  end
135
136
 
136
137
  end
@@ -144,8 +145,14 @@ module Blix::Rest
144
145
 
145
146
  def set_default_headers(headers)
146
147
  headers[CACHE_CONTROL] = CACHE_NO_STORE
147
- headers[PRAGMA] = NO_CACHE
148
- headers[CONTENT_TYPE] = CONTENT_TYPE_HTML
148
+ headers[PRAGMA] = NO_CACHE
149
+ headers[CONTENT_TYPE] = CONTENT_TYPE_HTML
150
+ # headers['X-Frame-Options'] = 'SAMEORIGIN'
151
+ # headers['X-XSS-Protection'] = '1; mode=block'
152
+ # headers['X-Content-Type-Options'] = 'nosniff'
153
+ # headers['X-Download-Options'] = 'noopen'
154
+ # headers['X-Permitted-Cross-Domain-Policies'] = 'none'
155
+ # headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
149
156
  end
150
157
 
151
158
  def format_error(message)
@@ -160,7 +167,7 @@ module Blix::Rest
160
167
  end
161
168
 
162
169
  def format_response(value, response)
163
- response.content = value.to_s
170
+ response.content = [value.to_s]
164
171
  end
165
172
 
166
173
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require_relative 'cache'
5
+ require_relative 'string_hash'
6
+ #
7
+ # options:
8
+ # :expire_secs - how long store should save data.
9
+ # :reset_expire_on_get - start the expire timer again on read.
10
+
11
+ module Blix
12
+ module Rest
13
+ class RedisCache < Cache
14
+
15
+ STORE_PREFIX = 'blixcache'
16
+
17
+ #---------------------------------------------------------------------------
18
+
19
+ # clear all data from the cache
20
+ def clear
21
+ reset
22
+ end
23
+
24
+ # retrieve data from the cache
25
+ def get(id)
26
+ key = _key(id)
27
+ str = redis.get(key)
28
+ data = str && begin
29
+ _decode(str)
30
+ rescue StandardError
31
+ redis.del( key)
32
+ nil
33
+ end
34
+ redis.expire(key, _opts[:expire_secs]) if data && _opts[:reset_expire_on_get] && _opts.key?(:expire_secs)
35
+ data
36
+ end
37
+
38
+ # set data in the cache
39
+ def set(id, data)
40
+ params = {}
41
+ params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
42
+ redis.set(_key(id), _encode(data), **params)
43
+ data
44
+ end
45
+
46
+ # is key present in the cache
47
+ def key?(id)
48
+ redis.get(_key(id)) != nil
49
+ end
50
+
51
+ def delete(id)
52
+ redis.del(_key(id)) > 0
53
+ end
54
+
55
+ #---------------------------------------------------------------------------
56
+
57
+ def _key(name)
58
+ _prefix + name
59
+ end
60
+
61
+ def _opts
62
+ @_opts ||= begin
63
+ o = ::Blix::Rest::StringHash.new
64
+ o[:prefix] = STORE_PREFIX
65
+ o.merge!(params)
66
+ o
67
+ end
68
+ end
69
+
70
+ def _prefix
71
+ @_prefix ||= _opts[:prefix]
72
+ end
73
+
74
+ # remove all sessions from the store
75
+ def reset(name = nil)
76
+ keys = _all_keys(name)
77
+ redis.del(*keys) unless keys.empty?
78
+ end
79
+
80
+ def _all_keys(name = nil)
81
+ redis.keys("#{_prefix}#{name}*") || []
82
+ end
83
+
84
+ # the redis session store
85
+ def redis
86
+ @redis ||= begin
87
+ r = Redis.new
88
+ begin
89
+ r.ping
90
+ rescue Exception => e
91
+ Blix::Rest.logger.error "cannot reach redis server:#{e}"
92
+ raise
93
+ end
94
+ r
95
+ end
96
+ end
97
+
98
+ # the number of sessions in the store
99
+ def length
100
+ _all_keys.length
101
+ end
102
+
103
+ # delete expired sessions from the store. this should be handled
104
+ # automatically by redis if the ttl is set on save correctly
105
+ def cleanup(opts = nil); end
106
+
107
+ def _encode(data)
108
+ Marshal.dump(data)
109
+ end
110
+
111
+ def _decode(msg)
112
+ Marshal.load(msg)
113
+ end
114
+
115
+ def get_expiry_time
116
+ if expire = _opts[:expire_secs] || _opts['expire_secs']
117
+ Time.now - expire
118
+ end
119
+ end
120
+
121
+ def get_expiry_secs
122
+ _opts[:expire_secs] || _opts['expire_secs']
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Blix
2
2
  module Rest
3
- VERSION = "0.1.30"
3
+ VERSION = "0.7.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,6 +148,7 @@ 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
153
  # require 'blix/rest/provider'
133
154
  require 'blix/rest/controller'
@@ -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