Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 80ac4de

Browse files
author
Vicent Marti
committed
statsd: Abstract the Secure Client
All the secure client implementation has been moved into the SecureUDPClient class; the `StatsD` core no longer has any secure-related features. Hence, the secure client must be initialized directly with the shared key, or with `StatsD#add_shard`, which has been updated to transparently forward its arguments to the client constructor. The hashing/HMAC is now performed right before writing to the socket, which allows for greater composability.
1 parent 1ddb9a4 commit 80ac4de

File tree

1 file changed

+47
-36
lines changed

1 file changed

+47
-36
lines changed

lib/statsd.rb

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,52 @@
1414
# statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
1515
# statsd.increment 'activate'
1616
class Statsd
17-
class RubyUdpClient
18-
attr_reader :key, :sock
17+
class SecureUDPClient < UDPClient
18+
def initialize(address, port, key)
19+
super(address, port)
20+
@key = key
21+
end
22+
23+
def send(msg)
24+
super(signed_payload(msg))
25+
end
26+
27+
private
28+
# defer loading openssl and securerandom unless needed. this shaves ~10ms off
29+
# of baseline require load time for environments that don't require message signing.
30+
def self.setup_openssl
31+
@sha256 ||= begin
32+
require 'securerandom'
33+
require 'openssl'
34+
OpenSSL::Digest::SHA256.new
35+
end
36+
end
37+
38+
def signed_payload(message)
39+
sha256 = SecureUDPClient.setup_openssl
40+
payload = timestamp + nonce + message
41+
signature = OpenSSL::HMAC.digest(sha256, @key, payload)
42+
signature + payload
43+
end
44+
45+
def timestamp
46+
[Time.now.to_i].pack("Q<")
47+
end
48+
49+
def nonce
50+
SecureRandom.random_bytes(4)
51+
end
52+
end
53+
54+
class UDPClient
55+
attr_reader :sock
1956

20-
def initialize(address, port, key = nil)
57+
def initialize(address, port = nil)
58+
address, port = address.split(':') if address.include?(':')
2159
addrinfo = Addrinfo.ip(address)
60+
2261
@sock = UDPSocket.new(addrinfo.pfamily)
2362
@sock.connect(addrinfo.ip_address, port)
24-
@key = key
2563
end
2664

2765
def send(msg)
@@ -52,17 +90,16 @@ def namespace=(namespace)
5290

5391
def initialize(client_class = nil)
5492
@shards = []
55-
@client_class = client_class || RubyUdpClient
93+
@client_class = client_class || UDPClient
5694
self.namespace = nil
5795
end
5896

5997
def self.simple(addr, port = nil)
6098
self.new.add_shard(addr, port)
6199
end
62100

63-
def add_shard(addr, port = nil, key = nil)
64-
addr, port = addr.split(':') if addr.include?(':')
65-
@shards << @client_class.new(addr, port.to_i, key)
101+
def add_shard(*args)
102+
@shards << @client_class.new(*args)
66103
self
67104
end
68105

@@ -129,7 +166,6 @@ def time(stat, sample_rate=1)
129166
def histogram(stat, value, sample_rate=1); send stat, value, HISTOGRAM_TYPE, sample_rate end
130167

131168
private
132-
133169
def sampled(sample_rate)
134170
yield unless sample_rate < 1 and rand > sample_rate
135171
end
@@ -140,7 +176,7 @@ def send(stat, delta, type, sample_rate=1)
140176
stat.gsub!(/::/, ".".freeze)
141177
stat.gsub!(RESERVED_CHARS_REGEX, "_".freeze)
142178

143-
msg = ""
179+
msg = String.new
144180
msg << @prefix
145181
msg << stat
146182
msg << ":".freeze
@@ -153,7 +189,7 @@ def send(stat, delta, type, sample_rate=1)
153189
end
154190

155191
shard = select_shard(stat)
156-
shard.send(shard.key ? signed_payload(shard.key, msg) : msg)
192+
shard.send(msg)
157193
end
158194
end
159195

@@ -164,29 +200,4 @@ def select_shard(stat)
164200
@shards[Zlib.crc32(stat) % @shards.size]
165201
end
166202
end
167-
168-
def signed_payload(key, message)
169-
sha256 = Statsd.setup_openssl
170-
payload = timestamp + nonce + message
171-
signature = OpenSSL::HMAC.digest(sha256, key, payload)
172-
signature + payload
173-
end
174-
175-
# defer loading openssl and securerandom unless needed. this shaves ~10ms off
176-
# of baseline require load time for environments that don't require message signing.
177-
def self.setup_openssl
178-
@sha256 ||= begin
179-
require 'securerandom'
180-
require 'openssl'
181-
OpenSSL::Digest::SHA256.new
182-
end
183-
end
184-
185-
def timestamp
186-
[Time.now.to_i].pack("Q<")
187-
end
188-
189-
def nonce
190-
SecureRandom.random_bytes(4)
191-
end
192203
end

0 commit comments

Comments
 (0)