Class: PuppetEditorServices::Protocol::JsonRPC

Inherits:
Base
  • Object
show all
Defined in:
lib/puppet_editor_services/protocol/json_rpc.rb

Constant Summary collapse

CODE_INVALID_JSON =
-32_700
MSG_INVALID_JSON =
'invalid JSON'
CODE_INVALID_REQUEST =
-32_600
MSG_INVALID_REQ_JSONRPC =
"invalid request: doesn't include \"jsonrpc\": \"2.0\""
MSG_INVALID_REQ_ID =
'invalid request: wrong id'
MSG_INVALID_REQ_METHOD =
'invalid request: wrong method'
MSG_INVALID_REQ_PARAMS =
'invalid request: wrong params'
CODE_METHOD_NOT_FOUND =
-32_601
MSG_METHOD_NOT_FOUND =
'method not found'
CODE_INVALID_PARAMS =
-32_602
MSG_INVALID_PARAMS =
'invalid parameter(s)'
CODE_INTERNAL_ERROR =
-32_603
MSG_INTERNAL_ERROR =
'internal error'
PARSING_ERROR_RESPONSE =
'{"jsonrpc":"2.0","id":null,"error":{' \
"\"code\":#{CODE_INVALID_JSON}," \
"\"message\":\"#{MSG_INVALID_JSON}\"}}"
BATCH_NOT_SUPPORTED_RESPONSE =
'{"jsonrpc":"2.0","id":null,"error":{' \
'"code":-32099,' \
'"message":"batch mode not implemented"}}'
KEY_JSONRPC =
'jsonrpc'
JSONRPC_VERSION =
'2.0'
KEY_ID =
'id'
KEY_METHOD =
'method'
KEY_PARAMS =
'params'
KEY_RESULT =
'result'
KEY_ERROR =
'error'
KEY_CODE =
'code'
KEY_MESSAGE =
'message'

Instance Attribute Summary

Attributes inherited from Base

#connection, #handler

Instance Method Summary collapse

Methods inherited from Base

#close_connection, #connection_error?

Constructor Details

#initialize(connection) ⇒ JsonRPC

Returns a new instance of JsonRPC.



47
48
49
50
51
52
53
54
55
56
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 47

def initialize(connection)
  super

  @state = :data
  @buffer = []

  @request_sequence_id = 0
  @requests = {}
  @request_mutex = Mutex.new
end

Instance Method Details

#add_client_request(request) ⇒ Object

Stores the request so it can later be correlated with an incoming repsonse



220
221
222
223
224
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 220

def add_client_request(request)
  @request_mutex.synchronize do
    @requests[request.id] = request
  end
end

#client_request!(id) ⇒ Object

Retrieve the request to a client. Note that this removes it from the requests queue.



228
229
230
231
232
233
234
235
236
237
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 228

def client_request!(id)
  value = nil
  @request_mutex.synchronize do
    unless @requests[id].nil?
      value = @requests[id]
      @requests.delete(id)
    end
  end
  value
end

#client_request_id!Object

Thread-safe way to get a new request id



209
210
211
212
213
214
215
216
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 209

def client_request_id!
  value = nil
  @request_mutex.synchronize do
    value = @request_sequence_id
    @request_sequence_id += 1
  end
  value
end

#encode_and_send(object) ⇒ Object



120
121
122
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 120

def encode_and_send(object)
  send_json_string(::JSON.generate(object))
end

#extract_headers(raw_header) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 58

def extract_headers(raw_header)
  header = {}
  raw_header.split("\r\n").each do |item|
    name, value = item.split(':', 2)

    if name.casecmp('Content-Length').zero?
      header['Content-Length'] = value.strip.to_i
    elsif name.casecmp('Content-Type').zero?
      header['Content-Length'] = value.strip
    else
      raise("Unknown header #{name} in JSON message")
    end
  end
  header
end

#receive_data(data) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 74

def receive_data(data)
  # Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs
  return if data.empty?
  return if @state == :ignore

  # TODO: Thread/Atomic safe? probably not
  @buffer += data.bytes.to_a

  while @buffer.length > 4
    # Check if we have enough data for the headers
    # Need to find the first instance of '\r\n\r\n'
    offset = 0
    while offset < @buffer.length - 4
      break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10

      offset += 1
    end
    return unless offset < @buffer.length - 4

    # Extract the headers
    raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded
    headers = extract_headers(raw_header)
    raise('Missing Content-Length header') if headers['Content-Length'].nil?

    # Now we have the headers and the content length, do we have enough data now
    minimum_buf_length = offset + 3 + headers['Content-Length'] + 1 # Need to add one as we're converting from offset (zero based) to length (1 based) arrays
    return if @buffer.length < minimum_buf_length

    # Extract the message content
    content = @buffer.slice(offset + 3 + 1, headers['Content-Length']).pack('C*').force_encoding('utf-8') # TODO: default is utf-8.  Need to enode based on Content-Type
    # Purge the buffer
    @buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length)
    @buffer = [] if @buffer.nil?

    PuppetEditorServices.log_message(:debug, "--- INBOUND\n#{content}\n---")
    receive_json_message_as_string(content)
  end
end

#receive_json_message_as_hash(json_obj) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 140

def receive_json_message_as_hash(json_obj)
  # There's no need to convert it to an object quite yet
  # Need to validate that this is indeed a valid message
  id = json_obj[KEY_ID]
  unless json_obj[KEY_JSONRPC] == JSONRPC_VERSION
    PuppetEditorServices.log_message(:error, 'Invalid JSON RPC version')
    reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_JSONRPC
    return false
  end

  # Requests must have an ID and Method
  is_request = json_obj.key?(KEY_ID) && json_obj.key?(KEY_METHOD)
  # Notifications must have a Method but no ID
  is_notification = json_obj.key?(KEY_METHOD) && !json_obj.key?(KEY_ID)
  # Responses must have an ID, no Method but one of Result or Error
  is_response = json_obj.key?(KEY_ID) && !json_obj.key?(KEY_METHOD) && (json_obj.key?(KEY_RESULT) || json_obj.key?(KEY_ERROR))

  # The 'params' attribute must be a hash or an array
  if (params = json_obj[KEY_PARAMS]) && !(params.is_a?(Array) || params.is_a?(Hash))
    reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_PARAMS
    return false
  end

  # Requests and Responses must have an ID that is either a string or integer
  if (is_request || is_response) && !(id.is_a?(String) || id.is_a?(Integer))
    reply_error nil, CODE_INVALID_REQUEST, MSG_INVALID_REQ_ID
    return false
  end

  # Requests and Notifications must have a method
  if (is_request || is_notification) && !((json_obj[KEY_METHOD]).is_a? String)
    reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_METHOD
    return false
  end

  # Responses must have a matching request originating from this instance
  # Otherwise ignore it
  if is_response
    original_request = client_request!(json_obj[KEY_ID])
    return false if original_request.nil?
  end

  if is_request
    handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::RequestMessage.new(json_obj))
    return true
  elsif is_notification
    handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::NotificationMessage.new(json_obj))
    return true
  elsif is_response
    # Responses are special as they need the context of the original request
    handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::ResponseMessage.new(json_obj), request: original_request)
    return true
  end
  false
end

#receive_json_message_as_string(content) ⇒ Object

Seperate method so async JSON processing can be supported.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 125

def receive_json_message_as_string(content)
  json_obj = ::JSON.parse(content)
  return receive_json_message_as_hash(json_obj) if json_obj.is_a?(Hash)
  return unless json_obj.is_a?(Array)

  # Batch: multiple requests/notifications in an array.
  # NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket.

  PuppetEditorServices.log_message(:error, 'Batch request received but not implemented')
  send_json_string BATCH_NOT_SUPPORTED_RESPONSE

  connection.close_after_writing
  @state = :ignore
end

#reply_error(id, code, message) ⇒ Object



196
197
198
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 196

def reply_error(id, code, message)
  send_json_string ::PuppetEditorServices::Protocol::JsonRPCMessages.reply_error_by_id(id, code, message).to_json
end

#send_client_request(rpc_method, params) ⇒ Object

region Server-to-Client request/response methods



201
202
203
204
205
206
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 201

def send_client_request(rpc_method, params)
  request = ::PuppetEditorServices::Protocol::JsonRPCMessages.new_request(client_request_id!, rpc_method, params)
  encode_and_send(request)
  add_client_request(request)
  request.id
end

#send_json_string(string) ⇒ Object



113
114
115
116
117
118
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 113

def send_json_string(string)
  PuppetEditorServices.log_message(:debug, "--- OUTBOUND\n#{string}\n---")

  size = string.bytesize if string.respond_to?(:bytesize)
  connection.send_data "Content-Length: #{size}\r\n\r\n" + string
end