kafkr 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 839d5db49f24a6d283f12d213016c498834b12e90dcbb29a8e261100870be59f
4
+ data.tar.gz: b2700c8cb00ef4ea12207dc7323251a3e9ece950c60ccc17b9254ba3481763f7
5
+ SHA512:
6
+ metadata.gz: c3b88b1c980c88d577d75d056f723b59f358829f5f231429d15bdf7e4db3487a00d777dfe63cd32e7b12b60742c1c3affb9087fddfafb5fb81f1917ea86bee1f
7
+ data.tar.gz: fb0971030fa3a95cda35423beef5a4da7dcfffaad41c9fa4f7c2081c09074eeadf2afc7ec90df0f4bcd29fb3120dced5702869c998f045ce3fd61f5becac6a00
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Kafkr
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/kafkr`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/kafkr.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
data/exe/kafkr ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ PORT = ENV["KAFKR_PORT"] || 4000
3
+
4
+ begin
5
+ require "kafkr"
6
+ rescue LoadError => e
7
+ puts "Failed to load Kafkr: #{e.message}"
8
+ exit(1)
9
+ end
10
+
11
+ begin
12
+ server = Kafkr::Log.new(PORT.to_i)
13
+ puts "Log started on port #{PORT}!"
14
+ server.start
15
+ rescue => e
16
+ puts "An error occurred: #{e.message}"
17
+ exit(1)
18
+ rescue Interrupt
19
+ puts "\nLog server shutting down gracefully..."
20
+ server.stop if server.respond_to?(:stop)
21
+ exit(0)
22
+ end
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "kafkr"
4
+ require "find"
5
+ require "digest"
6
+
7
+ # Accepting command line arguments for host and port
8
+ host = ARGV[0] || "localhost"
9
+ port = ARGV[1] || 4000
10
+
11
+ puts "Running on host: #{host} and port: #{port}"
12
+
13
+ $current_consumer = nil
14
+ $restart_required = false
15
+ $handlers_loaded = false
16
+ $handlers_changed = false
17
+ $loaded_handlers = {}
18
+
19
+ Signal.trap("USR1") do
20
+ $restart_required = true
21
+ end
22
+
23
+ def stop_consumer
24
+ $current_consumer = nil if $current_consumer
25
+ end
26
+
27
+ def list_registered_handlers
28
+ puts "Registered Handlers:"
29
+ Kafkr::Consumer.handlers.each do |handler|
30
+ $loaded_handlers = {}
31
+ handler_name = handler.class.name.split("::").last.gsub(/Handler$/, "")
32
+ puts "#{handler_name} handler registered."
33
+ end
34
+ end
35
+
36
+ def start_consumer(port,host)
37
+ puts "Starting consumer on port #{port}!"
38
+ $handlers_changed = false
39
+
40
+ Kafkr::Consumer.configure do |config|
41
+ config.port = port
42
+ config.host = host
43
+ end
44
+
45
+ unless $handlers_loaded
46
+ Kafkr::Consumer.load_handlers if $handlers_changed == false
47
+ list_registered_handlers
48
+ $handlers_loaded = true
49
+ end
50
+
51
+ $current_consumer = Kafkr::Consumer.new
52
+ $current_consumer.listen do |message|
53
+ # Processing of the message
54
+ end
55
+ end
56
+
57
+ def reload_handlers(file_checksums)
58
+ handlers_before_reload = Kafkr::Consumer.handlers.dup # Store current handlers
59
+ Find.find(Kafkr::Consumer::HANDLERS_DIRECTORY) do |path|
60
+ next unless File.file?(path)
61
+ load path
62
+ end
63
+
64
+ if $handlers_changed
65
+ Kafkr::Consumer.load_handlers
66
+ new_handlers = Kafkr::Consumer.handlers - handlers_before_reload
67
+ if new_handlers.any?
68
+ new_handlers.each do |handler|
69
+ handler_name = handler.class.name.split("::").last.gsub(/Handler$/, "").capitalize
70
+ puts "#{handler_name} handler updated - ok!"
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def monitor_handlers(file_checksums)
77
+ loop do
78
+ changed = false
79
+ Find.find(Kafkr::Consumer::HANDLERS_DIRECTORY) do |path|
80
+ next unless File.file?(path)
81
+
82
+ current_checksum = Digest::MD5.file(path).hexdigest
83
+ if file_checksums[path] != current_checksum
84
+ file_checksums[path] = current_checksum
85
+ changed = true
86
+ end
87
+ end
88
+
89
+ $handlers_changed = changed # Set outside the loop
90
+
91
+ reload_handlers(file_checksums) if $handlers_changed
92
+ sleep 5
93
+ end
94
+ end
95
+
96
+ file_checksums = {}
97
+ monitoring_thread = Thread.new { monitor_handlers(file_checksums) }
98
+ start_consumer(port,host) # Pass the port here
99
+
100
+ begin
101
+ loop do
102
+ if $restart_required
103
+ stop_consumer
104
+ start_consumer(port,host)
105
+ $restart_required = false
106
+ end
107
+ sleep 1
108
+ end
109
+ rescue LoadError => e
110
+ exit(1)
111
+ rescue => e
112
+ exit(1)
113
+ rescue Interrupt
114
+ stop_consumer
115
+ exit(0)
116
+ ensure
117
+ monitoring_thread.kill if monitoring_thread
118
+ end
data/exe/kafkr-keys ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require "openssl"
3
+ require "base64"
4
+
5
+ key = OpenSSL::Random.random_bytes(32)
6
+ encoded_key = Base64.encode64(key).chomp # .chomp to remove newline character from encoded key
7
+
8
+ puts "keys: #{encoded_key}"
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ require 'readline'
3
+
4
+ # Parsing command line arguments for host and port
5
+ host = ARGV[0] || "localhost"
6
+ port = ARGV[1] || 4000
7
+
8
+ puts "Running on host: #{host} and port: #{port}"
9
+
10
+ begin
11
+ require "kafkr"
12
+ rescue LoadError => e
13
+ puts "Failed to load Kafkr: #{e.message}"
14
+ exit(1)
15
+ end
16
+
17
+ begin
18
+ Kafkr::Producer.configure do |config|
19
+ config.host = host
20
+ config.port = port
21
+ end
22
+
23
+ while line = Readline.readline("> ", true)
24
+ break if line == "exit"
25
+
26
+ if line.include? "<=>"
27
+ puts Kafkr::Producer.send_message_and_wait(line)
28
+ else
29
+ Kafkr::Producer.send_message(line)
30
+ end
31
+ end
32
+ rescue => e
33
+ puts "An error occurred: #{e.message}"
34
+ exit(1)
35
+ rescue Interrupt
36
+ puts "\nProducer server shutting down gracefully..."
37
+ exit(0)
38
+ end
@@ -0,0 +1,12 @@
1
+ class WebHandler < Kafkr::Consumer::Handler
2
+ def handle?(message)
3
+ can_handle? message, "web"
4
+ end
5
+
6
+ def handle(message)
7
+ puts message
8
+ if message["sync"]
9
+ reply to: message, payload: {test: "set"}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,278 @@
1
+ require "socket"
2
+ require "timeout"
3
+ require "ostruct"
4
+ require "fileutils"
5
+ require "json"
6
+
7
+ module Kafkr
8
+ class LostConnection < StandardError; end
9
+
10
+ class Consumer
11
+ @handlers = []
12
+
13
+ HANDLERS_DIRECTORY = "./handlers"
14
+
15
+ class << self
16
+ attr_reader :handlers
17
+
18
+ def configuration
19
+ FileUtils.mkdir_p "./.kafkr"
20
+ @configuration ||= OpenStruct.new
21
+ @configuration
22
+ end
23
+
24
+ def configure
25
+ yield(configuration) if block_given?
26
+ end
27
+
28
+ def register_handler(handler)
29
+ @handlers << handler
30
+ end
31
+
32
+ $loaded_handlers = {}
33
+ $handlers_changed = true
34
+
35
+ def list_registered_handlers
36
+ puts "Registered handlers:"
37
+ $loaded_handlers.keys.each { |handler| puts "- #{handler}" }
38
+ end
39
+
40
+ def load_handlers(directory = "./handlers")
41
+ # Load handlers and check for new additions
42
+ Dir.glob("#{directory}/*.rb").each do |file|
43
+ handler_name = File.basename(file, ".rb")
44
+ unless $loaded_handlers[handler_name]
45
+ require file
46
+ $loaded_handlers[handler_name] = true
47
+ $handlers_changed = true
48
+ end
49
+ end
50
+
51
+ # Display handlers if there are changes
52
+ if $handlers_changed
53
+ $handlers_changed = false
54
+ end
55
+ end
56
+ end
57
+
58
+ class Handler
59
+ def handle?(message)
60
+ raise NotImplementedError, "You must implement the handle? method"
61
+ end
62
+
63
+ def handle(message)
64
+ raise NotImplementedError, "You must implement the handle method"
65
+ end
66
+
67
+ # ... rest of your existing Handler methods ...
68
+ def self.inherited(subclass)
69
+ Consumer.register_handler(subclass.new)
70
+ end
71
+
72
+ protected
73
+
74
+ def reply to:, payload:
75
+
76
+ Kafkr::Producer.configure do |config|
77
+ config.host = Consumer.configuration.host
78
+ config.port = Consumer.configuration.port
79
+ end
80
+
81
+ Kafkr::Producer.send_message({reply: {payload: payload, uuid: to['sync_uid']}},acknowledge: false)
82
+ end
83
+
84
+ private
85
+
86
+ def can_handle?(message, name, ignore: :any)
87
+ if message.is_a?(Numeric)
88
+ return true if message == name.to_i
89
+ elsif ignore == :hash
90
+ return true if message[:message] && message[:message][:body] && message[:message][:body] == name
91
+ return true if message[:message] && message[:message][:body] && message[:message][:body].start_with?(name)
92
+ elsif ignore == :string
93
+ return true if message.key? name
94
+ else
95
+ return true if message.key? name
96
+ return true if message[:message] && message[:message][:body] && message[:message][:body] == name
97
+ return true if message[:message] && message[:message][:body] && message[:message][:body].start_with?(name)
98
+ end
99
+ false
100
+ end
101
+ end
102
+
103
+ def initialize(host = Consumer.configuration.host, port = Consumer.configuration.port)
104
+ @host = host
105
+ @port = port
106
+ end
107
+
108
+ def fibonacci(n)
109
+ (n <= 1) ? n : fibonacci(n - 1) + fibonacci(n - 2)
110
+ end
111
+
112
+ def backoff_time(attempt)
113
+ [fibonacci(attempt), fibonacci(5)].min
114
+ end
115
+
116
+ def valid_class_name?(name)
117
+ # A valid class name starts with an uppercase letter and
118
+ # followed by zero or more letters, numbers, or underscores.
119
+ /^[A-Z]\w*$/.match?(name)
120
+ end
121
+
122
+ # sugests a working handler
123
+ def print_handler_class(name)
124
+ return if name.is_a?(Numeric)
125
+
126
+ # If name is a Hash, use its first key
127
+ name = name.keys.first if name.is_a?(Hash)
128
+
129
+ # Generate the handler name based on the naming convention
130
+ handler_name = "#{name.downcase}_handler"
131
+
132
+ # Check if the handler is already loaded
133
+ if $loaded_handlers.key?(handler_name)
134
+ return
135
+ end
136
+
137
+ if valid_class_name? name.capitalize
138
+ puts "No handler for this message, you could use this one."
139
+ puts ""
140
+
141
+ handler_class_string = <<~HANDLER_CLASS
142
+
143
+ class #{name.capitalize}Handler < Kafkr::Consumer::Handler
144
+ def handle?(message)
145
+ can_handle? message, '#{name}'
146
+ end
147
+
148
+ def handle(message)
149
+ puts message
150
+ end
151
+ end
152
+
153
+ save the file to ./handlers/#{name}_handler.rb
154
+
155
+ HANDLER_CLASS
156
+
157
+ puts handler_class_string
158
+ end
159
+ end
160
+
161
+ require "timeout"
162
+
163
+ def listen_for(message, send_message)
164
+ attempt = 0
165
+ begin
166
+ socket = TCPSocket.new( Consumer.configuration.host, Consumer.configuration.port)
167
+ puts "Connected to server. #{Consumer.configuration.host} #{Consumer.configuration.port} " if attempt == 0
168
+ attempt = 0
169
+
170
+ Timeout.timeout(20) do
171
+ # Call the provided send_message method or lambda, passing the message as an argument
172
+ sync_uid = send_message.call(message, acknowledge: false)
173
+
174
+ loop do
175
+ received_message = socket.gets
176
+ raise LostConnection if received_message.nil?
177
+ # Assuming Kafkr::Encryptor is defined elsewhere
178
+ received_message = Kafkr::Encryptor.new.decrypt(received_message.chomp)
179
+ # Yield every received message to the given block
180
+ if valid_json?(received_message)
181
+ payload =yield JSON.parse(received_message),sync_uid if block_given?
182
+ return payload if payload
183
+ end
184
+ end
185
+ end
186
+ rescue Timeout::Error
187
+ puts "Listening timed out after 20 seconds."
188
+ socket&.close
189
+ rescue LostConnection
190
+ attempt += 1
191
+ wait_time = backoff_time(attempt)
192
+ puts "Connection lost. Reconnecting in #{wait_time} seconds..."
193
+ sleep(wait_time)
194
+ rescue Errno::ECONNREFUSED, Timeout::Error
195
+ attempt += 1
196
+ wait_time = backoff_time(attempt)
197
+ puts "Failed to connect on attempt #{attempt}. Retrying in #{wait_time} seconds..."
198
+ sleep(wait_time)
199
+ rescue Interrupt
200
+ puts "Received interrupt signal. Shutting down consumer gracefully..."
201
+ socket&.close
202
+ exit(0)
203
+ end
204
+ end
205
+
206
+ def listen
207
+ attempt = 0
208
+ loop do
209
+ socket = TCPSocket.new(Consumer.configuration.host, Consumer.configuration.port)
210
+ puts "Connected to server. #{Consumer.configuration.host} #{Consumer.configuration.port} " if attempt == 0
211
+ attempt = 0
212
+
213
+ loop do
214
+ message = socket.gets
215
+ raise LostConnection if message.nil?
216
+
217
+ # Assuming Kafkr::Encryptor is defined elsewhere
218
+ message = Kafkr::Encryptor.new.decrypt(message.chomp)
219
+ if valid_json?(message)
220
+ dispatch_to_handlers(JSON.parse(message)) do |message|
221
+ yield message if block_given?
222
+ end
223
+ else
224
+ dispatch_to_handlers(message) do |message|
225
+ yield message if block_given?
226
+ end
227
+ end
228
+ end
229
+ rescue LostConnection
230
+ attempt += 1
231
+ wait_time = backoff_time(attempt)
232
+ puts "Connection lost. Reconnecting in #{wait_time} seconds..."
233
+ sleep(wait_time)
234
+ rescue Errno::ECONNREFUSED, Timeout::Error
235
+ attempt += 1
236
+ wait_time = backoff_time(attempt)
237
+ puts "Failed to connect on attempt #{attempt}. Retrying in #{wait_time} seconds..."
238
+ sleep(wait_time)
239
+ rescue Interrupt
240
+ puts "Received interrupt signal. Shutting down consumer gracefully..."
241
+ socket&.close
242
+ exit(0)
243
+ end
244
+ end
245
+
246
+ def valid_json?(json)
247
+ JSON.parse(json)
248
+ true
249
+ rescue JSON::ParserError
250
+ false
251
+ end
252
+
253
+ alias_method :consume, :listen
254
+ alias_method :receive, :listen
255
+ alias_method :connect, :listen
256
+ alias_method :monitor, :listen
257
+ alias_method :observe, :listen
258
+
259
+ private
260
+
261
+ def dispatch_to_handlers(message)
262
+ message_hash = message.is_a?(String) ? {message: {body: message}} : message
263
+
264
+ self.class.handlers.each do |handler|
265
+ if handler.handle?(message_hash)
266
+ handler.handle(message_hash)
267
+ end
268
+ end
269
+
270
+ print_handler_class(message)
271
+
272
+ yield message_hash if block_given?
273
+ end
274
+ end
275
+
276
+ # Assuming the handlers directory is the default location
277
+ Consumer.load_handlers
278
+ end
@@ -0,0 +1,34 @@
1
+ require "openssl"
2
+ require "base64"
3
+
4
+ module Kafkr
5
+ class Encryptor
6
+ ALGORITHM = "aes-256-cbc"
7
+
8
+ def initialize
9
+ @key = Base64.decode64("2wZ85yxQe0lmiQ5nsqdmPWoGB0W6HZW8S/UXVTLQ6WY=")
10
+ end
11
+
12
+ def encrypt(data)
13
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
14
+ cipher.encrypt
15
+ cipher.key = @key
16
+ iv = cipher.random_iv
17
+ encrypted_data = cipher.update(data) + cipher.final
18
+ encrypted_data = Base64.strict_encode64(iv + encrypted_data)
19
+ end
20
+
21
+ def decrypt(encrypted_data)
22
+ # puts "Encrypted data before decoding: #{encrypted_data.inspect}"
23
+ decipher = OpenSSL::Cipher.new(ALGORITHM)
24
+ decipher.decrypt
25
+ decipher.key = @key
26
+ raw_data = Base64.strict_decode64(encrypted_data)
27
+ decipher.iv = raw_data[0, decipher.iv_len]
28
+ decipher.update(raw_data[decipher.iv_len..-1]) + decipher.final
29
+ rescue OpenSSL::Cipher::CipherError => e
30
+ puts "Decryption failed: #{e.message}"
31
+ nil
32
+ end
33
+ end
34
+ end
data/lib/kafkr/log.rb ADDED
@@ -0,0 +1,118 @@
1
+ require "socket"
2
+ require "rubygems"
3
+
4
+ module Kafkr
5
+ class Log
6
+ def initialize(port)
7
+ @server = TCPServer.new(port)
8
+ @received_file = "./.kafkr/log.txt"
9
+ @broker = MessageBroker.new
10
+ @whitelist = load_whitelist
11
+ @acknowledged_message_ids = load_acknowledged_message_ids
12
+ end
13
+
14
+ def load_acknowledged_message_ids
15
+ unless File.exist?("./.kafkr/acknowledged_message_ids.txt")
16
+ `mkdir -p ./.kafkr`
17
+ `touch ./.kafkr/acknowledged_message_ids.txt`
18
+ end
19
+
20
+ config_path = File.expand_path("./.kafkr/acknowledged_message_ids.txt")
21
+ return [] unless File.exist?(config_path)
22
+
23
+ File.readlines(config_path).map(&:strip)
24
+ rescue Errno::ENOENT, Errno::EACCES => e
25
+ puts "Error loading acknowledged_message_ids: #{e.message}"
26
+ []
27
+ end
28
+
29
+ def start
30
+ loop do
31
+ client = @server.accept
32
+ client_ip = client.peeraddr[3]
33
+
34
+ unless whitelisted?(client_ip)
35
+ puts "Connection from non-whitelisted IP: #{client_ip}. Ignored."
36
+ client.close
37
+ next
38
+ end
39
+
40
+ @broker.add_subscriber(client)
41
+
42
+ Thread.new do
43
+ loop do
44
+ encrypted_message = client.gets
45
+ if encrypted_message.nil?
46
+ @broker.last_sent.delete(client)
47
+ client.close
48
+ @broker.subscribers.delete(client)
49
+ puts "Client connection closed. Removed from subscribers list."
50
+ break
51
+ else
52
+ decryptor = Kafkr::Encryptor.new
53
+ message = decryptor.decrypt(encrypted_message.chomp) # Decrypt the message here
54
+ uuid, message_content = extract_uuid(message)
55
+ if uuid && message_content
56
+ if @acknowledged_message_ids.include?(uuid)
57
+ acknowledge_existing_message(uuid, client)
58
+ else
59
+ acknowledge_message(uuid, client)
60
+ persist_received_message(uuid)
61
+ @acknowledged_message_ids << uuid
62
+ @broker.broadcast(message_content)
63
+ end
64
+ else
65
+ puts "Received invalid message format: #{message}"
66
+ end
67
+ end
68
+ rescue Errno::ECONNRESET
69
+ puts "Connection reset by client. Closing connection..."
70
+ client.close
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def load_whitelist
77
+ whitelist = ["localhost", "::1","127.0.0.1"]
78
+ if File.exist?("whitelist.txt")
79
+ File.readlines("whitelist.txt").each do |line|
80
+ ip = line.strip.sub(/^::ffff:/, "")
81
+ whitelist << ip
82
+ end
83
+ end
84
+ whitelist
85
+ end
86
+
87
+ def whitelisted?(ip)
88
+ @whitelist.include?(ip.gsub("::ffff:", ""))
89
+ end
90
+
91
+ private
92
+
93
+ def extract_uuid(message)
94
+ match_data = /^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}): (.+)$/.match(message)
95
+ match_data ? [match_data[1], match_data[2]] : [nil, nil]
96
+ end
97
+
98
+ def acknowledge_message(uuid, client)
99
+ puts "Received message with UUID #{uuid}. Acknowledged."
100
+ acknowledgment_message = "ACK: #{uuid}"
101
+ client.puts(acknowledgment_message)
102
+ puts "Acknowledgment sent to producer: #{acknowledgment_message}"
103
+ end
104
+
105
+ def acknowledge_existing_message(uuid, client)
106
+ puts "Received duplicate message with UUID #{uuid}. Already Acknowledged."
107
+ acknowledgment_message = "ACK-DUPLICATE: #{uuid}"
108
+ client.puts(acknowledgment_message)
109
+ puts "Duplicate acknowledgment sent to producer: #{acknowledgment_message}"
110
+ end
111
+
112
+ def persist_received_message(uuid)
113
+ File.open("./.kafkr/acknowledged_message_ids.txt", "a") do |file|
114
+ file.puts(uuid)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,33 @@
1
+ module Kafkr
2
+ class MessageBroker
3
+ attr_accessor :last_sent, :subscribers
4
+
5
+ def initialize
6
+ @subscribers = []
7
+ @last_sent = {}
8
+ end
9
+
10
+ def add_subscriber(socket)
11
+ @subscribers << socket
12
+ @last_sent[socket] = nil
13
+ end
14
+
15
+ def broadcast(message)
16
+ Kafkr.log message
17
+
18
+ encrypted_message = Kafkr::Encryptor.new.encrypt(message)
19
+
20
+ @subscribers.each do |subscriber|
21
+ if !subscriber.closed?
22
+ subscriber.puts(encrypted_message)
23
+ @last_sent[subscriber] = encrypted_message
24
+ end
25
+ rescue Errno::EPIPE
26
+ # Optionally, handle broken pipe error
27
+ rescue IOError
28
+ @subscribers.delete(subscriber)
29
+ @last_sent.delete(subscriber)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,200 @@
1
+ require "readline"
2
+ require "socket"
3
+ require "fileutils"
4
+ require "securerandom"
5
+ require "ostruct"
6
+ require "json"
7
+ require "fiber"
8
+ module Kafkr
9
+ module Producer
10
+ @@file_mutex = Mutex.new
11
+
12
+ MESSAGE_QUEUE = "./.kafkr/message_queue.txt"
13
+ ACKNOWLEDGED_MESSAGE_QUEUE = "./.kafkr/acknowledged_messages.txt"
14
+
15
+ def self.configuration
16
+ FileUtils.mkdir_p "./.kafkr"
17
+ @configuration ||= OpenStruct.new
18
+ @configuration.queue_file = MESSAGE_QUEUE
19
+ @configuration.acknowledged_file = ACKNOWLEDGED_MESSAGE_QUEUE
20
+ @configuration.message_queue = []
21
+ @configuration.acknowledged_messages = load_acknowledged_messages
22
+ load_queue_from_file
23
+ @configuration
24
+ end
25
+
26
+ def self.configure
27
+ yield(configuration)
28
+ rescue => e
29
+ logger.error("Configuration error: #{e.message}")
30
+ end
31
+
32
+ def self.structured_data_to_hash(input:, sync_uid:)
33
+ # Check the overall structure with regex and make quotes optional
34
+ unless /\A\w+\s*(=>|<=>)\s*((\w+:\s*['"]?[^'",]*['"]?,\s*)*(\w+:\s*['"]?[^'",]*['"]?)\s*)\z/.match?(input)
35
+ return input
36
+ end
37
+
38
+ if input.include?("<=>")
39
+ # puts "sync message"
40
+ # Extract the type and key-value pairs
41
+ type, key_values_str = input.split("<=>").map(&:strip)
42
+
43
+ # puts type
44
+ # puts key_values_str
45
+
46
+ key_values = key_values_str.scan(/(\w+):\s*['"]?([^'",]*)['"]?/)
47
+
48
+ # Convert the array of pairs into a hash, stripping quotes if they exist
49
+ hash_body = key_values.to_h do |key, value|
50
+ [key.to_sym, value.strip.gsub(/\A['"]|['"]\z/, "")]
51
+ end
52
+
53
+ # Return the final hash with the type as the key
54
+ {type.to_sym => hash_body, :sync => true, :sync_uid => sync_uid}
55
+
56
+ else
57
+ # puts "async message"
58
+ # Extract the type and key-value pairs
59
+ type, key_values_str = input.split("=>").map(&:strip)
60
+ key_values = key_values_str.scan(/(\w+):\s*['"]?([^'",]*)['"]?/)
61
+
62
+ # Convert the array of pairs into a hash, stripping quotes if they exist
63
+ hash_body = key_values.to_h do |key, value|
64
+ [key.to_sym, value.strip.gsub(/\A['"]|['"]\z/, "")]
65
+ end
66
+
67
+ # Return the final hash with the type as the key
68
+ {type.to_sym => hash_body}
69
+ end
70
+ end
71
+
72
+ def self.send_message(message, acknowledge: true)
73
+ uuid = SecureRandom.uuid
74
+
75
+ if message.is_a? String
76
+ message = structured_data_to_hash(input: message, sync_uid: uuid)
77
+ message_with_uuid = "#{uuid}: #{message}"
78
+ end
79
+
80
+ if message.is_a?(Hash)
81
+ message_with_uuid = "#{uuid}: #{JSON.generate(message)}"
82
+ end
83
+
84
+ # Encrypt the message here
85
+ encrypted_message_with_uuid = Kafkr::Encryptor.new.encrypt(message_with_uuid)
86
+
87
+ begin
88
+ if acknowledge
89
+ if !@configuration.acknowledged_messages.include?(uuid)
90
+ socket = TCPSocket.new(@configuration.host, @configuration.port)
91
+ listen_for_acknowledgments(socket) if acknowledge
92
+ send_queued_messages(socket)
93
+ # Send the encrypted message instead of the plain one
94
+ socket.puts(encrypted_message_with_uuid)
95
+ else
96
+ puts "Message with UUID #{uuid} has already been acknowledged. Skipping."
97
+ end
98
+ else
99
+ socket = TCPSocket.new(@configuration.host, @configuration.port)
100
+ send_queued_messages(socket)
101
+ socket.puts(encrypted_message_with_uuid)
102
+ end
103
+ rescue Errno::ECONNREFUSED
104
+ puts "Connection refused. Queuing message: #{encrypted_message_with_uuid}"
105
+ # Queue the encrypted message
106
+ @configuration.message_queue.push(encrypted_message_with_uuid)
107
+ save_queue_to_file
108
+ rescue Errno::EPIPE
109
+ puts "Broken pipe error. Retrying connection..."
110
+ retry_connection(encrypted_message_with_uuid)
111
+ end
112
+
113
+ uuid
114
+ end
115
+
116
+ def self.send_message_and_wait(message)
117
+ # Using method(:send_message) to pass the send_message method as a callable object
118
+
119
+ payload = Consumer.new.listen_for(message, self.method(:send_message)) do |received_message,sync_uid|
120
+ if received_message.key? "reply"
121
+ if received_message["reply"].dig('uuid') == sync_uid
122
+ received_message["reply"].dig('payload')
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ payload
129
+ end
130
+
131
+ private
132
+
133
+ def self.listen_for_acknowledgments(socket)
134
+ Thread.new do
135
+ while line = socket.gets
136
+ line = line.chomp
137
+ if line.start_with?("ACK:")
138
+ uuid = line.split(" ")[1]
139
+ handle_acknowledgment(uuid)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def self.handle_acknowledgment(uuid)
146
+ @configuration.acknowledged_messages << uuid
147
+ save_acknowledged_messages
148
+ end
149
+
150
+ def self.retry_connection(message_with_uuid)
151
+ sleep(5)
152
+ send_message(message_with_uuid)
153
+ end
154
+
155
+ def self.send_queued_messages(socket)
156
+ until @configuration.message_queue.empty?
157
+ queued_message = @configuration.message_queue.shift
158
+ socket.puts(queued_message)
159
+ end
160
+ end
161
+
162
+ def self.save_queue_to_file
163
+ @@file_mutex.synchronize do
164
+ File.open(@configuration.queue_file, "w") do |file|
165
+ file.puts(@configuration.message_queue)
166
+ end
167
+ end
168
+ end
169
+
170
+ def self.load_queue_from_file
171
+ @@file_mutex.synchronize do
172
+ if File.exist?(@configuration.queue_file)
173
+ @configuration.message_queue = File.readlines(@configuration.queue_file).map(&:chomp)
174
+ end
175
+ end
176
+ end
177
+
178
+ def self.load_acknowledged_messages
179
+ @@file_mutex.synchronize do
180
+ if File.exist?(@configuration.acknowledged_file)
181
+ File.readlines(@configuration.acknowledged_file).map(&:chomp)
182
+ else
183
+ []
184
+ end
185
+ end
186
+ end
187
+
188
+ def self.save_acknowledged_messages
189
+ @@file_mutex.synchronize do
190
+ File.open(@configuration.acknowledged_file, "w") do |file|
191
+ file.puts(@configuration.acknowledged_messages)
192
+ end
193
+ end
194
+ end
195
+
196
+ def self.logger
197
+ @logger ||= Logger.new(STDOUT)
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kafkr
4
+ VERSION = "0.5.5"
5
+ end
data/lib/kafkr.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "openssl"
5
+ require "securerandom"
6
+ require "ostruct"
7
+ require "gibberish"
8
+
9
+ require_relative "kafkr/encryptor"
10
+ require_relative "kafkr/message_broker"
11
+ require_relative "kafkr/log"
12
+ require_relative "kafkr/consumer"
13
+ require_relative "kafkr/producer"
14
+
15
+ module Kafkr
16
+ class << self
17
+ attr_accessor :current_environment
18
+ def logger
19
+ @logger ||= configure_logger
20
+ end
21
+
22
+ def configure_logger(output = default_output)
23
+ begin
24
+ @logger = ::Logger.new(output)
25
+ rescue Errno::EACCES, Errno::ENOENT => e
26
+ @logger = ::Logger.new(STDOUT)
27
+ @logger.error("Could not open log file: #{e.message}")
28
+ end
29
+ set_logger_level
30
+ @logger
31
+ end
32
+
33
+ def default_output
34
+ case current_environment
35
+ when "production"
36
+ "/var/log/kafkr.log"
37
+ else
38
+ STDOUT
39
+ end
40
+ end
41
+
42
+ def set_logger_level
43
+ @logger.level = case current_environment
44
+ when "development"
45
+ ::Logger::DEBUG
46
+ when "staging"
47
+ ::Logger::INFO
48
+ when "production"
49
+ ::Logger::WARN
50
+ else
51
+ ::Logger::DEBUG
52
+ end
53
+ end
54
+
55
+ def current_environment
56
+ @current_environment ||= ENV["KAFKR_ENV"] || "development"
57
+ end
58
+
59
+ def development?
60
+ current_environment == "development"
61
+ end
62
+
63
+ def test?
64
+ current_environment == "test"
65
+ end
66
+
67
+ def staging?
68
+ current_environment == "staging"
69
+ end
70
+
71
+ def production?
72
+ current_environment == "production"
73
+ end
74
+
75
+ def write(message, unique_id = nil)
76
+ begin
77
+ unique_id ||= SecureRandom.uuid
78
+ rescue => e
79
+ unique_id = "unknown"
80
+ @logger.error("Failed to generate UUID: #{e.message}")
81
+ end
82
+ formatted_message = "[#{unique_id}] #{message}"
83
+
84
+ begin
85
+ puts formatted_message if development?
86
+ logger.info(formatted_message)
87
+ rescue IOError => e
88
+ @logger.error("Failed to write log: #{e.message}")
89
+ end
90
+ end
91
+
92
+ alias_method :log, :write
93
+ alias_method :output, :write
94
+ alias_method :info, :write
95
+ alias_method :record, :write
96
+ alias_method :trace, :write
97
+ end
98
+
99
+ class Error < StandardError; end
100
+
101
+ def self.configuration
102
+ @configuration ||= OpenStruct.new
103
+ end
104
+
105
+ def self.configure
106
+ yield(configuration)
107
+ rescue => e
108
+ logger.error("Configuration error: #{e.message}")
109
+ end
110
+ end
data/sig/kafkr.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Kafkr
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kafkr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.5
5
+ platform: ruby
6
+ authors:
7
+ - Delaney Kuldvee Burke
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gibberish
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ description: A homage to kafkr implmented in ruby
28
+ email:
29
+ - delaney@zero2one.ee
30
+ executables:
31
+ - kafkr
32
+ - kafkr-consumer
33
+ - kafkr-keys
34
+ - kafkr-producer
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".rspec"
39
+ - ".standard.yml"
40
+ - README.md
41
+ - Rakefile
42
+ - exe/kafkr
43
+ - exe/kafkr-consumer
44
+ - exe/kafkr-keys
45
+ - exe/kafkr-producer
46
+ - handlers/web_handler.rb
47
+ - lib/kafkr.rb
48
+ - lib/kafkr/consumer.rb
49
+ - lib/kafkr/encryptor.rb
50
+ - lib/kafkr/log.rb
51
+ - lib/kafkr/message_broker.rb
52
+ - lib/kafkr/producer.rb
53
+ - lib/kafkr/version.rb
54
+ - sig/kafkr.rbs
55
+ homepage: https://zero2one.ee/gems/kafkr
56
+ licenses: []
57
+ metadata:
58
+ homepage_uri: https://zero2one.ee/gems/kafkr
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.6.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.4.21
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: A homage to kafkr implmented in ruby
78
+ test_files: []