From 76749fe47c65c4dc954987af5501a82152b856d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Fri, 6 Mar 2026 16:33:58 +0000 Subject: [PATCH 1/6] Create rb_http_agent.rb --- resources/scripts/rb_http_agent.rb | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 resources/scripts/rb_http_agent.rb diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb new file mode 100644 index 00000000..9147e0f5 --- /dev/null +++ b/resources/scripts/rb_http_agent.rb @@ -0,0 +1,137 @@ +#!/usr/bin/env ruby +####################################################################### +## Copyright (c) 2026 ENEO TecnologĂ­a S.L. +## This file is part of redBorder. +## redBorder is free software: you can redistribute it and/or modify +## it under the terms of the GNU Affero General Public License License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## redBorder is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Affero General Public License License for more details. +## You should have received a copy of the GNU Affero General Public License License +## along with redBorder. If not, see . +######################################################################## + +require 'optparse' +require 'json' +require 'logger' +require 'net/http' + +logger = Logger.new($stdout) +logger.level = Logger::DEBUG +logger.formatter = proc do |severity, _datetime, _progname, msg| + "#{severity}: #{msg}\n" +end + +options = { redirect: false, status: 200 } + +OptionParser.new do |opts| + opts.banner = "Usage: #{__FILE__} [-i IP_ADDRESS] [-p PORT] [-t TYPE] [-b BODY] [-h HEADERS] [-status STATUS]" + opts.on('-u URL', '--url URL', 'URL to request') { |v| options[:url] = v } + opts.on('-X TYPE', '--type TYPE', 'Request type') { |v| options[:type] = v } + opts.on('-d BODY', '--body BODY', 'Request body') { |v| options[:body] = v } + opts.on('-H HEADERS', '--headers HEADERS', 'Request headers (JSON)') { |v| options[:headers] = JSON.parse(v) } + opts.on('-s STATUS', '--status STATUS', Integer, 'Expected response status') { |v| options[:status] = v } + + opts.on('-L', '--redirect', 'Follow redirects') { options[:redirect] = true } + opts.on('-x PROXY', '--proxy PROXY', 'Proxy URL') { |v| options[:proxy] = v } + + opts.on('-a AUTH', '--http-auth AUTH', 'HTTP server authentication method') { |v| options[:http_auth] = v } + opts.on('-U USER', '--user USER', 'HTTP server authentication user') { |v| options[:http_user] = v } + opts.on('-P PASS', '--password PASS', 'HTTP server authentication password') { |v| options[:http_pass] = v } + + opts.on('--ssl-verify-peer', 'Verify SSL peer') { options[:ssl_peer] = true } + opts.on('--ssl-cert CERT', 'Path to SSL certificate') { |v| options[:ssl_cert] = v } + opts.on('--ssl-key KEY', 'Path to SSL private key') { |v| options[:ssl_key] = v } + opts.on('--ssl-key-pass PASS', 'Password for SSL private key') { |v| options[:ssl_key_pass] = v } + opts.on('--timeout TIMEOUT', Integer, 'Request timeout in seconds') { |v| options[:timeout] = v } + opts.on('-h', '--help', 'Show this help') { puts opts; exit } +end.parse! + +unless options[:url] && options[:type] + logger.error('Must specify --url URL and --type TYPE') + exit 1 +end + +begin + uri = URI(options[:url]) + http = if options[:proxy] + proxy_uri = URI(options[:proxy]) + Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port) + else + Net::HTTP.new(uri.host, uri.port) + end + http.use_ssl = (uri.scheme == 'https') + http.verify_mode = options[:ssl_peer] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE + if options[:ssl_cert] && options[:ssl_key] + http.cert = OpenSSL::X509::Certificate.new(File.read(options[:ssl_cert])) + http.key = OpenSSL::PKey.read(File.read(options[:ssl_key]), options[:ssl_key_pass]) + elsif options[:ssl_cert] || options[:ssl_key] + logger.error('Both -ssl-cert and -ssl-key must be specified together') + exit 1 + end + http.open_timeout = options[:timeout] if options[:timeout] + http.read_timeout = options[:timeout] if options[:timeout] + + request_class = case options[:type].upcase + when 'GET' then Net::HTTP::Get + when 'POST' then Net::HTTP::Post + when 'PUT' then Net::HTTP::Put + when 'HEAD' then Net::HTTP::Head + else + logger.error("Unsupported request type: #{options[:type]}") + exit 1 + end + + request = request_class.new(uri) + options[:headers]&.each { |k, v| request[k] = v } + request.body = options[:body] if options[:body] + + if options[:http_auth] + case options[:http_auth]&.downcase + when 'basic' + request.basic_auth(options[:http_user], options[:http_pass]) + # TODO: Implement other authentication methods if needed + # when 'digest' + # when 'ntlm' + # when 'kerberos' + else + logger.error("Unsupported HTTP authentication method: #{options[:http_auth]}") + exit 1 + end + end + + response = http.request(request) + + if options[:redirect] + while response.is_a?(Net::HTTPRedirection) + uri = URI(response['location']) + request = request_class.new(uri) + response = http.request(request) + end + end + + if response + result = { + status: response.code.to_i, + message: response.message, + headers: response.each_header.to_h, + body: response.body + } + + puts JSON.pretty_generate(result) + end + + if response.code.to_i == options[:status] + logger.info("Request successful with expected status #{options[:status]}") + exit 0 + else + logger.error("Unexpected response status: #{response.code}") + exit 1 + end +rescue => e + logger.error("Request failed: #{e.message}") + exit 1 +end From 102ef9d98bf45101b0b452d7f7a5546535b59e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Fri, 6 Mar 2026 17:14:20 +0000 Subject: [PATCH 2/6] Better arguments descriptions and validations --- resources/scripts/rb_http_agent.rb | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) mode change 100644 => 100755 resources/scripts/rb_http_agent.rb diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb old mode 100644 new mode 100755 index 9147e0f5..203e4886 --- a/resources/scripts/rb_http_agent.rb +++ b/resources/scripts/rb_http_agent.rb @@ -25,18 +25,24 @@ "#{severity}: #{msg}\n" end -options = { redirect: false, status: 200 } +# Define default options +options = { redirect: false, status: 200, timeout: 5, headers: {} } OptionParser.new do |opts| opts.banner = "Usage: #{__FILE__} [-i IP_ADDRESS] [-p PORT] [-t TYPE] [-b BODY] [-h HEADERS] [-status STATUS]" - opts.on('-u URL', '--url URL', 'URL to request') { |v| options[:url] = v } - opts.on('-X TYPE', '--type TYPE', 'Request type') { |v| options[:type] = v } + opts.on('-u URL', '--url URL', 'URL to connect to and retrieve data') { |v| options[:url] = v } + opts.on('-X TYPE', '--type TYPE', 'Request method type: GET, POST, PUT or HEAD') { |v| options[:type] = v.upcase } opts.on('-d BODY', '--body BODY', 'Request body') { |v| options[:body] = v } - opts.on('-H HEADERS', '--headers HEADERS', 'Request headers (JSON)') { |v| options[:headers] = JSON.parse(v) } - opts.on('-s STATUS', '--status STATUS', Integer, 'Expected response status') { |v| options[:status] = v } + opts.on('-H HEADERS', '--headers HEADERS', 'Custom request headers (JSON)') do |v| + options[:headers] = JSON.parse(v) + rescue JSON::ParserError + logger.error("Invalid JSON for headers: #{v}") + exit 1 + end + opts.on('-s STATUS', '--status STATUS', Integer, 'Expected HTTP response status') { |v| options[:status] = v } - opts.on('-L', '--redirect', 'Follow redirects') { options[:redirect] = true } - opts.on('-x PROXY', '--proxy PROXY', 'Proxy URL') { |v| options[:proxy] = v } + opts.on('-L', '--redirect', 'Follow HTTP redirects') { options[:redirect] = true } + opts.on('-x PROXY', '--proxy PROXY', 'HTTP Proxy URL using format [protocol://][username[:password]@]proxy.example.com[:port]') { |v| options[:proxy] = v } opts.on('-a AUTH', '--http-auth AUTH', 'HTTP server authentication method') { |v| options[:http_auth] = v } opts.on('-U USER', '--user USER', 'HTTP server authentication user') { |v| options[:http_user] = v } @@ -46,7 +52,8 @@ opts.on('--ssl-cert CERT', 'Path to SSL certificate') { |v| options[:ssl_cert] = v } opts.on('--ssl-key KEY', 'Path to SSL private key') { |v| options[:ssl_key] = v } opts.on('--ssl-key-pass PASS', 'Password for SSL private key') { |v| options[:ssl_key_pass] = v } - opts.on('--timeout TIMEOUT', Integer, 'Request timeout in seconds') { |v| options[:timeout] = v } + + opts.on('--timeout TIMEOUT', Integer, 'Request timeout in seconds') { |v| options[:timeout] = v if v > 0 } opts.on('-h', '--help', 'Show this help') { puts opts; exit } end.parse! @@ -55,6 +62,11 @@ exit 1 end +if (options[:ssl_cert] && !options[:ssl_key]) || (!options[:ssl_cert] && options[:ssl_key]) + logger.error('Both --ssl-cert and --ssl-key must be provided together') + exit 1 +end + begin uri = URI(options[:url]) http = if options[:proxy] From 7b17ae6c58040d48eb55ba3d91fcfc06022f57aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Mon, 9 Mar 2026 13:02:07 +0000 Subject: [PATCH 3/6] Add user and password for proxy --- resources/scripts/rb_http_agent.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb index 203e4886..86b2776d 100755 --- a/resources/scripts/rb_http_agent.rb +++ b/resources/scripts/rb_http_agent.rb @@ -71,7 +71,9 @@ uri = URI(options[:url]) http = if options[:proxy] proxy_uri = URI(options[:proxy]) - Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port) + proxy_user = proxy_uri.user + proxy_pass = proxy_uri.password + Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port, proxy_user, proxy_pass) else Net::HTTP.new(uri.host, uri.port) end From 34790ade39351e15f9dd6757eec095559d6ebb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Mon, 9 Mar 2026 18:22:13 +0000 Subject: [PATCH 4/6] Add support to ssl_ca_file and fix ssl_peer --- resources/scripts/rb_http_agent.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb index 86b2776d..71e2b9aa 100755 --- a/resources/scripts/rb_http_agent.rb +++ b/resources/scripts/rb_http_agent.rb @@ -49,6 +49,7 @@ opts.on('-P PASS', '--password PASS', 'HTTP server authentication password') { |v| options[:http_pass] = v } opts.on('--ssl-verify-peer', 'Verify SSL peer') { options[:ssl_peer] = true } + opts.on('--ssl-ca-file FILE', 'Path to CA certificate file for SSL verification') { |v| options[:ssl_ca_file] = v } opts.on('--ssl-cert CERT', 'Path to SSL certificate') { |v| options[:ssl_cert] = v } opts.on('--ssl-key KEY', 'Path to SSL private key') { |v| options[:ssl_key] = v } opts.on('--ssl-key-pass PASS', 'Password for SSL private key') { |v| options[:ssl_key_pass] = v } @@ -77,8 +78,23 @@ else Net::HTTP.new(uri.host, uri.port) end + http.use_ssl = (uri.scheme == 'https') - http.verify_mode = options[:ssl_peer] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE + + if http.use_ssl? + if options[:ssl_peer] + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + + cert_store = OpenSSL::X509::Store.new + cert_store.set_default_paths + cert_store.add_file(options[:ssl_ca_file]) if options[:ssl_ca_file] + + http.cert_store = cert_store + else + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + if options[:ssl_cert] && options[:ssl_key] http.cert = OpenSSL::X509::Certificate.new(File.read(options[:ssl_cert])) http.key = OpenSSL::PKey.read(File.read(options[:ssl_key]), options[:ssl_key_pass]) From 0516387e870d697fe849ffb2dae03448ace4cc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Tue, 10 Mar 2026 15:04:32 +0000 Subject: [PATCH 5/6] Read the RSA instead of PKey --- resources/scripts/rb_http_agent.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb index 71e2b9aa..41be20aa 100755 --- a/resources/scripts/rb_http_agent.rb +++ b/resources/scripts/rb_http_agent.rb @@ -97,7 +97,7 @@ if options[:ssl_cert] && options[:ssl_key] http.cert = OpenSSL::X509::Certificate.new(File.read(options[:ssl_cert])) - http.key = OpenSSL::PKey.read(File.read(options[:ssl_key]), options[:ssl_key_pass]) + http.key = OpenSSL::PKey::RSA.new(File.read(options[:ssl_key]), options[:ssl_key_pass]) elsif options[:ssl_cert] || options[:ssl_key] logger.error('Both -ssl-cert and -ssl-key must be specified together') exit 1 From a8a92166411f39af16012642cd3e56c8f477498c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20P=C3=A9rez?= Date: Tue, 10 Mar 2026 15:08:23 +0000 Subject: [PATCH 6/6] Lint --- resources/scripts/rb_http_agent.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/scripts/rb_http_agent.rb b/resources/scripts/rb_http_agent.rb index 41be20aa..3ccec739 100755 --- a/resources/scripts/rb_http_agent.rb +++ b/resources/scripts/rb_http_agent.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby + ####################################################################### ## Copyright (c) 2026 ENEO TecnologĂ­a S.L. ## This file is part of redBorder. @@ -42,7 +43,10 @@ opts.on('-s STATUS', '--status STATUS', Integer, 'Expected HTTP response status') { |v| options[:status] = v } opts.on('-L', '--redirect', 'Follow HTTP redirects') { options[:redirect] = true } - opts.on('-x PROXY', '--proxy PROXY', 'HTTP Proxy URL using format [protocol://][username[:password]@]proxy.example.com[:port]') { |v| options[:proxy] = v } + opts.on('-x PROXY', '--proxy PROXY', + 'HTTP Proxy URL using format [protocol://][username[:password]@]proxy.example.com[:port]') do |v| + options[:proxy] = v + end opts.on('-a AUTH', '--http-auth AUTH', 'HTTP server authentication method') { |v| options[:http_auth] = v } opts.on('-U USER', '--user USER', 'HTTP server authentication user') { |v| options[:http_user] = v } @@ -54,8 +58,11 @@ opts.on('--ssl-key KEY', 'Path to SSL private key') { |v| options[:ssl_key] = v } opts.on('--ssl-key-pass PASS', 'Password for SSL private key') { |v| options[:ssl_key_pass] = v } - opts.on('--timeout TIMEOUT', Integer, 'Request timeout in seconds') { |v| options[:timeout] = v if v > 0 } - opts.on('-h', '--help', 'Show this help') { puts opts; exit } + opts.on('--timeout TIMEOUT', Integer, 'Request timeout in seconds') { |v| options[:timeout] = v if v.positive? } + opts.on('-h', '--help', 'Show this help') do + puts opts + exit + end end.parse! unless options[:url] && options[:type]