blix-rest 0.1.30 → 0.8.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.
@@ -3,6 +3,7 @@
3
3
  require 'base64'
4
4
  require 'erb'
5
5
  require 'securerandom'
6
+ require 'digest'
6
7
 
7
8
  module Blix::Rest
8
9
  # base class for controllers. within your block handling a particular route you
@@ -21,8 +22,19 @@ module Blix::Rest
21
22
  # to accept requests other thatn json then set :accept=>[:json,:html] as options in the route
22
23
  # eg post '/myform' :accept=>[:html] # this will only accept html requests.
23
24
 
25
+ Context = Struct.new(
26
+ :path_params,
27
+ :params,
28
+ :req,
29
+ :format,
30
+ :response,
31
+ :method,
32
+ :server
33
+ )
34
+
24
35
  class Controller
25
36
 
37
+
26
38
  #--------------------------------------------------------------------------------------------------------
27
39
  # convenience methods
28
40
  #--------------------------------------------------------------------------------------------------------
@@ -35,6 +47,14 @@ module Blix::Rest
35
47
  @_server_options
36
48
  end
37
49
 
50
+ def server_cache
51
+ @_server_cache
52
+ end
53
+
54
+ def server_cache_get(key)
55
+ server_cache[key] ||= yield if block_given?
56
+ end
57
+
38
58
  def logger
39
59
  Blix::Rest.logger
40
60
  end
@@ -60,10 +80,21 @@ module Blix::Rest
60
80
  # env['rack.input'].rewindreq.POST #env["body"]
61
81
  end
62
82
 
83
+ # ovverride the path method to return the internal path.
63
84
  def path
64
- req.path
85
+ p = req.path
86
+ p = '/' + p if p[0, 1] != '/' # ensure a leading slash on path
87
+ idx = RequestMapper.path_root_length
88
+ if idx > 0
89
+ p = p[idx..-1] || '/'
90
+ p = '/' + p if p[0, 1] != '/' # ensure a leading slash on path
91
+ p
92
+ else
93
+ p
94
+ end
65
95
  end
66
96
 
97
+
67
98
  def form_hash
68
99
  StringHash.new(req.POST)
69
100
  end
@@ -113,7 +144,7 @@ module Blix::Rest
113
144
  end
114
145
 
115
146
  def path_for(path)
116
- File.join(RequestMapper.path_root, path)
147
+ File.join(RequestMapper.path_root, path || '')
117
148
  end
118
149
 
119
150
  def url_for(path)
@@ -128,6 +159,26 @@ module Blix::Rest
128
159
  @_verb
129
160
  end
130
161
 
162
+ def method
163
+ @_method
164
+ end
165
+
166
+ def route_parameters
167
+ @_parameters
168
+ end
169
+
170
+ def route_params
171
+ @_parameters
172
+ end
173
+
174
+ def route_options
175
+ @_parameters
176
+ end
177
+
178
+ def response
179
+ @_response
180
+ end
181
+
131
182
  def method
132
183
  env['REQUEST_METHOD'].downcase
133
184
  end
@@ -147,7 +198,7 @@ module Blix::Rest
147
198
  end
148
199
 
149
200
  def redirect(path, status = 302)
150
- raise ServiceError.new(nil, status, 'Location' => path)
201
+ raise ServiceError.new(nil, status, 'location' => RequestMapper.ensure_full_path(path))
151
202
  end
152
203
 
153
204
  alias redirect_to redirect
@@ -190,12 +241,36 @@ module Blix::Rest
190
241
  [login, password]
191
242
  end
192
243
 
244
+ # set the cors headers
245
+ def set_accept_cors(opts={})
246
+ origin = opts[:origin] || env['HTTP_ORIGIN'] || '*'
247
+ origin = origin.to_s
248
+ if method=='options'
249
+ methods = [opts[:methods] || []].to_a.flatten
250
+ max_age = opts[:max_age] || 86400
251
+ headers = [opts[:headers] || []].to_a.flatten
252
+ credentials = opts.key?(:credentials) ? !!opts[:credentials] : true
253
+ methods = [:get] if methods.empty?
254
+ methods = methods.map{|m| m.to_s.upcase}
255
+ headers = ['Content-Type'] if headers.empty?
256
+ max_age = max_age.to_i
257
+
258
+ add_headers 'Access-Control-Allow-Origin' => origin,
259
+ 'Access-Control-Allow-Methods'=>methods.join(', '),
260
+ 'Access-Control-Allow-Headers'=>headers,
261
+ 'Access-Control-Max-Age'=>max_age, #86400,
262
+ 'Access-Control-Allow-Credentials'=>'true'
263
+ else
264
+ add_headers 'Access-Control-Allow-Origin' => origin
265
+ end
266
+ end
267
+
193
268
  def set_status(value)
194
- @_response.status = value
269
+ @_response.status = value.to_i
195
270
  end
196
271
 
197
272
  def add_headers(headers)
198
- @_response.headers.merge!(headers)
273
+ @_response.headers.merge!(headers.map{|k,v| [k.to_s.downcase,v]}.to_h)
199
274
  end
200
275
 
201
276
  # the following is copied from Rack::Utils
@@ -231,6 +306,19 @@ module Blix::Rest
231
306
  raise ServiceError.new(message, status, headers)
232
307
  end
233
308
 
309
+ # send data to browser as attachment
310
+ def send_data(data, opts = {})
311
+ add_headers 'content-type'=> opts[:type] || 'application/octet-stream'
312
+ if opts[:filename]
313
+ add_headers 'content-disposition'=>'attachment;filename='+ opts[:filename]
314
+ elsif opts[:disposition] == 'attachment'
315
+ add_headers 'content-disposition'=>'attachment'
316
+ elsif opts[:disposition] == 'inline'
317
+ add_headers 'content-disposition'=>'inline'
318
+ end
319
+ raise RawResponse.new(data, opts[:status] || 200)
320
+ end
321
+
234
322
  def auth_error(*params)
235
323
  if params[0].kind_of?(String)
236
324
  message = params[0]
@@ -279,7 +367,7 @@ module Blix::Rest
279
367
  # else
280
368
  # cookie_header = cookie_text
281
369
  # end
282
- @_response.headers['Set-Cookie'] = @_cookies.values.join("\n")
370
+ @_response.headers['set-cookie'] = @_cookies.values.join("\n")
283
371
  value
284
372
  end
285
373
 
@@ -315,6 +403,16 @@ module Blix::Rest
315
403
  store_cookie(session_name, session_id, opts)
316
404
  end
317
405
 
406
+ # perform the before hooks.
407
+ def __before(*a)
408
+ self.class._do_before(self, *a)
409
+ end
410
+
411
+ # perform the after hooks
412
+ def __after(*a)
413
+ self.class._do_after(self, *a)
414
+ end
415
+
318
416
  #----------------------------------------------------------------------------------------------------------
319
417
  # template methods that can be overwritten
320
418
 
@@ -327,17 +425,32 @@ module Blix::Rest
327
425
  response
328
426
  end
329
427
 
428
+ def session_before(opts); end # empty session before hooh
429
+ def session_after; end # empty session after hook
430
+
330
431
  #----------------------------------------------------------------------------------------------------------
331
432
 
332
- def initialize(path_params, _params, req, format, verb, response, server_options)
333
- @_req = req
334
- @_env = req.env
433
+ def _setup(context, _verb, _path, _parameters)
434
+ @_context = context
435
+ @_req = context.req
436
+ @_env = req.env
335
437
  @_query_params = StringHash.new(req.GET)
336
- @_path_params = StringHash.new(path_params)
337
- @_format = format
338
- @_verb = verb
339
- @_response = response
340
- @_server_options = server_options
438
+ @_path_params = StringHash.new(context.path_params)
439
+ @_format = context.format
440
+ @_verb = _verb
441
+ @_response = context.response
442
+ @_server_options = context.server._options
443
+ @_parameters = _parameters
444
+ @_server_cache = context.server._cache
445
+ @_method = context.method
446
+ end
447
+
448
+ def to_s
449
+ "<#{self.class.to_s}:#{object_id}>"
450
+ end
451
+
452
+ def inspect
453
+ to_s
341
454
  end
342
455
 
343
456
  # do not cache templates in development mode
@@ -376,9 +489,9 @@ module Blix::Rest
376
489
  path = opts[:path] || __erb_path || Controller.erb_root
377
490
 
378
491
  layout = layout_name && if no_template_cache
379
- ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
492
+ ERB.new(File.read(File.join(path, layout_name + '.html.erb')),:trim_mode=>'-')
380
493
  else
381
- erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
494
+ erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),:trim_mode=>'-')
382
495
  end
383
496
 
384
497
  begin
@@ -388,8 +501,8 @@ module Blix::Rest
388
501
  text
389
502
  end
390
503
  rescue Exception
391
- puts $!
392
- puts $@
504
+ ::Blix::Rest.logger << $!
505
+ ::Blix::Rest.logger << $@
393
506
  '*** TEMPLATE ERROR ***'
394
507
  end
395
508
  end
@@ -401,15 +514,15 @@ module Blix::Rest
401
514
  path = opts[:erb_dir] || __erb_path || Controller.erb_root
402
515
 
403
516
  layout = layout_name && if no_template_cache
404
- ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
517
+ ERB.new(File.read(File.join(path, layout_name + '.html.erb')),:trim_mode=>'-')
405
518
  else
406
- erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
519
+ erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),:trim_mode=>'-')
407
520
  end
408
521
 
409
522
  erb = if no_template_cache
410
- ERB.new(File.read(File.join(path, name + '.html.erb')),nil,'-')
523
+ ERB.new(File.read(File.join(path, name + '.html.erb')),:trim_mode=>'-')
411
524
  else
412
- erb_templates[name] ||= ERB.new(File.read(File.join(path, name + '.html.erb')),nil,'-')
525
+ erb_templates[name] ||= ERB.new(File.read(File.join(path, name + '.html.erb')),:trim_mode=>'-')
413
526
  end
414
527
 
415
528
  begin
@@ -421,8 +534,8 @@ module Blix::Rest
421
534
  erb.result(bind)
422
535
  end
423
536
  rescue Exception
424
- puts $!
425
- puts $@
537
+ ::Blix::Rest.logger << $!
538
+ ::Blix::Rest.logger << $@
426
539
  '*** TEMPLATE ERROR ***'
427
540
  end
428
541
  end
@@ -447,19 +560,36 @@ module Blix::Rest
447
560
  raise ServiceError, 'invalid format for this request' unless accept.index format
448
561
  end
449
562
 
563
+ def _do_route_hook(route)
564
+ if @_route_hook
565
+ superclass._do_route_hook(route) if superclass.respond_to? :_do_route_hook
566
+ @_route_hook.call(route)
567
+ end
568
+ end
569
+
450
570
  def route(verb, path, opts = {}, &blk)
451
- proc = lambda do |_path_params, _params, _req, _format, _response, server_options|
571
+ path = '/' + path unless path[0] == '/'
572
+ path = String.new(path) # in case frozen.
573
+ route = Route.new(verb, path, opts)
574
+ _do_route_hook(route)
575
+ proc = lambda do |context|
452
576
  unless opts[:force] && (opts[:accept] == :*)
453
- check_format(opts[:accept], _format)
577
+ check_format(opts[:accept], context.format)
454
578
  end
455
- app = new(_path_params, _params, _req, _format, verb, _response, server_options)
579
+ app = new
580
+ app._setup(context, verb, path, opts)
456
581
  begin
582
+ app.session_before(opts)
457
583
  app.before(opts)
458
- response = app.instance_eval( &blk )
584
+ app.__before
585
+ context.response = app.instance_eval( &blk )
459
586
  rescue
460
587
  raise
461
588
  ensure
462
- app.after(opts, response)
589
+ app.__after
590
+ app.after(opts, context.response)
591
+ app.session_after
592
+ context.response
463
593
  end
464
594
  end
465
595
 
@@ -498,6 +628,49 @@ module Blix::Rest
498
628
  route 'OPTIONS', *a, &b
499
629
  end
500
630
 
631
+ def before_route(&b)
632
+ @_route_hook = b if b
633
+ end
634
+
635
+
636
+ def _before_hooks
637
+ @_before_hooks ||= {}
638
+ end
639
+
640
+ def _after_hooks
641
+ @_after_hooks ||= {}
642
+ end
643
+
644
+ def _do_before(ctx, *a)
645
+ superclass._do_before(ctx, *a) if superclass.respond_to? :_do_before
646
+ _before_hooks.each_value{ |h| ctx.instance_eval(&h) }
647
+ end
648
+
649
+ def _do_after(ctx, *a)
650
+ _after_hooks.each_value{ |h| ctx.instance_eval(&h) }
651
+ superclass._do_after(ctx, *a) if superclass.respond_to? :_do_after
652
+ end
653
+
654
+ # define a before hook for a controller. only one hook can be defined per
655
+ # controller in a single source file.
656
+ def before(&block)
657
+ if block
658
+ file = block.source_location[0]
659
+ warn("warning: before hook already defined in #{file}") if _before_hooks[file]
660
+ _before_hooks[file] = block
661
+ end
662
+ end
663
+
664
+ # define an after hook for a controller. only one hook can be defined per
665
+ # controller in a single source file.
666
+ def after(&block)
667
+ if block
668
+ file = block.source_location[0]
669
+ warn("warning: after hook already defined in #{file}") if _after_hooks[file]
670
+ _after_hooks[file] = block
671
+ end
672
+ end
673
+
501
674
  end
502
675
 
503
676
  end
@@ -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