blix-rest 0.1.30

Sign up to get free protection for your applications and to get access to all the features.
@@ -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