Artifact [483336bdd6]

Artifact 483336bdd6103c9b1e11f6ea2eb518770d7cd852227f40d5c51121f5dda544aa:


#! /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();