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:
50
sdks/java/README.md
Normal file
50
sdks/java/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# QuicProQuo Java SDK
|
||||||
|
|
||||||
|
Java wrapper over `libquicproquo_ffi` via JNI for JVM and Android.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- JDK 17+
|
||||||
|
- `libquicproquo_ffi` built for the target platform
|
||||||
|
- JNI bridge compiled (shared with Kotlin SDK: `../kotlin/jni/`)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build Rust FFI library
|
||||||
|
cargo build --release -p quicproquo-ffi
|
||||||
|
|
||||||
|
# Build Java SDK
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```java
|
||||||
|
import dev.quicproquo.QpqClient;
|
||||||
|
|
||||||
|
try (QpqClient client = new QpqClient("127.0.0.1:5001", "ca.pem")) {
|
||||||
|
client.login("alice", "secret");
|
||||||
|
client.send("bob", "hello".getBytes());
|
||||||
|
|
||||||
|
var messages = client.receive(5000);
|
||||||
|
messages.forEach(msg -> System.out.println("Received: " + msg));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `new QpqClient(server, caCertPath)` | Connect to server |
|
||||||
|
| `client.login(username, password)` | OPAQUE authentication |
|
||||||
|
| `client.send(recipient, message)` | Send message by username |
|
||||||
|
| `client.receive(timeoutMs)` | Receive pending messages |
|
||||||
|
| `client.close()` / `client.disconnect()` | Disconnect |
|
||||||
|
| `client.isConnected()` | Connection status |
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `src/main/java/dev/quicproquo/QpqClient.java` -- High-level client
|
||||||
|
- `src/main/java/dev/quicproquo/NativeBridge.java` -- JNI declarations
|
||||||
|
- JNI C bridge shared with Kotlin SDK at `../kotlin/jni/`
|
||||||
24
sdks/java/build.gradle.kts
Normal file
24
sdks/java/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
plugins {
|
||||||
|
java
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "dev.quicproquo"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
30
sdks/java/src/main/java/dev/quicproquo/NativeBridge.java
Normal file
30
sdks/java/src/main/java/dev/quicproquo/NativeBridge.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.quicproquo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JNI bridge to libquicproquo_ffi native library.
|
||||||
|
*
|
||||||
|
* <p>Load the library with:
|
||||||
|
* <pre>{@code
|
||||||
|
* System.loadLibrary("quicproquo_ffi");
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
final class NativeBridge {
|
||||||
|
static final int QPQ_OK = 0;
|
||||||
|
static final int QPQ_ERROR = 1;
|
||||||
|
static final int QPQ_AUTH_FAILED = 2;
|
||||||
|
static final int QPQ_TIMEOUT = 3;
|
||||||
|
static final int QPQ_NOT_CONNECTED = 4;
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("quicproquo_ffi");
|
||||||
|
}
|
||||||
|
|
||||||
|
static native long nativeConnect(String server, String caCert, String serverName);
|
||||||
|
static native int nativeLogin(long handle, String username, String password);
|
||||||
|
static native int nativeSend(long handle, String recipient, byte[] message);
|
||||||
|
static native String nativeReceive(long handle, int timeoutMs);
|
||||||
|
static native void nativeDisconnect(long handle);
|
||||||
|
static native String nativeLastError(long handle);
|
||||||
|
|
||||||
|
private NativeBridge() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.quicproquo;
|
||||||
|
|
||||||
|
/** OPAQUE authentication failed (bad credentials). */
|
||||||
|
public class QpqAuthException extends QpqException {
|
||||||
|
public QpqAuthException(String message) { super(message); }
|
||||||
|
}
|
||||||
127
sdks/java/src/main/java/dev/quicproquo/QpqClient.java
Normal file
127
sdks/java/src/main/java/dev/quicproquo/QpqClient.java
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package dev.quicproquo;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level quicproquo client for JVM.
|
||||||
|
*
|
||||||
|
* <p>Wraps {@code libquicproquo_ffi} via JNI.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* try (QpqClient client = new QpqClient("127.0.0.1:5001", "ca.pem")) {
|
||||||
|
* client.login("alice", "secret");
|
||||||
|
* client.send("bob", "hello".getBytes());
|
||||||
|
* List<String> messages = client.receive(5000);
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public final class QpqClient implements AutoCloseable {
|
||||||
|
private volatile long handle;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
private static final Type STRING_LIST_TYPE =
|
||||||
|
new TypeToken<List<String>>() {}.getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to a quicproquo server.
|
||||||
|
*
|
||||||
|
* @param server server address as {@code host:port}
|
||||||
|
* @param caCertPath path to PEM-encoded CA certificate
|
||||||
|
*/
|
||||||
|
public QpqClient(String server, String caCertPath) {
|
||||||
|
this(server, caCertPath, server.split(":")[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to a quicproquo server with explicit TLS server name.
|
||||||
|
*
|
||||||
|
* @param server server address as {@code host:port}
|
||||||
|
* @param caCertPath path to PEM-encoded CA certificate
|
||||||
|
* @param serverName TLS SNI server name
|
||||||
|
*/
|
||||||
|
public QpqClient(String server, String caCertPath, String serverName) {
|
||||||
|
long h = NativeBridge.nativeConnect(server, caCertPath, serverName);
|
||||||
|
if (h == 0) {
|
||||||
|
throw new QpqException("qpq_connect failed for " + server);
|
||||||
|
}
|
||||||
|
this.handle = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the client is connected. */
|
||||||
|
public boolean isConnected() {
|
||||||
|
return handle != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate with OPAQUE credentials.
|
||||||
|
*
|
||||||
|
* @throws QpqAuthException on bad credentials
|
||||||
|
*/
|
||||||
|
public void login(String username, String password) {
|
||||||
|
checkConnected();
|
||||||
|
int code = NativeBridge.nativeLogin(handle, username, password);
|
||||||
|
checkStatus(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a recipient by username.
|
||||||
|
*
|
||||||
|
* @param recipient recipient username
|
||||||
|
* @param message message payload (arbitrary bytes)
|
||||||
|
*/
|
||||||
|
public void send(String recipient, byte[] message) {
|
||||||
|
checkConnected();
|
||||||
|
int code = NativeBridge.nativeSend(handle, recipient, message);
|
||||||
|
checkStatus(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive pending messages, blocking up to {@code timeoutMs} milliseconds.
|
||||||
|
*
|
||||||
|
* @return list of message strings (UTF-8)
|
||||||
|
*/
|
||||||
|
public List<String> receive(int timeoutMs) {
|
||||||
|
checkConnected();
|
||||||
|
String json = NativeBridge.nativeReceive(handle, timeoutMs);
|
||||||
|
if (json == null) return Collections.emptyList();
|
||||||
|
return gson.fromJson(json, STRING_LIST_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disconnect from the server and release resources. */
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
long h = handle;
|
||||||
|
if (h != 0) {
|
||||||
|
handle = 0;
|
||||||
|
NativeBridge.nativeDisconnect(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias for {@link #close()}. */
|
||||||
|
public void disconnect() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkConnected() {
|
||||||
|
if (handle == 0) throw new QpqException("not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkStatus(int code) {
|
||||||
|
if (code == NativeBridge.QPQ_OK) return;
|
||||||
|
String msg = NativeBridge.nativeLastError(handle);
|
||||||
|
if (msg == null) msg = "unknown error";
|
||||||
|
switch (code) {
|
||||||
|
case NativeBridge.QPQ_AUTH_FAILED:
|
||||||
|
throw new QpqAuthException(msg);
|
||||||
|
case NativeBridge.QPQ_TIMEOUT:
|
||||||
|
throw new QpqTimeoutException(msg);
|
||||||
|
case NativeBridge.QPQ_NOT_CONNECTED:
|
||||||
|
throw new QpqException("not connected: " + msg);
|
||||||
|
default:
|
||||||
|
throw new QpqException(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
sdks/java/src/main/java/dev/quicproquo/QpqException.java
Normal file
7
sdks/java/src/main/java/dev/quicproquo/QpqException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.quicproquo;
|
||||||
|
|
||||||
|
/** Base exception for quicproquo SDK errors. */
|
||||||
|
public class QpqException extends RuntimeException {
|
||||||
|
public QpqException(String message) { super(message); }
|
||||||
|
public QpqException(String message, Throwable cause) { super(message, cause); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.quicproquo;
|
||||||
|
|
||||||
|
/** Operation timed out. */
|
||||||
|
public class QpqTimeoutException extends QpqException {
|
||||||
|
public QpqTimeoutException(String message) { super(message); }
|
||||||
|
}
|
||||||
82
sdks/ruby/README.md
Normal file
82
sdks/ruby/README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# QuicProQuo Ruby SDK
|
||||||
|
|
||||||
|
Ruby FFI gem wrapping `libquicproquo_ffi` for the quicproquo E2E encrypted messenger.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Ruby 3.1+
|
||||||
|
- `libquicproquo_ffi` built for the target platform
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gem install quicproquo
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add to your Gemfile:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gem "quicproquo"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building the Native Library
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release -p quicproquo-ffi
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `QPQ_LIB_PATH` if the library is not in the default search path.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
require "quicproquo"
|
||||||
|
|
||||||
|
# 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 from Ruby!")
|
||||||
|
|
||||||
|
messages = client.receive(timeout_ms: 5000)
|
||||||
|
messages.each { |msg| puts msg }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Manual lifecycle
|
||||||
|
client = QuicProQuo::Client.new("127.0.0.1:5001", ca_cert: "ca.pem")
|
||||||
|
client.login("alice", "secret")
|
||||||
|
client.send("bob", "hello")
|
||||||
|
client.disconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `Client.new(server, ca_cert:, server_name:)` | Connect to server |
|
||||||
|
| `Client.open(server, **opts) { \|c\| ... }` | Connect with auto-disconnect |
|
||||||
|
| `client.login(username, password)` | OPAQUE authentication |
|
||||||
|
| `client.send(recipient, message)` | Send message by username |
|
||||||
|
| `client.receive(timeout_ms: 5000)` | Receive pending messages |
|
||||||
|
| `client.disconnect` | Disconnect |
|
||||||
|
| `client.connected?` | Connection status |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
begin
|
||||||
|
client.login("alice", "wrong")
|
||||||
|
rescue QuicProQuo::AuthError => e
|
||||||
|
puts "Auth failed: #{e.message}"
|
||||||
|
rescue QuicProQuo::TimeoutError => e
|
||||||
|
puts "Timeout: #{e.message}"
|
||||||
|
rescue QuicProQuo::Error => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `lib/quicproquo/client.rb` -- High-level client
|
||||||
|
- `lib/quicproquo/ffi_bindings.rb` -- FFI function declarations
|
||||||
|
- `lib/quicproquo/errors.rb` -- Exception classes
|
||||||
|
- `examples/demo.rb` -- Usage example
|
||||||
43
sdks/ruby/examples/demo.rb
Normal file
43
sdks/ruby/examples/demo.rb
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Example: quicproquo Ruby SDK demo.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ruby demo.rb --server 127.0.0.1:5001 --ca-cert ca.pem \
|
||||||
|
# --user alice --pass secret --recipient bob --message "hello"
|
||||||
|
|
||||||
|
require "optparse"
|
||||||
|
require_relative "../lib/quicproquo"
|
||||||
|
|
||||||
|
options = {
|
||||||
|
server: "127.0.0.1:5001",
|
||||||
|
ca_cert: "ca.pem",
|
||||||
|
message: "hello from Ruby SDK!",
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: demo.rb [options]"
|
||||||
|
opts.on("--server ADDR", "Server address") { |v| options[:server] = v }
|
||||||
|
opts.on("--ca-cert PATH", "CA certificate") { |v| options[:ca_cert] = v }
|
||||||
|
opts.on("--user NAME", "Username") { |v| options[:user] = v }
|
||||||
|
opts.on("--pass PASSWORD", "Password") { |v| options[:pass] = v }
|
||||||
|
opts.on("--recipient NAME", "Recipient") { |v| options[:recipient] = v }
|
||||||
|
opts.on("--message TEXT", "Message") { |v| options[:message] = v }
|
||||||
|
end.parse!
|
||||||
|
|
||||||
|
QuicProQuo::Client.open(options[:server], ca_cert: options[:ca_cert]) do |client|
|
||||||
|
puts "Connected to #{options[:server]}"
|
||||||
|
|
||||||
|
client.login(options[:user], options[:pass])
|
||||||
|
puts "Logged in as #{options[:user]}"
|
||||||
|
|
||||||
|
client.send(options[:recipient], options[:message])
|
||||||
|
puts "Sent message to #{options[:recipient]}"
|
||||||
|
|
||||||
|
puts "Waiting for messages (5s)..."
|
||||||
|
messages = client.receive(timeout_ms: 5000)
|
||||||
|
messages.each { |msg| puts " received: #{msg}" }
|
||||||
|
|
||||||
|
puts "Done."
|
||||||
|
end
|
||||||
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
|
||||||
20
sdks/ruby/quicproquo.gemspec
Normal file
20
sdks/ruby/quicproquo.gemspec
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = "quicproquo"
|
||||||
|
s.version = "0.1.0"
|
||||||
|
s.summary = "Ruby SDK for quicproquo E2E encrypted messenger"
|
||||||
|
s.description = "Ruby FFI bindings to libquicproquo_ffi for the quicproquo " \
|
||||||
|
"end-to-end encrypted messaging system."
|
||||||
|
s.authors = ["quicproquo contributors"]
|
||||||
|
s.license = "MIT"
|
||||||
|
s.homepage = "https://github.com/nicholasgasior/quicproquo"
|
||||||
|
|
||||||
|
s.required_ruby_version = ">= 3.1"
|
||||||
|
|
||||||
|
s.files = Dir["lib/**/*.rb", "README.md", "LICENSE"]
|
||||||
|
|
||||||
|
s.add_dependency "ffi", "~> 1.16"
|
||||||
|
s.add_dependency "json", "~> 2.7"
|
||||||
|
|
||||||
|
s.add_development_dependency "rspec", "~> 3.13"
|
||||||
|
s.add_development_dependency "rubocop", "~> 1.60"
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user