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.
@@ -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