diff --git a/lib/args.nim b/lib/args.nim new file mode 100644 index 0000000..d2ebe7a --- /dev/null +++ b/lib/args.nim @@ -0,0 +1,48 @@ +import parseopt +import options +import modes +import os + +type Args* = object + name*: Option[string] + cmd*: Option[string] + profile*: Option[string] + +proc getCmd*(args: Args): string = + return args.cmd.get(getEnv("SHELL", "/bin/bash")) + +proc getProfile*(args: Args, mode: Modes): string = + if args.profile.isSome: + return args.profile.unsafeGet + + return case mode + of Modes.Shell: "shell" + of Modes.Box: "gui" + +proc parseOpt(args: var Args, key: string, value: string): bool = + case key + of "command", "c": + args.cmd = some(value) + of "profile", "p": + args.profile = some(value) + else: + return false + + return true + +proc parseArgs*(): Option[Args] = + var p = initOptParser() + var args = Args() + + while true: + p.next() + case p.kind + of cmdEnd: break + of cmdShortOption, cmdLongOption: + if p.val == "" or args.parseOpt(p.key, p.val) == false: + echo "Invalid argument ", p.val + return + of cmdArgument: + args.name = some(p.key.string) + + return some(args) \ No newline at end of file diff --git a/lib/config.nim b/lib/config.nim index d079f24..8c2992b 100644 --- a/lib/config.nim +++ b/lib/config.nim @@ -1,11 +1,43 @@ +import sequtils import options +import bwrap +import utils +import json type Link* = object src*: string dst*: string type Config* = object - extends*: Option[seq[string]] + extends*: Option[string] mount*: Option[seq[string]] romount*: Option[seq[string]] symlinks*: Option[seq[Link]] + +proc applyConfig*(call: var BwrapCall, config: Config) = + for mount in config.mount.get(@[]): + call.addMount("--bind", checkRelativePath(mount)) + + for mount in config.romount.get(@[]): + call.addMount("--ro-bind", checkRelativePath(mount)) + + for symlink in config.symlinks.get(@[]): + call.addArg("--symlink", symlink.src, symlink.dst) + +proc loadConfig*(path: string): Config = + return readFile(path) + .parseJson() + .to(Config) + +proc extendConfig*(config: var Config): Config {.discardable.} = + if config.extends.isNone: + return + + var eConf = loadConfig(getProfilePath(config.extends.unsafeGet)) + eConf.extendConfig() + + config.mount = some(config.mount.get(@[]).concat(eConf.mount.get(@[]))) + config.romount = some(config.romount.get(@[]).concat(eConf.romount.get(@[]))) + config.symlinks = some(config.symlinks.get(@[]).concat(eConf.symlinks.get(@[]))) + + return config diff --git a/lib/sandbox.nim b/lib/sandbox.nim index 40eabbf..8bf257f 100644 --- a/lib/sandbox.nim +++ b/lib/sandbox.nim @@ -1,59 +1,49 @@ import os -import json +import args +import utils import modes import bwrap import config import options -proc homePath(p: string): string = - joinPath(getHomeDir(), p) - -const CONFIG_LOCATION = homePath(joinPath(".sandboxes", "config.json")) - -proc checkRelativePath(p: string): string = - if p[0] == '/': - return p - homePath(p) - -proc applyConfig(call: var BwrapCall, config: Config) = - for mount in config.mount.get(@[]): - call.addMount("--bind", checkRelativePath(mount)) - - for mount in config.romount.get(@[]): - call.addMount("--ro-bind", checkRelativePath(mount)) - - for symlink in config.symlinks.get(@[]): - call.addArg("--symlink", symlink.src, symlink.dst) - -proc loadConfig(path: string): Config = - return readFile(path).parseJson().to(Config) - -proc sandboxExec*(name: string, command: string, mode: Modes) = - let sandboxPath = homePath(joinPath(".sandboxes", name)) - let sandboxFiles = joinPath(sandboxPath, "files") - let sandboxInfo = joinPath(sandboxPath, "info") - - createDir(sandboxFiles) +proc sandboxExec*(mode: Modes, args: Args) = var call = BwrapCall() + var userConfig = none(Config) + + let hostname = args.name.get("sandbox") + let profilePath = getProfilePath(args, mode) + + if args.name.isSome: + let name = args.name.unsafeGet + let sandboxPath = getSandboxPath(name) + let sandboxFiles = sandboxPath.joinPath("files") + let configPath = sandboxPath.joinPath("config.json") + + if fileExists(configPath): + userConfig = some(loadConfig(configPath)) + + createDir(sandboxFiles) + call.addArg("--bind", sandboxFiles, getHomeDir()) + + var profile = loadConfig(profilePath) + profile.extendConfig() call - .addArg("--bind", sandboxFiles, getHomeDir()) - .addMount("--dev-bind", "/dev") + .addMount("--dev-bind", "/dev/null") .addArg("--tmpfs", "/tmp") .addArg("--proc", "/proc") .addArg("--unshare-all") .addArg("--share-net") .addArg("--die-with-parent") - .addArg("--hostname", name) - .applyConfig(loadConfig(CONFIG_LOCATION)) + .applyConfig(profile) if mode == Modes.Shell: call .addMount("--bind", getCurrentDir()) .addArg("--chdir", getCurrentDir()) + .addArg("--hostname", hostname) - let configPath = sandboxPath.joinPath("config.json") - if fileExists(configPath): - call.applyConfig(loadConfig(configPath)) + if userConfig.isSome: + call.applyConfig(userConfig.unsafeGet) - call.addArg(command).exec() + call.addArg(args.getCmd).exec() diff --git a/lib/utils.nim b/lib/utils.nim new file mode 100644 index 0000000..7a569f7 --- /dev/null +++ b/lib/utils.nim @@ -0,0 +1,26 @@ +import os +import args +import modes + +const APP_NAME = "bwsandbox" + +proc getDataDir*(): string = + getEnv("XDG_DATA_DIR", getHomeDir().joinPath(".local/share")) + +proc checkRelativePath*(p: string): string = + if p[0] == '/': + return p + getHomeDir().joinPath(p) + +proc getProfilePath*(profile: string): string = + getConfigDir() + .joinPath(APP_NAME) + .joinPath(profile) + +proc getProfilePath*(args: Args, mode: Modes): string = + getProfilePath(args.getProfile(mode)) + +proc getSandboxPath*(name: string): string = + getDataDir() + .joinPath(APP_NAME) + .joinPath(name) \ No newline at end of file diff --git a/main.nim b/main.nim index dba577c..5705617 100644 --- a/main.nim +++ b/main.nim @@ -1,27 +1,19 @@ import lib/sandbox import lib/modes +import lib/args import strformat import strutils +import options import os -proc main() = - let mode = parseEnum[Modes](paramStr(0)) - let args = commandLineParams() - let argc = paramCount() +proc main(): int = + let mode = parseEnum[Modes](paramStr(0), Modes.Shell) + let args = parseArgs() - if argc == 0: - echo &"Usage: {mode} [command]" - quit(1) - - - let name = args[0] - var command: string - - if argc > 1: - command = args[1] + if args.isNone: + echo &"Usage: {mode} --command=cmd --profile=profile " + return 1 else: - command = getEnv("SHELL", "/bin/sh") + sandboxExec(mode, args.unsafeGet) - sandboxExec(name, command, mode) - -main() +quit(main())