6 Commits

Author SHA1 Message Date
Hadi 098400ba77 pflag -> cobra: Sub commands, completions, ...
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 11:36:46 +02:00
Hadi 4922f3704d fix: make empty text responsive
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 11:13:57 +02:00
Hadi 1aa4b92bb3 add titles
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 10:51:10 +02:00
Hadi 437e8f883d fix: govulncheck & fix vulns
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 10:46:32 +02:00
Hadi 5c74eda48b fix: gosec vulns
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 10:29:18 +02:00
Hadi 1190276bab add gosec
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 10:06:32 +02:00
28 changed files with 274 additions and 197 deletions
+7 -6
View File
@@ -40,7 +40,6 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
<img alt="demo" src="./.github/assets/demo.gif" width="700" /> <img alt="demo" src="./.github/assets/demo.gif" width="700" />
<!-- exec: cat ./docs/legal-disclaimer.md --> <!-- exec: cat ./docs/legal-disclaimer.md -->
## Legal Disclaimer ## Legal Disclaimer
**This tool is provided for educational purposes and authorized security testing only.** **This tool is provided for educational purposes and authorized security testing only.**
@@ -48,7 +47,6 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction). Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction).
The author(s) and contributors are not responsible for any misuse, damage, or legal consequences resulting from the use of this software. By using Spilltea, you agree that you are solely responsible for ensuring your usage is lawful and authorized. The author(s) and contributors are not responsible for any misuse, damage, or legal consequences resulting from the use of this software. By using Spilltea, you agree that you are solely responsible for ensuring your usage is lawful and authorized.
<!-- endexec --> <!-- endexec -->
## Features ## Features
@@ -103,7 +101,6 @@ environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ]
</details> </details>
<!-- exec: cat ./docs/basics.md --> <!-- exec: cat ./docs/basics.md -->
## Project Management ## Project Management
Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files. Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files.
@@ -124,19 +121,23 @@ Colors and styles can be customized using [ilovetui](https://github.com/anotherh
## CLI Flags ## CLI Flags
``` ```
Usage: spilltea [flags] A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
Usage:
spilltea [flags]
Flags:
--add-default-config copy the default config file to the config path and exit --add-default-config copy the default config file to the config path and exit
--add-default-plugins copy built-in example plugins into the plugins dir and exit --add-default-plugins copy built-in example plugins into the plugins dir and exit
-c, --config string path to config file -c, --config string path to config file
-h, --help help for spilltea
--host string proxy host (overrides config) --host string proxy host (overrides config)
--plugins-dir string path to plugins dir (overrides config) --plugins-dir string path to plugins dir (overrides config)
-p, --port int proxy port (overrides config) -p, --port int proxy port (overrides config)
-P, --project string project name to open directly, or "tmp" for a temporary session -P, --project string project name to open directly, or "tmp" for a temporary session
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config) --upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
-v, --version print version -v, --version version for spilltea
``` ```
<!-- endexec --> <!-- endexec -->
## Plugin System ## Plugin System
+67 -68
View File
@@ -15,7 +15,7 @@ import (
"github.com/anotherhadi/spilltea/internal/style" "github.com/anotherhadi/spilltea/internal/style"
appUI "github.com/anotherhadi/spilltea/internal/ui/app" appUI "github.com/anotherhadi/spilltea/internal/ui/app"
homeUI "github.com/anotherhadi/spilltea/internal/ui/home" homeUI "github.com/anotherhadi/spilltea/internal/ui/home"
flag "github.com/spf13/pflag" "github.com/spf13/cobra"
) )
// Version is overwritten at build time by goreleaser/ldflag with the current version tag, or "dev" if not set. // Version is overwritten at build time by goreleaser/ldflag with the current version tag, or "dev" if not set.
@@ -32,94 +32,83 @@ func init() {
func main() { func main() {
var ( var (
flagConfig = flag.StringP("config", "c", "", "path to config file") flagConfig string
flagPluginsDir = flag.String("plugins-dir", "", "path to plugins dir (overrides config)") flagPluginsDir string
flagHost = flag.String("host", "", "proxy host (overrides config)") flagHost string
flagPort = flag.IntP("port", "p", 0, "proxy port (overrides config)") flagPort int
flagUpstreamProxy = flag.String("upstream-proxy", "", "upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)") flagUpstreamProxy string
flagVersion = flag.BoolP("version", "v", false, "print version") flagProject string
flagProject = flag.StringP("project", "P", "", `project name to open directly, or "tmp" for a temporary session`) flagAddDefaultPlugins bool
flagAddDefaultPlugins = flag.Bool("add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit") flagAddDefaultConfig bool
flagAddDefaultConfig = flag.Bool("add-default-config", false, "copy the default config file to the config path and exit")
) )
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = func() {
fmt.Println("Usage: spilltea [flags]")
fmt.Println("")
flag.PrintDefaults()
}
flag.Parse()
if *flagVersion { rootCmd := &cobra.Command{
fmt.Println(version) Use: "spilltea",
os.Exit(0) Short: "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.",
} Version: version,
SilenceUsage: true,
if *flagAddDefaultPlugins { SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if flagAddDefaultPlugins {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml") cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
if *flagConfig != "" { if flagConfig != "" {
cfgPath = *flagConfig cfgPath = flagConfig
} }
if err := config.Load(cfgPath); err != nil { if err := config.Load(cfgPath); err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err) return fmt.Errorf("config: %w", err)
os.Exit(1)
} }
dir := config.ExpandPath(config.Global.App.PluginsDir) dir := config.ExpandPath(config.Global.App.PluginsDir)
if *flagPluginsDir != "" { if flagPluginsDir != "" {
dir = *flagPluginsDir dir = flagPluginsDir
} }
n, err := spilltea.InstallDefaultPlugins(dir) n, err := spilltea.InstallDefaultPlugins(dir)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "add-default-plugins: %v\n", err) return fmt.Errorf("add-default-plugins: %w", err)
os.Exit(1)
} }
fmt.Printf("added %d plugin(s) to %s\n", n, dir) fmt.Printf("added %d plugin(s) to %s\n", n, dir)
os.Exit(0) return nil
} }
if *flagAddDefaultConfig { if flagAddDefaultConfig {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml") cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
if *flagConfig != "" { if flagConfig != "" {
cfgPath = *flagConfig cfgPath = flagConfig
} }
if err := config.WriteDefaultConfig(cfgPath); err != nil { if err := config.WriteDefaultConfig(cfgPath); err != nil {
fmt.Fprintf(os.Stderr, "add-default-config: %v\n", err) return fmt.Errorf("add-default-config: %w", err)
os.Exit(1)
} }
fmt.Printf("default config written to %s\n", cfgPath) fmt.Printf("default config written to %s\n", cfgPath)
os.Exit(0) return nil
} }
if *flagProject != "" && !homeUI.IsValidProjectName(*flagProject) { if flagProject != "" && !homeUI.IsValidProjectName(flagProject) {
fmt.Fprintf(os.Stderr, "project: invalid name %q (only lowercase letters, digits, - and _ are allowed)\n", *flagProject) return fmt.Errorf("project: invalid name %q (only lowercase letters, digits, - and _ are allowed)", flagProject)
os.Exit(1)
} }
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml") cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
if *flagConfig != "" { if flagConfig != "" {
cfgPath = *flagConfig cfgPath = flagConfig
} }
if err := config.Load(cfgPath); err != nil { if err := config.Load(cfgPath); err != nil {
fmt.Fprintf(os.Stderr, "config: %v\n", err) return fmt.Errorf("config: %w", err)
os.Exit(1)
} }
config.Global.Version = version config.Global.Version = version
if *flagPluginsDir != "" { if flagPluginsDir != "" {
config.Global.App.PluginsDir = *flagPluginsDir config.Global.App.PluginsDir = flagPluginsDir
} }
if *flagHost != "" { if flagHost != "" {
config.Global.App.Host = *flagHost config.Global.App.Host = flagHost
} }
if *flagPort != 0 { if flagPort != 0 {
config.Global.App.Port = *flagPort config.Global.App.Port = flagPort
} }
if *flagUpstreamProxy != "" { if flagUpstreamProxy != "" {
config.Global.App.UpstreamProxy = *flagUpstreamProxy config.Global.App.UpstreamProxy = flagUpstreamProxy
} }
style.Init() style.Init()
@@ -128,40 +117,50 @@ func main() {
projectDir := config.ExpandPath(config.Global.App.ProjectDir) projectDir := config.ExpandPath(config.Global.App.ProjectDir)
// If --project flag is set, skip the home screen entirely. if flagProject != "" {
if *flagProject != "" { project, err := homeUI.OpenProject(projectDir, flagProject)
project, err := homeUI.OpenProject(projectDir, *flagProject)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "project: %v\n", err) return fmt.Errorf("project: %w", err)
os.Exit(1)
} }
broker := intercept.NewBroker() broker := intercept.NewBroker()
m := appUI.New(broker, project.Name, project.Path) m := appUI.New(broker, project.Name, project.Path)
final, err := tea.NewProgram(m).Run() final, err := tea.NewProgram(m).Run()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "tui: %v\n", err) return fmt.Errorf("tui: %w", err)
os.Exit(1)
} }
if app, ok := final.(appUI.Model); ok { if app, ok := final.(appUI.Model); ok {
if ferr := app.FatalErr(); ferr != nil { if ferr := app.FatalErr(); ferr != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", ferr) return ferr
os.Exit(1)
} }
} }
return return nil
} }
// Run home + app in a single program to avoid a blank flash on transition.
root := rootModel{home: homeUI.New(projectDir)} root := rootModel{home: homeUI.New(projectDir)}
final, err := tea.NewProgram(root).Run() final, err := tea.NewProgram(root).Run()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "tui: %v\n", err) return fmt.Errorf("tui: %w", err)
os.Exit(1)
} }
if r, ok := final.(rootModel); ok { if r, ok := final.(rootModel); ok {
if ferr := r.FatalErr(); ferr != nil { if ferr := r.FatalErr(); ferr != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", ferr) return ferr
}
}
return nil
},
}
rootCmd.Flags().StringVarP(&flagConfig, "config", "c", "", "path to config file")
rootCmd.Flags().StringVar(&flagPluginsDir, "plugins-dir", "", "path to plugins dir (overrides config)")
rootCmd.Flags().StringVar(&flagHost, "host", "", "proxy host (overrides config)")
rootCmd.Flags().IntVarP(&flagPort, "port", "p", 0, "proxy port (overrides config)")
rootCmd.Flags().StringVar(&flagUpstreamProxy, "upstream-proxy", "", "upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)")
rootCmd.Flags().StringVarP(&flagProject, "project", "P", "", `project name to open directly, or "tmp" for a temporary session`)
rootCmd.Flags().BoolVar(&flagAddDefaultPlugins, "add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit")
rootCmd.Flags().BoolVar(&flagAddDefaultConfig, "add-default-config", false, "copy the default config file to the config path and exit")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
}
} }
+7 -2
View File
@@ -19,16 +19,21 @@ Colors and styles can be customized using [ilovetui](https://github.com/anotherh
<!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' --> <!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' -->
``` ```
Usage: spilltea [flags] A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
Usage:
spilltea [flags]
Flags:
--add-default-config copy the default config file to the config path and exit --add-default-config copy the default config file to the config path and exit
--add-default-plugins copy built-in example plugins into the plugins dir and exit --add-default-plugins copy built-in example plugins into the plugins dir and exit
-c, --config string path to config file -c, --config string path to config file
-h, --help help for spilltea
--host string proxy host (overrides config) --host string proxy host (overrides config)
--plugins-dir string path to plugins dir (overrides config) --plugins-dir string path to plugins dir (overrides config)
-p, --port int proxy port (overrides config) -p, --port int proxy port (overrides config)
-P, --project string project name to open directly, or "tmp" for a temporary session -P, --project string project name to open directly, or "tmp" for a temporary session
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config) --upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
-v, --version print version -v, --version version for spilltea
``` ```
<!-- endexec --> <!-- endexec -->
+4 -2
View File
@@ -16,7 +16,7 @@ require (
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
golang.org/x/net v0.54.0 golang.org/x/net v0.55.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.50.1 modernc.org/sqlite v1.50.1
) )
@@ -41,6 +41,7 @@ require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect
@@ -55,6 +56,7 @@ require (
github.com/satori/go.uuid v1.2.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark v1.8.2 // indirect
@@ -62,7 +64,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.44.0 // indirect golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect golang.org/x/text v0.37.0 // indirect
modernc.org/libc v1.72.3 // indirect modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
+11 -4
View File
@@ -44,6 +44,7 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
@@ -72,6 +73,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -104,6 +107,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
@@ -116,6 +120,9 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
@@ -140,12 +147,12 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
+1 -1
View File
@@ -86,7 +86,7 @@ func Load(path string) error {
} }
func WriteDefaultConfig(path string) error { func WriteDefaultConfig(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return fmt.Errorf("create config dir: %w", err) return fmt.Errorf("create config dir: %w", err)
} }
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil { if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
+16 -5
View File
@@ -2,6 +2,7 @@ package db
import ( import (
"database/sql" "database/sql"
"log"
"sync" "sync"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -25,17 +26,25 @@ func Open(path string) (*DB, error) {
conn.SetMaxOpenConns(1) conn.SetMaxOpenConns(1)
d := &DB{conn: conn, path: path} d := &DB{conn: conn, path: path}
if err := d.migrate(); err != nil { if err := d.migrate(); err != nil {
conn.Close() if cerr := conn.Close(); cerr != nil {
log.Printf("db: close conn: %v", cerr)
}
return nil, err return nil, err
} }
roConn, err := sql.Open("sqlite", path) roConn, err := sql.Open("sqlite", path)
if err != nil { if err != nil {
conn.Close() if cerr := conn.Close(); cerr != nil {
log.Printf("db: close conn: %v", cerr)
}
return nil, err return nil, err
} }
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil { if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
conn.Close() if cerr := conn.Close(); cerr != nil {
roConn.Close() log.Printf("db: close conn: %v", cerr)
}
if cerr := roConn.Close(); cerr != nil {
log.Printf("db: close roConn: %v", cerr)
}
return nil, err return nil, err
} }
d.roConn = roConn d.roConn = roConn
@@ -114,6 +123,8 @@ func CountEntriesAt(path string) int {
} }
defer conn.Close() defer conn.Close()
var n int var n int
conn.QueryRow(`SELECT COUNT(*) FROM entries`).Scan(&n) if err := conn.QueryRow(`SELECT COUNT(*) FROM entries`).Scan(&n); err != nil {
log.Printf("db: count entries: %v", err)
}
return n return n
} }
+1 -1
View File
@@ -121,7 +121,7 @@ func (d *DB) SearchEntries(term string) ([]Entry, error) {
// Uses the persistent read-only connection (PRAGMA query_only=ON) so that any // Uses the persistent read-only connection (PRAGMA query_only=ON) so that any
// DML or DDL in the user-supplied expression is rejected by SQLite before it executes. // DML or DDL in the user-supplied expression is rejected by SQLite before it executes.
func (d *DB) QueryEntries(where string) ([]Entry, error) { func (d *DB) QueryEntries(where string) ([]Entry, error) {
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where) q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where) // #nosec G202 -- intentional: user-supplied WHERE clause on a PRAGMA query_only=ON connection
rows, err := d.roConn.Query(q) rows, err := d.roConn.Query(q)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -281,7 +281,7 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
c := exec.CommandContext(ctx, "sh", "-c", cmd) c := exec.CommandContext(ctx, "sh", "-c", cmd) // #nosec G204 -- intentional: shell_pipe is a Lua plugin API for user-written scripts
c.Stdin = strings.NewReader(input) c.Stdin = strings.NewReader(input)
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
+1 -1
View File
@@ -27,7 +27,7 @@ func OpenPluginsFile(dbPath string) (*PluginsFile, error) {
path: path, path: path,
data: pluginsFileData{Plugins: make(map[string]pluginFileEntry)}, data: pluginsFileData{Plugins: make(map[string]pluginFileEntry)},
} }
raw, err := os.ReadFile(path) raw, err := os.ReadFile(path) // #nosec G304 -- path is filepath.Join(dbDir, "plugins.yaml"), hardcoded filename
if os.IsNotExist(err) { if os.IsNotExist(err) {
return pf, nil return pf, nil
} }
+1 -1
View File
@@ -178,7 +178,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
editor = "vi" editor = "vi"
} }
logPath := filepath.Join(filepath.Dir(m.projectPath), "logs.log") logPath := filepath.Join(filepath.Dir(m.projectPath), "logs.log")
return m, tea.ExecProcess(exec.Command(editor, logPath), nil) return m, tea.ExecProcess(exec.Command(editor, logPath), nil) // #nosec G204 G702 -- editor from trusted $EDITOR env var, logPath is a fixed path
} }
if !m.activeIsEditing() { if !m.activeIsEditing() {
+11 -9
View File
@@ -6,25 +6,28 @@ import (
ilovetui "github.com/anotherhadi/ilovetui" ilovetui "github.com/anotherhadi/ilovetui"
) )
func (m Model) newView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
v.WindowTitle = "Spilltea: " + m.projectName
return v
}
func (m Model) View() tea.View { func (m Model) View() tea.View {
if m.width == 0 { if m.width == 0 {
v := tea.NewView("") return m.newView("")
v.AltScreen = true
return v
} }
normal := m.renderNormal() normal := m.renderNormal()
if m.copyAs.IsOpen() { if m.copyAs.IsOpen() {
v := tea.NewView(m.copyAs.View(normal)) v := m.newView(m.copyAs.View(normal))
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion v.MouseMode = tea.MouseModeCellMotion
return v return v
} }
if m.copy.IsOpen() { if m.copy.IsOpen() {
v := tea.NewView(m.copy.View(normal)) v := m.newView(m.copy.View(normal))
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion v.MouseMode = tea.MouseModeCellMotion
return v return v
} }
@@ -34,8 +37,7 @@ func (m Model) View() tea.View {
rendered = m.notifications.View(normal) rendered = m.notifications.View(normal)
} }
v := tea.NewView(rendered) v := m.newView(rendered)
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion v.MouseMode = tea.MouseModeCellMotion
return v return v
} }
+2 -2
View File
@@ -300,7 +300,7 @@ func (m *Model) refreshViewports() {
placeholder := lipgloss.Place( placeholder := lipgloss.Place(
m.leftViewport.Width(), m.leftViewport.Height(), m.leftViewport.Width(), m.leftViewport.Height(),
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
ilovetui.S.Faint.Render(util.CenterLines("<(^_^)>", "send two entries here to compare")), ilovetui.S.Faint.Render(util.EmptyState(m.leftViewport.Width(), "<(^_^)>", "send two entries here to compare")),
) )
m.leftViewport.SetContent(placeholder) m.leftViewport.SetContent(placeholder)
m.rightViewport.SetContent("") m.rightViewport.SetContent("")
@@ -312,7 +312,7 @@ func (m *Model) refreshViewports() {
placeholder := lipgloss.Place( placeholder := lipgloss.Place(
m.rightViewport.Width(), m.rightViewport.Height(), m.rightViewport.Width(), m.rightViewport.Height(),
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
ilovetui.S.Faint.Render(util.CenterLines("(・3・)", "waiting for second entry…")), ilovetui.S.Faint.Render(util.EmptyState(m.rightViewport.Width(), "(・3・)", "waiting for second entry…")),
) )
m.rightViewport.SetContent(placeholder) m.rightViewport.SetContent(placeholder)
return return
+1 -1
View File
@@ -52,7 +52,7 @@ func (m *Model) renderList() string {
return lipgloss.Place( return lipgloss.Place(
m.listViewport.Width(), m.listViewport.Height(), m.listViewport.Width(), m.listViewport.Height(),
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")), ilovetui.S.Faint.Render(util.EmptyState(m.listViewport.Width(), "(҂◡_◡) ᕤ", "no findings")),
) )
} }
+14 -5
View File
@@ -2,6 +2,7 @@ package history
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
@@ -234,7 +235,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, h.Flag): case key.Matches(msg, h.Flag):
if len(m.entries) > 0 && m.database != nil { if len(m.entries) > 0 && m.database != nil {
m.database.ToggleFlag(m.entries[m.cursor].ID) if err := m.database.ToggleFlag(m.entries[m.cursor].ID); err != nil {
log.Printf("history: toggle flag: %v", err)
}
return m, m.RefreshCmd() return m, m.RefreshCmd()
} }
@@ -242,7 +245,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if len(m.entries) > 0 { if len(m.entries) > 0 {
id := m.entries[m.cursor].ID id := m.entries[m.cursor].ID
if m.database != nil { if m.database != nil {
m.database.DeleteEntry(id) if err := m.database.DeleteEntry(id); err != nil {
log.Printf("history: delete entry: %v", err)
}
} }
return m, LoadEntriesCmd(m.database) return m, LoadEntriesCmd(m.database)
} }
@@ -261,10 +266,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if hasUnflagged && e.Flagged { if hasUnflagged && e.Flagged {
continue continue
} }
m.database.DeleteEntry(e.ID) if err := m.database.DeleteEntry(e.ID); err != nil {
log.Printf("history: delete entry: %v", err)
}
} }
} else { } else {
m.database.DeleteAllExceptFlagged() if err := m.database.DeleteAllExceptFlagged(); err != nil {
log.Printf("history: delete all unflagged: %v", err)
}
} }
} }
return m, m.clearSearch() return m, m.clearSearch()
@@ -346,7 +355,7 @@ func (m *Model) refreshBody() {
} }
if raw == "" { if raw == "" {
w, h := m.bodyViewport.Width(), m.bodyViewport.Height() w, h := m.bodyViewport.Width(), m.bodyViewport.Height()
m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(˘・_・˘)", "no response stored")))) m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(w, "(˘・_・˘)", "no response stored"))))
return return
} }
m.bodyViewport.SetContent(style.HighlightHTTP(raw)) m.bodyViewport.SetContent(style.HighlightHTTP(raw))
+2 -2
View File
@@ -82,9 +82,9 @@ func (m *Model) renderList() string {
) )
} }
if len(m.entries) == 0 { if len(m.entries) == 0 {
msg := util.CenterLines("(⌐■_■)", "no history yet") msg := util.EmptyState(m.listViewport.Width(), "(⌐■_■)", "no history yet")
if m.searchKind != searchKindOff { if m.searchKind != searchKindOff {
msg = util.CenterLines("ʕノ•ᴥ•ʔノ ︵ ┻━┻", "no results") msg = util.EmptyState(m.listViewport.Width(), "ʕノ•ᴥ•ʔノ ︵ ┻━┻", "no results")
} }
return lipgloss.Place( return lipgloss.Place(
m.listViewport.Width(), m.listViewport.Height(), m.listViewport.Width(), m.listViewport.Height(),
+12 -7
View File
@@ -3,6 +3,7 @@ package home
import ( import (
crypto "crypto/rand" crypto "crypto/rand"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -72,7 +73,7 @@ func (m Model) handleSelection() (tea.Model, tea.Cmd) {
return m, m.nameInput.Focus() return m, m.nameInput.Focus()
case kindTemp: case kindTemp:
dir := tempDir() dir := tempDir()
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o750); err != nil {
return m, nil return m, nil
} }
initProjectFiles(dir) initProjectFiles(dir)
@@ -90,7 +91,9 @@ func (m Model) deleteSelected() (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
dir := filepath.Dir(item.path) // parent dir of data.db dir := filepath.Dir(item.path) // parent dir of data.db
os.RemoveAll(dir) if err := os.RemoveAll(dir); err != nil {
log.Printf("home: remove project dir: %v", err)
}
idx := m.list.GlobalIndex() idx := m.list.GlobalIndex()
m.list.RemoveItem(idx) m.list.RemoveItem(idx)
if idx > 0 && idx >= len(m.list.Items()) { if idx > 0 && idx >= len(m.list.Items()) {
@@ -113,7 +116,7 @@ func (m Model) updateNaming(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.mode = modeSelect m.mode = modeSelect
m.nameInput.Blur() m.nameInput.Blur()
dir := filepath.Join(m.projectDir, name) dir := filepath.Join(m.projectDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o750); err != nil {
return m, nil return m, nil
} }
initProjectFiles(dir) initProjectFiles(dir)
@@ -147,14 +150,14 @@ func IsValidProjectName(s string) bool {
func OpenProject(projectDir, name string) (*Project, error) { func OpenProject(projectDir, name string) (*Project, error) {
if name == "tmp" || name == "temp" || name == "temporary" { if name == "tmp" || name == "temp" || name == "temporary" {
dir := tempDir() dir := tempDir()
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o750); err != nil {
return nil, err return nil, err
} }
initProjectFiles(dir) initProjectFiles(dir)
return &Project{Name: "temporary", Path: filepath.Join(dir, "data.db")}, nil return &Project{Name: "temporary", Path: filepath.Join(dir, "data.db")}, nil
} }
dir := filepath.Join(projectDir, name) dir := filepath.Join(projectDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o750); err != nil {
return nil, err return nil, err
} }
initProjectFiles(dir) initProjectFiles(dir)
@@ -171,9 +174,11 @@ func initProjectFiles(dir string) {
for _, name := range []string{"data.db", "logs.log"} { for _, name := range []string{"data.db", "logs.log"} {
p := filepath.Join(dir, name) p := filepath.Join(dir, name)
if _, err := os.Stat(p); os.IsNotExist(err) { if _, err := os.Stat(p); os.IsNotExist(err) {
f, err := os.Create(p) f, err := os.Create(p) // #nosec G304 -- p is filepath.Join(dir, hardcoded_name), no traversal possible
if err == nil { if err == nil {
f.Close() if cerr := f.Close(); cerr != nil {
log.Printf("home: close project file: %v", cerr)
}
} }
} }
} }
+1
View File
@@ -45,6 +45,7 @@ func (m Model) View() tea.View {
v := tea.NewView(content) v := tea.NewView(content)
v.AltScreen = true v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion v.MouseMode = tea.MouseModeCellMotion
v.WindowTitle = "Spilltea"
return v return v
} }
+2 -2
View File
@@ -100,7 +100,7 @@ func (m *Model) renderStatusBar() string {
func (m *Model) renderList() string { func (m *Model) renderList() string {
if len(m.queue) == 0 { if len(m.queue) == 0 {
return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(。◕‿‿◕。)", "waiting for a request"))) return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.listViewport.Width(), "(。◕‿‿◕。)", "waiting for a request")))
} }
start, end := util.PageBounds(m.pager, len(m.queue)) start, end := util.PageBounds(m.pager, len(m.queue))
@@ -149,7 +149,7 @@ func (m *Model) renderList() string {
func (m *Model) renderResponseList() string { func (m *Model) renderResponseList() string {
if len(m.responseQueue) == 0 { if len(m.responseQueue) == 0 {
return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡)", "no response yet"))) return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.responseViewport.Width(), "(҂◡_◡)", "no response yet")))
} }
start, end := util.PageBounds(m.responsePager, len(m.responseQueue)) start, end := util.PageBounds(m.responsePager, len(m.responseQueue))
+3 -3
View File
@@ -15,7 +15,7 @@ import (
func (m Model) View() tea.View { func (m Model) View() tea.View {
if m.width == 0 || m.manager == nil { if m.width == 0 || m.manager == nil {
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(._.)~*.'", "no plugins loaded")))) return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.width, "(._.)~*.'", "no plugins loaded"))))
} }
listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4) listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4)
@@ -127,9 +127,9 @@ func (m *Model) renderStatusBar() string {
func (m *Model) renderList() string { func (m *Model) renderList() string {
if len(m.filtered) == 0 { if len(m.filtered) == 0 {
msg := util.CenterLines("(ง •̀_•́)ง", "no plugins", "", "spilltea --add-default-plugins") msg := util.EmptyState(m.listViewport.Width(), "(ง •̀_•́)ง", "no plugins", "", "spilltea --add-default-plugins")
if m.filter != "" { if m.filter != "" {
msg = util.CenterLines("= _ =", "no results") msg = util.EmptyState(m.listViewport.Width(), "= _ =", "no results")
} }
return lipgloss.Place( return lipgloss.Place(
m.listViewport.Width(), m.listViewport.Height(), m.listViewport.Width(), m.listViewport.Height(),
+13 -6
View File
@@ -7,6 +7,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -79,7 +80,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
e.ResponseRaw = "Error: " + msg.err.Error() e.ResponseRaw = "Error: " + msg.err.Error()
} }
if m.database != nil && e.DBID != 0 { if m.database != nil && e.DBID != 0 {
m.database.UpdateReplayEntry(entryToDB(*e)) if err := m.database.UpdateReplayEntry(entryToDB(*e)); err != nil {
log.Printf("replay: update entry: %v", err)
}
} }
} }
m.refreshListViewport() m.refreshListViewport()
@@ -216,7 +219,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
if len(m.entries) > 0 { if len(m.entries) > 0 {
e := m.entries[m.cursor] e := m.entries[m.cursor]
if m.database != nil && e.DBID != 0 { if m.database != nil && e.DBID != 0 {
m.database.DeleteReplayEntry(e.DBID) if err := m.database.DeleteReplayEntry(e.DBID); err != nil {
log.Printf("replay: delete entry: %v", err)
}
} }
m.entries = append(m.entries[:m.cursor], m.entries[m.cursor+1:]...) m.entries = append(m.entries[:m.cursor], m.entries[m.cursor+1:]...)
if m.cursor >= len(m.entries) && m.cursor > 0 { if m.cursor >= len(m.entries) && m.cursor > 0 {
@@ -229,7 +234,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, r.DeleteAll): case key.Matches(msg, r.DeleteAll):
if m.database != nil { if m.database != nil {
m.database.DeleteAllReplayEntries() if err := m.database.DeleteAllReplayEntries(); err != nil {
log.Printf("replay: delete all entries: %v", err)
}
} }
m.entries = nil m.entries = nil
m.cursor = 0 m.cursor = 0
@@ -351,11 +358,11 @@ func (m *Model) refreshBody() {
m.requestViewport.SetXOffset(0) m.requestViewport.SetXOffset(0)
if e.Sending { if e.Sending {
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(ノ◕ヮ◕)ノ*:・゚", "sending...")))) m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.responseViewport.Width(), "(ノ◕ヮ◕)ノ*:・゚", "sending..."))))
} else if e.ResponseRaw != "" { } else if e.ResponseRaw != "" {
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw)) m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
} else { } else {
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("( •_•)>⌐■", "press send to fire")))) m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.responseViewport.Width(), "( •_•)>⌐■", "press send to fire"))))
} }
m.responseViewport.SetYOffset(0) m.responseViewport.SetYOffset(0)
m.responseViewport.SetXOffset(0) m.responseViewport.SetXOffset(0)
@@ -399,7 +406,7 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
client := &http.Client{ client := &http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402 -- intentional for replay feature
}, },
CheckRedirect: func(_ *http.Request, _ []*http.Request) error { CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
+1 -1
View File
@@ -81,7 +81,7 @@ func (m *Model) renderList() string {
return lipgloss.Place( return lipgloss.Place(
m.listViewport.Width(), m.listViewport.Height(), m.listViewport.Width(), m.listViewport.Height(),
lipgloss.Center, lipgloss.Center, lipgloss.Center, lipgloss.Center,
ilovetui.S.Faint.Render(util.CenterLines("(╥﹏╥)", "send a request from History or Intercept")), ilovetui.S.Faint.Render(util.EmptyState(m.listViewport.Width(), "(╥﹏╥)", "send a request from History or Intercept")),
) )
} }
+5 -3
View File
@@ -35,8 +35,10 @@ func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cm
if _, err := f.WriteString(content); err != nil { if _, err := f.WriteString(content); err != nil {
log.Printf("editor: writing temp file: %v", err) log.Printf("editor: writing temp file: %v", err)
} }
f.Close() if err := f.Close(); err != nil {
return tea.ExecProcess(exec.Command(resolveEditor(), tmpPath), func(err error) tea.Msg { log.Printf("editor: closing temp file: %v", err)
}
return tea.ExecProcess(exec.Command(resolveEditor(), tmpPath), func(err error) tea.Msg { // #nosec G204 -- editor from trusted config/$EDITOR, tmpPath from os.CreateTemp
defer os.Remove(tmpPath) defer os.Remove(tmpPath)
return callback(tmpPath, err) return callback(tmpPath, err)
}) })
@@ -47,7 +49,7 @@ func OpenExternalEditor(content string) tea.Cmd {
if err != nil { if err != nil {
return EditorFinishedMsg{Err: err} return EditorFinishedMsg{Err: err}
} }
data, readErr := os.ReadFile(tmpPath) data, readErr := os.ReadFile(tmpPath) // #nosec G304 -- tmpPath is from os.CreateTemp, controlled by this process
if readErr != nil { if readErr != nil {
return EditorFinishedMsg{Err: readErr} return EditorFinishedMsg{Err: readErr}
} }
+8
View File
@@ -14,6 +14,14 @@ func Truncate(s string, max int) string {
return s[:max-1] + "…" return s[:max-1] + "…"
} }
// EmptyState renders centered placeholder lines, word-wrapping to fit maxW.
func EmptyState(maxW int, lines ...string) string {
return lipgloss.NewStyle().
Width(maxW).
AlignHorizontal(lipgloss.Center).
Render(strings.Join(lines, "\n"))
}
// CenterLines centers each line horizontally relative to the longest one. // CenterLines centers each line horizontally relative to the longest one.
func CenterLines(lines ...string) string { func CenterLines(lines ...string) string {
maxWidth := 0 maxWidth := 0
+12 -4
View File
@@ -105,6 +105,10 @@ schema = 3
version = 'v1.5.0' version = 'v1.5.0'
hash = 'sha256-EYVgkSEMo4HaVrsWKqnsYRp8SSS8gNf7t+Elva02Ofc=' hash = 'sha256-EYVgkSEMo4HaVrsWKqnsYRp8SSS8gNf7t+Elva02Ofc='
[mod.'github.com/inconshreveable/mousetrap']
version = 'v1.1.0'
hash = 'sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE='
[mod.'github.com/klauspost/compress'] [mod.'github.com/klauspost/compress']
version = 'v1.17.8' version = 'v1.17.8'
hash = 'sha256-8rgCCfHX29le8m6fyVn6gwFde5TPUHjwQqZqv9JIubs=' hash = 'sha256-8rgCCfHX29le8m6fyVn6gwFde5TPUHjwQqZqv9JIubs='
@@ -173,6 +177,10 @@ schema = 3
version = 'v1.10.0' version = 'v1.10.0'
hash = 'sha256-dQ6Qqf26IZsa6XsGKP7GDuCj+WmSsBmkBwGTDfue/rk=' hash = 'sha256-dQ6Qqf26IZsa6XsGKP7GDuCj+WmSsBmkBwGTDfue/rk='
[mod.'github.com/spf13/cobra']
version = 'v1.10.2'
hash = 'sha256-nbRCTFiDCC2jKK7AHi79n7urYCMP5yDZnWtNVJrDi+k='
[mod.'github.com/spf13/pflag'] [mod.'github.com/spf13/pflag']
version = 'v1.0.10' version = 'v1.0.10'
hash = 'sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU=' hash = 'sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU='
@@ -210,16 +218,16 @@ schema = 3
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4=' hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
[mod.'golang.org/x/net'] [mod.'golang.org/x/net']
version = 'v0.54.0' version = 'v0.55.0'
hash = 'sha256-/EoIXzTQzK/yP/lxOyx0Z/bhns4FdPTIF4uyt4gIP80=' hash = 'sha256-Phi2mSmBGOJcvqPPAit3uqF3UP8SKRI9dHj6yTM3s5s='
[mod.'golang.org/x/sync'] [mod.'golang.org/x/sync']
version = 'v0.20.0' version = 'v0.20.0'
hash = 'sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y=' hash = 'sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y='
[mod.'golang.org/x/sys'] [mod.'golang.org/x/sys']
version = 'v0.44.0' version = 'v0.45.0'
hash = 'sha256-JDlj+PKsG6I6kjv5JyOUNreY51u5An0oZ5OZMHZSk+A=' hash = 'sha256-hkBoNazrDA67ER6sWhb+EKxx9nJ24+nz3zGy+zT5Hvw='
[mod.'golang.org/x/text'] [mod.'golang.org/x/text']
version = 'v0.37.0' version = 'v0.37.0'
+8
View File
@@ -9,6 +9,14 @@
inherit pname version ldflags; inherit pname version ldflags;
src = ../.; src = ../.;
modules = ./gomod2nix.toml; modules = ./gomod2nix.toml;
nativeBuildInputs = [ pkgs.installShellFiles ];
env.GOTOOLCHAIN = "local";
postInstall = ''
installShellCompletion --cmd spilltea \
--bash <($out/bin/spilltea completion bash) \
--zsh <($out/bin/spilltea completion zsh) \
--fish <($out/bin/spilltea completion fish)
'';
meta = with pkgs.lib; { meta = with pkgs.lib; {
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players."; description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
homepage = "https://github.com/anotherhadi/spilltea"; homepage = "https://github.com/anotherhadi/spilltea";
+2
View File
@@ -52,6 +52,8 @@ in
packages = with pkgs; packages = with pkgs;
[ [
go go
gosec
govulncheck
python3 python3
doctoc doctoc
stylua stylua
+2 -2
View File
@@ -17,7 +17,7 @@ var PluginsFS embed.FS
// InstallDefaultPlugins copies embedded default plugins into dir, skipping // InstallDefaultPlugins copies embedded default plugins into dir, skipping
// files that already exist. Returns the number of files written. // files that already exist. Returns the number of files written.
func InstallDefaultPlugins(dir string) (int, error) { func InstallDefaultPlugins(dir string) (int, error) {
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o750); err != nil {
return 0, fmt.Errorf("create plugins dir: %w", err) return 0, fmt.Errorf("create plugins dir: %w", err)
} }
@@ -39,7 +39,7 @@ func InstallDefaultPlugins(dir string) (int, error) {
if err != nil { if err != nil {
return written, fmt.Errorf("read embedded %s: %w", e.Name(), err) return written, fmt.Errorf("read embedded %s: %w", e.Name(), err)
} }
if err := os.WriteFile(dst, data, 0o644); err != nil { if err := os.WriteFile(dst, data, 0o600); err != nil {
return written, fmt.Errorf("write %s: %w", dst, err) return written, fmt.Errorf("write %s: %w", dst, err)
} }
written++ written++