Compare commits
11 Commits
ebf9afb4e1
...
046deccc26
| Author | SHA1 | Date | |
|---|---|---|---|
| 046deccc26 | |||
| b522ef8a98 | |||
| 53857bc613 | |||
| 538405fec9 | |||
| 4eceba338c | |||
| ef9e578e25 | |||
| 5df58e9d28 | |||
| 4b22bd726f | |||
| 31712d5efa | |||
| 27ecce1211 | |||
| 30d03e3739 |
@@ -1,99 +0,0 @@
|
|||||||
using Gtk;
|
|
||||||
|
|
||||||
public class Connection {
|
|
||||||
public signal void response_received(uint8[] response);
|
|
||||||
|
|
||||||
private TlsClientConnection tls_client;
|
|
||||||
|
|
||||||
public Connection() throws Error {
|
|
||||||
var loopback = new InetAddress.loopback(SocketFamily.IPV6);
|
|
||||||
var host = new InetSocketAddress(loopback, 12888);
|
|
||||||
|
|
||||||
var db_type = TlsBackend.get_default().get_file_database_type();
|
|
||||||
const string ca_path = Config.CERT_DIR + "/ca.pem";
|
|
||||||
var db = Object.new(db_type, "anchors", ca_path) as TlsDatabase;
|
|
||||||
var cert =
|
|
||||||
new TlsCertificate.from_file(Config.CERT_DIR + "/client.pem");
|
|
||||||
|
|
||||||
var plain_client = new SocketClient();
|
|
||||||
var plain_connection = plain_client.connect(host);
|
|
||||||
tls_client = TlsClientConnection.new(plain_connection, host);
|
|
||||||
|
|
||||||
tls_client.set_database(db);
|
|
||||||
tls_client.set_certificate(cert);
|
|
||||||
tls_client.handshake();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void send(uint8[] message) throws Error {
|
|
||||||
yield tls_client.output_stream.write_async(message);
|
|
||||||
var response = new uint8[1024];
|
|
||||||
var len = yield tls_client.input_stream.read_async(response);
|
|
||||||
response_received(response[0:len]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MainWindow : Gtk.ApplicationWindow {
|
|
||||||
private Connection connection;
|
|
||||||
private Gtk.Button send_button;
|
|
||||||
private Gtk.Label response_label;
|
|
||||||
|
|
||||||
public MainWindow(Gtk.Application app, Connection connection) {
|
|
||||||
Object(application: app);
|
|
||||||
|
|
||||||
default_width = 600;
|
|
||||||
default_height = 400;
|
|
||||||
title = "Study System Client";
|
|
||||||
|
|
||||||
this.connection = connection;
|
|
||||||
connection.response_received.connect((response) => {
|
|
||||||
response_label.label = "Response: " + (string)response;
|
|
||||||
});
|
|
||||||
|
|
||||||
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
|
|
||||||
box.margin_start = 10;
|
|
||||||
box.margin_end = 10;
|
|
||||||
box.margin_top = 10;
|
|
||||||
box.margin_bottom = 10;
|
|
||||||
|
|
||||||
send_button = new Gtk.Button.with_label("Send");
|
|
||||||
send_button.clicked.connect(on_send_clicked);
|
|
||||||
box.append(send_button);
|
|
||||||
|
|
||||||
response_label = new Gtk.Label("");
|
|
||||||
response_label.wrap = true;
|
|
||||||
box.append(response_label);
|
|
||||||
|
|
||||||
set_child(box);
|
|
||||||
|
|
||||||
present();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void on_send_clicked() {
|
|
||||||
try {
|
|
||||||
yield connection.send("Foo".data);
|
|
||||||
} catch (Error e) {
|
|
||||||
response_label.label = "Error: " + e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StudySystemClient : Gtk.Application {
|
|
||||||
public StudySystemClient() {
|
|
||||||
Object(application_id: "sh.wip.study-system-client");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void activate() {
|
|
||||||
try {
|
|
||||||
var connection = new Connection();
|
|
||||||
new MainWindow(this, connection);
|
|
||||||
} catch (Error e) {
|
|
||||||
stderr.printf("Failed to initialize connection: %s\n",
|
|
||||||
e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int main(string[] args) {
|
|
||||||
var app = new StudySystemClient();
|
|
||||||
return app.run(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,21 +10,5 @@ add_project_arguments('-w', language: 'c')
|
|||||||
|
|
||||||
gtk_dep = dependency('gtk4')
|
gtk_dep = dependency('gtk4')
|
||||||
|
|
||||||
conf = configuration_data()
|
subdir('src')
|
||||||
conf.set_quoted(
|
subdir('tests')
|
||||||
'CONFIG_CERT_DIR',
|
|
||||||
join_paths(meson.project_source_root(), '..', 'test'))
|
|
||||||
configure_file(
|
|
||||||
output: 'config.h',
|
|
||||||
configuration: conf
|
|
||||||
)
|
|
||||||
|
|
||||||
exe = executable(
|
|
||||||
'study-system-client',
|
|
||||||
[
|
|
||||||
'main.vala',
|
|
||||||
'config.vapi'
|
|
||||||
],
|
|
||||||
dependencies: [gtk_dep],
|
|
||||||
c_args: ['-w']
|
|
||||||
)
|
|
||||||
|
|||||||
221
client/src/connection.vala
Normal file
221
client/src/connection.vala
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
public class Connection {
|
||||||
|
public signal void received(uint8[] msg);
|
||||||
|
|
||||||
|
private SessionManager session_manager;
|
||||||
|
private Worker worker;
|
||||||
|
|
||||||
|
public Connection(string cert_dir) throws Error {
|
||||||
|
var loopback = new InetAddress.loopback(SocketFamily.IPV6);
|
||||||
|
var session_factory
|
||||||
|
= new SessionFactory(loopback, 12888, cert_dir);
|
||||||
|
session_manager = new SessionManager(
|
||||||
|
session_factory, (msg) => {
|
||||||
|
var msg_copy = new uint8[msg.length];
|
||||||
|
Memory.copy(msg_copy, msg, msg.length);
|
||||||
|
Idle.add(() => {
|
||||||
|
received(msg_copy);
|
||||||
|
return false;
|
||||||
|
}, GLib.Priority.DEFAULT_IDLE);
|
||||||
|
});
|
||||||
|
worker = new Worker(session_manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(owned uint8[] msg) {
|
||||||
|
session_manager.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Worker {
|
||||||
|
private uint TASK_PERIOD_MS = 10;
|
||||||
|
|
||||||
|
private SessionManager session_manager;
|
||||||
|
private bool exit;
|
||||||
|
private Thread<void> thread;
|
||||||
|
|
||||||
|
public Worker(SessionManager session_manager) {
|
||||||
|
this.session_manager = session_manager;
|
||||||
|
exit = false;
|
||||||
|
thread = new Thread<void>("connection_worker", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
~Worker() {
|
||||||
|
exit = true;
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void body() {
|
||||||
|
while (!exit) {
|
||||||
|
session_manager.task();
|
||||||
|
Thread.usleep(1000 * TASK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SessionManager {
|
||||||
|
public delegate void ReceiveCallback(uint8[] msg);
|
||||||
|
|
||||||
|
private const uint INIT_RECONNECT_WAIT_MS = 500;
|
||||||
|
private const uint MAX_RECONNECT_WAIT_MS = 60000;
|
||||||
|
private const double RECONNECT_BACKOFF = 1.6;
|
||||||
|
|
||||||
|
private SessionFactory session_factory;
|
||||||
|
private ReceiveCallback receive_callback;
|
||||||
|
private Session? session;
|
||||||
|
private AsyncQueue<OutgoingMessage> queue;
|
||||||
|
private uint reconnect_wait_ms;
|
||||||
|
|
||||||
|
public SessionManager(SessionFactory session_factory,
|
||||||
|
owned ReceiveCallback receive_callback) {
|
||||||
|
this.session_factory = session_factory;
|
||||||
|
this.receive_callback = (owned) receive_callback;
|
||||||
|
this.session = null;
|
||||||
|
queue = new AsyncQueue<OutgoingMessage>();
|
||||||
|
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(uint8[] msg) {
|
||||||
|
queue.push(new OutgoingMessage(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void task() {
|
||||||
|
if (session != null) {
|
||||||
|
var failed_msg = session.task(queue);
|
||||||
|
if (failed_msg != null)
|
||||||
|
handle_failed_msg(failed_msg);
|
||||||
|
} else {
|
||||||
|
try_start_session();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handle_failed_msg(OutgoingMessage msg) {
|
||||||
|
msg.has_failed();
|
||||||
|
if (msg.should_retry())
|
||||||
|
queue.push(msg);
|
||||||
|
session = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void try_start_session() {
|
||||||
|
try {
|
||||||
|
session = session_factory.start_session();
|
||||||
|
session.received.connect(
|
||||||
|
(msg) => receive_callback(msg));
|
||||||
|
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
|
||||||
|
} catch (Error _) {
|
||||||
|
Thread.usleep(1000 * reconnect_wait_ms);
|
||||||
|
update_reconnect_wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_reconnect_wait() {
|
||||||
|
var new_wait = RECONNECT_BACKOFF * reconnect_wait_ms;
|
||||||
|
if (new_wait < MAX_RECONNECT_WAIT_MS)
|
||||||
|
reconnect_wait_ms = (uint)new_wait;
|
||||||
|
else
|
||||||
|
reconnect_wait_ms = MAX_RECONNECT_WAIT_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SessionFactory {
|
||||||
|
private const string CA_FILENAME = "/ca.pem";
|
||||||
|
private const string CERT_FILENAME = "/client.pem";
|
||||||
|
private const uint TIMEOUT_S = 1;
|
||||||
|
|
||||||
|
private InetSocketAddress host;
|
||||||
|
private TlsCertificate cert;
|
||||||
|
private TlsDatabase ca_db;
|
||||||
|
|
||||||
|
public SessionFactory(InetAddress host_addr, uint16 host_port,
|
||||||
|
string cert_dir) throws Error {
|
||||||
|
host = new InetSocketAddress(host_addr, host_port);
|
||||||
|
var cert_path = cert_dir + CERT_FILENAME;
|
||||||
|
cert = new TlsCertificate.from_file(cert_path);
|
||||||
|
var ca_path = cert_dir + CA_FILENAME;
|
||||||
|
var db_type = TlsBackend.get_default().get_file_database_type();
|
||||||
|
ca_db = Object.new(db_type, "anchors", ca_path) as TlsDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session start_session() throws Error {
|
||||||
|
var plain_client = new SocketClient();
|
||||||
|
plain_client.set_timeout(TIMEOUT_S);
|
||||||
|
var plain_connection = plain_client.connect(host);
|
||||||
|
var connection = TlsClientConnection.new(plain_connection, host);
|
||||||
|
connection.set_database(ca_db);
|
||||||
|
connection.set_certificate(cert);
|
||||||
|
connection.handshake();
|
||||||
|
return new Session(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Session {
|
||||||
|
public signal void received(uint8[] msg);
|
||||||
|
|
||||||
|
private const uint MAX_BATCH_SIZE = 10;
|
||||||
|
private const uint MAX_MSG_LEN = 1024;
|
||||||
|
|
||||||
|
private TlsClientConnection connection;
|
||||||
|
|
||||||
|
public Session(TlsClientConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutgoingMessage? task(AsyncQueue<OutgoingMessage> queue) {
|
||||||
|
for (int i = 0; i < MAX_BATCH_SIZE; ++i) {
|
||||||
|
if (queue.length() == 0)
|
||||||
|
break;
|
||||||
|
var msg = queue.pop();
|
||||||
|
var success = true;
|
||||||
|
success &= send(msg);
|
||||||
|
success &= receive();
|
||||||
|
if (!success)
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool send(OutgoingMessage msg) {
|
||||||
|
try {
|
||||||
|
size_t written;
|
||||||
|
connection.output_stream.write_all(msg.content, out written);
|
||||||
|
return true;
|
||||||
|
} catch (IOError _) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool receive() {
|
||||||
|
try {
|
||||||
|
var buffer = new uint8[MAX_MSG_LEN];
|
||||||
|
var len = connection.input_stream.read(buffer);
|
||||||
|
if (len <= 0)
|
||||||
|
return false;
|
||||||
|
received(buffer[0:len]);
|
||||||
|
return true;
|
||||||
|
} catch (IOError _) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OutgoingMessage {
|
||||||
|
public uint8[] content { get; private set; }
|
||||||
|
|
||||||
|
private const uint MAX_FAIL_COUNT = 10;
|
||||||
|
|
||||||
|
private uint fail_count;
|
||||||
|
|
||||||
|
public OutgoingMessage(owned uint8[] content) {
|
||||||
|
this.content = (owned)content;
|
||||||
|
fail_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void has_failed() {
|
||||||
|
++fail_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool should_retry() {
|
||||||
|
return fail_count < MAX_FAIL_COUNT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
client/src/der.vala
Normal file
265
client/src/der.vala
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
namespace StudySystemClient.Der {
|
||||||
|
public errordomain DecodeError {
|
||||||
|
INCOMPLETE,
|
||||||
|
INVALID_CONTENT,
|
||||||
|
UNKNOWN_TYPE,
|
||||||
|
}
|
||||||
|
|
||||||
|
private const uint BASE_HEADER_SIZE = 2;
|
||||||
|
|
||||||
|
public static Datum decode(uint8[] bytes, out uint? size = null)
|
||||||
|
throws DecodeError {
|
||||||
|
if (bytes.length < BASE_HEADER_SIZE) {
|
||||||
|
throw new DecodeError.INCOMPLETE(
|
||||||
|
"Message is fewer than %u bytes", BASE_HEADER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint header_size;
|
||||||
|
var length = decode_length(bytes, out header_size);
|
||||||
|
if (header_size + length > bytes.length) {
|
||||||
|
throw new DecodeError.INCOMPLETE(
|
||||||
|
"Length %u but only %u bytes available", length,
|
||||||
|
bytes.length - header_size);
|
||||||
|
}
|
||||||
|
var content = bytes[header_size:header_size + length];
|
||||||
|
size = header_size + length;
|
||||||
|
return decode_datum(bytes[0], content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint decode_length(uint8[] bytes, out uint header_size)
|
||||||
|
throws DecodeError {
|
||||||
|
if ((bytes[1] & 0x80) != 0) {
|
||||||
|
var length_size = bytes[1] & 0x7f;
|
||||||
|
if (BASE_HEADER_SIZE + length_size > bytes.length) {
|
||||||
|
throw new DecodeError.INCOMPLETE(
|
||||||
|
"Length with size %u but only %u bytes available",
|
||||||
|
length_size, bytes.length - BASE_HEADER_SIZE);
|
||||||
|
}
|
||||||
|
var length = 0;
|
||||||
|
for (int i = 0; i < length_size; ++i)
|
||||||
|
length = length << 8 | bytes[BASE_HEADER_SIZE + i];
|
||||||
|
header_size = BASE_HEADER_SIZE + length_size;
|
||||||
|
return length;
|
||||||
|
} else {
|
||||||
|
header_size = BASE_HEADER_SIZE;
|
||||||
|
return bytes[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Datum decode_datum(uint8 type, uint8[] content)
|
||||||
|
throws DecodeError {
|
||||||
|
switch (type) {
|
||||||
|
case Boolean.TYPE:
|
||||||
|
return new Boolean.from_content(content);
|
||||||
|
case Integer.TYPE:
|
||||||
|
return new Integer.from_content(content);
|
||||||
|
case Utf8String.TYPE:
|
||||||
|
return new Utf8String.from_content(content);
|
||||||
|
case Sequence.TYPE:
|
||||||
|
return new Sequence.from_content(content);
|
||||||
|
default:
|
||||||
|
throw new DecodeError.UNKNOWN_TYPE("Unsupported type: %02x",
|
||||||
|
type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class Datum {
|
||||||
|
internal uint8 type;
|
||||||
|
internal uint8[] content;
|
||||||
|
|
||||||
|
public uint8[] encode() {
|
||||||
|
var buffer = new ByteArray();
|
||||||
|
|
||||||
|
buffer.append({type});
|
||||||
|
if (content.length >= 0x80) {
|
||||||
|
var length_bytes = encode_length(content.length);
|
||||||
|
buffer.append({0x80 | (uint8)length_bytes.length});
|
||||||
|
buffer.append(length_bytes);
|
||||||
|
} else {
|
||||||
|
buffer.append({(uint8)content.length});
|
||||||
|
}
|
||||||
|
buffer.append(content);
|
||||||
|
|
||||||
|
return buffer.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint8[] encode_length(uint length) {
|
||||||
|
var buffer = new ByteArray();
|
||||||
|
int shift = 0;
|
||||||
|
while (length >> (shift + 8) != 0)
|
||||||
|
shift += 8;
|
||||||
|
for (; shift >= 0; shift -= 8)
|
||||||
|
buffer.append({(uint8)(length >> shift)});
|
||||||
|
return buffer.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Boolean : Datum {
|
||||||
|
internal const uint8 TYPE = 0x01;
|
||||||
|
|
||||||
|
public bool value { get; private set; }
|
||||||
|
|
||||||
|
public Boolean(bool val) {
|
||||||
|
type = TYPE;
|
||||||
|
content = new uint8[] { val ? 0xff : 0x00 };
|
||||||
|
value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Boolean.from_content(uint8[] bytes) throws DecodeError {
|
||||||
|
type = TYPE;
|
||||||
|
content = bytes;
|
||||||
|
value = decode_bool(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool decode_bool(uint8[] bytes)
|
||||||
|
throws DecodeError {
|
||||||
|
if (bytes.length != 1) {
|
||||||
|
throw new DecodeError.INVALID_CONTENT(
|
||||||
|
"Invalid length for boolean: %u", bytes.length);
|
||||||
|
}
|
||||||
|
switch (bytes[0]) {
|
||||||
|
case 0xff:
|
||||||
|
return true;
|
||||||
|
case 0x00:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
throw new DecodeError.INVALID_CONTENT(
|
||||||
|
"Invalid value of boolean: %02x", bytes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Integer : Datum {
|
||||||
|
internal const uint8 TYPE = 0x02;
|
||||||
|
private const uint8 MAX_BYTES = 8;
|
||||||
|
|
||||||
|
public int64 value { get; private set; }
|
||||||
|
|
||||||
|
public Integer(int64 val) {
|
||||||
|
type = TYPE;
|
||||||
|
content = encode_int64(val);
|
||||||
|
value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Integer.from_content(uint8[] bytes) throws DecodeError {
|
||||||
|
type = TYPE;
|
||||||
|
content = bytes;
|
||||||
|
value = decode_int64(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint64 twos_complement(uint64 x) {
|
||||||
|
return ~x + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int min_bits(bool negative, uint64 x) {
|
||||||
|
int n = 0;
|
||||||
|
if (negative) {
|
||||||
|
while ((x >> (n + 8) & 0xff) != 0xff)
|
||||||
|
n += 8;
|
||||||
|
} else {
|
||||||
|
while (x >> (n + 8) > 0)
|
||||||
|
n += 8;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint8[] encode_int64(int64 val) {
|
||||||
|
var negative = val < 0;
|
||||||
|
var uval = negative ? twos_complement(val.abs()) : val;
|
||||||
|
var shift = min_bits(negative, uval);
|
||||||
|
|
||||||
|
var buffer = new ByteArray();
|
||||||
|
for (; shift >= 0; shift -= 8)
|
||||||
|
buffer.append({(uint8)(uval >> shift)});
|
||||||
|
|
||||||
|
if (!negative && (buffer.data[0] & 0x80) != 0)
|
||||||
|
buffer.prepend({0x00});
|
||||||
|
|
||||||
|
return buffer.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int64 decode_int64(uint8[] bytes) throws DecodeError {
|
||||||
|
if (bytes.length > MAX_BYTES) {
|
||||||
|
throw new DecodeError.INVALID_CONTENT(
|
||||||
|
"int64 too small for %u bytes", bytes.length);
|
||||||
|
}
|
||||||
|
var negative = (bytes[0] & 0x80) != 0;
|
||||||
|
var val = decode_start_val(negative, bytes.length);
|
||||||
|
foreach (var byte in bytes)
|
||||||
|
val = val << 8 | byte;
|
||||||
|
return negative ? -(int64)twos_complement(val) : (int64)val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint64 decode_start_val(bool negative, uint length)
|
||||||
|
{
|
||||||
|
if (!negative)
|
||||||
|
return 0;
|
||||||
|
var val = 0;
|
||||||
|
for (uint i = 0; i < MAX_BYTES - length; ++i)
|
||||||
|
val = val << 8 | 0xff;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Utf8String : Datum {
|
||||||
|
internal const uint8 TYPE = 0x0c;
|
||||||
|
|
||||||
|
public string value { get; private set; }
|
||||||
|
|
||||||
|
public Utf8String(string val) {
|
||||||
|
type = TYPE;
|
||||||
|
content = val.data;
|
||||||
|
value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Utf8String.from_content(uint8[] bytes) {
|
||||||
|
type = TYPE;
|
||||||
|
content = bytes;
|
||||||
|
value = decode_string(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string decode_string(uint8[] bytes) {
|
||||||
|
var buffer = new uint8[bytes.length + 1];
|
||||||
|
Memory.copy(buffer, bytes, bytes.length);
|
||||||
|
buffer[bytes.length] = 0;
|
||||||
|
return (string)buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Sequence : Datum {
|
||||||
|
internal const uint8 TYPE = 0x30;
|
||||||
|
|
||||||
|
public Datum[] value { get; private set; }
|
||||||
|
|
||||||
|
public Sequence(Datum[] val) {
|
||||||
|
type = TYPE;
|
||||||
|
content = encode_array(val);
|
||||||
|
value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Sequence.from_content(uint8[] bytes) throws DecodeError {
|
||||||
|
type = TYPE;
|
||||||
|
content = bytes;
|
||||||
|
value = decode_array(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint8[] encode_array(Datum[] val) {
|
||||||
|
var buffer = new ByteArray();
|
||||||
|
foreach (var datum in val)
|
||||||
|
buffer.append(datum.encode());
|
||||||
|
return buffer.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Datum[] decode_array(uint8[] bytes)
|
||||||
|
throws DecodeError {
|
||||||
|
var elems = new GenericArray<Datum>();
|
||||||
|
uint offset = 0;
|
||||||
|
uint size;
|
||||||
|
while (offset < bytes.length) {
|
||||||
|
elems.add(decode(bytes[offset:], out size));
|
||||||
|
offset += size;
|
||||||
|
}
|
||||||
|
return elems.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
client/src/main.vala
Normal file
24
client/src/main.vala
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Gtk;
|
||||||
|
|
||||||
|
namespace StudySystemClient {
|
||||||
|
public class App : Gtk.Application {
|
||||||
|
public App() {
|
||||||
|
Object(application_id: "sh.wip.study-system-client");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void activate() {
|
||||||
|
try {
|
||||||
|
var connection = new Connection(Config.CERT_DIR);
|
||||||
|
new MainWindow(this, connection);
|
||||||
|
} catch (Error e) {
|
||||||
|
stderr.printf("Failed to initialize connection: %s\n",
|
||||||
|
e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(string[] args) {
|
||||||
|
var app = new StudySystemClient.App();
|
||||||
|
return app.run(args);
|
||||||
|
}
|
||||||
39
client/src/main_window.vala
Normal file
39
client/src/main_window.vala
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
namespace StudySystemClient {
|
||||||
|
const string title = "Study System Client";
|
||||||
|
|
||||||
|
public class MainWindow : Gtk.ApplicationWindow {
|
||||||
|
private Connection connection;
|
||||||
|
private Gtk.Button send_button;
|
||||||
|
private Gtk.Label response_label;
|
||||||
|
|
||||||
|
public MainWindow(Gtk.Application app, Connection connection) {
|
||||||
|
Object(application: app);
|
||||||
|
|
||||||
|
default_width = 360;
|
||||||
|
default_height = 580;
|
||||||
|
|
||||||
|
this.connection = connection;
|
||||||
|
connection.received.connect((msg) => {
|
||||||
|
response_label.label = "Response: " + (string)msg;
|
||||||
|
});
|
||||||
|
|
||||||
|
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
|
||||||
|
box.margin_start = 10;
|
||||||
|
box.margin_end = 10;
|
||||||
|
box.margin_top = 10;
|
||||||
|
box.margin_bottom = 10;
|
||||||
|
|
||||||
|
send_button = new Gtk.Button.with_label("Send");
|
||||||
|
send_button.clicked.connect(() => connection.send("Foo".data));
|
||||||
|
box.append(send_button);
|
||||||
|
|
||||||
|
response_label = new Gtk.Label("");
|
||||||
|
response_label.wrap = true;
|
||||||
|
box.append(response_label);
|
||||||
|
|
||||||
|
set_child(box);
|
||||||
|
|
||||||
|
present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
client/src/meson.build
Normal file
33
client/src/meson.build
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
conf = configuration_data()
|
||||||
|
conf.set_quoted(
|
||||||
|
'CONFIG_CERT_DIR',
|
||||||
|
join_paths(meson.project_source_root(), '..', 'test'))
|
||||||
|
configure_file(
|
||||||
|
output: 'config.h',
|
||||||
|
configuration: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
lib = library(
|
||||||
|
'study-system-client',
|
||||||
|
sources: files(
|
||||||
|
'connection.vala',
|
||||||
|
'der.vala',
|
||||||
|
'main_window.vala',
|
||||||
|
),
|
||||||
|
dependencies: [gtk_dep],
|
||||||
|
vala_vapi: 'study-system-client.vapi'
|
||||||
|
)
|
||||||
|
lib_dep = declare_dependency(
|
||||||
|
link_with: lib,
|
||||||
|
include_directories: include_directories('.')
|
||||||
|
)
|
||||||
|
|
||||||
|
exe = executable(
|
||||||
|
'study-system-client',
|
||||||
|
sources: files(
|
||||||
|
'config.vapi',
|
||||||
|
'main.vala',
|
||||||
|
),
|
||||||
|
dependencies: [lib_dep, gtk_dep],
|
||||||
|
c_args: ['-w']
|
||||||
|
)
|
||||||
259
client/tests/der_tests.vala
Normal file
259
client/tests/der_tests.vala
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
using StudySystemClient;
|
||||||
|
using StudySystemClient.Der;
|
||||||
|
|
||||||
|
static bool bytes_equal(uint8[] expected, uint8[] actual) {
|
||||||
|
if (expected.length != actual.length)
|
||||||
|
return false;
|
||||||
|
return Memory.cmp(expected, actual, expected.length) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string bytes_to_string(uint8[] bytes) {
|
||||||
|
var s = "";
|
||||||
|
foreach (var byte in bytes)
|
||||||
|
s += "%02x".printf(byte);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_encode(Datum datum, uint8[] expected) {
|
||||||
|
var bytes = datum.encode();
|
||||||
|
if (!bytes_equal(expected, bytes)) {
|
||||||
|
Test.message("Encoding is incorrect: expected %s got %s",
|
||||||
|
bytes_to_string(expected), bytes_to_string(bytes));
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_decode_boolean(uint8[] bytes, bool expected) {
|
||||||
|
Boolean boolean;
|
||||||
|
try {
|
||||||
|
boolean = decode(bytes) as Boolean;
|
||||||
|
} catch (DecodeError err) {
|
||||||
|
Test.message("Decoding failed: %s", err.message);
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boolean == null) {
|
||||||
|
Test.message("Bytes were not decoded as a BOOLEAN");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (boolean.value != expected) {
|
||||||
|
Test.message(@"Expected $expected got $(boolean.value)");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_decode_integer(uint8[] bytes, int64 expected) {
|
||||||
|
Datum datum;
|
||||||
|
try {
|
||||||
|
datum = decode(bytes);
|
||||||
|
} catch (DecodeError err) {
|
||||||
|
Test.message("Decoding failed: %s", err.message);
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
test_integer_value(expected, datum);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_decode_utf8string(uint8[] bytes, string expected) {
|
||||||
|
Datum datum;
|
||||||
|
try {
|
||||||
|
datum = decode(bytes);
|
||||||
|
} catch (DecodeError err) {
|
||||||
|
Test.message("Decoding failed: %s", err.message);
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
test_utf8string_value(expected, datum);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_integer_value(int64 expected, Datum datum) {
|
||||||
|
var integer = datum as Integer;
|
||||||
|
if (integer == null) {
|
||||||
|
Test.message("Bytes were not decoded as a INTEGER");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (integer.value != expected) {
|
||||||
|
Test.message(@"Expected $expected got $(integer.value)");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_utf8string_value(string expected, Datum datum) {
|
||||||
|
var utf8string = datum as Utf8String;
|
||||||
|
if (utf8string == null) {
|
||||||
|
Test.message("Bytes were not decoded as a UTF8String");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (utf8string.value != expected) {
|
||||||
|
Test.message(@"Expected $expected got $(utf8string.value)");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(string[] args) {
|
||||||
|
Test.init(ref args);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Encoding
|
||||||
|
*/
|
||||||
|
|
||||||
|
Test.add_func("/encode/boolean/true", () => {
|
||||||
|
test_encode(new Boolean(true), {0x01, 0x01, 0xff});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/boolean/false", () => {
|
||||||
|
test_encode(new Boolean(false), {0x01, 0x01, 0x00});
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/encode/integer/small/0", () => {
|
||||||
|
test_encode(new Integer(0), {0x02, 0x01, 0x00});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/small/5", () => {
|
||||||
|
test_encode(new Integer(5), {0x02, 0x01, 0x05});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/small/42", () => {
|
||||||
|
test_encode(new Integer(42), {0x02, 0x01, 0x2a});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/large/1337", () => {
|
||||||
|
test_encode(new Integer(1337), {0x02, 0x02, 0x05, 0x39});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/sign/128", () => {
|
||||||
|
test_encode(new Integer(128), {0x02, 0x02, 0x00, 0x80});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/sign/0xbeef", () => {
|
||||||
|
test_encode(new Integer(0xbeef), {0x02, 0x03, 0x00, 0xbe, 0xef});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/sign/-128", () => {
|
||||||
|
test_encode(new Integer(-128), {0x02, 0x01, 0x80});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/integer/sign/-1337", () => {
|
||||||
|
test_encode(new Integer(-1337), {0x02, 0x02, 0xfa, 0xc7});
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/encode/utf8string/short/foo", () => {
|
||||||
|
test_encode(new Utf8String("foo"),
|
||||||
|
{0x0c, 0x03, 0x66, 0x6f, 0x6f});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/utf8string/short/bar", () => {
|
||||||
|
test_encode(new Utf8String("bar"),
|
||||||
|
{0x0c, 0x03, 0x62, 0x61, 0x72});
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/utf8string/long/x300", () => {
|
||||||
|
var expected = new uint8[304];
|
||||||
|
expected[0] = 0x0c;
|
||||||
|
expected[1] = 0x82;
|
||||||
|
expected[2] = 0x01;
|
||||||
|
expected[3] = 0x2c;
|
||||||
|
Memory.set(expected[4:], 0x78, 300);
|
||||||
|
test_encode(new Utf8String(string.nfill(300, 'x')), expected);
|
||||||
|
});
|
||||||
|
Test.add_func("/encode/utf8string/long/x128", () => {
|
||||||
|
var expected = new uint8[131];
|
||||||
|
expected[0] = 0x0c;
|
||||||
|
expected[1] = 0x81;
|
||||||
|
expected[2] = 0x80;
|
||||||
|
Memory.set(expected[3:], 0x78, 128);
|
||||||
|
test_encode(new Utf8String(string.nfill(128, 'x')), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/encode/sequence/foo,42", () => {
|
||||||
|
var sequence = new Der.Sequence(
|
||||||
|
{new Utf8String("foo"), new Integer(42)});
|
||||||
|
var expected = new uint8[] {
|
||||||
|
0x30, 0x08, 0x0c, 0x03, 0x66, 0x6f, 0x6f, 0x02, 0x01, 0x2a
|
||||||
|
};
|
||||||
|
test_encode(sequence, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Decoding
|
||||||
|
*/
|
||||||
|
|
||||||
|
Test.add_func("/decode/boolean/true", () => {
|
||||||
|
test_decode_boolean({0x01, 0x01, 0xff}, true);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/boolean/false", () => {
|
||||||
|
test_decode_boolean({0x01, 0x01, 0x00}, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/decode/integer/small/42", () => {
|
||||||
|
test_decode_integer({0x02, 0x01, 0x2a}, 42);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/integer/large/1337", () => {
|
||||||
|
test_decode_integer({0x02, 0x02, 0x05, 0x39}, 1337);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/integer/sign/128", () => {
|
||||||
|
test_decode_integer({0x02, 0x02, 0x00, 0x80}, 128);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/integer/sign/0xbeef", () => {
|
||||||
|
test_decode_integer({0x02, 0x03, 0x00, 0xbe, 0xef}, 0xbeef);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/integer/sign/-128", () => {
|
||||||
|
test_decode_integer({0x02, 0x01, 0x80}, -128);
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/integer/sign/-1337", () => {
|
||||||
|
test_decode_integer({0x02, 0x02, 0xfa, 0xc7}, -1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/decode/utf8string/short/foo", () => {
|
||||||
|
test_decode_utf8string({0x0c, 0x03, 0x66, 0x6f, 0x6f}, "foo");
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/utf8string/short/bar", () => {
|
||||||
|
test_decode_utf8string({0x0c, 0x03, 0x62, 0x61, 0x72}, "bar");
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/utf8string/long/x300", () => {
|
||||||
|
var bytes = new uint8[304];
|
||||||
|
bytes[0] = 0x0c;
|
||||||
|
bytes[1] = 0x82;
|
||||||
|
bytes[2] = 0x01;
|
||||||
|
bytes[3] = 0x2c;
|
||||||
|
Memory.set(bytes[4:], 0x78, 300);
|
||||||
|
test_decode_utf8string(bytes, string.nfill(300, 'x'));
|
||||||
|
});
|
||||||
|
Test.add_func("/decode/utf8string/long/x128", () => {
|
||||||
|
var bytes = new uint8[131];
|
||||||
|
bytes[0] = 0x0c;
|
||||||
|
bytes[1] = 0x81;
|
||||||
|
bytes[2] = 0x80;
|
||||||
|
Memory.set(bytes[3:], 0x78, 128);
|
||||||
|
test_decode_utf8string(bytes, string.nfill(128, 'x'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.add_func("/decode/sequence/foo,42", () => {
|
||||||
|
var bytes = new uint8[] {
|
||||||
|
0x30, 0x08, 0x0c, 0x03, 0x66, 0x6f, 0x6f, 0x02, 0x01, 0x2a
|
||||||
|
};
|
||||||
|
var expected = 2;
|
||||||
|
Der.Sequence sequence;
|
||||||
|
try {
|
||||||
|
sequence = decode(bytes) as Der.Sequence;
|
||||||
|
} catch (DecodeError err) {
|
||||||
|
Test.message("Decoding failed: %s", err.message);
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sequence == null) {
|
||||||
|
Test.message("Bytes were not decoded as a SEQUENCE");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Datum[] elems = sequence.value;
|
||||||
|
if (elems.length != expected) {
|
||||||
|
Test.message(
|
||||||
|
@"Expected $expected elements, got $(elems.length)");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
test_utf8string_value("foo", elems[0]);
|
||||||
|
test_integer_value(42, elems[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Test.run();
|
||||||
|
}
|
||||||
8
client/tests/meson.build
Normal file
8
client/tests/meson.build
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
test(
|
||||||
|
'DER tests',
|
||||||
|
executable(
|
||||||
|
'der_tests',
|
||||||
|
'der_tests.vala',
|
||||||
|
dependencies: [lib_dep, gtk_dep]
|
||||||
|
)
|
||||||
|
)
|
||||||
46
plan.txt
Normal file
46
plan.txt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
STUDY SYSTEM
|
||||||
|
|
||||||
|
GOALS
|
||||||
|
|
||||||
|
1. Support consistent and balanced study across multiple subjects
|
||||||
|
2. Help prioritize what to study when time/energy is available
|
||||||
|
3. Low friction of usage
|
||||||
|
4. Apply cybernetic principles to self-directed education
|
||||||
|
|
||||||
|
NON-GOALS
|
||||||
|
|
||||||
|
1. Setting targets for time spent studying
|
||||||
|
2. Measuring effectiveness of studies
|
||||||
|
3. Simplicity of set-up for non-technical users
|
||||||
|
4. Reading mail
|
||||||
|
|
||||||
|
CORE FEATURES
|
||||||
|
|
||||||
|
Priority System:
|
||||||
|
|
||||||
|
- Prioritized list of potential study activities
|
||||||
|
- Separate tracking of reading and exercises
|
||||||
|
- Neglected subjects increase in priority
|
||||||
|
- Balance maintenance between reading and exercises
|
||||||
|
|
||||||
|
Study health monitoring:
|
||||||
|
|
||||||
|
- Subject-level algedonic signals indicating consistency and
|
||||||
|
balance between reading and exercises
|
||||||
|
- System-wide algedonic signal indicating balance across subjects
|
||||||
|
|
||||||
|
IMPLEMENTATION DECISIONS
|
||||||
|
|
||||||
|
- Client/server to enable consistency across multiple devices
|
||||||
|
- Simple over-the-wire protocol so that future client
|
||||||
|
implementations for different platforms are feasible
|
||||||
|
(e.g. Android app)
|
||||||
|
- TCP with TLS for persistent connection
|
||||||
|
- Client authentication via certificates (mTLS) for simplicity of
|
||||||
|
implementation and security
|
||||||
|
- Server implementation in Erlang/OTP as it's very cool and I want
|
||||||
|
more practise with it
|
||||||
|
- ASN.1/DER for message serialization as it's flexible and reasonably
|
||||||
|
compact and has good Erlang integration
|
||||||
|
- Native Linux client because I hate the web
|
||||||
|
- Client implementation in GTK+Vala because it's boring
|
||||||
13
scripts/build-run.sh
Executable file
13
scripts/build-run.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd client
|
||||||
|
if [ ! -d build ]; then
|
||||||
|
meson setup build client
|
||||||
|
fi
|
||||||
|
meson compile -C build
|
||||||
|
build/src/study-system-client &
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd server
|
||||||
|
rebar3 shell --apps study_system_server
|
||||||
@@ -14,5 +14,10 @@ init([Port, CertDir]) ->
|
|||||||
SupFlags = #{stragegy => one_for_all,
|
SupFlags = #{stragegy => one_for_all,
|
||||||
intensity => 1,
|
intensity => 1,
|
||||||
period => 5},
|
period => 5},
|
||||||
ChildSpecs = [tcp_server:child_spec(Port, CertDir)],
|
ChildSpecs = [#{id => tcp_server,
|
||||||
|
start => {tcp_server, start_link, [Port, CertDir]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 5000,
|
||||||
|
type => worker,
|
||||||
|
modules => [tcp_server]}],
|
||||||
{ok, {SupFlags, ChildSpecs}}.
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-module(tcp_server).
|
-module(tcp_server).
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-export([start_link/2, child_spec/2]).
|
-export([start_link/2]).
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3]).
|
terminate/2, code_change/3]).
|
||||||
|
|
||||||
@@ -13,22 +13,13 @@
|
|||||||
start_link(Port, CertDir) ->
|
start_link(Port, CertDir) ->
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir], []).
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir], []).
|
||||||
|
|
||||||
child_spec(Port, CertDir) ->
|
|
||||||
#{id => ?MODULE,
|
|
||||||
start => {?MODULE, start_link, [Port, CertDir]},
|
|
||||||
restart => permanent,
|
|
||||||
shutdown => 5000,
|
|
||||||
type => worker,
|
|
||||||
modules => [?MODULE]}.
|
|
||||||
|
|
||||||
init([Port, CertDir]) ->
|
init([Port, CertDir]) ->
|
||||||
io:format("cert dir: ~p~n", [CertDir]),
|
TcpOpts = [binary, inet6, {active, false}, {reuseaddr, true}],
|
||||||
SslOpts = [{certfile, filename:join([CertDir, "server.pem"])},
|
SslOpts = [{certfile, filename:join([CertDir, "server.pem"])},
|
||||||
{cacertfile, filename:join([CertDir, "ca.pem"])},
|
{cacertfile, filename:join([CertDir, "ca.pem"])},
|
||||||
{verify, verify_peer},
|
{verify, verify_peer},
|
||||||
{fail_if_no_peer_cert, true}],
|
{fail_if_no_peer_cert, true}],
|
||||||
{ok, Socket} =
|
{ok, Socket} = ssl:listen(Port, TcpOpts ++ SslOpts),
|
||||||
ssl:listen(Port, [binary, inet6, {active, true} | SslOpts]),
|
|
||||||
Pid = spawn_link(fun() -> acceptor_loop(Socket) end),
|
Pid = spawn_link(fun() -> acceptor_loop(Socket) end),
|
||||||
{ok, #state{socket = Socket, acceptor = Pid}}.
|
{ok, #state{socket = Socket, acceptor = Pid}}.
|
||||||
|
|
||||||
@@ -58,7 +49,6 @@ acceptor_loop(Socket) ->
|
|||||||
Pid = spawn(
|
Pid = spawn(
|
||||||
fun() -> handle_connection(ClientSocket) end),
|
fun() -> handle_connection(ClientSocket) end),
|
||||||
ok = ssl:controlling_process(ClientSocket, Pid),
|
ok = ssl:controlling_process(ClientSocket, Pid),
|
||||||
ssl:setopts(ClientSocket, [{active, true}]),
|
|
||||||
acceptor_loop(Socket);
|
acceptor_loop(Socket);
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
acceptor_loop(Socket)
|
acceptor_loop(Socket)
|
||||||
@@ -68,10 +58,17 @@ acceptor_loop(Socket) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
handle_connection(Socket) ->
|
handle_connection(Socket) ->
|
||||||
|
ssl:setopts(Socket, [{active, true}]),
|
||||||
|
handle_connection_loop(Socket).
|
||||||
|
|
||||||
|
handle_connection_loop(Socket) ->
|
||||||
receive
|
receive
|
||||||
{ssl, Socket, Data} ->
|
{ssl, Socket, Data} ->
|
||||||
ssl:send(Socket, Data),
|
handle_client_msg(Socket, Data),
|
||||||
handle_connection(Socket);
|
handle_connection_loop(Socket);
|
||||||
{ssl_closed, Socket} ->
|
{ssl_closed, Socket} ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
handle_client_msg(Socket, Msg) ->
|
||||||
|
ssl:send(Socket, Msg).
|
||||||
|
|||||||
Reference in New Issue
Block a user