Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions spec/sparoid_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,20 @@ describe Sparoid::Server do
s.try &.close
end
end

describe Sparoid::Client do
describe ".format_send_errors" do
it "joins host, address and message for each error" do
ip1 = Socket::IPAddress.new("1.1.1.1", 1)
ip2 = Socket::IPAddress.new("2.2.2.2", 2)
errors = [
{ip1, Exception.new("no route")},
{ip2, Exception.new("network down")},
]
msg = Sparoid::Client.format_send_errors("example.com", errors)
msg.should contain("example.com")
msg.should contain("1.1.1.1:1: no route")
msg.should contain("2.2.2.2:2: network down")
end
end
end
4 changes: 4 additions & 0 deletions src/client-cli.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
require "log"
require "option_parser"
require "./client"
require "./version"

# Route logs to STDERR (in :connect mode STDOUT is the unix-domain socket used for FD passing).
Log.setup_from_env(default_level: :info, backend: Log::IOBackend.new(STDERR))

subcommand = :none
host = "0.0.0.0"
port = 8484
Expand Down
25 changes: 23 additions & 2 deletions src/client.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "log"
require "socket"
require "random/secure"
require "openssl/cipher"
Expand All @@ -11,6 +12,10 @@ require "wait_group"

module Sparoid
class Client
Log = ::Log.for(self)

class SendError < Exception; end

def self.new(config_path = "~/.sparoid.ini")
key = ENV.fetch("SPAROID_KEY", "")
hmac_key = ENV.fetch("SPAROID_HMAC_KEY", "")
Expand Down Expand Up @@ -70,25 +75,41 @@ module Sparoid
exit 1 # only if all connects fails
end

# Send to all resolved IPs for the hostname
# Send to all resolved IPs for the hostname.
# Per-address failures are logged as warnings if at least one address succeeded.
# If every address fails, raises SendError.
private def self.udp_send(host, port, key : String, hmac_key : String, public_ip : String? = nil) : Array(String)
host_addresses = Socket::Addrinfo.udp(host, port)
errors = [] of {Socket::IPAddress, Exception}
successes = 0
host_addresses.each do |addrinfo|
packages = generate_messages(addrinfo.ip_address, public_ip).map { |message| generate_package(key, hmac_key, message) }
socket = UDPSocket.new(addrinfo.family)
begin
packages.each do |data|
socket.send data, to: addrinfo.ip_address
end
successes += 1
rescue ex
STDERR << "Sparoid error sending " << ex.inspect << "\n"
errors << {addrinfo.ip_address, ex}
Comment thread
oskgu360 marked this conversation as resolved.
Outdated
ensure
socket.close
end
end
if successes.zero? && !errors.empty?
raise SendError.new(format_send_errors(host, errors))
end
errors.each do |ip, ex|
Log.warn { "skip #{host} (#{ip}): #{ex.message}" }
Comment thread
oskgu360 marked this conversation as resolved.
Outdated
end
host_addresses.map &.ip_address.address
end

def self.format_send_errors(host : String, errors : Array({Socket::IPAddress, Exception})) : String
Comment thread
oskgu360 marked this conversation as resolved.
Outdated
details = errors.map { |ip, ex| "#{ip}: #{ex.message}" }.join("; ")
"Sparoid: failed to send to any address for #{host}: #{details}"
Comment thread
oskgu360 marked this conversation as resolved.
Outdated
end

private def self.encrypt(key, hmac_key, data) : Bytes
cipher = OpenSSL::Cipher.new("aes-256-cbc")
cipher.encrypt
Expand Down
Loading