Compare commits
4 Commits
18541786e1
...
e3f0eaa24d
| Author | SHA1 | Date | |
|---|---|---|---|
| e3f0eaa24d | |||
| f5cb3b7166 | |||
| 218c1e3644 | |||
| 10560371ab |
11
asn1/StudySystemProtocol.asn1
Normal file
11
asn1/StudySystemProtocol.asn1
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
StudySystemProtocol DEFINITIONS EXPLICIT TAGS ::= BEGIN
|
||||||
|
|
||||||
|
Request ::= CHOICE {
|
||||||
|
foo [0] NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
Response ::= CHOICE {
|
||||||
|
msg [0] UTF8String
|
||||||
|
}
|
||||||
|
|
||||||
|
END
|
||||||
@@ -201,7 +201,7 @@ namespace StudySystemClient {
|
|||||||
private class OutgoingMessage {
|
private class OutgoingMessage {
|
||||||
public uint8[] content { get; private set; }
|
public uint8[] content { get; private set; }
|
||||||
|
|
||||||
private const uint MAX_FAIL_COUNT = 10;
|
private const uint MAX_FAIL_COUNT = 4;
|
||||||
|
|
||||||
private uint fail_count;
|
private uint fail_count;
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace StudySystemClient.Der {
|
|||||||
|
|
||||||
private static Datum decode_datum(uint8 type, uint8[] content)
|
private static Datum decode_datum(uint8 type, uint8[] content)
|
||||||
throws DecodeError {
|
throws DecodeError {
|
||||||
if ((type & 0xc0) == Choice.BASE_TYPE)
|
if ((type & ~Choice.ID_MASK) == Choice.BASE_TYPE)
|
||||||
return new Choice.from_content(type, content);
|
return new Choice.from_content(type, content);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Boolean.TYPE:
|
case Boolean.TYPE:
|
||||||
@@ -59,6 +59,8 @@ namespace StudySystemClient.Der {
|
|||||||
return new Utf8String.from_content(content);
|
return new Utf8String.from_content(content);
|
||||||
case Sequence.TYPE:
|
case Sequence.TYPE:
|
||||||
return new Sequence.from_content(content);
|
return new Sequence.from_content(content);
|
||||||
|
case Null.TYPE:
|
||||||
|
return new Null.from_content(content);
|
||||||
default:
|
default:
|
||||||
throw new DecodeError.UNKNOWN_TYPE("Unsupported type: %02x",
|
throw new DecodeError.UNKNOWN_TYPE("Unsupported type: %02x",
|
||||||
type);
|
type);
|
||||||
@@ -266,7 +268,8 @@ namespace StudySystemClient.Der {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class Choice : Datum {
|
public class Choice : Datum {
|
||||||
internal const uint8 BASE_TYPE = 0x80;
|
internal const uint8 BASE_TYPE = 0xa0;
|
||||||
|
internal const uint8 ID_MASK = 0x1f;
|
||||||
|
|
||||||
public int id { get; private set; }
|
public int id { get; private set; }
|
||||||
public Datum value { get; private set; }
|
public Datum value { get; private set; }
|
||||||
@@ -282,8 +285,26 @@ namespace StudySystemClient.Der {
|
|||||||
throws DecodeError {
|
throws DecodeError {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
content = bytes;
|
content = bytes;
|
||||||
id = type & 0x3f;
|
id = type & ID_MASK;
|
||||||
value = decode(bytes);
|
value = decode(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Null : Datum {
|
||||||
|
internal const uint8 TYPE = 0x05;
|
||||||
|
|
||||||
|
public Null() {
|
||||||
|
type = TYPE;
|
||||||
|
content = new uint8[] {};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Null.from_content(uint8[] bytes) throws DecodeError {
|
||||||
|
if (bytes.length != 0) {
|
||||||
|
throw new DecodeError.INVALID_CONTENT(
|
||||||
|
"Non-empty content for NULL");
|
||||||
|
}
|
||||||
|
type = TYPE;
|
||||||
|
content = bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ namespace StudySystemClient {
|
|||||||
|
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
connection.received.connect((msg) => {
|
connection.received.connect((msg) => {
|
||||||
response_label.label = "Response: " + (string)msg;
|
var der = Der.decode(msg) as Der.Choice;
|
||||||
|
var str = der.value as Der.Utf8String;
|
||||||
|
response_label.label = "Response: " + str.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
|
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
|
||||||
@@ -24,7 +26,10 @@ namespace StudySystemClient {
|
|||||||
box.margin_bottom = 10;
|
box.margin_bottom = 10;
|
||||||
|
|
||||||
send_button = new Gtk.Button.with_label("Send");
|
send_button = new Gtk.Button.with_label("Send");
|
||||||
send_button.clicked.connect(() => connection.send("Foo".data));
|
send_button.clicked.connect(() => {
|
||||||
|
var msg = new Der.Choice(0, new Der.Null());
|
||||||
|
connection.send(msg.encode());
|
||||||
|
});
|
||||||
box.append(send_button);
|
box.append(send_button);
|
||||||
|
|
||||||
response_label = new Gtk.Label("");
|
response_label = new Gtk.Label("");
|
||||||
|
|||||||
@@ -175,11 +175,15 @@ void main(string[] args) {
|
|||||||
Test.add_func("/encode/choice/1:foo", () => {
|
Test.add_func("/encode/choice/1:foo", () => {
|
||||||
var choice = new Der.Choice(1, new Utf8String("foo"));
|
var choice = new Der.Choice(1, new Utf8String("foo"));
|
||||||
var expected = new uint8[] {
|
var expected = new uint8[] {
|
||||||
0x81, 0x05, 0x0c, 0x03, 0x66, 0x6f, 0x6f
|
0xa1, 0x05, 0x0c, 0x03, 0x66, 0x6f, 0x6f
|
||||||
};
|
};
|
||||||
test_encode(choice, expected);
|
test_encode(choice, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Test.add_func("/encode/null", () => {
|
||||||
|
test_encode(new Der.Null(), { 0x05, 0x00 });
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Decoding
|
* Decoding
|
||||||
*/
|
*/
|
||||||
@@ -266,7 +270,7 @@ void main(string[] args) {
|
|||||||
Test.add_func("/decode/choice/1:foo", () => {
|
Test.add_func("/decode/choice/1:foo", () => {
|
||||||
var expected_id = 1;
|
var expected_id = 1;
|
||||||
var bytes = new uint8[] {
|
var bytes = new uint8[] {
|
||||||
0x81, 0x05, 0x0c, 0x03, 0x66, 0x6f, 0x6f
|
0xa1, 0x05, 0x0c, 0x03, 0x66, 0x6f, 0x6f
|
||||||
};
|
};
|
||||||
Der.Choice choice;
|
Der.Choice choice;
|
||||||
try {
|
try {
|
||||||
@@ -289,5 +293,23 @@ void main(string[] args) {
|
|||||||
test_utf8string_value("foo", choice.value);
|
test_utf8string_value("foo", choice.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Test.add_func("/decode/null", () => {
|
||||||
|
var bytes = new uint8[] { 0x05, 0x00 };
|
||||||
|
Der.Null @null;
|
||||||
|
try {
|
||||||
|
@null = decode(bytes) as Der.Null;
|
||||||
|
} catch (DecodeError err) {
|
||||||
|
Test.message("Decoding failed: %s", err.message);
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (@null == null) {
|
||||||
|
Test.message("Bytes were not decoded as a NULL");
|
||||||
|
Test.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
Test.run();
|
Test.run();
|
||||||
}
|
}
|
||||||
|
|||||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
_build/*
|
_build/*
|
||||||
*.beam
|
*.beam
|
||||||
|
src/StudySystemProtocol.erl
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info]}.
|
||||||
{deps, []}.
|
{deps, []}.
|
||||||
|
{plugins, [{provider_asn1, "0.4.1"}]}.
|
||||||
|
{provider_hooks, [{pre, [{compile, {asn, compile}}]},
|
||||||
|
{post, [{clean, {asn, clean}}]}]}.
|
||||||
|
{asn1_args, [{compile_opts, [der]}]}.
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ start_link(Port, CertDir) ->
|
|||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir]).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir]).
|
||||||
|
|
||||||
init([Port, CertDir]) ->
|
init([Port, CertDir]) ->
|
||||||
SupFlags = #{stragegy => one_for_all,
|
SupFlags = #{stragegy => one_for_one,
|
||||||
intensity => 1,
|
intensity => 1,
|
||||||
period => 5},
|
period => 5},
|
||||||
ChildSpecs = [#{id => tcp_server,
|
ChildSpecs = [#{id => session_sup,
|
||||||
|
start => {session_sup, start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 5000,
|
||||||
|
type => supervisor,
|
||||||
|
modules => [session_sup]},
|
||||||
|
#{id => tcp_server,
|
||||||
start => {tcp_server, start_link, [Port, CertDir]},
|
start => {tcp_server, start_link, [Port, CertDir]},
|
||||||
restart => permanent,
|
restart => permanent,
|
||||||
shutdown => 5000,
|
shutdown => 5000,
|
||||||
|
|||||||
42
server/src/session_server.erl
Normal file
42
server/src/session_server.erl
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
% Copyright (c) Camden Dixie O'Brien
|
||||||
|
% SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
-module(session_server).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/1]).
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
|
terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
start_link(Socket) ->
|
||||||
|
gen_server:start_link(?MODULE, Socket, []).
|
||||||
|
|
||||||
|
init(Socket) ->
|
||||||
|
ssl:setopts(Socket, [{active, true}]),
|
||||||
|
{ok, #{socket => Socket}}.
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info({ssl, Socket, Data}, State) ->
|
||||||
|
case 'StudySystemProtocol':decode('Request', Data) of
|
||||||
|
{ok, {foo, _}} ->
|
||||||
|
{ok, Encoded}
|
||||||
|
= 'StudySystemProtocol':encode('Response', {msg, "Foo"}),
|
||||||
|
ssl:send(Socket, Encoded);
|
||||||
|
Result ->
|
||||||
|
io:format("Invalid message: ~p~n", [Result]),
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
{noreply, State};
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, #{socket := Socket}) ->
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
25
server/src/session_sup.erl
Normal file
25
server/src/session_sup.erl
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
% Copyright (c) Camden Dixie O'Brien
|
||||||
|
% SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
-module(session_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0, init/1, start_session/1]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{strategy => simple_one_for_one,
|
||||||
|
intensity => 10,
|
||||||
|
period => 1},
|
||||||
|
ChildSpec = #{id => session_server,
|
||||||
|
start => {session_server, start_link, []},
|
||||||
|
restart => temporary,
|
||||||
|
shutdown => 5000,
|
||||||
|
type => worker,
|
||||||
|
modules => [session_server]},
|
||||||
|
{ok, {SupFlags, [ChildSpec]}}.
|
||||||
|
|
||||||
|
start_session(Socket) ->
|
||||||
|
supervisor:start_child(?MODULE, [Socket]).
|
||||||
@@ -20,8 +20,8 @@ init([Port, CertDir]) ->
|
|||||||
{verify, verify_peer},
|
{verify, verify_peer},
|
||||||
{fail_if_no_peer_cert, true}],
|
{fail_if_no_peer_cert, true}],
|
||||||
{ok, Socket} = ssl:listen(Port, TcpOpts ++ SslOpts),
|
{ok, Socket} = ssl:listen(Port, TcpOpts ++ SslOpts),
|
||||||
Pid = spawn_link(fun() -> acceptor_loop(Socket) end),
|
self() ! accept,
|
||||||
{ok, #state{socket = Socket, acceptor = Pid}}.
|
{ok, #state{socket = Socket}}.
|
||||||
|
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
{reply, ok, State}.
|
{reply, ok, State}.
|
||||||
@@ -29,46 +29,30 @@ handle_call(_Request, _From, State) ->
|
|||||||
handle_cast(_Msg, State) ->
|
handle_cast(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({'EXIT', Pid, Reason}, State = #state{acceptor = Pid}) ->
|
handle_info(accept, State = #state{socket = Socket}) ->
|
||||||
{stop, {acceptor_died, Reason}, State};
|
case ssl:transport_accept(Socket) of
|
||||||
|
{ok, TlsSocket} ->
|
||||||
|
self() ! {handshake, TlsSocket},
|
||||||
|
self() ! accept;
|
||||||
|
{error, closed} ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
{noreply, State};
|
||||||
|
handle_info({handshake, TlsSocket}, State) ->
|
||||||
|
case ssl:handshake(TlsSocket) of
|
||||||
|
{ok, ClientSocket} ->
|
||||||
|
{ok, Pid} = session_sup:start_session(ClientSocket),
|
||||||
|
ok = ssl:controlling_process(ClientSocket, Pid);
|
||||||
|
{error, _Reason} ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
{noreply, State};
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #state{socket = Socket}) ->
|
terminate(_Reason, #state{socket = Socket}) ->
|
||||||
gen_tcp:close(Socket),
|
ssl:close(Socket),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
acceptor_loop(Socket) ->
|
|
||||||
case ssl:transport_accept(Socket) of
|
|
||||||
{ok, TlsSocket} ->
|
|
||||||
case ssl:handshake(TlsSocket) of
|
|
||||||
{ok, ClientSocket} ->
|
|
||||||
Pid = spawn(
|
|
||||||
fun() -> handle_connection(ClientSocket) end),
|
|
||||||
ok = ssl:controlling_process(ClientSocket, Pid),
|
|
||||||
acceptor_loop(Socket);
|
|
||||||
{error, _Reason} ->
|
|
||||||
acceptor_loop(Socket)
|
|
||||||
end;
|
|
||||||
{error, closed} ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
handle_connection(Socket) ->
|
|
||||||
ssl:setopts(Socket, [{active, true}]),
|
|
||||||
handle_connection_loop(Socket).
|
|
||||||
|
|
||||||
handle_connection_loop(Socket) ->
|
|
||||||
receive
|
|
||||||
{ssl, Socket, Data} ->
|
|
||||||
handle_client_msg(Socket, Data),
|
|
||||||
handle_connection_loop(Socket);
|
|
||||||
{ssl_closed, Socket} ->
|
|
||||||
ok
|
|
||||||
end.
|
|
||||||
|
|
||||||
handle_client_msg(Socket, Msg) ->
|
|
||||||
ssl:send(Socket, Msg).
|
|
||||||
|
|||||||
Reference in New Issue
Block a user