#! /opt/appfs/rkeene.org/crystal/platform/latest/bin/crystal
require "http/server"
private class Session
@id : Int32;
@process : Process | Nil;
@used : Time::Span;
@peer : HTTP::WebSocket;
def initialize(id : Int32, socket : HTTP::WebSocket);
@id = id;
@used = Time.monotonic();
@peer = socket;
end
def finalize
processCopy = @process;
if processCopy.nil?
return;
end
processCopy.kill(Signal::KILL);
end
def id
@id
end
def age : Int64
now = Time.monotonic();
diff = now - @used;
return(diff.to_i());
end
private def input
if @process.nil?
process = Process.new(
command: "./secure-wrap",
args: [@id.to_s, "/tmp/x.123", "/tcl"],
clear_env: true,
input: Process::Redirect::Pipe, output: Process::Redirect::Pipe, error: Process::Redirect::Pipe
);
spawn {
process.output.each_line {|line|
::puts("[TCL-OUT->WS:#{@id}] #{line}")
@peer.send(line);
}
@process = nil;
}
spawn {
process.error.each_line {|line|
::puts("[TCL-ERR->WS:#{@id}] #{line}")
@peer.send(line);
}
@process = nil;
}
@process = process;
end
return(@process.as(Process).input);
end
def puts(command : String)
@used = Time.monotonic();
::puts("[WS->TCL:#{@id}] #{command}")
input.puts(command);
return ""
end
end
private class Sessions
@@sessions = Hash(String, Session).new();
@@nextRemoteID = 0;
@session : Session;
def initialize(id : String, socket : HTTP::WebSocket)
@id = id;
session = @@sessions[id]?;
if session.nil?
validRemoteID = false;
remoteIDTries = 0
while !validRemoteID && remoteIDTries < 8192;
remoteIDTries += 1;
remoteID = @@nextRemoteID;
@@nextRemoteID = (remoteID + 1) % 8192;
validRemoteID = true;
@@sessions.each_value {|other_session|
if other_session.id === remoteID
validRemoteID = false;
break;
end
}
end
if remoteID.nil?
raise "No available sessions";
end
session = Session.new(remoteID, socket);
@@sessions[id] = session;
end
@session = session;
end
def finalize
@session.finalize
@@sessions.delete(@id)
end
def self.cleanupOldSessions
@@sessions.each {|other_key, other_session|
if other_session.age < 1800
next;
end
other_session.finalize
@@sessions.delete(other_key);
}
end
def to_s
return "id = #{@id}; remoteID = #{remoteID}";
end
def remoteID
@session.id
end
def puts(*args)
@session.puts(*args);
end
end
server = HTTP::Server.new([
HTTP::ErrorHandler.new(),
HTTP::LogHandler.new(),
HTTP::WebSocketHandler.new {|socket, context|
Sessions.cleanupOldSessions();
sessionID = context.request.query;
session = nil;
socket.on_message {|input|
if session.nil?
session = Sessions.new(sessionID.as(String), socket);
end
# I have to create a new variable here because
# Crystal is not smart enough to prune the types
# list :-(
currentSession = session.as(Sessions);
currentSession.puts(input);
};
socket.on_close {
if !session.nil?
currentSession = session.as(Sessions);
currentSession.finalize();
end
session = nil;
};
},
HTTP::StaticFileHandler.new("../web")
]);
address = server.bind_tcp("0.0.0.0", 8080);
puts("Listening on http://#{address}");
server.listen();