#!/usr/bin/env ruby
# frozen_string_literal: true

#
# Syd: rock-solid application kernel
# lib/examples/ruby/rbshell.rb: Remote shell using syd via rbsyd
#
# Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
# SPDX-License-Identifier: GPL-3.0

require "fileutils"
require "socket"
require "tempfile"
require "tmpdir"
require "syd"

SYD_CONFIG = <<~SYD
  # Sandboxing types.
  # Enable all except stat sandboxing.
  sandbox/read:on
  sandbox/stat:off
  sandbox/write:on
  sandbox/exec:on
  sandbox/net:on
  sandbox/pid:on
  sandbox/mem:on

  # Define a modest limit for PID sandboxing
  pid/max:64

  # Define modest limits for Memory sandboxing
  mem/max:256M
  mem/vm_max:2G

  # Allow /dev/null
  allow/read+/dev/null
  allow/write+/dev/null

  # Allow reading dynamic libraries under system paths.
  allow/read+/lib*/**
  allow/read+/usr/**/lib*/**

  # Allow PTYs
  allow/read+/dev/ptmx
  allow/write+/dev/ptmx
  allow/read+/dev/pty/[0-9]*
  allow/write+/dev/pty/[0-9]*

  # Allow execution of binaries under system paths.
  allow/exec+/bin/*
  allow/exec+/usr/**/bin/*

  # Allow /proc but deny pid1=syd
  allow/read+/proc/***
  allow/write+/proc/***
  deny/read+/proc/1/***
  deny/write+/proc/1/***
SYD

def main
  port = ARGV[0] || "65432"

  begin
    Syd.check
  rescue StandardError => e
    warn "Not running under syd: #{e}"
    puts 'Run "syd -plib -pcontainer ./rbshell.rb"'
    exit 1
  end

  puts "Initializing"

  temp_dir = Dir.mktmpdir("rbshell-tmp-")
  puts "Temporary directory created: #{temp_dir}"

  at_exit { FileUtils.remove_entry(temp_dir) }

  Dir.chdir(temp_dir)

  abs_path = File.absolute_path(temp_dir)
  cwd = File.realpath(abs_path)
  ENV["HOME"] = cwd

  Tempfile.open("syd_config") do |file|
    file.write(SYD_CONFIG)
    file.rewind

    Syd.load(file.fileno)
    puts "Load: ok"
  end

  Syd.allow_read_add("#{cwd}/***")
  puts "AllowReadAdd(#{cwd}/***): ok"
  Syd.allow_write_add("#{cwd}/**")
  puts "AllowWriteAdd(#{cwd}/**): ok"
  Syd.allow_net_bind_add("127.0.0.1!#{port}")
  puts "AllowNetBind(127.0.0.1!#{port}): ok"

  Syd.lock(Syd::LOCK_ON)
  puts "LockOn: ok"

  start_tcp_server(port)
end

def start_tcp_server(port)
  server = TCPServer.new("127.0.0.1", port)
  puts "Listening on localhost:#{port}"

  loop do
    Thread.start(server.accept) do |client|
      handle_client(client)
    end
  end
ensure
  server&.close
end

def handle_client(client)
  # Use 'r+' for bidirectional communication and merge STDERR with STDOUT
  IO.popen("/bin/sh", "r+", err: %i[child out]) do |shell|
    Thread.new { IO.copy_stream(client, shell) }
    IO.copy_stream(shell, client)
  end
rescue StandardError => e
  client.puts "Error running shell: #{e}"
ensure
  client.close
end

main if __FILE__ == $PROGRAM_NAME
