jsonrpc2 0.0.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'digest'
1
5
  require 'jsonrpc2'
2
6
  require 'jsonrpc2/accept'
3
7
  require 'jsonrpc2/textile'
@@ -8,6 +12,16 @@ require 'json'
8
12
  require 'base64'
9
13
 
10
14
  module JSONRPC2
15
+ module_function
16
+
17
+ def environment
18
+ ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
19
+ end
20
+
21
+ def development?
22
+ environment.eql?('development')
23
+ end
24
+
11
25
  # Authentication failed error - only used if transport level authentication isn't.
12
26
  # e.g. BasicAuth returns a 401 HTTP response, rather than throw this.
13
27
  class AuthFail < RuntimeError; end
@@ -15,301 +29,381 @@ module JSONRPC2
15
29
  # API error - thrown when an error is detected, e.g. params of wrong type.
16
30
  class APIFail < RuntimeError; end
17
31
 
18
- # Base class for JSONRPC2 interface
19
- class Interface
20
- class << self
32
+ # KnownError - thrown when a predictable error occurs.
33
+ class KnownError < RuntimeError
34
+ attr_accessor :code, :data
35
+ def self.exception(args)
36
+ code, message, data = *args
37
+ exception = new(message)
38
+ exception.code = code
39
+ exception.data = data
40
+ exception
41
+ end
42
+ end
21
43
 
22
- # @!group Authentication
44
+ # Base class for JSONRPC2 interface
45
+ class Interface
46
+ class << self
47
+ # @!group Authentication
48
+ # Get/set authenticator object for API interface - see {JSONRPC2::Auth} and {JSONRPC2::BasicAuth}
49
+ #
50
+ # @param [#check] args An object that responds to check(environment, json_call_data)
51
+ # @return [#check, nil] Currently set object or nil
52
+ def auth_with *args
53
+ if args.empty?
54
+ return @auth_with
55
+ else
56
+ @auth_with = args[0]
57
+ end
58
+ end
59
+ # @!endgroup
60
+
61
+ # @!group Rack-related
62
+ # Rack compatible call handler
63
+ #
64
+ # @param [Hash] environment Rack environment hash
65
+ # @return [Array<Fixnum, Hash<String,String>, Array<String>>] Rack-compatible response
66
+ def call(environment)
67
+ environment['json.request-id'] = Digest::MD5.hexdigest("#{$host ||= Socket.gethostname}-#{$$}-#{Time.now.to_f}")[0,8]
68
+ request = Rack::Request.new(environment)
69
+ catch :rack_response do
70
+ best = JSONRPC2::HTTPUtils.which(environment['HTTP_ACCEPT'], %w[text/html application/json-rpc application/json])
71
+
72
+ if request.path_info =~ %r'/_assets' or request.path_info == '/favicon.ico'
73
+ best = 'text/html' # hack for assets
74
+ end
75
+
76
+ case best
77
+ when 'text/html', 'text/css', 'image/png' # Assume browser
78
+ monitor_time(environment, request.POST['__json__']) { JSONRPC2::HTML.call(self, request) }
79
+ when 'application/json-rpc', 'application/json', nil # Assume correct by default
80
+ environment['rack.input'].rewind
81
+ raw = environment['rack.input'].read
82
+ data = JSON.parse(raw) if raw.to_s.size >= 2
83
+ monitor_time(environment, raw) { self.new(environment).rack_dispatch(data) }
84
+ else
85
+ [406, {'Content-Type' => 'text/html'},
86
+ ["<!DOCTYPE html><html><head><title>Media type mismatch</title></head><body>I am unable to acquiesce to your request</body></html>"]]
87
+ end
88
+ end
23
89
 
24
- # Get/set authenticator object for API interface - see {JSONRPC2::Auth} and {JSONRPC2::BasicAuth}
25
- #
26
- # @param [#check] args An object that responds to check(environment, json_call_data)
27
- # @return [#check, nil] Currently set object or nil
28
- def auth_with *args
29
- if args.empty?
30
- return @auth_with
31
- else
32
- @auth_with = args[0]
33
- end
90
+ rescue Exception => e
91
+ if environment['rack.logger'].respond_to?(:error)
92
+ environment['rack.logger'].error "#{e.class}: #{e.message} - #{e.backtrace * "\n "}"
93
+ end
94
+ raise e.class, e.message, e.backtrace
95
+ end
96
+
97
+ private
98
+
99
+ def monitor_time(env, data, &block)
100
+ if env['rack.logger'].respond_to?(:info)
101
+ if env["HTTP_AUTHORIZATION"].to_s =~ /Basic /i
102
+ auth = Base64.decode64(env["HTTP_AUTHORIZATION"].to_s.sub(/Basic /i, '')) rescue nil
103
+ auth ||= env["HTTP_AUTHORIZATION"]
104
+ else
105
+ auth = env["HTTP_AUTHORIZATION"]
106
+ end
107
+ env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} #{env['REQUEST_URI']} - Auth: #{auth}, Data: #{data.is_a?(String) ? data : data.inspect}")
108
+ end
109
+ t = Time.now.to_f
110
+ return yield
111
+ ensure
112
+ if env['rack.logger'].respond_to?(:info)
113
+ env['rack.logger'].info("[JSON-RPC2] #{env['json.request-id']} Completed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms#{ $! ? " - exception = #{$!.class}:#{$!.message}" : "" }")
114
+ end
115
+ end
116
+ # @!endgroup
34
117
  end
35
118
 
36
- # @!endgroup
119
+ # Create new interface object
120
+ #
121
+ # @param [Hash] env Rack environment
122
+ def initialize(env)
123
+ @_jsonrpc_env = env
124
+ @_jsonrpc_request = Rack::Request.new(env)
125
+ end
37
126
 
38
- # @!group Rack-related
127
+ # Internal
128
+ def rack_dispatch(rpcData)
129
+ catch(:rack_response) do
130
+ json = dispatch(rpcData)
131
+ [200, {'Content-Type' => 'application/json-rpc'}, [json]]
132
+ end
133
+ end
39
134
 
40
- # Rack compatible call handler
135
+ # Dispatch call to api method(s)
41
136
  #
42
- # @param [Hash] environment Rack environment hash
43
- # @return [Array<Fixnum, Hash<String,String>, Array<String>>] Rack-compatible response
44
- def call(environment)
45
- request = Rack::Request.new(environment)
46
- catch :rack_response do
47
- best = JSONRPC2::HTTPUtils.which(environment['HTTP_ACCEPT'], %w[text/html application/json-rpc application/json])
48
-
49
- if request.path_info =~ %r'/_assets'
50
- best = 'text/html' # hack for assets
51
- end
52
-
53
- case best
54
- when 'text/html', 'text/css', 'image/png' # Assume browser
55
- JSONRPC2::HTML.call(self, request)
56
- when 'application/json-rpc', 'application/json', nil # Assume correct by default
57
- environment['rack.input'].rewind
58
- data = JSON.parse(environment['rack.input'].read)
59
- self.new(environment).rack_dispatch(data)
60
- else
61
- [406, {'Content-Type' => 'text/html'},
62
- ["<!DOCTYPE html><html><head><title>Media type mismatch</title></head><body>I am unable to acquiesce to your request</body></html>"]]
63
- end
137
+ # @param [Hash,Array] rpc_data Array of calls or Hash containing one call
138
+ # @return [Hash,Array] Depends on input, but either a hash result or an array of results corresponding to calls.
139
+ def dispatch(rpc_data)
140
+ result = case rpc_data
141
+ when Array
142
+ rpc_data.map { |rpc| dispatch_single(rpc) }
143
+ else
144
+ dispatch_single(rpc_data)
64
145
  end
146
+
147
+ return result.to_json
65
148
  end
66
149
 
67
- # @!endgroup
150
+ protected
68
151
 
69
- end
152
+ # JSON result helper
153
+ def response_ok(id, result)
154
+ { 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
155
+ end
70
156
 
71
- # Create new interface object
72
- #
73
- # @param [Hash] env Rack environment
74
- def initialize(env)
75
- @_jsonrpc_env = env
76
- @_jsonrpc_request = Rack::Request.new(env)
77
- end
78
- # Internal
79
- def rack_dispatch(rpcData)
80
- catch(:rack_response) do
81
- json = dispatch(rpcData)
82
- [200, {'Content-Type' => 'application/json-rpc'}, [json]]
157
+ # JSON error helper
158
+ def response_error(code, message, data)
159
+ { 'jsonrpc' => '2.0', 'error' => { 'code' => code, 'message' => message, 'data' => data }, 'id' => (@_jsonrpc_call && @_jsonrpc_call['id'] || nil) }
83
160
  end
84
- end
85
161
 
86
- # Dispatch call to api method(s)
87
- #
88
- # @param [Hash,Array] rpc_data Array of calls or Hash containing one call
89
- # @return [Hash,Array] Depends on input, but either a hash result or an array of results corresponding to calls.
90
- def dispatch(rpc_data)
91
- case rpc_data
92
- when Array
93
- rpc_data.map { |rpc| dispatch_single(rpc) }.to_json
94
- else
95
- dispatch_single(rpc_data).to_json
162
+ # Params helper
163
+ def params
164
+ @_jsonrpc_call['params']
96
165
  end
97
- end
98
166
 
99
- protected
100
- # JSON result helper
101
- def response_ok(id, result)
102
- { 'jsonrpc' => '2.0', 'result' => result, 'id' => id }
103
- end
104
- # JSON error helper
105
- def response_error(code, message, data)
106
- { 'jsonrpc' => '2.0', 'error' => { 'code' => code, 'message' => message, 'data' => data }, 'id' => (@_jsonrpc_call && @_jsonrpc_call['id'] || nil) }
107
- end
108
- # Params helper
109
- def params
110
- @_jsonrpc_call['params']
111
- end
112
- # Auth info
113
- def auth
114
- @_jsonrpc_auth
115
- end
116
- # Rack::Request
117
- def request
118
- @_jsonrpc_request
119
- end
120
- # Check call validity and authentication & make a single method call
121
- #
122
- # @param [Hash] rpc JSON-RPC-2 call
123
- def dispatch_single(rpc)
124
- unless rpc.has_key?('id') && rpc.has_key?('method') && rpc['jsonrpc'].eql?('2.0')
125
- return response_error(-32600, 'Invalid request', nil)
167
+ # Auth info
168
+ def auth
169
+ @_jsonrpc_auth
126
170
  end
127
- @_jsonrpc_call = rpc
128
171
 
129
- begin
130
- if self.class.auth_with && ! @_jsonrpc_auth
131
- (@_jsonrpc_auth = self.class.auth_with.client_check(@_jsonrpc_env, rpc)) or raise AuthFail, "Invalid credentials"
132
- end
172
+ # Rack::Request
173
+ def request
174
+ @_jsonrpc_request
175
+ end
133
176
 
134
- call(rpc['method'], rpc['id'], rpc['params'])
135
- rescue AuthFail => e
136
- response_error(-32000, "AuthFail: #{e.class}: #{e.message}", {}) # XXX: Change me
137
- rescue APIFail => e
138
- response_error(-32000, "APIFail: #{e.class}: #{e.message}", {}) # XXX: Change me
139
- rescue Exception => e
140
- response_error(-32000, "#{e.class}: #{e.message}", e.backtrace) # XXX: Change me
177
+ def env
178
+ @_jsonrpc_env
141
179
  end
142
- end
143
- # List API methods
144
- #
145
- # @return [Array] List of api method names
146
- def api_methods
147
- public_methods(false).map(&:to_s) - ['rack_dispatch', 'dispatch']
148
- end
149
180
 
150
- # Call method, checking param and return types
151
- #
152
- # @param [String] method Method name
153
- # @param [Integer] id Method call ID - for response
154
- # @param [Hash] params Method parameters
155
- # @return [Hash] JSON response
156
- def call(method, id, params)
157
- if api_methods.include?(method)
158
- begin
159
- Types.valid_params?(self.class, method, params)
160
- rescue Exception => e
161
- return response_error(-32602, "Invalid params - #{e.message}", {})
181
+ # Logger
182
+ def logger
183
+ @_jsonrpc_logger ||= (@_jsonrpc_env['rack.logger'] || Rack::NullLogger.new("null"))
184
+ end
185
+
186
+ # Check call validity and authentication & make a single method call
187
+ #
188
+ # @param [Hash] rpc JSON-RPC-2 call
189
+ def dispatch_single(rpc)
190
+ t = Time.now.to_f
191
+
192
+ result = _dispatch_single(rpc)
193
+
194
+ if result['result']
195
+ logger.info("[JSON-RPC2] #{env['json.request-id']} Call completed OK in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms")
196
+ elsif result['error']
197
+ logger.info("[JSON-RPC2] #{env['json.request-id']} Call to ##{rpc['method']} failed in #{'%.3f' % ((Time.now.to_f - t) * 1000)}ms with error #{result['error']['code']} - #{result['error']['message']}")
162
198
  end
163
199
 
164
- if self.method(method).arity.zero?
165
- result = send(method)
166
- else
167
- result = send(method, params)
200
+ result
201
+ end
202
+
203
+ def _dispatch_single(rpc)
204
+ unless rpc.has_key?('id') && rpc.has_key?('method') && rpc['jsonrpc'].eql?('2.0')
205
+ return response_error(-32600, 'Invalid request', nil)
168
206
  end
207
+ @_jsonrpc_call = rpc
169
208
 
170
209
  begin
171
- Types.valid_result?(self.class, method, result)
210
+ if self.class.auth_with && ! @_jsonrpc_auth
211
+ (@_jsonrpc_auth = self.class.auth_with.client_check(@_jsonrpc_env, rpc)) or raise AuthFail, "Invalid credentials"
212
+ end
213
+
214
+ call(rpc['method'], rpc['id'], rpc['params'])
215
+ rescue AuthFail => e
216
+ response_error(-32000, 'AuthFail: Invalid credentials', {})
217
+ rescue APIFail => e
218
+ response_error(-32000, 'APIFail', {})
219
+ rescue KnownError => e
220
+ response_error(e.code, 'An error occurred', {})
172
221
  rescue Exception => e
173
- return response_error(-32602, "Invalid result - #{e.message}", {})
222
+ log_error("Internal error calling #{rpc.inspect} - #{e.class}: #{e.message} #{e.backtrace.join("\n ")}")
223
+ invoke_server_error_hook(e)
224
+ response_error(-32000, "An error occurred. Check logs for details", {})
174
225
  end
175
-
176
- response_ok(id, result)
177
- else
178
- response_error(-32601, "Unknown method `#{method.inspect}'", {})
179
226
  end
180
- end
181
227
 
182
- class << self
183
- # Store parameter in internal hash when building API
184
- def ___append_param name, type, options
185
- @params ||= []
186
- unless options.has_key?(:required)
187
- options[:required] = true
188
- end
189
- @params << options.merge({ :name => name, :type => type })
228
+ # List API methods
229
+ #
230
+ # @return [Array] List of api method names
231
+ def api_methods
232
+ public_methods(false).map(&:to_s) - ['rack_dispatch', 'dispatch']
190
233
  end
191
- private :___append_param
192
234
 
193
- # @!group DSL
194
-
195
- # Define a named parameter of type #type for next method
235
+ # Call method, checking param and return types
196
236
  #
197
- # @param [String] name parameter name
198
- # @param [String] type description of type see {Types}
199
- def param name, type, desc = nil, options = nil
200
- if options.nil? && desc.is_a?(Hash)
201
- options, desc = desc, nil
237
+ # @param [String] method Method name
238
+ # @param [Integer] id Method call ID - for response
239
+ # @param [Hash] params Method parameters
240
+ # @return [Hash] JSON response
241
+ def call(method, id, params)
242
+ if api_methods.include?(method)
243
+ begin
244
+ Types.valid_params?(self.class, method, params)
245
+ rescue Types::InvalidParamsError => e
246
+ return response_error(-32602, "Invalid params - #{e.message}", {})
247
+ end
248
+
249
+ if self.method(method).arity.zero?
250
+ result = send(method)
251
+ else
252
+ result = send(method, params)
253
+ end
254
+
255
+ Types.valid_result?(self.class, method, result)
256
+
257
+ response_ok(id, result)
258
+ else
259
+ response_error(-32601, "Unknown method `#{method.inspect}'", {})
202
260
  end
203
- options ||= {}
204
- options[:desc] = desc if desc.is_a?(String)
205
-
206
- ___append_param name, type, options
207
261
  end
208
262
 
209
- # Define an optional parameter for next method
210
- def optional name, type, desc = nil, options = nil
211
- if options.nil? && desc.is_a?(Hash)
212
- options, desc = desc, nil
263
+ class << self
264
+ # Store parameter in internal hash when building API
265
+ def ___append_param name, type, options
266
+ @params ||= []
267
+ unless options.has_key?(:required)
268
+ options[:required] = true
269
+ end
270
+ @params << options.merge({ :name => name, :type => type })
213
271
  end
214
- options ||= {}
215
- options[:desc] = desc if desc.is_a?(String)
272
+ private :___append_param
273
+
274
+ # @!group DSL
275
+ # Define a named parameter of type #type for next method
276
+ #
277
+ # @param [String] name parameter name
278
+ # @param [String] type description of type see {Types}
279
+ def param name, type, desc = nil, options = nil
280
+ if options.nil? && desc.is_a?(Hash)
281
+ options, desc = desc, nil
282
+ end
283
+ options ||= {}
284
+ options[:desc] = desc if desc.is_a?(String)
216
285
 
217
- ___append_param(name, type, options.merge(:required => false))
218
- end
286
+ ___append_param name, type, options
287
+ end
219
288
 
220
- # Define type of return value for next method
221
- def result type, desc = nil
222
- @result = { :type => type, :desc => desc }
223
- end
289
+ # Define an optional parameter for next method
290
+ def optional name, type, desc = nil, options = nil
291
+ if options.nil? && desc.is_a?(Hash)
292
+ options, desc = desc, nil
293
+ end
294
+ options ||= {}
295
+ options[:desc] = desc if desc.is_a?(String)
224
296
 
225
- # Set description for next method
226
- def desc str
227
- @desc = str
228
- end
297
+ ___append_param(name, type, options.merge(:required => false))
298
+ end
229
299
 
230
- # Add an example for next method
231
- def example desc, code
232
- @examples ||= []
233
- @examples << { :desc => desc, :code => code }
234
- end
300
+ # Define type of return value for next method
301
+ def result type, desc = nil
302
+ @result = { :type => type, :desc => desc }
303
+ end
235
304
 
236
- # Define a custom type
237
- def type name, *fields
238
- @types ||= {}
239
- type = JsonObjectType.new(name, fields)
305
+ # Set description for next method
306
+ def desc str
307
+ @desc = str
308
+ end
240
309
 
241
- if block_given?
242
- yield(type)
243
- end
310
+ # Add an example for next method
311
+ def example desc, code
312
+ @examples ||= []
313
+ @examples << { :desc => desc, :code => code }
314
+ end
244
315
 
245
- @types[name] = type
246
- end
316
+ # Define a custom type
317
+ def type name, *fields
318
+ @types ||= {}
319
+ type = JsonObjectType.new(name, fields)
247
320
 
248
- # Group methods
249
- def section name, summary=nil
250
- @sections ||= []
251
- @sections << {:name => name, :summary => summary}
321
+ if block_given?
322
+ yield(type)
323
+ end
252
324
 
253
- @current_section = name
254
- if block_given?
255
- yield
256
- @current_section = nil
257
- end
258
- end
325
+ @types[name] = type
326
+ end
259
327
 
260
- # Exclude next method from documentation
261
- def nodoc
262
- @nodoc = true
263
- end
328
+ # Group methods
329
+ def section name, summary=nil
330
+ @sections ||= []
331
+ @sections << {:name => name, :summary => summary}
264
332
 
265
- # Set interface title
266
- def title str = nil
267
- @title = str if str
268
- end
333
+ @current_section = name
334
+ if block_given?
335
+ yield
336
+ @current_section = nil
337
+ end
338
+ end
269
339
 
270
- # Sets introduction for interface
271
- def introduction str = nil
272
- @introduction = str if str
273
- end
340
+ # Exclude next method from documentation
341
+ def nodoc
342
+ @nodoc = true
343
+ end
274
344
 
275
- # @!endgroup
276
-
277
- # Catch methods added to class & store documentation
278
- def method_added(name)
279
- return if self == JSONRPC2::Interface
280
- @about ||= {}
281
- method = {}
282
- method[:params] = @params if @params
283
- method[:returns] = @result if @result
284
- method[:desc] = @desc if @desc
285
- method[:examples] = @examples if @examples
286
-
287
- if method.empty?
288
- if public_methods(false).include?(name)
289
- unless @nodoc
290
- #logger.info("#{name} has no API documentation... :(")
345
+ # Set interface title
346
+ def title str = nil
347
+ @title = str if str
348
+ end
349
+
350
+ # Sets introduction for interface
351
+ def introduction str = nil
352
+ @introduction = str if str
353
+ end
354
+ # @!endgroup
355
+
356
+ # Catch methods added to class & store documentation
357
+ def method_added(name)
358
+ return if self == JSONRPC2::Interface
359
+ @about ||= {}
360
+ method = {}
361
+ method[:params] = @params if @params
362
+ method[:returns] = @result if @result
363
+ method[:desc] = @desc if @desc
364
+ method[:examples] = @examples if @examples
365
+
366
+ if method.empty?
367
+ if public_methods(false).include?(name)
368
+ unless @nodoc
369
+ #logger.info("#{name} has no API documentation... :(")
370
+ end
371
+ else
372
+ #logger.debug("#{name} isn't public - so no API")
291
373
  end
292
374
  else
293
- #logger.debug("#{name} isn't public - so no API")
375
+ method[:name] = name
376
+ method[:section] = @current_section
377
+ method[:index] = @about.size
378
+ @about[name.to_s] = method
294
379
  end
295
- else
296
- method[:name] = name
297
- method[:section] = @current_section
298
- method[:index] = @about.size
299
- @about[name.to_s] = method
380
+
381
+ @result = nil
382
+ @params = nil
383
+ @desc = nil
384
+ @examples = nil
385
+ @nodoc = false
300
386
  end
387
+ private :method_added
388
+ attr_reader :about, :types
389
+ end
390
+
391
+ extend JSONRPC2::TextileEmitter
301
392
 
302
- @result = nil
303
- @params = nil
304
- @desc = nil
305
- @examples = nil
306
- @nodoc = false
393
+ private
394
+
395
+ def invoke_server_error_hook(error)
396
+ on_server_error(request_id: env['json.request-id'], error: error)
397
+ rescue => error
398
+ log_error("Server error hook failed - #{error.class}: #{error.message} #{error.backtrace.join("\n ")}")
307
399
  end
308
- private :method_added
309
- attr_reader :about, :types
310
400
 
311
- end
401
+ # Available for reimplementation by a subclass, noop by default
402
+ def on_server_error(request_id:, error:)
403
+ end
312
404
 
313
- extend JSONRPC2::TextileEmitter
314
- end
405
+ def log_error(message)
406
+ logger.error("#{env['json.request-id']} #{message}") if logger.respond_to?(:error)
407
+ end
408
+ end
315
409
  end