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

package main

import (
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path/filepath"

	syd "git.sr.ht/~alip/syd/lib/src"
	"github.com/tmthrgd/tmpfile"
)

const 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/***
`

func main() {
	// Determine the port to listen on
	port := "65432" // default port
	if len(os.Args) > 1 {
		port = os.Args[1]
	}

	if err := syd.Check(); err != nil {
		fmt.Fprintf(os.Stderr, "Not running under syd: %v\n", err)
		fmt.Println("Run \"syd -plib -pcontainer ./goshell\"")
		os.Exit(1)
	}
	fmt.Println("Initializing")

	// Create a temporary directory with a specific prefix
	tempDir, err := ioutil.TempDir("", "goshell-tmp-")
	if err != nil {
		panic(err)
	}
	fmt.Println("Temporary directory created:", tempDir)

	// Defer the deletion of the temporary directory
	defer os.RemoveAll(tempDir)

	// Change to the temporary directory
	if err := os.Chdir(tempDir); err != nil {
		panic(err)
	}

	// Get the canonical absolute path of the temporary directory
	absPath, err := filepath.Abs(tempDir)
	if err != nil {
		panic(err)
	}

	// Resolve any symbolic links
	canonicalPath, err := filepath.EvalSymlinks(absPath)
	if err != nil {
		panic(err)
	}
	cwd := canonicalPath
	os.Setenv("HOME", cwd)

	// Create a new temporary file
	tempFile, remove, err := tmpfile.TempFile("")
	if err != nil {
		panic(err)
	}
	defer tempFile.Close()

	// If remove is true, it's the caller's responsibility to remove
	// the file when no longer needed
	if remove {
		os.Remove(tempFile.Name())
	}

	// Write the syd configuration to the file
	_, err = tempFile.WriteString(SYD)
	if err != nil {
		panic(err)
	}

	// Seek the file to the beginning
	_, err = tempFile.Seek(0, os.SEEK_SET)
	if err != nil {
		panic(err)
	}

	// Load the profile into syd.
	if err := syd.Load(int(tempFile.Fd())); err != nil {
		panic(err)
	} else {
		fmt.Println("Load: ok")
	}

	// Allow current working directory for read+write
	if err := syd.ReadAdd(syd.ActionAllow, cwd+"/***"); err != nil {
		panic(err)
	} else {
		fmt.Printf("AllowReadAdd(%s/***): ok\n", cwd)
	}
	if err := syd.WriteAdd(syd.ActionAllow, cwd+"/**"); err != nil {
		panic(err)
	} else {
		fmt.Printf("AllowWriteAdd(%s/**): ok\n", cwd)
	}

	// Allow binding to requested address.
	if err := syd.NetBindAdd(syd.ActionAllow, "127.0.0.1!"+port); err != nil {
		panic(err)
	} else {
		fmt.Printf("AllowNetBind(127.0.0.1!%s)\n", port)
	}

	// Finally, lock syd so tampering is no longer possible.
	if err := syd.Lock(syd.LockOn); err != nil {
		panic(err)
	} else {
		fmt.Println("LockOn: ok")
	}

	// Listen on TCP port
	listener, err := net.Listen("tcp", "127.0.0.1:"+port)
	if err != nil {
		panic(err)
	}
	defer listener.Close()
	fmt.Println("Listening on localhost:" + port)

	for {
		// Accept a connection
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting: ", err.Error())
			os.Exit(1)
		}

		// Handle the connection in a new goroutine
		go handleRequest(conn)
	}
}

func handleRequest(conn net.Conn) {
	// Close the connection when function ends
	defer conn.Close()

	// Start /bin/sh and connect its input/output to the connection
	cmd := exec.Command("/bin/sh")
	cmd.Stdin, cmd.Stdout, cmd.Stderr = conn, conn, conn

	// Run the shell
	if err := cmd.Run(); err != nil {
		fmt.Fprintf(conn, "Error running shell: %s\n", err)
		return
	}
}
