feat(sdk): add Java and Ruby SDK wrappers over C FFI
Java SDK: JNI bindings to libquicproquo_ffi with QpqClient class, Gradle build, and exception hierarchy matching Kotlin SDK. Ruby SDK: FFI gem wrapping libquicproquo_ffi with Client class, block-form auto-disconnect, gemspec for RubyGems publishing, and example script.
This commit is contained in:
23
sdks/ruby/lib/quicproquo.rb
Normal file
23
sdks/ruby/lib/quicproquo.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "quicproquo/ffi_bindings"
|
||||
require_relative "quicproquo/client"
|
||||
require_relative "quicproquo/errors"
|
||||
require_relative "quicproquo/version"
|
||||
|
||||
# Ruby SDK for the quicproquo E2E encrypted messenger.
|
||||
#
|
||||
# Two usage patterns:
|
||||
#
|
||||
# # Block form (auto-disconnect)
|
||||
# QuicProQuo::Client.open("127.0.0.1:5001", ca_cert: "ca.pem") do |client|
|
||||
# client.login("alice", "secret")
|
||||
# client.send("bob", "hello")
|
||||
# end
|
||||
#
|
||||
# # Manual lifecycle
|
||||
# client = QuicProQuo::Client.new("127.0.0.1:5001", ca_cert: "ca.pem")
|
||||
# client.login("alice", "secret")
|
||||
# client.disconnect
|
||||
module QuicProQuo
|
||||
end
|
||||
122
sdks/ruby/lib/quicproquo/client.rb
Normal file
122
sdks/ruby/lib/quicproquo/client.rb
Normal file
@@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
|
||||
module QuicProQuo
|
||||
# High-level quicproquo client for Ruby.
|
||||
#
|
||||
# Wraps +libquicproquo_ffi+ via the +ffi+ gem.
|
||||
#
|
||||
# client = QuicProQuo::Client.new("127.0.0.1:5001", ca_cert: "ca.pem")
|
||||
# client.login("alice", "secret")
|
||||
# client.send("bob", "hello")
|
||||
# messages = client.receive(timeout_ms: 5000)
|
||||
# client.disconnect
|
||||
#
|
||||
# Or use the block form for automatic cleanup:
|
||||
#
|
||||
# QuicProQuo::Client.open("127.0.0.1:5001", ca_cert: "ca.pem") do |c|
|
||||
# c.login("alice", "secret")
|
||||
# c.send("bob", "hello")
|
||||
# end
|
||||
class Client
|
||||
# Connect to a quicproquo server.
|
||||
#
|
||||
# @param server [String] Server address as +host:port+.
|
||||
# @param ca_cert [String] Path to PEM-encoded CA certificate.
|
||||
# @param server_name [String] TLS SNI server name (defaults to host).
|
||||
# @raise [ConnectionError] if the connection fails.
|
||||
def initialize(server, ca_cert:, server_name: nil)
|
||||
sn = server_name || server.split(":").first
|
||||
@handle = FFIBindings.qpq_connect(server, ca_cert, sn)
|
||||
raise ConnectionError, "qpq_connect failed for #{server}" if @handle.null?
|
||||
end
|
||||
|
||||
# Connect and yield the client, disconnecting when the block returns.
|
||||
def self.open(server, **opts)
|
||||
client = new(server, **opts)
|
||||
begin
|
||||
yield client
|
||||
ensure
|
||||
client.disconnect
|
||||
end
|
||||
end
|
||||
|
||||
# Authenticate with OPAQUE credentials.
|
||||
#
|
||||
# @param username [String]
|
||||
# @param password [String]
|
||||
# @raise [AuthError] on bad credentials.
|
||||
def login(username, password)
|
||||
check_connected!
|
||||
code = FFIBindings.qpq_login(@handle, username, password)
|
||||
check_status!(code)
|
||||
end
|
||||
|
||||
# Send a message to a recipient by username.
|
||||
#
|
||||
# @param recipient [String] Recipient username.
|
||||
# @param message [String] Message payload (UTF-8 string or binary).
|
||||
def send(recipient, message)
|
||||
check_connected!
|
||||
msg_bytes = message.encode("BINARY")
|
||||
buf = FFI::MemoryPointer.new(:uint8, msg_bytes.bytesize)
|
||||
buf.put_bytes(0, msg_bytes)
|
||||
code = FFIBindings.qpq_send(@handle, recipient, buf, msg_bytes.bytesize)
|
||||
check_status!(code)
|
||||
end
|
||||
|
||||
# Receive pending messages, blocking up to +timeout_ms+ milliseconds.
|
||||
#
|
||||
# @param timeout_ms [Integer] Timeout in milliseconds (default 5000).
|
||||
# @return [Array<String>] Message strings (UTF-8).
|
||||
def receive(timeout_ms: 5000)
|
||||
check_connected!
|
||||
out_ptr = FFI::MemoryPointer.new(:pointer)
|
||||
code = FFIBindings.qpq_receive(@handle, timeout_ms, out_ptr)
|
||||
check_status!(code)
|
||||
|
||||
json_ptr = out_ptr.read_pointer
|
||||
return [] if json_ptr.null?
|
||||
|
||||
json_str = json_ptr.read_string
|
||||
FFIBindings.qpq_free_string(json_ptr)
|
||||
JSON.parse(json_str)
|
||||
end
|
||||
|
||||
# Disconnect from the server and release resources.
|
||||
def disconnect
|
||||
return if @handle.nil? || @handle.null?
|
||||
|
||||
FFIBindings.qpq_disconnect(@handle)
|
||||
@handle = nil
|
||||
end
|
||||
|
||||
# Whether the client is connected.
|
||||
def connected?
|
||||
!@handle.nil? && !@handle.null?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_connected!
|
||||
raise NotConnectedError, "not connected" unless connected?
|
||||
end
|
||||
|
||||
def check_status!(code)
|
||||
return if code == FFIBindings::QPQ_OK
|
||||
|
||||
msg = FFIBindings.qpq_last_error(@handle) || "unknown error"
|
||||
case code
|
||||
when FFIBindings::QPQ_AUTH_FAILED
|
||||
raise AuthError, msg
|
||||
when FFIBindings::QPQ_TIMEOUT
|
||||
raise TimeoutError, msg
|
||||
when FFIBindings::QPQ_NOT_CONNECTED
|
||||
raise NotConnectedError, msg
|
||||
else
|
||||
raise Error, msg
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
18
sdks/ruby/lib/quicproquo/errors.rb
Normal file
18
sdks/ruby/lib/quicproquo/errors.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module QuicProQuo
|
||||
# Base error for quicproquo SDK.
|
||||
class Error < StandardError; end
|
||||
|
||||
# OPAQUE authentication failed (bad credentials).
|
||||
class AuthError < Error; end
|
||||
|
||||
# Operation timed out.
|
||||
class TimeoutError < Error; end
|
||||
|
||||
# Client is not connected.
|
||||
class NotConnectedError < Error; end
|
||||
|
||||
# Connection to the server failed.
|
||||
class ConnectionError < Error; end
|
||||
end
|
||||
56
sdks/ruby/lib/quicproquo/ffi_bindings.rb
Normal file
56
sdks/ruby/lib/quicproquo/ffi_bindings.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "ffi"
|
||||
|
||||
module QuicProQuo
|
||||
# Low-level FFI bindings to libquicproquo_ffi.
|
||||
#
|
||||
# The library is located via:
|
||||
# 1. QPQ_LIB_PATH environment variable
|
||||
# 2. Common cargo build output paths
|
||||
# 3. System library path
|
||||
module FFIBindings
|
||||
extend FFI::Library
|
||||
|
||||
# Status codes (mirrors crates/quicproquo-ffi/src/lib.rs).
|
||||
QPQ_OK = 0
|
||||
QPQ_ERROR = 1
|
||||
QPQ_AUTH_FAILED = 2
|
||||
QPQ_TIMEOUT = 3
|
||||
QPQ_NOT_CONNECTED = 4
|
||||
|
||||
# Locate and load the shared library.
|
||||
LIB_SEARCH_PATHS = [
|
||||
ENV.fetch("QPQ_LIB_PATH", nil),
|
||||
File.expand_path("../../../../target/release/libquicproquo_ffi.so", __dir__),
|
||||
File.expand_path("../../../../target/debug/libquicproquo_ffi.so", __dir__),
|
||||
File.expand_path("../../../../target/release/libquicproquo_ffi.dylib", __dir__),
|
||||
File.expand_path("../../../../target/debug/libquicproquo_ffi.dylib", __dir__),
|
||||
"quicproquo_ffi",
|
||||
].compact.freeze
|
||||
|
||||
lib_path = LIB_SEARCH_PATHS.find { |p| File.exist?(p) } || "quicproquo_ffi"
|
||||
ffi_lib lib_path
|
||||
|
||||
# QpqHandle* qpq_connect(const char*, const char*, const char*)
|
||||
attach_function :qpq_connect, %i[string string string], :pointer
|
||||
|
||||
# int qpq_login(QpqHandle*, const char*, const char*)
|
||||
attach_function :qpq_login, %i[pointer string string], :int
|
||||
|
||||
# int qpq_send(QpqHandle*, const char*, :pointer, size_t)
|
||||
attach_function :qpq_send, %i[pointer string pointer size_t], :int
|
||||
|
||||
# int qpq_receive(QpqHandle*, uint32, :pointer)
|
||||
attach_function :qpq_receive, %i[pointer uint32 pointer], :int
|
||||
|
||||
# void qpq_disconnect(QpqHandle*)
|
||||
attach_function :qpq_disconnect, [:pointer], :void
|
||||
|
||||
# const char* qpq_last_error(const QpqHandle*)
|
||||
attach_function :qpq_last_error, [:pointer], :string
|
||||
|
||||
# void qpq_free_string(char*)
|
||||
attach_function :qpq_free_string, [:pointer], :void
|
||||
end
|
||||
end
|
||||
5
sdks/ruby/lib/quicproquo/version.rb
Normal file
5
sdks/ruby/lib/quicproquo/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module QuicProQuo
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
Reference in New Issue
Block a user