openc3 6.9.0 → 6.9.2
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.
- checksums.yaml +4 -4
- data/bin/openc3cli +289 -0
- data/lib/openc3/interfaces.rb +6 -4
- data/lib/openc3/microservices/interface_microservice.rb +9 -5
- data/lib/openc3/microservices/queue_microservice.rb +18 -27
- data/lib/openc3/models/microservice_model.rb +1 -1
- data/lib/openc3/models/plugin_model.rb +63 -6
- data/lib/openc3/models/queue_model.rb +24 -24
- data/lib/openc3/models/reaction_model.rb +1 -1
- data/lib/openc3/models/target_model.rb +7 -3
- data/lib/openc3/models/trigger_model.rb +1 -1
- data/lib/openc3/script/queue.rb +11 -3
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/system/system.rb +3 -3
- data/lib/openc3/utilities/authentication.rb +25 -6
- data/lib/openc3/utilities/cli_generator.rb +347 -3
- data/lib/openc3/utilities/running_script.rb +2 -3
- data/lib/openc3/version.rb +5 -5
- data/templates/command_validator/command_validator.py +49 -0
- data/templates/command_validator/command_validator.rb +54 -0
- data/templates/tool_angular/package.json +48 -2
- data/templates/tool_react/package.json +51 -1
- data/templates/tool_svelte/package.json +48 -1
- data/templates/tool_vue/package.json +36 -3
- data/templates/widget/package.json +28 -2
- metadata +5 -5
- data/templates/tool_vue/.browserslistrc +0 -16
- data/templates/widget/.browserslistrc +0 -16
|
@@ -56,6 +56,13 @@ module OpenC3
|
|
|
56
56
|
attr_accessor :plugin_txt_lines
|
|
57
57
|
attr_accessor :needs_dependencies
|
|
58
58
|
attr_accessor :store_id
|
|
59
|
+
attr_accessor :title
|
|
60
|
+
attr_accessor :description
|
|
61
|
+
attr_accessor :licenses
|
|
62
|
+
attr_accessor :homepage
|
|
63
|
+
attr_accessor :repository
|
|
64
|
+
attr_accessor :keywords
|
|
65
|
+
attr_accessor :img_path
|
|
59
66
|
|
|
60
67
|
# NOTE: The following three class methods are used by the ModelController
|
|
61
68
|
# and are reimplemented to enable various Model class methods to work
|
|
@@ -179,11 +186,31 @@ module OpenC3
|
|
|
179
186
|
raise "Invalid screen filename: #{filename}. Screen filenames must be lowercase."
|
|
180
187
|
end
|
|
181
188
|
end
|
|
189
|
+
|
|
190
|
+
# Process app store metadata
|
|
191
|
+
plugin_model.title = pkg.spec.metadata['openc3_store_title'] || pkg.spec.summary.strip
|
|
192
|
+
plugin_model.description = pkg.spec.metadata['openc3_store_description'] || pkg.spec.description.strip
|
|
193
|
+
plugin_model.licenses = pkg.spec.licenses
|
|
194
|
+
plugin_model.homepage = pkg.spec.homepage
|
|
195
|
+
plugin_model.repository = pkg.spec.metadata['source_code_uri'] # this key because it's in the official gemspec examples
|
|
196
|
+
plugin_model.keywords = pkg.spec.metadata['openc3_store_keywords']&.split(/, ?/)
|
|
197
|
+
img_path = pkg.spec.metadata['openc3_store_image']
|
|
198
|
+
unless img_path
|
|
199
|
+
default_img_path = 'public/store_img.png'
|
|
200
|
+
full_default_path = File.join(gem_path, default_img_path)
|
|
201
|
+
img_path = default_img_path if File.exist? full_default_path
|
|
202
|
+
end
|
|
203
|
+
plugin_model.img_path = File.join('gems', gem_name.split(".gem")[0], img_path) if img_path # convert this filesystem path to volumes mount path
|
|
204
|
+
plugin_model.update() unless validate_only
|
|
205
|
+
|
|
182
206
|
needs_dependencies = pkg.spec.runtime_dependencies.length > 0
|
|
183
207
|
needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
|
|
184
208
|
|
|
185
|
-
# Handle python requirements.txt
|
|
186
|
-
|
|
209
|
+
# Handle python dependencies (pyproject.toml or requirements.txt)
|
|
210
|
+
pyproject_path = File.join(gem_path, 'pyproject.toml')
|
|
211
|
+
requirements_path = File.join(gem_path, 'requirements.txt')
|
|
212
|
+
|
|
213
|
+
if File.exist?(pyproject_path) || File.exist?(requirements_path)
|
|
187
214
|
begin
|
|
188
215
|
pypi_url = get_setting('pypi_url', scope: scope)
|
|
189
216
|
if pypi_url
|
|
@@ -202,11 +229,20 @@ module OpenC3
|
|
|
202
229
|
end
|
|
203
230
|
end
|
|
204
231
|
unless validate_only
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
232
|
+
if File.exist?(pyproject_path)
|
|
233
|
+
Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
|
|
234
|
+
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
235
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} #{gem_path}"
|
|
236
|
+
else
|
|
237
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} #{gem_path}"
|
|
238
|
+
end
|
|
208
239
|
else
|
|
209
|
-
|
|
240
|
+
Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
|
|
241
|
+
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
242
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} -r #{requirements_path}"
|
|
243
|
+
else
|
|
244
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{requirements_path}"
|
|
245
|
+
end
|
|
210
246
|
end
|
|
211
247
|
puts `/openc3/bin/pipinstall #{pip_args}`
|
|
212
248
|
end
|
|
@@ -302,6 +338,13 @@ module OpenC3
|
|
|
302
338
|
plugin_txt_lines: [],
|
|
303
339
|
needs_dependencies: false,
|
|
304
340
|
store_id: nil,
|
|
341
|
+
title: nil,
|
|
342
|
+
description: nil,
|
|
343
|
+
keywords: nil,
|
|
344
|
+
licenses: nil,
|
|
345
|
+
homepage: nil,
|
|
346
|
+
repository: nil,
|
|
347
|
+
img_path: nil,
|
|
305
348
|
updated_at: nil,
|
|
306
349
|
scope:
|
|
307
350
|
)
|
|
@@ -310,6 +353,13 @@ module OpenC3
|
|
|
310
353
|
@plugin_txt_lines = plugin_txt_lines
|
|
311
354
|
@needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
|
|
312
355
|
@store_id = store_id
|
|
356
|
+
@title = title
|
|
357
|
+
@description = description
|
|
358
|
+
@keywords = keywords
|
|
359
|
+
@licenses = licenses
|
|
360
|
+
@homepage = homepage
|
|
361
|
+
@repository = repository
|
|
362
|
+
@img_path = img_path
|
|
313
363
|
end
|
|
314
364
|
|
|
315
365
|
def create(update: false, force: false, queued: false)
|
|
@@ -329,6 +379,13 @@ module OpenC3
|
|
|
329
379
|
'plugin_txt_lines' => @plugin_txt_lines,
|
|
330
380
|
'needs_dependencies' => @needs_dependencies,
|
|
331
381
|
'store_id' => @store_id,
|
|
382
|
+
'title' => @title,
|
|
383
|
+
'description' => @description,
|
|
384
|
+
'keywords' => @keywords,
|
|
385
|
+
'licenses' => @licenses,
|
|
386
|
+
'homepage' => @homepage,
|
|
387
|
+
'repository' => @repository,
|
|
388
|
+
'img_path' => @img_path,
|
|
332
389
|
'updated_at' => @updated_at
|
|
333
390
|
}
|
|
334
391
|
end
|
|
@@ -51,11 +51,11 @@ module OpenC3
|
|
|
51
51
|
if model.state != 'DISABLE'
|
|
52
52
|
result = Store.zrevrange("#{scope}:#{name}", 0, 0, with_scores: true)
|
|
53
53
|
if result.empty?
|
|
54
|
-
|
|
54
|
+
id = 1.0
|
|
55
55
|
else
|
|
56
|
-
|
|
56
|
+
id = result[0][1].to_f + 1
|
|
57
57
|
end
|
|
58
|
-
Store.zadd("#{scope}:#{name}",
|
|
58
|
+
Store.zadd("#{scope}:#{name}", id, { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }.to_json)
|
|
59
59
|
model.notify(kind: 'command')
|
|
60
60
|
else
|
|
61
61
|
raise QueueError, "Queue '#{name}' is disabled. Command '#{command}' not queued."
|
|
@@ -103,55 +103,55 @@ module OpenC3
|
|
|
103
103
|
QueueTopic.write_notification(notification, scope: @scope)
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def insert_command(
|
|
106
|
+
def insert_command(id, command_data)
|
|
107
107
|
if @state == 'DISABLE'
|
|
108
108
|
raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_data['value']}' not queued."
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
unless
|
|
111
|
+
unless id
|
|
112
112
|
result = Store.zrevrange("#{@scope}:#{@name}", 0, 0, with_scores: true)
|
|
113
113
|
if result.empty?
|
|
114
|
-
|
|
114
|
+
id = 1.0
|
|
115
115
|
else
|
|
116
|
-
|
|
116
|
+
id = result[0][1].to_f + 1
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
|
-
Store.zadd("#{@scope}:#{@name}",
|
|
119
|
+
Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
|
|
120
120
|
notify(kind: 'command')
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
def update_command(
|
|
123
|
+
def update_command(id:, command:, username:)
|
|
124
124
|
if @state == 'DISABLE'
|
|
125
|
-
raise QueueError, "Queue '#{@name}' is disabled. Command at
|
|
125
|
+
raise QueueError, "Queue '#{@name}' is disabled. Command at id #{id} not updated."
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
-
# Check if command exists at the given
|
|
129
|
-
existing = Store.zrangebyscore("#{@scope}:#{@name}",
|
|
128
|
+
# Check if command exists at the given id
|
|
129
|
+
existing = Store.zrangebyscore("#{@scope}:#{@name}", id, id)
|
|
130
130
|
if existing.empty?
|
|
131
|
-
raise QueueError, "No command found at
|
|
131
|
+
raise QueueError, "No command found at id #{id} in queue '#{@name}'"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# Remove the existing command and add the new one at the same
|
|
135
|
-
Store.zremrangebyscore("#{@scope}:#{@name}",
|
|
134
|
+
# Remove the existing command and add the new one at the same id
|
|
135
|
+
Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
|
|
136
136
|
command_data = { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }
|
|
137
|
-
Store.zadd("#{@scope}:#{@name}",
|
|
137
|
+
Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
|
|
138
138
|
notify(kind: 'command')
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
def remove_command(
|
|
141
|
+
def remove_command(id = nil)
|
|
142
142
|
if @state == 'DISABLE'
|
|
143
143
|
raise QueueError, "Queue '#{@name}' is disabled. Command not removed."
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
if
|
|
147
|
-
# Remove specific
|
|
148
|
-
result = Store.zrangebyscore("#{@scope}:#{@name}",
|
|
146
|
+
if id
|
|
147
|
+
# Remove specific id
|
|
148
|
+
result = Store.zrangebyscore("#{@scope}:#{@name}", id, id)
|
|
149
149
|
if result.empty?
|
|
150
150
|
return nil
|
|
151
151
|
else
|
|
152
|
-
Store.zremrangebyscore("#{@scope}:#{@name}",
|
|
152
|
+
Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
|
|
153
153
|
command_data = JSON.parse(result[0])
|
|
154
|
-
command_data['
|
|
154
|
+
command_data['id'] = id.to_f
|
|
155
155
|
notify(kind: 'command')
|
|
156
156
|
return command_data
|
|
157
157
|
end
|
|
@@ -164,7 +164,7 @@ module OpenC3
|
|
|
164
164
|
score = result[0][1]
|
|
165
165
|
Store.zremrangebyscore("#{@scope}:#{@name}", score, score)
|
|
166
166
|
command_data = JSON.parse(result[0][0])
|
|
167
|
-
command_data['
|
|
167
|
+
command_data['id'] = score.to_f
|
|
168
168
|
notify(kind: 'command')
|
|
169
169
|
return command_data
|
|
170
170
|
end
|
|
@@ -174,7 +174,7 @@ module OpenC3
|
|
|
174
174
|
def list
|
|
175
175
|
return Store.zrange("#{@scope}:#{@name}", 0, -1, with_scores: true).map do |item|
|
|
176
176
|
result = JSON.parse(item[0])
|
|
177
|
-
result['
|
|
177
|
+
result['id'] = item[1].to_f
|
|
178
178
|
result
|
|
179
179
|
end
|
|
180
180
|
end
|
|
@@ -143,7 +143,7 @@ module OpenC3
|
|
|
143
143
|
unless triggers.is_a?(Array)
|
|
144
144
|
raise ReactionInputError.new "invalid triggers, must be array of hashes: #{triggers}"
|
|
145
145
|
end
|
|
146
|
-
trigger_hash =
|
|
146
|
+
trigger_hash = {}
|
|
147
147
|
triggers.each do | trigger |
|
|
148
148
|
unless trigger.is_a?(Hash)
|
|
149
149
|
raise ReactionInputError.new "invalid trigger, must be hash: #{trigger}"
|
|
@@ -166,10 +166,14 @@ module OpenC3
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def self.download(target_name, scope:)
|
|
169
|
+
# Validate target_name to not allow directory traversal
|
|
170
|
+
if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
|
|
171
|
+
raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
|
|
172
|
+
end
|
|
169
173
|
tmp_dir = Dir.mktmpdir
|
|
170
174
|
zip_filename = File.join(tmp_dir, "#{target_name}.zip")
|
|
171
175
|
Zip.continue_on_exists_proc = true
|
|
172
|
-
zip = Zip::File.open(zip_filename,
|
|
176
|
+
zip = Zip::File.open(zip_filename, create: true)
|
|
173
177
|
|
|
174
178
|
if ENV['OPENC3_LOCAL_MODE']
|
|
175
179
|
OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
|
|
@@ -756,7 +760,7 @@ module OpenC3
|
|
|
756
760
|
prefix = File.dirname(target_folder) + '/'
|
|
757
761
|
output_file = File.join(temp_dir, @name + '_' + @id + '.zip')
|
|
758
762
|
Zip.continue_on_exists_proc = true
|
|
759
|
-
Zip::File.open(output_file,
|
|
763
|
+
Zip::File.open(output_file, create: true) do |zipfile|
|
|
760
764
|
target_files.each do |target_file|
|
|
761
765
|
zip_file_path = target_file.delete_prefix(prefix)
|
|
762
766
|
if File.directory?(target_file)
|
|
@@ -807,7 +811,7 @@ module OpenC3
|
|
|
807
811
|
Logger.error("Invalid text present in #{target_name} #{packet_name} tlm packet")
|
|
808
812
|
raise e
|
|
809
813
|
end
|
|
810
|
-
json_hash =
|
|
814
|
+
json_hash = {}
|
|
811
815
|
packet.sorted_items.each do |item|
|
|
812
816
|
json_hash[item.name] = nil
|
|
813
817
|
TargetModel.add_to_target_allitems_list(target_name, item.name, scope: @scope)
|
data/lib/openc3/script/queue.rb
CHANGED
|
@@ -31,7 +31,7 @@ module OpenC3
|
|
|
31
31
|
raise "Failed to #{action}. No response from server."
|
|
32
32
|
elsif response.status != 200 and response.status != 201
|
|
33
33
|
result = JSON.parse(response.body, allow_nan: true, create_additions: true)
|
|
34
|
-
raise "
|
|
34
|
+
raise "#{action} failed with status #{response.status}. #{result['message']}."
|
|
35
35
|
end
|
|
36
36
|
return JSON.parse(response.body, allow_nan: true, create_additions: true)
|
|
37
37
|
end
|
|
@@ -66,12 +66,20 @@ module OpenC3
|
|
|
66
66
|
return _make_request(action: 'disable queue', verb: 'post', uri: "/openc3-api/queues/#{name}/disable", scope: scope)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
def queue_exec(name,
|
|
69
|
+
def queue_exec(name, id=nil, scope: $openc3_scope)
|
|
70
70
|
data = {}
|
|
71
|
-
data['
|
|
71
|
+
data['id'] = id if id
|
|
72
72
|
return _make_request(action: 'exec command', verb: 'post', uri: "/openc3-api/queues/#{name}/exec_command", data: data, scope: scope)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# No queue_insert because we do that through the cmd APIs with the queue kwarg
|
|
76
|
+
|
|
77
|
+
def queue_remove(name, id=nil, scope: $openc3_scope)
|
|
78
|
+
data = {}
|
|
79
|
+
data['id'] = id if id
|
|
80
|
+
return _make_request(action: 'remove command', verb: 'post', uri: "/openc3-api/queues/#{name}/remove_command", data: data, scope: scope)
|
|
81
|
+
end
|
|
82
|
+
|
|
75
83
|
def queue_delete(name, scope: $openc3_scope)
|
|
76
84
|
return _make_request(action: 'delete queue', verb: 'delete', uri: "/openc3-api/queues/#{name}", scope: scope)
|
|
77
85
|
end
|
data/lib/openc3/script/script.rb
CHANGED
|
@@ -232,7 +232,7 @@ module OpenC3
|
|
|
232
232
|
raise "initialize_offline_access only valid in COSMOS Enterprise. OPENC3_KEYCLOAK_URL environment variable must be set."
|
|
233
233
|
end
|
|
234
234
|
auth = OpenC3KeycloakAuthentication.new(keycloak_url)
|
|
235
|
-
auth.token(include_bearer: true, openid_scope: 'openid
|
|
235
|
+
auth.token(include_bearer: true, openid_scope: 'openid offline_access')
|
|
236
236
|
set_offline_access(auth.refresh_token)
|
|
237
237
|
end
|
|
238
238
|
|
data/lib/openc3/system/system.rb
CHANGED
|
@@ -95,11 +95,11 @@ module OpenC3
|
|
|
95
95
|
bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_current.zip"
|
|
96
96
|
Logger.info("Retrieving #{bucket_key} from targets bucket")
|
|
97
97
|
bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: zip_path)
|
|
98
|
+
targets_path = "#{base_dir}/targets"
|
|
99
|
+
FileUtils.mkdir_p(targets_path)
|
|
98
100
|
Zip::File.open(zip_path) do |zip_file|
|
|
99
101
|
zip_file.each do |entry|
|
|
100
|
-
|
|
101
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
102
|
-
zip_file.extract(entry, path) unless File.exist?(path)
|
|
102
|
+
zip_file.extract(entry.name, destination_directory: targets_path)
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
require 'openc3/version'
|
|
24
24
|
require 'openc3/io/json_drb'
|
|
25
25
|
require 'faraday'
|
|
26
|
+
require 'uri'
|
|
26
27
|
|
|
27
28
|
module OpenC3
|
|
28
29
|
# Basic exception for known errors
|
|
@@ -112,9 +113,13 @@ module OpenC3
|
|
|
112
113
|
client_id = ENV['OPENC3_API_CLIENT'] || 'api'
|
|
113
114
|
if ENV['OPENC3_API_USER'] and ENV['OPENC3_API_PASSWORD']
|
|
114
115
|
# Username and password
|
|
115
|
-
data =
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
data = {
|
|
117
|
+
'username' => ENV['OPENC3_API_USER'],
|
|
118
|
+
'password' => ENV['OPENC3_API_PASSWORD'],
|
|
119
|
+
'client_id' => client_id,
|
|
120
|
+
'grant_type' => 'password',
|
|
121
|
+
'scope' => openid_scope
|
|
122
|
+
}
|
|
118
123
|
headers = {
|
|
119
124
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
120
125
|
'User-Agent' => "OpenC3KeycloakAuthorization / #{OPENC3_VERSION} (ruby/openc3/lib/utilities/authentication)",
|
|
@@ -134,7 +139,11 @@ module OpenC3
|
|
|
134
139
|
# Refresh the token and save token to instance
|
|
135
140
|
def _refresh_token(current_time)
|
|
136
141
|
client_id = ENV['OPENC3_API_CLIENT'] || 'api'
|
|
137
|
-
data =
|
|
142
|
+
data = {
|
|
143
|
+
'client_id' => client_id,
|
|
144
|
+
'refresh_token' => @refresh_token,
|
|
145
|
+
'grant_type' => 'refresh_token'
|
|
146
|
+
}
|
|
138
147
|
headers = {
|
|
139
148
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
140
149
|
'User-Agent' => "OpenC3KeycloakAuthorization / #{OPENC3_VERSION} (ruby/openc3/lib/utilities/authentication)",
|
|
@@ -150,11 +159,21 @@ module OpenC3
|
|
|
150
159
|
def _make_request(headers, data)
|
|
151
160
|
realm = ENV['OPENC3_KEYCLOAK_REALM'] || 'openc3'
|
|
152
161
|
uri = URI("#{@url}/realms/#{realm}/protocol/openid-connect/token")
|
|
153
|
-
|
|
162
|
+
# Obfuscate password and refresh token in logs unless debug mode is enabled
|
|
163
|
+
if JsonDRb.debug?
|
|
164
|
+
log_data = data.inspect
|
|
165
|
+
else
|
|
166
|
+
# Create a copy of the hash with sensitive values obfuscated
|
|
167
|
+
log_data = data.dup
|
|
168
|
+
log_data['password'] = '***' if log_data.key?('password')
|
|
169
|
+
log_data['refresh_token'] = '***' if log_data.key?('refresh_token')
|
|
170
|
+
log_data = log_data.inspect
|
|
171
|
+
end
|
|
172
|
+
@log[0] = "request uri: #{uri} header: #{headers} body: #{log_data}"
|
|
154
173
|
STDOUT.puts @log[0] if JsonDRb.debug?
|
|
155
174
|
saved_verbose = $VERBOSE; $VERBOSE = nil
|
|
156
175
|
begin
|
|
157
|
-
resp = @http.post(uri, data, headers)
|
|
176
|
+
resp = @http.post(uri, URI.encode_www_form(data), headers)
|
|
158
177
|
ensure
|
|
159
178
|
$VERBOSE = saved_verbose
|
|
160
179
|
end
|