mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-07-06 12:42:32 +02:00
Compare commits
6 Commits
6128dcc15e
...
098400ba77
| Author | SHA1 | Date | |
|---|---|---|---|
| 098400ba77 | |||
| 4922f3704d | |||
| 1aa4b92bb3 | |||
| 437e8f883d | |||
| 5c74eda48b | |||
| 1190276bab |
@@ -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" />
|
||||
|
||||
<!-- exec: cat ./docs/legal-disclaimer.md -->
|
||||
|
||||
## Legal Disclaimer
|
||||
|
||||
**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).
|
||||
|
||||
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 -->
|
||||
|
||||
## Features
|
||||
@@ -103,7 +101,6 @@ environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ]
|
||||
</details>
|
||||
|
||||
<!-- exec: cat ./docs/basics.md -->
|
||||
|
||||
## 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.
|
||||
@@ -124,19 +121,23 @@ Colors and styles can be customized using [ilovetui](https://github.com/anotherh
|
||||
## 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-plugins copy built-in example plugins into the plugins dir and exit
|
||||
-c, --config string path to config file
|
||||
-h, --help help for spilltea
|
||||
--host string proxy host (overrides config)
|
||||
--plugins-dir string path to plugins dir (overrides config)
|
||||
-p, --port int proxy port (overrides config)
|
||||
-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)
|
||||
-v, --version print version
|
||||
-v, --version version for spilltea
|
||||
```
|
||||
|
||||
<!-- endexec -->
|
||||
|
||||
## Plugin System
|
||||
|
||||
+125
-126
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
appUI "github.com/anotherhadi/spilltea/internal/ui/app"
|
||||
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.
|
||||
@@ -32,136 +32,135 @@ func init() {
|
||||
|
||||
func main() {
|
||||
var (
|
||||
flagConfig = flag.StringP("config", "c", "", "path to config file")
|
||||
flagPluginsDir = flag.String("plugins-dir", "", "path to plugins dir (overrides config)")
|
||||
flagHost = flag.String("host", "", "proxy host (overrides config)")
|
||||
flagPort = flag.IntP("port", "p", 0, "proxy port (overrides config)")
|
||||
flagUpstreamProxy = flag.String("upstream-proxy", "", "upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)")
|
||||
flagVersion = flag.BoolP("version", "v", false, "print version")
|
||||
flagProject = flag.StringP("project", "P", "", `project name to open directly, or "tmp" for a temporary session`)
|
||||
flagAddDefaultPlugins = flag.Bool("add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit")
|
||||
flagAddDefaultConfig = flag.Bool("add-default-config", false, "copy the default config file to the config path and exit")
|
||||
flagConfig string
|
||||
flagPluginsDir string
|
||||
flagHost string
|
||||
flagPort int
|
||||
flagUpstreamProxy string
|
||||
flagProject string
|
||||
flagAddDefaultPlugins bool
|
||||
flagAddDefaultConfig bool
|
||||
)
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: spilltea [flags]")
|
||||
fmt.Println("")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
fmt.Println(version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagAddDefaultPlugins {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if *flagConfig != "" {
|
||||
cfgPath = *flagConfig
|
||||
}
|
||||
if err := config.Load(cfgPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
dir := config.ExpandPath(config.Global.App.PluginsDir)
|
||||
if *flagPluginsDir != "" {
|
||||
dir = *flagPluginsDir
|
||||
}
|
||||
n, err := spilltea.InstallDefaultPlugins(dir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "add-default-plugins: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("added %d plugin(s) to %s\n", n, dir)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagAddDefaultConfig {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if *flagConfig != "" {
|
||||
cfgPath = *flagConfig
|
||||
}
|
||||
if err := config.WriteDefaultConfig(cfgPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "add-default-config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("default config written to %s\n", cfgPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagProject != "" && !homeUI.IsValidProjectName(*flagProject) {
|
||||
fmt.Fprintf(os.Stderr, "project: invalid name %q (only lowercase letters, digits, - and _ are allowed)\n", *flagProject)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if *flagConfig != "" {
|
||||
cfgPath = *flagConfig
|
||||
}
|
||||
|
||||
if err := config.Load(cfgPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Global.Version = version
|
||||
|
||||
if *flagPluginsDir != "" {
|
||||
config.Global.App.PluginsDir = *flagPluginsDir
|
||||
}
|
||||
if *flagHost != "" {
|
||||
config.Global.App.Host = *flagHost
|
||||
}
|
||||
if *flagPort != 0 {
|
||||
config.Global.App.Port = *flagPort
|
||||
}
|
||||
if *flagUpstreamProxy != "" {
|
||||
config.Global.App.UpstreamProxy = *flagUpstreamProxy
|
||||
}
|
||||
|
||||
style.Init()
|
||||
icons.Init(config.Global)
|
||||
keys.Init(config.Global)
|
||||
|
||||
projectDir := config.ExpandPath(config.Global.App.ProjectDir)
|
||||
|
||||
// If --project flag is set, skip the home screen entirely.
|
||||
if *flagProject != "" {
|
||||
project, err := homeUI.OpenProject(projectDir, *flagProject)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "project: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
broker := intercept.NewBroker()
|
||||
m := appUI.New(broker, project.Name, project.Path)
|
||||
final, err := tea.NewProgram(m).Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if app, ok := final.(appUI.Model); ok {
|
||||
if ferr := app.FatalErr(); ferr != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||
os.Exit(1)
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "spilltea",
|
||||
Short: "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.",
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if flagAddDefaultPlugins {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if flagConfig != "" {
|
||||
cfgPath = flagConfig
|
||||
}
|
||||
if err := config.Load(cfgPath); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
dir := config.ExpandPath(config.Global.App.PluginsDir)
|
||||
if flagPluginsDir != "" {
|
||||
dir = flagPluginsDir
|
||||
}
|
||||
n, err := spilltea.InstallDefaultPlugins(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add-default-plugins: %w", err)
|
||||
}
|
||||
fmt.Printf("added %d plugin(s) to %s\n", n, dir)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
if flagAddDefaultConfig {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if flagConfig != "" {
|
||||
cfgPath = flagConfig
|
||||
}
|
||||
if err := config.WriteDefaultConfig(cfgPath); err != nil {
|
||||
return fmt.Errorf("add-default-config: %w", err)
|
||||
}
|
||||
fmt.Printf("default config written to %s\n", cfgPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if flagProject != "" && !homeUI.IsValidProjectName(flagProject) {
|
||||
return fmt.Errorf("project: invalid name %q (only lowercase letters, digits, - and _ are allowed)", flagProject)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
|
||||
if flagConfig != "" {
|
||||
cfgPath = flagConfig
|
||||
}
|
||||
|
||||
if err := config.Load(cfgPath); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
config.Global.Version = version
|
||||
|
||||
if flagPluginsDir != "" {
|
||||
config.Global.App.PluginsDir = flagPluginsDir
|
||||
}
|
||||
if flagHost != "" {
|
||||
config.Global.App.Host = flagHost
|
||||
}
|
||||
if flagPort != 0 {
|
||||
config.Global.App.Port = flagPort
|
||||
}
|
||||
if flagUpstreamProxy != "" {
|
||||
config.Global.App.UpstreamProxy = flagUpstreamProxy
|
||||
}
|
||||
|
||||
style.Init()
|
||||
icons.Init(config.Global)
|
||||
keys.Init(config.Global)
|
||||
|
||||
projectDir := config.ExpandPath(config.Global.App.ProjectDir)
|
||||
|
||||
if flagProject != "" {
|
||||
project, err := homeUI.OpenProject(projectDir, flagProject)
|
||||
if err != nil {
|
||||
return fmt.Errorf("project: %w", err)
|
||||
}
|
||||
broker := intercept.NewBroker()
|
||||
m := appUI.New(broker, project.Name, project.Path)
|
||||
final, err := tea.NewProgram(m).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("tui: %w", err)
|
||||
}
|
||||
if app, ok := final.(appUI.Model); ok {
|
||||
if ferr := app.FatalErr(); ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
root := rootModel{home: homeUI.New(projectDir)}
|
||||
final, err := tea.NewProgram(root).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("tui: %w", err)
|
||||
}
|
||||
if r, ok := final.(rootModel); ok {
|
||||
if ferr := r.FatalErr(); ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Run home + app in a single program to avoid a blank flash on transition.
|
||||
root := rootModel{home: homeUI.New(projectDir)}
|
||||
final, err := tea.NewProgram(root).Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||
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)
|
||||
}
|
||||
if r, ok := final.(rootModel); ok {
|
||||
if ferr := r.FatalErr(); ferr != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -19,16 +19,21 @@ Colors and styles can be customized using [ilovetui](https://github.com/anotherh
|
||||
|
||||
<!-- 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-plugins copy built-in example plugins into the plugins dir and exit
|
||||
-c, --config string path to config file
|
||||
-h, --help help for spilltea
|
||||
--host string proxy host (overrides config)
|
||||
--plugins-dir string path to plugins dir (overrides config)
|
||||
-p, --port int proxy port (overrides config)
|
||||
-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)
|
||||
-v, --version print version
|
||||
-v, --version version for spilltea
|
||||
```
|
||||
<!-- endexec -->
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
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
|
||||
modernc.org/sqlite v1.50.1
|
||||
)
|
||||
@@ -41,6 +41,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // 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/mattn/go-isatty v0.0.22 // 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/spf13/afero v1.15.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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
@@ -62,7 +64,7 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
@@ -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/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
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/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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/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/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/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
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/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
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.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
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/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
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/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
|
||||
@@ -86,7 +86,7 @@ func Load(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)
|
||||
}
|
||||
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
|
||||
|
||||
+16
-5
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
@@ -25,17 +26,25 @@ func Open(path string) (*DB, error) {
|
||||
conn.SetMaxOpenConns(1)
|
||||
d := &DB{conn: conn, path: path}
|
||||
if err := d.migrate(); err != nil {
|
||||
conn.Close()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
log.Printf("db: close conn: %v", cerr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
roConn, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
log.Printf("db: close conn: %v", cerr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
|
||||
conn.Close()
|
||||
roConn.Close()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
log.Printf("db: close conn: %v", cerr)
|
||||
}
|
||||
if cerr := roConn.Close(); cerr != nil {
|
||||
log.Printf("db: close roConn: %v", cerr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
d.roConn = roConn
|
||||
@@ -114,6 +123,8 @@ func CountEntriesAt(path string) int {
|
||||
}
|
||||
defer conn.Close()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// DML or DDL in the user-supplied expression is rejected by SQLite before it executes.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -281,7 +281,7 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
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)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
@@ -27,7 +27,7 @@ func OpenPluginsFile(dbPath string) (*PluginsFile, error) {
|
||||
path: path,
|
||||
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) {
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
editor = "vi"
|
||||
}
|
||||
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() {
|
||||
|
||||
+11
-9
@@ -6,25 +6,28 @@ import (
|
||||
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 {
|
||||
if m.width == 0 {
|
||||
v := tea.NewView("")
|
||||
v.AltScreen = true
|
||||
return v
|
||||
return m.newView("")
|
||||
}
|
||||
|
||||
normal := m.renderNormal()
|
||||
|
||||
if m.copyAs.IsOpen() {
|
||||
v := tea.NewView(m.copyAs.View(normal))
|
||||
v.AltScreen = true
|
||||
v := m.newView(m.copyAs.View(normal))
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
return v
|
||||
}
|
||||
|
||||
if m.copy.IsOpen() {
|
||||
v := tea.NewView(m.copy.View(normal))
|
||||
v.AltScreen = true
|
||||
v := m.newView(m.copy.View(normal))
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
return v
|
||||
}
|
||||
@@ -34,8 +37,7 @@ func (m Model) View() tea.View {
|
||||
rendered = m.notifications.View(normal)
|
||||
}
|
||||
|
||||
v := tea.NewView(rendered)
|
||||
v.AltScreen = true
|
||||
v := m.newView(rendered)
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func (m *Model) refreshViewports() {
|
||||
placeholder := lipgloss.Place(
|
||||
m.leftViewport.Width(), m.leftViewport.Height(),
|
||||
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.rightViewport.SetContent("")
|
||||
@@ -312,7 +312,7 @@ func (m *Model) refreshViewports() {
|
||||
placeholder := lipgloss.Place(
|
||||
m.rightViewport.Width(), m.rightViewport.Height(),
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m *Model) renderList() string {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")),
|
||||
ilovetui.S.Faint.Render(util.EmptyState(m.listViewport.Width(), "(҂◡_◡) ᕤ", "no findings")),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package history
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"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):
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -242,7 +245,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if len(m.entries) > 0 {
|
||||
id := m.entries[m.cursor].ID
|
||||
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)
|
||||
}
|
||||
@@ -261,10 +266,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if hasUnflagged && e.Flagged {
|
||||
continue
|
||||
}
|
||||
m.database.DeleteEntry(e.ID)
|
||||
if err := m.database.DeleteEntry(e.ID); err != nil {
|
||||
log.Printf("history: delete entry: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.database.DeleteAllExceptFlagged()
|
||||
if err := m.database.DeleteAllExceptFlagged(); err != nil {
|
||||
log.Printf("history: delete all unflagged: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, m.clearSearch()
|
||||
@@ -346,7 +355,7 @@ func (m *Model) refreshBody() {
|
||||
}
|
||||
if raw == "" {
|
||||
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
|
||||
}
|
||||
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
||||
|
||||
@@ -82,9 +82,9 @@ func (m *Model) renderList() string {
|
||||
)
|
||||
}
|
||||
if len(m.entries) == 0 {
|
||||
msg := util.CenterLines("(⌐■_■)", "no history yet")
|
||||
msg := util.EmptyState(m.listViewport.Width(), "(⌐■_■)", "no history yet")
|
||||
if m.searchKind != searchKindOff {
|
||||
msg = util.CenterLines("ʕノ•ᴥ•ʔノ ︵ ┻━┻", "no results")
|
||||
msg = util.EmptyState(m.listViewport.Width(), "ʕノ•ᴥ•ʔノ ︵ ┻━┻", "no results")
|
||||
}
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
|
||||
@@ -3,6 +3,7 @@ package home
|
||||
import (
|
||||
crypto "crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -72,7 +73,7 @@ func (m Model) handleSelection() (tea.Model, tea.Cmd) {
|
||||
return m, m.nameInput.Focus()
|
||||
case kindTemp:
|
||||
dir := tempDir()
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return m, nil
|
||||
}
|
||||
initProjectFiles(dir)
|
||||
@@ -90,7 +91,9 @@ func (m Model) deleteSelected() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
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()
|
||||
m.list.RemoveItem(idx)
|
||||
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.nameInput.Blur()
|
||||
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
|
||||
}
|
||||
initProjectFiles(dir)
|
||||
@@ -147,14 +150,14 @@ func IsValidProjectName(s string) bool {
|
||||
func OpenProject(projectDir, name string) (*Project, error) {
|
||||
if name == "tmp" || name == "temp" || name == "temporary" {
|
||||
dir := tempDir()
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initProjectFiles(dir)
|
||||
return &Project{Name: "temporary", Path: filepath.Join(dir, "data.db")}, nil
|
||||
}
|
||||
dir := filepath.Join(projectDir, name)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initProjectFiles(dir)
|
||||
@@ -171,9 +174,11 @@ func initProjectFiles(dir string) {
|
||||
for _, name := range []string{"data.db", "logs.log"} {
|
||||
p := filepath.Join(dir, name)
|
||||
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 {
|
||||
f.Close()
|
||||
if cerr := f.Close(); cerr != nil {
|
||||
log.Printf("home: close project file: %v", cerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func (m Model) View() tea.View {
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.WindowTitle = "Spilltea"
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ func (m *Model) renderStatusBar() string {
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
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))
|
||||
@@ -149,7 +149,7 @@ func (m *Model) renderList() string {
|
||||
|
||||
func (m *Model) renderResponseList() string {
|
||||
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))
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
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)
|
||||
@@ -127,9 +127,9 @@ func (m *Model) renderStatusBar() string {
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
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 != "" {
|
||||
msg = util.CenterLines("= _ =", "no results")
|
||||
msg = util.EmptyState(m.listViewport.Width(), "= _ =", "no results")
|
||||
}
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -79,7 +80,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
e.ResponseRaw = "Error: " + msg.err.Error()
|
||||
}
|
||||
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()
|
||||
@@ -216,7 +219,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
if len(m.entries) > 0 {
|
||||
e := m.entries[m.cursor]
|
||||
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:]...)
|
||||
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):
|
||||
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.cursor = 0
|
||||
@@ -351,11 +358,11 @@ func (m *Model) refreshBody() {
|
||||
m.requestViewport.SetXOffset(0)
|
||||
|
||||
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 != "" {
|
||||
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
||||
} 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.SetXOffset(0)
|
||||
@@ -399,7 +406,7 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
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 {
|
||||
return http.ErrUseLastResponse
|
||||
|
||||
@@ -81,7 +81,7 @@ func (m *Model) renderList() string {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
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")),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,10 @@ func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cm
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
log.Printf("editor: writing temp file: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
return tea.ExecProcess(exec.Command(resolveEditor(), tmpPath), func(err error) tea.Msg {
|
||||
if err := f.Close(); err != nil {
|
||||
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)
|
||||
return callback(tmpPath, err)
|
||||
})
|
||||
@@ -47,7 +49,7 @@ func OpenExternalEditor(content string) tea.Cmd {
|
||||
if err != nil {
|
||||
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 {
|
||||
return EditorFinishedMsg{Err: readErr}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ func Truncate(s string, max int) string {
|
||||
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.
|
||||
func CenterLines(lines ...string) string {
|
||||
maxWidth := 0
|
||||
|
||||
+12
-4
@@ -105,6 +105,10 @@ schema = 3
|
||||
version = 'v1.5.0'
|
||||
hash = 'sha256-EYVgkSEMo4HaVrsWKqnsYRp8SSS8gNf7t+Elva02Ofc='
|
||||
|
||||
[mod.'github.com/inconshreveable/mousetrap']
|
||||
version = 'v1.1.0'
|
||||
hash = 'sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE='
|
||||
|
||||
[mod.'github.com/klauspost/compress']
|
||||
version = 'v1.17.8'
|
||||
hash = 'sha256-8rgCCfHX29le8m6fyVn6gwFde5TPUHjwQqZqv9JIubs='
|
||||
@@ -173,6 +177,10 @@ schema = 3
|
||||
version = 'v1.10.0'
|
||||
hash = 'sha256-dQ6Qqf26IZsa6XsGKP7GDuCj+WmSsBmkBwGTDfue/rk='
|
||||
|
||||
[mod.'github.com/spf13/cobra']
|
||||
version = 'v1.10.2'
|
||||
hash = 'sha256-nbRCTFiDCC2jKK7AHi79n7urYCMP5yDZnWtNVJrDi+k='
|
||||
|
||||
[mod.'github.com/spf13/pflag']
|
||||
version = 'v1.0.10'
|
||||
hash = 'sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU='
|
||||
@@ -210,16 +218,16 @@ schema = 3
|
||||
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
|
||||
|
||||
[mod.'golang.org/x/net']
|
||||
version = 'v0.54.0'
|
||||
hash = 'sha256-/EoIXzTQzK/yP/lxOyx0Z/bhns4FdPTIF4uyt4gIP80='
|
||||
version = 'v0.55.0'
|
||||
hash = 'sha256-Phi2mSmBGOJcvqPPAit3uqF3UP8SKRI9dHj6yTM3s5s='
|
||||
|
||||
[mod.'golang.org/x/sync']
|
||||
version = 'v0.20.0'
|
||||
hash = 'sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y='
|
||||
|
||||
[mod.'golang.org/x/sys']
|
||||
version = 'v0.44.0'
|
||||
hash = 'sha256-JDlj+PKsG6I6kjv5JyOUNreY51u5An0oZ5OZMHZSk+A='
|
||||
version = 'v0.45.0'
|
||||
hash = 'sha256-hkBoNazrDA67ER6sWhb+EKxx9nJ24+nz3zGy+zT5Hvw='
|
||||
|
||||
[mod.'golang.org/x/text']
|
||||
version = 'v0.37.0'
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
inherit pname version ldflags;
|
||||
src = ../.;
|
||||
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; {
|
||||
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
||||
homepage = "https://github.com/anotherhadi/spilltea";
|
||||
|
||||
@@ -52,6 +52,8 @@ in
|
||||
packages = with pkgs;
|
||||
[
|
||||
go
|
||||
gosec
|
||||
govulncheck
|
||||
python3
|
||||
doctoc
|
||||
stylua
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ var PluginsFS embed.FS
|
||||
// InstallDefaultPlugins copies embedded default plugins into dir, skipping
|
||||
// files that already exist. Returns the number of files written.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func InstallDefaultPlugins(dir string) (int, error) {
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
written++
|
||||
|
||||
Reference in New Issue
Block a user