blix-rest 0.1.30

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.
@@ -0,0 +1,145 @@
1
+ require 'base64'
2
+ require 'logger'
3
+ require 'time'
4
+
5
+ module Blix
6
+ module Rest
7
+ MIME_TYPE_JSON = 'application/json'.freeze
8
+ # EXPIRED_TOKEN_MESSAGE = 'token expired'
9
+ # INVALID_TOKEN_MESSAGE = 'invalid token'
10
+
11
+ CONTENT_TYPE = 'Content-Type'.freeze
12
+ CONTENT_TYPE_JSON = 'application/json'.freeze
13
+ CONTENT_TYPE_HTML = 'text/html; charset=utf-8'.freeze
14
+ CONTENT_TYPE_XML = 'application/xml'.freeze
15
+ AUTH_HEADER = 'WWW-Authenticate'.freeze
16
+ CACHE_CONTROL = 'Cache-Control'.freeze
17
+ CACHE_NO_STORE = 'no-store'.freeze
18
+ PRAGMA = 'Pragma'.freeze
19
+ NO_CACHE = 'no-cache'.freeze
20
+ URL_ENCODED = %r{^application/x-www-form-urlencoded}.freeze
21
+ JSON_ENCODED = %r{^application/json}.freeze # NOTE: "text/json" and "text/javascript" are deprecated forms
22
+ HTML_ENCODED = %r{^text/html}.freeze
23
+ XML_ENCODED = %r{^application/xml}.freeze
24
+
25
+ HTTP_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'.freeze
26
+ HTTP_VERBS = %w[GET HEAD POST PUT DELETE OPTIONS PATCH].freeze
27
+ HTTP_BODY_VERBS = %w[POST PUT PATCH].freeze
28
+
29
+ # the test/development/production environment
30
+ def self.environment
31
+ @_environment ||= ENV['RACK_ENV'] || 'development'
32
+ end
33
+
34
+ def self.environment?(val)
35
+ environment == val.to_s
36
+ end
37
+
38
+ def self.environment=(val)
39
+ @_environment = val.to_s
40
+ end
41
+
42
+ def self.logger=(val)
43
+ @_logger = val
44
+ end
45
+
46
+ def self.logger
47
+ @_logger ||= begin
48
+ l = Logger.new(STDOUT)
49
+ unless l.respond_to? :write # common logger needs a write method
50
+ def l.write(*args)
51
+ self.<<(*args)
52
+ end
53
+ end
54
+ l
55
+ end
56
+ end
57
+
58
+
59
+ class BinaryData < String
60
+ def as_json(*_a)
61
+ { 'base64Binary' => Base64.encode64(self) }
62
+ end
63
+
64
+ def to_json(*a)
65
+ as_json.to_json(*a)
66
+ end
67
+ end
68
+
69
+ # interpret payload string as json
70
+ class RawJsonString
71
+ def initialize(str)
72
+ @str = str
73
+ end
74
+
75
+ def as_json(*_a)
76
+ @str
77
+ end
78
+
79
+ def to_json(*a)
80
+ as_json.to_json(*a)
81
+ end
82
+ end
83
+
84
+ class BadRequestError < StandardError; end
85
+
86
+ class ServiceError < StandardError
87
+ attr_reader :status
88
+ attr_reader :headers
89
+
90
+ def initialize(message, status = nil, headers = nil)
91
+ super(message || "")
92
+ @status = status || 406
93
+ @headers = headers
94
+ end
95
+ end
96
+
97
+ class AuthorizationError < StandardError
98
+ attr_reader :realm, :type
99
+ def initialize(message=nil, realm=nil, type=nil)
100
+ super(message || "")
101
+ @realm = realm || 'rest'
102
+ @type = type || 'Basic'
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ class NilClass
109
+ def empty?
110
+ true
111
+ end
112
+ end
113
+
114
+ # common classes
115
+ require 'multi_json'
116
+ require 'logger'
117
+ require 'blix/rest/version'
118
+ require 'blix/rest/string_hash'
119
+
120
+ # client classes
121
+ # require 'blix/rest/remote_service'
122
+ # require 'blix/rest/web_frame_service'
123
+ # require 'blix/rest/service'
124
+ # require 'blix/rest/service_resource'
125
+
126
+ # provider classes
127
+ require 'rack'
128
+ require 'blix/rest/response'
129
+ require 'blix/rest/format_parser'
130
+ require 'blix/rest/request_mapper'
131
+ require 'blix/rest/server'
132
+ # require 'blix/rest/provider'
133
+ require 'blix/rest/controller'
134
+ # require 'blix/rest/provider_controller'
135
+
136
+ # ensure that that times are sent in the correct json format
137
+ class Time
138
+ def as_json(*_a)
139
+ utc.iso8601
140
+ end
141
+
142
+ def to_json(*a)
143
+ as_json.to_json(*a)
144
+ end
145
+ end
@@ -0,0 +1,512 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'erb'
5
+ require 'securerandom'
6
+
7
+ module Blix::Rest
8
+ # base class for controllers. within your block handling a particular route you
9
+ # have access to a number of methods
10
+ #
11
+ # env : the request environment hash
12
+ # body : the request body as a string
13
+ # body_hash : the request body as a hash constructed from json
14
+ # query_params : a hash of parameters as passed in the url as parameters
15
+ # path_params : a hash of parameters constructed from variable parts of the path
16
+ # params : all the params combined
17
+ # user : the user making this request ( or nil if
18
+ # format : the format the response should be in :json or :html
19
+ # session : the rack session if middleware has been used
20
+ #
21
+ # to accept requests other thatn json then set :accept=>[:json,:html] as options in the route
22
+ # eg post '/myform' :accept=>[:html] # this will only accept html requests.
23
+
24
+ class Controller
25
+
26
+ #--------------------------------------------------------------------------------------------------------
27
+ # convenience methods
28
+ #--------------------------------------------------------------------------------------------------------
29
+ def env
30
+ @_env
31
+ end
32
+
33
+ # options that were passed to the server at create time.
34
+ def server_options
35
+ @_server_options
36
+ end
37
+
38
+ def logger
39
+ Blix::Rest.logger
40
+ end
41
+
42
+ def rack_env
43
+ ENV['RACK_ENV']
44
+ end
45
+
46
+ def mode_test?
47
+ rack_env == 'test'
48
+ end
49
+
50
+ def mode_development?
51
+ rack_env == 'development'
52
+ end
53
+
54
+ def mode_production?
55
+ rack_env == 'production'
56
+ end
57
+
58
+ def body
59
+ @_body ||= env['rack.input'].read
60
+ # env['rack.input'].rewindreq.POST #env["body"]
61
+ end
62
+
63
+ def path
64
+ req.path
65
+ end
66
+
67
+ def form_hash
68
+ StringHash.new(req.POST)
69
+ end
70
+
71
+ def body_hash
72
+ @_body_hash ||= if body.empty?
73
+ {}
74
+ else
75
+ # should we check the content type here?
76
+ begin
77
+ StringHash.new(MultiJson.load(body))
78
+ rescue StandardError
79
+ raise ServiceError, "error in data json format/#{body}/"
80
+ end
81
+ end
82
+ end
83
+
84
+ def get_data(field)
85
+ body_hash['data'] && body_hash['data'][field]
86
+ end
87
+
88
+ def format
89
+ @_format
90
+ end
91
+
92
+ def query_params
93
+ @_query_params
94
+ end
95
+
96
+ def path_params
97
+ @_path_params
98
+ end
99
+
100
+ def params
101
+ @_params ||= StringHash.new(@_query_params,@_path_params)
102
+ end
103
+
104
+ def post_params
105
+ @_post_params ||= begin
106
+ type = req.media_type
107
+ if type && Rack::Request::FORM_DATA_MEDIA_TYPES.include?(type)
108
+ form_hash
109
+ else
110
+ body_hash
111
+ end
112
+ end
113
+ end
114
+
115
+ def path_for(path)
116
+ File.join(RequestMapper.path_root, path)
117
+ end
118
+
119
+ def url_for(path)
120
+ req.base_url + path_for(path)
121
+ end
122
+
123
+ def req
124
+ @_req
125
+ end
126
+
127
+ def verb
128
+ @_verb
129
+ end
130
+
131
+ def method
132
+ env['REQUEST_METHOD'].downcase
133
+ end
134
+
135
+ def session
136
+ req.session
137
+ end
138
+
139
+ # add on the root path
140
+ def full_path(path)
141
+ RequestMapper.full_path(path)
142
+ end
143
+
144
+ # the full url of this path.
145
+ def full_url(_path)
146
+ raise 'not yet implemented'
147
+ end
148
+
149
+ def redirect(path, status = 302)
150
+ raise ServiceError.new(nil, status, 'Location' => path)
151
+ end
152
+
153
+ alias redirect_to redirect
154
+
155
+ def request_ip
156
+ req.ip
157
+ end
158
+
159
+ # render an erb template with the variables in the controller
160
+ def render_erb(template_name, opts = {})
161
+ self.class.render_erb(template_name, self, opts)
162
+ end
163
+
164
+ def render(text, opts = {})
165
+ self.class.render_erb(text, self, opts)
166
+ end
167
+
168
+ def rawjson(str)
169
+ RawJsonString.new(str)
170
+ end
171
+
172
+ def _get_binding
173
+ binding
174
+ end
175
+
176
+ # extract the user and login from the basic authentication
177
+ def get_basic_auth(realm=nil)
178
+ data = env['HTTP_AUTHORIZATION']
179
+ raise AuthorizationError.new('authentication missing',realm) unless data
180
+
181
+ type = data[0, 5]
182
+ rest = data[6..-1]
183
+
184
+ raise AuthorizationError.new('wrong authentication method',realm) unless type == 'Basic'
185
+ raise AuthorizationError.new('username:password missing',realm) unless rest
186
+
187
+ auth_parts = Base64.decode64(rest).split(':')
188
+ login = auth_parts[0]
189
+ password = auth_parts[1]
190
+ [login, password]
191
+ end
192
+
193
+ def set_status(value)
194
+ @_response.status = value
195
+ end
196
+
197
+ def add_headers(headers)
198
+ @_response.headers.merge!(headers)
199
+ end
200
+
201
+ # the following is copied from Rack::Utils
202
+ ESCAPE_HTML = {
203
+ '&' => '&amp;',
204
+ '<' => '&lt;',
205
+ '>' => '&gt;',
206
+ "'" => '&#x27;',
207
+ '"' => '&quot;',
208
+ '/' => '&#x2F;'
209
+ }.freeze
210
+
211
+ JS_ESCAPE_MAP = { '\\' => '\\\\', '</' => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }.freeze
212
+
213
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
214
+
215
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
216
+ def h(string)
217
+ string.to_s.gsub(ESCAPE_HTML_PATTERN) { |c| ESCAPE_HTML[c] }
218
+ end
219
+
220
+ # escape javascript
221
+ def escape_javascript(javascript)
222
+ if javascript
223
+ javascript.gsub(%r{(\|</|\r\n|\342\200\250|\342\200\251|[\n\r"'])}u) { |match| JS_ESCAPE_MAP[match] }
224
+ else
225
+ ''
226
+ end
227
+ end
228
+
229
+ # send a (default) error
230
+ def send_error(message, status = nil, headers = nil)
231
+ raise ServiceError.new(message, status, headers)
232
+ end
233
+
234
+ def auth_error(*params)
235
+ if params[0].kind_of?(String)
236
+ message = params[0]
237
+ opts = params[1] || {}
238
+ else
239
+ message = nil
240
+ opts = params[-1] || {}
241
+ end
242
+ raise AuthorizationError.new(message,opts[:realm], opts[:type])
243
+ end
244
+
245
+ def get_cookie(name)
246
+ cookie_header = env['HTTP_COOKIE']
247
+ cookie_length = name.length
248
+ parts = cookie_header&.split(';')
249
+ value = nil
250
+ parts&.reverse&.each do |cookie|
251
+ cookie.strip!
252
+ if cookie[0..cookie_length] == name + '='
253
+ value = cookie[cookie_length + 1..-1]
254
+ break
255
+ end
256
+ end
257
+ value
258
+ end
259
+
260
+ def store_cookie(name, value, opts={})
261
+ cookie_text = String.new("#{name}=#{value}")
262
+ cookie_text << '; Secure' if _opt?(opts,:secure)
263
+ cookie_text << '; HttpOnly' if _opt?(opts,:http)
264
+ cookie_text << "; HostOnly=#{_opt(opts,:hostOnly)}" if _opt?(opts,:hostOnly)
265
+ cookie_text << "; Expires=#{_opt(opts,:expires).httpdate}" if _opt?(opts,:expires)
266
+ cookie_text << "; Max-Age=#{_opt(opts,:max_age)}" if _opt?(opts,:max_age)
267
+ cookie_text << "; Domain=#{_opt(opts,:domain)}" if _opt?(opts,:domain)
268
+ cookie_text << "; Path=#{_opt(opts,:path)}" if _opt?(opts,:path)
269
+ if policy = _opt(opts,:samesite)
270
+ cookie_text << '; SameSite=Strict' if policy.to_s.downcase == 'strict'
271
+ cookie_text << '; SameSite=Lax' if policy.to_s.downcase == 'lax'
272
+ cookie_text << '; SameSite=None' if policy.to_s.downcase == 'none'
273
+ end
274
+ @_cookies ||= {}
275
+ @_cookies[name] = cookie_text
276
+ # cookie_header = @_response.headers['Set-Cookie']
277
+ # if cookie_header
278
+ # cookie_header = cookie_header << "\n" << cookie_text
279
+ # else
280
+ # cookie_header = cookie_text
281
+ # end
282
+ @_response.headers['Set-Cookie'] = @_cookies.values.join("\n")
283
+ value
284
+ end
285
+
286
+ # manage session handling --------------------------------------------------
287
+ # setup the session and retrieve the session_id
288
+ # this id can be used to retrieve and data associated
289
+ # with the session_id in eg: a database or a memory hash
290
+ def get_session_id(session_name, opts = {})
291
+ session_id = get_cookie(session_name)
292
+ session_id || refresh_session_id(session_name, opts)
293
+ end
294
+
295
+ # generate an new session_id for the current session
296
+ def refresh_session_id(session_name, opts = {})
297
+ session_id = SecureRandom.hex(32)
298
+ store_session_id(session_name, session_id, opts)
299
+ end
300
+
301
+ def _opt?(opts,key)
302
+ opts.key?(key.to_sym) || opts.key?(key.to_s)
303
+ end
304
+
305
+ def _opt(opts,key)
306
+ if opts.key?(key.to_sym)
307
+ opts[key.to_sym]
308
+ else
309
+ opts[key.to_s]
310
+ end
311
+ end
312
+
313
+ # set the cookie header that stores the session_id on the browser.
314
+ def store_session_id(session_name, session_id, opts = {})
315
+ store_cookie(session_name, session_id, opts)
316
+ end
317
+
318
+ #----------------------------------------------------------------------------------------------------------
319
+ # template methods that can be overwritten
320
+
321
+ # a hook used to insert processing for before the method call
322
+ def before(opts); end
323
+
324
+ # a hook used to insert processing for after the method call. return a hash containing
325
+ # the response.
326
+ def after(_opts, response)
327
+ response
328
+ end
329
+
330
+ #----------------------------------------------------------------------------------------------------------
331
+
332
+ def initialize(path_params, _params, req, format, verb, response, server_options)
333
+ @_req = req
334
+ @_env = req.env
335
+ @_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
341
+ end
342
+
343
+ # do not cache templates in development mode
344
+ def self.no_template_cache
345
+ @_no_template_cache = (Blix::Rest.environment != 'production') if @_no_template_cache.nil?
346
+ @_no_template_cache
347
+ end
348
+
349
+ def self.no_template_cache=(val)
350
+ @_no_template_cache = val
351
+ end
352
+
353
+ # cache templates here
354
+ def self.erb_templates
355
+ @_erb ||= {}
356
+ end
357
+
358
+ def self.set_erb_root(dir)
359
+ @_erb_root = dir
360
+ end
361
+
362
+ def self.erb_root
363
+ @_erb_root ||= begin
364
+ root = File.join(Dir.pwd, 'app', 'views')
365
+ raise('use set_erb_root() to specify the location of your views') unless Dir.exist?(root)
366
+
367
+ root
368
+ end
369
+ end
370
+
371
+ class << self
372
+
373
+ # render a string within a layout.
374
+ def render(text, context, opts = {})
375
+ layout_name = opts[:layout]
376
+ path = opts[:path] || __erb_path || Controller.erb_root
377
+
378
+ layout = layout_name && if no_template_cache
379
+ ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
380
+ else
381
+ erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
382
+ end
383
+
384
+ begin
385
+ if layout
386
+ layout.result(context._get_binding { |*_args| text })
387
+ else
388
+ text
389
+ end
390
+ rescue Exception
391
+ puts $!
392
+ puts $@
393
+ '*** TEMPLATE ERROR ***'
394
+ end
395
+ end
396
+
397
+ def render_erb(name, context, opts = {})
398
+ name = name.to_s
399
+ layout_name = opts[:layout] && opts[:layout].to_s
400
+ locals = opts[:locals]
401
+ path = opts[:erb_dir] || __erb_path || Controller.erb_root
402
+
403
+ layout = layout_name && if no_template_cache
404
+ ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
405
+ else
406
+ erb_templates[layout_name] ||= ERB.new(File.read(File.join(path, layout_name + '.html.erb')),nil,'-')
407
+ end
408
+
409
+ erb = if no_template_cache
410
+ ERB.new(File.read(File.join(path, name + '.html.erb')),nil,'-')
411
+ else
412
+ erb_templates[name] ||= ERB.new(File.read(File.join(path, name + '.html.erb')),nil,'-')
413
+ end
414
+
415
+ begin
416
+ bind = context._get_binding
417
+ locals&.each { |k, v| bind.local_variable_set(k, v) } # works from ruby 2.1
418
+ if layout
419
+ layout.result(context._get_binding { |*_args| erb.result(bind) })
420
+ else
421
+ erb.result(bind)
422
+ end
423
+ rescue Exception
424
+ puts $!
425
+ puts $@
426
+ '*** TEMPLATE ERROR ***'
427
+ end
428
+ end
429
+
430
+ # default method .. will be overridden with erb_path method
431
+ def __erb_path
432
+ nil
433
+ end
434
+
435
+ # redefine the __erb_path method for this and derived classes
436
+ def erb_dir(val)
437
+ str = "def self.__erb_path;\"#{val}\";end"
438
+ class_eval str
439
+ end
440
+
441
+ def check_format(accept, format)
442
+ return if (format == :json) && accept.nil? # the majority of cases
443
+ return if (format == :_) && accept.nil? # assume json by default.
444
+
445
+ accept ||= :json
446
+ accept = [accept].flatten
447
+ raise ServiceError, 'invalid format for this request' unless accept.index format
448
+ end
449
+
450
+ def route(verb, path, opts = {}, &blk)
451
+ proc = lambda do |_path_params, _params, _req, _format, _response, server_options|
452
+ unless opts[:force] && (opts[:accept] == :*)
453
+ check_format(opts[:accept], _format)
454
+ end
455
+ app = new(_path_params, _params, _req, _format, verb, _response, server_options)
456
+ begin
457
+ app.before(opts)
458
+ response = app.instance_eval( &blk )
459
+ rescue
460
+ raise
461
+ ensure
462
+ app.after(opts, response)
463
+ end
464
+ end
465
+
466
+ RequestMapper.add_path(verb.to_s.upcase, path, opts, &proc)
467
+ end
468
+
469
+ def get(*a, &b)
470
+ route 'GET', *a, &b
471
+ end
472
+
473
+ def head(*a, &b)
474
+ route 'HEAD', *a, &b
475
+ end
476
+
477
+ def post(*a, &b)
478
+ route 'POST', *a, &b
479
+ end
480
+
481
+ def put(*a, &b)
482
+ route 'PUT', *a, &b
483
+ end
484
+
485
+ def patch(*a, &b)
486
+ route 'PATCH', *a, &b
487
+ end
488
+
489
+ def delete(*a, &b)
490
+ route 'DELETE', *a, &b
491
+ end
492
+
493
+ def all(*a, &b)
494
+ route 'ALL', *a, &b
495
+ end
496
+
497
+ def options(*a, &b)
498
+ route 'OPTIONS', *a, &b
499
+ end
500
+
501
+ end
502
+
503
+ end
504
+
505
+ def self.set_erb_root(*args)
506
+ Controller.set_erb_root(*args)
507
+ end
508
+
509
+ def self.no_template_cache=(val)
510
+ Controller.no_template_cache = val
511
+ end
512
+ end