This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/bash/filesystem.go
Torjus Håkestad 8189a108d1 feat: add shell interface, registry, and bash shell emulator
Implement Phase 1.4: replaces the hardcoded banner/timeout stub with a
proper shell system. Adds a Shell interface with weighted registry for
shell selection, a RecordingChannel wrapper (pass-through for now, prep
for Phase 2.3 replay), and a bash-like shell with fake filesystem,
terminal line reader, and command handling (pwd, ls, cd, cat, whoami,
hostname, id, uname, exit). Sessions now log command/output pairs to
the store and record the shell name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 20:24:48 +01:00

167 lines
3.9 KiB
Go

package bash
import (
"fmt"
"path"
"strings"
)
type fsNode struct {
name string
isDir bool
content string
children map[string]*fsNode
}
type filesystem struct {
root *fsNode
}
func newFilesystem(hostname string) *filesystem {
fs := &filesystem{
root: &fsNode{name: "/", isDir: true, children: make(map[string]*fsNode)},
}
fs.mkdirAll("/etc")
fs.mkdirAll("/root")
fs.mkdirAll("/home")
fs.mkdirAll("/var/log")
fs.mkdirAll("/tmp")
fs.mkdirAll("/usr/bin")
fs.mkdirAll("/usr/local")
fs.writeFile("/etc/passwd", "root:x:0:0:root:/root:/bin/bash\n"+
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n"+
"www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\n"+
"mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/false\n")
fs.writeFile("/etc/hostname", hostname+"\n")
fs.writeFile("/etc/hosts", "127.0.0.1\tlocalhost\n"+
"127.0.1.1\t"+hostname+"\n"+
"::1\t\tlocalhost ip6-localhost ip6-loopback\n")
fs.writeFile("/root/.bash_history",
"apt update\n"+
"apt upgrade -y\n"+
"systemctl restart nginx\n"+
"tail -f /var/log/syslog\n"+
"df -h\n"+
"free -m\n"+
"netstat -tlnp\n"+
"cat /etc/passwd\n")
fs.writeFile("/root/.bashrc",
"# ~/.bashrc: executed by bash(1) for non-login shells.\n"+
"export PS1='\\u@\\h:\\w\\$ '\n"+
"alias ll='ls -alF'\n"+
"alias la='ls -A'\n")
fs.writeFile("/root/README.txt", "Production server - DO NOT MODIFY\n")
fs.writeFile("/var/log/syslog",
"Jan 12 03:14:22 "+hostname+" systemd[1]: Started Daily apt download activities.\n"+
"Jan 12 03:14:23 "+hostname+" systemd[1]: Started Daily Cleanup of Temporary Directories.\n"+
"Jan 12 04:00:01 "+hostname+" CRON[12345]: (root) CMD (/usr/local/bin/backup.sh)\n"+
"Jan 12 04:00:03 "+hostname+" kernel: [UFW BLOCK] IN=eth0 OUT= SRC=203.0.113.42 DST=10.0.0.5 PROTO=TCP DPT=22\n")
fs.writeFile("/tmp/notes.txt", "TODO: Update SSL certificates\n")
return fs
}
// resolvePath converts a potentially relative path to an absolute one.
func resolvePath(cwd, p string) string {
if !strings.HasPrefix(p, "/") {
p = cwd + "/" + p
}
return path.Clean(p)
}
func (fs *filesystem) lookup(p string) *fsNode {
p = path.Clean(p)
if p == "/" {
return fs.root
}
parts := strings.Split(strings.TrimPrefix(p, "/"), "/")
node := fs.root
for _, part := range parts {
if node.children == nil {
return nil
}
child, ok := node.children[part]
if !ok {
return nil
}
node = child
}
return node
}
func (fs *filesystem) exists(p string) bool {
return fs.lookup(p) != nil
}
func (fs *filesystem) isDirectory(p string) bool {
n := fs.lookup(p)
return n != nil && n.isDir
}
func (fs *filesystem) list(p string) ([]string, error) {
n := fs.lookup(p)
if n == nil {
return nil, fmt.Errorf("ls: cannot access '%s': No such file or directory", p)
}
if !n.isDir {
return nil, fmt.Errorf("ls: cannot access '%s': Not a directory", p)
}
names := make([]string, 0, len(n.children))
for name, child := range n.children {
if child.isDir {
name += "/"
}
names = append(names, name)
}
return names, nil
}
func (fs *filesystem) read(p string) (string, error) {
n := fs.lookup(p)
if n == nil {
return "", fmt.Errorf("cat: %s: No such file or directory", p)
}
if n.isDir {
return "", fmt.Errorf("cat: %s: Is a directory", p)
}
return n.content, nil
}
func (fs *filesystem) mkdirAll(p string) {
p = path.Clean(p)
parts := strings.Split(strings.TrimPrefix(p, "/"), "/")
node := fs.root
for _, part := range parts {
if node.children == nil {
node.children = make(map[string]*fsNode)
}
child, ok := node.children[part]
if !ok {
child = &fsNode{name: part, isDir: true, children: make(map[string]*fsNode)}
node.children[part] = child
}
node = child
}
}
func (fs *filesystem) writeFile(p string, content string) {
p = path.Clean(p)
dir := path.Dir(p)
base := path.Base(p)
fs.mkdirAll(dir)
parent := fs.lookup(dir)
parent.children[base] = &fsNode{name: base, content: content}
}