19 Commits

Author SHA1 Message Date
Hadi 31bb2aa439 v0.0.7
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-30 01:15:40 +02:00
Hadi b3caa9852c feat: add pre-configured librewolf browser
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-30 01:12:55 +02:00
Hadi 39248e2be6 fix: bad path
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-30 00:45:49 +02:00
Hadi 9d0ef598a7 Per-project configuration overwrites
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-30 00:14:54 +02:00
Hadi 77b102791f edit docs
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-30 00:14:35 +02:00
Hadi 2e0ad98a4a remove useless icons
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-29 22:26:09 +02:00
Hadi b0afb7cb56 add the ssl-insecure flag
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-29 22:15:58 +02:00
Hadi acee3df1bc fix: add ssl_insecure, expand values, edit keybindings, ...
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-29 21:59:54 +02:00
Hadi 61a112a302 add logs
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 13:41:09 +02:00
Hadi 677223c9b0 change logs fmt
Signed-off-by: Hadi <hadi@example.com>
2026-05-29 11:54:26 +02:00
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
Hadi 6128dcc15e Add ko-fi link
Signed-off-by: Hadi <hadi@example.com>
2026-05-27 14:57:52 +02:00
Hadi 41d5366952 edit badges
Signed-off-by: Hadi <hadi@example.com>
2026-05-27 14:56:17 +02:00
Hadi 8683d17242 Add fatal error when port is already in use
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
2026-05-26 22:44:31 +02:00
45 changed files with 588 additions and 265 deletions
+27 -8
View File
@@ -9,10 +9,10 @@
> A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players. > A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
> Think Burp Suite or Caido, but entirely in your terminal. > Think Burp Suite or Caido, but entirely in your terminal.
[![Go Version](https://img.shields.io/github/go-mod/go-version/anotherhadi/spilltea)](go.mod) ![GitHub Stars](https://www.shieldcn.dev/github/stars/anotherhadi/spilltea.svg?variant=outline&theme=violet)
[![Release](https://img.shields.io/github/v/release/anotherhadi/spilltea)](https://github.com/anotherhadi/spilltea/releases) ![Release](https://www.shieldcn.dev/github/release/anotherhadi/spilltea.svg?variant=outline&theme=violet)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![CI](https://www.shieldcn.dev/github/ci/anotherhadi/spilltea.svg?variant=outline&theme=violet)
[![Go Report Card](https://goreportcard.com/badge/github.com/anotherhadi/spilltea)](https://goreportcard.com/report/github.com/anotherhadi/spilltea) [![Ko-fi](https://www.shieldcn.dev/badge/Ko--fi-sponsor-FF5E5B.svg?logo=kofi&variant=secondary&theme=violet)](https://ko-fi.com/anotherhadi)
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@@ -23,6 +23,7 @@
- [Installation](#installation) - [Installation](#installation)
- [Project Management](#project-management) - [Project Management](#project-management)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Per-project configuration](#per-project-configuration)
- [CLI Flags](#cli-flags) - [CLI Flags](#cli-flags)
- [Plugin System](#plugin-system) - [Plugin System](#plugin-system)
- [Vim / Neovim Integration](#vim--neovim-integration) - [Vim / Neovim Integration](#vim--neovim-integration)
@@ -107,9 +108,9 @@ Spilltea organizes work into **projects**. Each project maps to a SQLite databas
On startup, you choose: On startup, you choose:
- **New project**: enter a name, stored in `~/.local/share/spilltea/projects/` by default - **New project**: enter a name, stored in `~/.local/share/spilltea/<name>/` by default
- **Existing project**: pick from a list of previous projects - **Existing project**: pick from a list of previous projects
- **Temporary**: no name needed, stored in `/tmp/spilltea/projects/` and will be deleted on your next reboot! - **Temporary**: no name needed, stored in `/tmp/spilltea/<random-id>/` and will be deleted on your next reboot!
## Configuration ## Configuration
@@ -118,20 +119,38 @@ Check the default configuration with all the options [here](./internal/config/de
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once. Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
### Per-project configuration
You can override any config value on a per-project basis by placing a `config.yaml` file inside the project directory (e.g. `~/.local/share/spilltea/projects/my-project/config.yaml`).
Only the keys present in that file are overridden; everything else falls back to the global config.
The priority order is:
1. Global config (`~/.config/spilltea/config.yaml`)
2. Project config (`<project-dir>/config.yaml`)
3. CLI flags (always win)
## 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
--ssl-insecure skip TLS certificate verification (overrides config)
--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 -->
+93 -65
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,95 +32,93 @@ 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`) flagSslInsecure bool
flagAddDefaultPlugins = flag.Bool("add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit") flagAddDefaultPlugins bool
flagAddDefaultConfig = flag.Bool("add-default-config", false, "copy the default config file to the config path and exit") flagAddDefaultConfig bool
) )
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 != "" { sslInsecureChanged := cmd.Flags().Changed("ssl-insecure")
config.Global.App.PluginsDir = *flagPluginsDir applyCLI := func(c *config.Config) {
if flagPluginsDir != "" {
c.App.PluginsDir = flagPluginsDir
} }
if *flagHost != "" { if flagHost != "" {
config.Global.App.Host = *flagHost c.App.Host = flagHost
} }
if *flagPort != 0 { if flagPort != 0 {
config.Global.App.Port = *flagPort c.App.Port = flagPort
} }
if *flagUpstreamProxy != "" { if flagUpstreamProxy != "" {
config.Global.App.UpstreamProxy = *flagUpstreamProxy c.App.UpstreamProxy = flagUpstreamProxy
} }
if sslInsecureChanged {
c.App.SslInsecure = flagSslInsecure
}
}
applyCLI(config.Global)
config.SetCLIOverrides(applyCLI)
style.Init() style.Init()
icons.Init(config.Global) icons.Init(config.Global)
@@ -128,26 +126,56 @@ 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)
} }
if err := config.MergeProjectConfig(filepath.Dir(project.Path)); err != nil {
return fmt.Errorf("project config: %w", err)
}
icons.Init(config.Global)
keys.Init(config.Global)
broker := intercept.NewBroker() broker := intercept.NewBroker()
m := appUI.New(broker, project.Name, project.Path) m := appUI.New(broker, project.Name, project.Path)
if _, err := tea.NewProgram(m).Run(); err != nil { final, err := tea.NewProgram(m).Run()
fmt.Fprintf(os.Stderr, "tui: %v\n", err) if err != nil {
os.Exit(1) return fmt.Errorf("tui: %w", err)
} }
return if app, ok := final.(appUI.Model); ok {
if ferr := app.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)} root := rootModel{home: homeUI.New(projectDir)}
if _, err := tea.NewProgram(root).Run(); err != nil { final, err := tea.NewProgram(root).Run()
fmt.Fprintf(os.Stderr, "tui: %v\n", err) 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
},
}
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().BoolVar(&flagSslInsecure, "ssl-insecure", false, "skip TLS certificate verification (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)
} }
} }
+20
View File
@@ -1,8 +1,14 @@
package main package main
import ( import (
"log"
"path/filepath"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/spilltea/internal/config"
"github.com/anotherhadi/spilltea/internal/icons"
"github.com/anotherhadi/spilltea/internal/intercept" "github.com/anotherhadi/spilltea/internal/intercept"
"github.com/anotherhadi/spilltea/internal/keys"
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"
) )
@@ -34,6 +40,11 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.state == rootStateHome { if m.state == rootStateHome {
if sel, ok := msg.(homeUI.ProjectSelectedMsg); ok { if sel, ok := msg.(homeUI.ProjectSelectedMsg); ok {
if err := config.MergeProjectConfig(filepath.Dir(sel.Project.Path)); err != nil {
log.Printf("project config: %v", err)
}
icons.Init(config.Global)
keys.Init(config.Global)
broker := intercept.NewBroker() broker := intercept.NewBroker()
app := appUI.New(broker, sel.Project.Name, sel.Project.Path) app := appUI.New(broker, sel.Project.Name, sel.Project.Path)
m.app = app m.app = app
@@ -58,3 +69,12 @@ func (m rootModel) View() tea.View {
} }
return m.home.View() return m.home.View()
} }
func (m rootModel) FatalErr() error {
if m.state == rootStateApp {
if app, ok := m.app.(appUI.Model); ok {
return app.FatalErr()
}
}
return nil
}
+22 -4
View File
@@ -4,9 +4,9 @@ Spilltea organizes work into **projects**. Each project maps to a SQLite databas
On startup, you choose: On startup, you choose:
- **New project**: enter a name, stored in `~/.local/share/spilltea/projects/` by default - **New project**: enter a name, stored in `~/.local/share/spilltea/<name>/` by default
- **Existing project**: pick from a list of previous projects - **Existing project**: pick from a list of previous projects
- **Temporary**: no name needed, stored in `/tmp/spilltea/projects/` and will be deleted on your next reboot! - **Temporary**: no name needed, stored in `/tmp/spilltea/<random-id>/` and will be deleted on your next reboot!
## Configuration ## Configuration
@@ -15,20 +15,38 @@ Check the default configuration with all the options [here](./internal/config/de
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once. Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
### Per-project configuration
You can override any config value on a per-project basis by placing a `config.yaml` file inside the project directory (e.g. `~/.local/share/spilltea/projects/my-project/config.yaml`).
Only the keys present in that file are overridden; everything else falls back to the global config.
The priority order is:
1. Global config (`~/.config/spilltea/config.yaml`)
2. Project config (`<project-dir>/config.yaml`)
3. CLI flags (always win)
## CLI Flags ## CLI Flags
<!-- 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
--ssl-insecure skip TLS certificate verification (overrides config)
--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 -->
+2
View File
@@ -11,5 +11,7 @@
# Spilltea Documentation # Spilltea Documentation
- **Version**: `{{.Cfg.Version}}` - **Version**: `{{.Cfg.Version}}`
- **Proxy**: `{{.Cfg.App.Host}}:{{.Cfg.App.Port}}`
- **SSL Insecure**: `{{.Cfg.App.SslInsecure}}`
- **Repository**: `https://github.com/anotherhadi/spilltea` - **Repository**: `https://github.com/anotherhadi/spilltea`
- **Sponsor this project**: `https://ko-fi.com/anotherhadi` - **Sponsor this project**: `https://ko-fi.com/anotherhadi`
+1 -1
View File
@@ -155,7 +155,7 @@ If the user **dismisses** a finding it is permanently hidden and will never reap
## Configuration ## Configuration
Plugin configuration is stored in a `plugins.yaml` file alongside the project database. Plugin configuration is stored in a `plugins.yaml` file alongside the project database and can be edited through the "Proxy" TUI page.
Each plugin is keyed by its filename (without the `.lua` extension) and has an `enable` toggle and an optional `config` block (arbitrary YAML). Each plugin is keyed by its filename (without the `.lua` extension) and has an `enable` toggle and an optional `config` block (arbitrary YAML).
```yaml ```yaml
+7 -1
View File
@@ -1,6 +1,12 @@
## Configuring your browser's proxy settings ## Configuring your browser's proxy settings
We recommend installing **FoxyProxy** to manage your browser's proxies. **If you use Nix**, you can launch a pre-configured browser (LibreWolf) with the proxy and certificate already set up:
```sh
nix run github:anotherhadi/spilltea#browser{{if or (ne .Cfg.App.Host "127.0.0.1") (ne .Cfg.App.Port 8080) (ne .Cfg.App.CertDir "~/.local/share/spilltea")}} --{{end}}{{if ne .Cfg.App.Host "127.0.0.1"}} --host {{.Cfg.App.Host}}{{end}}{{if ne .Cfg.App.Port 8080}} --port {{.Cfg.App.Port}}{{end}}{{if ne .Cfg.App.CertDir "~/.local/share/spilltea"}} --cert-dir {{.Cfg.App.CertDir}}{{end}}
```
Otherwise, we recommend installing **FoxyProxy** to manage your browser's proxies.
You can install it from the [Google Chrome extension store](https://chromewebstore.google.com/) or from the [Firefox extension store](https://addons.mozilla.org/en-US/firefox/extensions) You can install it from the [Google Chrome extension store](https://chromewebstore.google.com/) or from the [Firefox extension store](https://addons.mozilla.org/en-US/firefox/extensions)
1. Open FoxyProxy's options, then click on `Add New Proxy` button. 1. Open FoxyProxy's options, then click on `Add New Proxy` button.
+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=
+56 -2
View File
@@ -28,6 +28,7 @@ type Config struct {
ProxyAuth string `mapstructure:"proxy_auth"` ProxyAuth string `mapstructure:"proxy_auth"`
MaxBodySizeMB int `mapstructure:"max_body_size_mb"` MaxBodySizeMB int `mapstructure:"max_body_size_mb"`
ExternalEditor string `mapstructure:"external_editor"` ExternalEditor string `mapstructure:"external_editor"`
SslInsecure bool `mapstructure:"ssl_insecure"`
} `mapstructure:"app"` } `mapstructure:"app"`
TUI struct { TUI struct {
@@ -64,6 +65,43 @@ type GlobalPlugin struct {
var Global *Config var Global *Config
var projectCLIOverrides func(*Config)
// SetCLIOverrides stores a function that re-applies CLI flag overrides.
// It is called automatically after MergeProjectConfig so that CLI flags
// always win over both global and per-project config.
func SetCLIOverrides(fn func(*Config)) {
projectCLIOverrides = fn
}
// MergeProjectConfig merges <projectDir>/config.yaml on top of the current
// Global config, then re-applies any registered CLI overrides.
// If no config.yaml exists in the project directory, this is a no-op.
func MergeProjectConfig(projectDir string) error {
projectConfigPath := filepath.Join(projectDir, "config.yaml")
if _, err := os.Stat(projectConfigPath); errors.Is(err, os.ErrNotExist) {
return nil
}
pv := viper.New()
pv.SetConfigFile(projectConfigPath)
if err := pv.ReadInConfig(); err != nil {
return fmt.Errorf("project config: %w", err)
}
if err := viper.MergeConfigMap(pv.AllSettings()); err != nil {
return fmt.Errorf("merge project config: %w", err)
}
if err := viper.Unmarshal(Global); err != nil {
return err
}
Global.App.ProxyAuth = expandEnvValue(Global.App.ProxyAuth)
Global.App.UpstreamProxy = expandEnvValue(Global.App.UpstreamProxy)
if projectCLIOverrides != nil {
projectCLIOverrides(Global)
}
return nil
}
func Load(path string) error { func Load(path string) error {
var defaults map[string]any var defaults map[string]any
if err := yaml.Unmarshal(defaultConfig, &defaults); err != nil { if err := yaml.Unmarshal(defaultConfig, &defaults); err != nil {
@@ -82,11 +120,16 @@ func Load(path string) error {
} }
Global = &Config{} Global = &Config{}
return viper.Unmarshal(Global) if err := viper.Unmarshal(Global); err != nil {
return err
}
Global.App.ProxyAuth = expandEnvValue(Global.App.ProxyAuth)
Global.App.UpstreamProxy = expandEnvValue(Global.App.UpstreamProxy)
return nil
} }
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 {
@@ -106,6 +149,17 @@ func ExpandPath(p string) string {
return p return p
} }
// expandEnvValue replaces a value starting with "$" with the corresponding
// environment variable, enabling secrets to be kept out of config files.
func expandEnvValue(s string) string {
if len(s) > 1 && s[0] == '$' {
if val := os.Getenv(s[1:]); val != "" {
return val
}
}
return s
}
func flatten(prefix string, m map[string]any) map[string]any { func flatten(prefix string, m map[string]any) map[string]any {
out := make(map[string]any) out := make(map[string]any)
for k, v := range m { for k, v := range m {
+5 -3
View File
@@ -4,15 +4,16 @@ app:
cert_dir: ~/.local/share/spilltea cert_dir: ~/.local/share/spilltea
project_dir: ~/.local/share/spilltea project_dir: ~/.local/share/spilltea
plugins_dir: ~/.config/spilltea/plugins plugins_dir: ~/.config/spilltea/plugins
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888 upstream_proxy: "" # e.g. http://corporate-proxy:8888 or $ENV_VAR
proxy_auth: "" # require basic auth to use the proxy, format: user:pass (empty = disabled). Also run: chmod 600 ~/.config/spilltea/config.yaml proxy_auth: "" # require basic auth to use the spilltea proxy, format: user:pass or $ENV_VAR (empty = disabled).
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB) max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait) external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait)
ssl_insecure: false # bypass TLS certificate verification (enable for self-signed cert targets)
intercept: intercept:
default_intercept_enabled: true default_intercept_enabled: true
default_capture_response: false default_capture_response: false
queue_size: 64 # max pending intercepted requests/responses before the proxy blocks queue_size: 128 # max pending intercepted requests/responses before the proxy blocks
auto_forward_regex: auto_forward_regex:
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$' - '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
@@ -31,6 +32,7 @@ tui:
keybindings: keybindings:
global: global:
quit: "q,ctrl+c" quit: "q,ctrl+c"
escape: "esc,ctrl+c"
help: "?" help: "?"
open_logs: "ctrl+g" open_logs: "ctrl+g"
toggle_sidebar: "ctrl+b" toggle_sidebar: "ctrl+b"
+1
View File
@@ -2,6 +2,7 @@ package config
type GlobalKeys struct { type GlobalKeys struct {
Quit string `mapstructure:"quit"` Quit string `mapstructure:"quit"`
Escape string `mapstructure:"escape"`
OpenLogs string `mapstructure:"open_logs"` OpenLogs string `mapstructure:"open_logs"`
ToggleSidebar string `mapstructure:"toggle_sidebar"` ToggleSidebar string `mapstructure:"toggle_sidebar"`
Help string `mapstructure:"help"` Help string `mapstructure:"help"`
+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
} }
+6 -2
View File
@@ -81,7 +81,11 @@ func scanEntries(rows *sql.Rows) ([]Entry, error) {
if err := rows.Scan(&e.ID, &ts, &e.Method, &e.Host, &e.Path, &e.StatusCode, &e.RequestRaw, &e.ResponseRaw, &flagged); err != nil { if err := rows.Scan(&e.ID, &ts, &e.Method, &e.Host, &e.Path, &e.StatusCode, &e.RequestRaw, &e.ResponseRaw, &flagged); err != nil {
return nil, err return nil, err
} }
e.Timestamp, _ = time.Parse(time.RFC3339, ts) var tsErr error
e.Timestamp, tsErr = time.Parse(time.RFC3339, ts)
if tsErr != nil {
log.Printf("db: parse timestamp %q: %v", ts, tsErr)
}
e.Flagged = flagged != 0 e.Flagged = flagged != 0
entries = append(entries, e) entries = append(entries, e)
} }
@@ -121,7 +125,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
+6 -1
View File
@@ -1,6 +1,7 @@
package db package db
import ( import (
"log"
"time" "time"
) )
@@ -59,7 +60,11 @@ func (d *DB) ListReplayEntries() ([]ReplayEntry, error) {
&e.OriginalRaw, &e.RequestRaw, &e.ResponseRaw, &e.StatusCode, &e.ErrorMsg); err != nil { &e.OriginalRaw, &e.RequestRaw, &e.ResponseRaw, &e.StatusCode, &e.ErrorMsg); err != nil {
return nil, err return nil, err
} }
e.Timestamp, _ = time.Parse(time.RFC3339, ts) var tsErr error
e.Timestamp, tsErr = time.Parse(time.RFC3339, ts)
if tsErr != nil {
log.Printf("db: parse replay timestamp %q: %v", ts, tsErr)
}
entries = append(entries, e) entries = append(entries, e)
} }
return entries, rows.Err() return entries, rows.Err()
+1 -1
View File
@@ -41,7 +41,7 @@ func newGlobalKeyMap(cfg config.GlobalKeys) GlobalKeyMap {
CycleFocus: binding(cfg.CycleFocus, "cycle focus"), CycleFocus: binding(cfg.CycleFocus, "cycle focus"),
CopyAs: binding(cfg.CopyAs, "copy as..."), CopyAs: binding(cfg.CopyAs, "copy as..."),
Copy: binding(cfg.Copy, "copy..."), Copy: binding(cfg.Copy, "copy..."),
Escape: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "cancel")), Escape: binding(cfg.Escape, "cancel"),
SendToReplay: binding(cfg.SendToReplay, "send to replay"), SendToReplay: binding(cfg.SendToReplay, "send to replay"),
ScrollUp: binding(cfg.ScrollUp, "scroll up"), ScrollUp: binding(cfg.ScrollUp, "scroll up"),
ScrollDown: binding(cfg.ScrollDown, "scroll down"), ScrollDown: binding(cfg.ScrollDown, "scroll down"),
+5 -5
View File
@@ -46,7 +46,7 @@ func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) { func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
L.SetGlobal("log", L.NewFunction(func(L *lua.LState) int { L.SetGlobal("log", L.NewFunction(func(L *lua.LState) int {
msg := L.CheckString(1) msg := L.CheckString(1)
log.Printf("[plugin:%s] %s", p.Name, msg) log.Printf("plugin %s: %s", p.Name, msg)
return 0 return 0
})) }))
@@ -85,7 +85,7 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
CreatedAt: time.Now(), CreatedAt: time.Now(),
}) })
if err != nil { if err != nil {
log.Printf("[plugin:%s] create_finding error: %v", p.Name, err) log.Printf("plugin %s: create_finding error: %v", p.Name, err)
return 0 return 0
} }
_ = inserted _ = inserted
@@ -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
@@ -337,7 +337,7 @@ func luaTableString(t *lua.LTable, key string) string {
return "" return ""
} }
func pushRequest(L *lua.LState, f *goproxy.Flow) *lua.LTable { func pushRequest(L *lua.LState, p *Plugin, f *goproxy.Flow) *lua.LTable {
t := L.NewTable() t := L.NewTable()
r := f.Request r := f.Request
L.SetField(t, "method", lua.LString(r.Method)) L.SetField(t, "method", lua.LString(r.Method))
@@ -377,7 +377,7 @@ func pushRequest(L *lua.LState, f *goproxy.Flow) *lua.LTable {
L.SetField(t, "set_url", L.NewFunction(func(L *lua.LState) int { L.SetField(t, "set_url", L.NewFunction(func(L *lua.LState) int {
parsed, err := url.Parse(L.CheckString(2)) parsed, err := url.Parse(L.CheckString(2))
if err != nil { if err != nil {
log.Printf("[plugin] set_url: %v", err) log.Printf("plugin %s: set_url: %v", p.Name, err)
return 0 return 0
} }
r.URL = parsed r.URL = parsed
+4 -4
View File
@@ -395,25 +395,25 @@ func (m *Manager) runAsyncForPlugins(hookName string, argsFor func(*Plugin) []lu
func (m *Manager) RunSyncOnRequest(f *goproxy.Flow) intercept.Decision { func (m *Manager) RunSyncOnRequest(f *goproxy.Flow) intercept.Decision {
return m.runSyncDecisionForPlugins("on_request", func(p *Plugin) []lua.LValue { return m.runSyncDecisionForPlugins("on_request", func(p *Plugin) []lua.LValue {
return []lua.LValue{pushRequest(p.L, f)} return []lua.LValue{pushRequest(p.L, p, f)}
}) })
} }
func (m *Manager) RunAsyncOnRequest(f *goproxy.Flow) { func (m *Manager) RunAsyncOnRequest(f *goproxy.Flow) {
m.runAsyncForPlugins("on_request", func(p *Plugin) []lua.LValue { m.runAsyncForPlugins("on_request", func(p *Plugin) []lua.LValue {
return []lua.LValue{pushRequest(p.L, f)} return []lua.LValue{pushRequest(p.L, p, f)}
}) })
} }
func (m *Manager) RunSyncOnResponse(f *goproxy.Flow) intercept.Decision { func (m *Manager) RunSyncOnResponse(f *goproxy.Flow) intercept.Decision {
return m.runSyncDecisionForPlugins("on_response", func(p *Plugin) []lua.LValue { return m.runSyncDecisionForPlugins("on_response", func(p *Plugin) []lua.LValue {
return []lua.LValue{pushRequest(p.L, f), pushResponse(p.L, f)} return []lua.LValue{pushRequest(p.L, p, f), pushResponse(p.L, f)}
}) })
} }
func (m *Manager) RunAsyncOnResponse(f *goproxy.Flow) { func (m *Manager) RunAsyncOnResponse(f *goproxy.Flow) {
m.runAsyncForPlugins("on_response", func(p *Plugin) []lua.LValue { m.runAsyncForPlugins("on_response", func(p *Plugin) []lua.LValue {
return []lua.LValue{pushRequest(p.L, f), pushResponse(p.L, f)} return []lua.LValue{pushRequest(p.L, p, f), pushResponse(p.L, f)}
}) })
} }
+5 -2
View File
@@ -1,6 +1,7 @@
package plugins package plugins
import ( import (
"log"
"os" "os"
"path/filepath" "path/filepath"
@@ -27,7 +28,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
} }
@@ -76,7 +77,9 @@ func (pf *PluginsFile) register(id string, defaultEnabled bool, configText strin
} }
} }
pf.data.Plugins[id] = entry pf.data.Plugins[id] = entry
_ = pf.save() if err := pf.save(); err != nil {
log.Printf("plugin %s: register save: %v", id, err)
}
} }
} }
+1
View File
@@ -120,6 +120,7 @@ func Start(broker *intercept.Broker, mgr *plugins.Manager) error {
StreamLargeBodies: int64(cfg.MaxBodySizeMB) * 1024 * 1024, StreamLargeBodies: int64(cfg.MaxBodySizeMB) * 1024 * 1024,
CaRootPath: caPath, CaRootPath: caPath,
Upstream: cfg.UpstreamProxy, Upstream: cfg.UpstreamProxy,
SslInsecure: cfg.SslInsecure,
} }
p, err := goproxy.NewProxy(opts) p, err := goproxy.NewProxy(opts)
+23 -9
View File
@@ -1,7 +1,6 @@
package app package app
import ( import (
"fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -37,11 +36,9 @@ func tickCmd() tea.Cmd {
}) })
} }
var sidebarEntries = pageRegistry
var pageShortcuts = func() map[string]page { var pageShortcuts = func() map[string]page {
m := make(map[string]page, len(sidebarEntries)) m := make(map[string]page, len(pageRegistry))
for i, e := range sidebarEntries { for i, e := range pageRegistry {
m[strconv.Itoa(i+1)] = e.id m[strconv.Itoa(i+1)] = e.id
} }
return m return m
@@ -55,6 +52,8 @@ type Model struct {
database *db.DB database *db.DB
logFile *os.File logFile *os.File
pluginManager *plugins.Manager pluginManager *plugins.Manager
fatalErr error
logFileErr error
width int width int
height int height int
@@ -96,8 +95,8 @@ func New(broker *intercept.Broker, name, path string) Model {
d, err := db.Open(path) d, err := db.Open(path)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "db: %v\n", err) m.fatalErr = err
os.Exit(1) return m
} }
m.database = d m.database = d
broker.SetDB(d) broker.SetDB(d)
@@ -130,14 +129,18 @@ func New(broker *intercept.Broker, name, path string) Model {
m.logFile = lf m.logFile = lf
log.SetOutput(lf) log.SetOutput(lf)
logrus.SetOutput(lf) logrus.SetOutput(lf)
} else {
m.logFileErr = err
} }
return m return m
} }
func (m Model) FatalErr() error { return m.fatalErr }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
mgr := m.pluginManager mgr := m.pluginManager
return tea.Batch( cmds := []tea.Cmd{
intercept.WaitForRequest(m.broker), intercept.WaitForRequest(m.broker),
intercept.WaitForResponse(m.broker), intercept.WaitForResponse(m.broker),
tickCmd(), tickCmd(),
@@ -146,5 +149,16 @@ func (m Model) Init() tea.Cmd {
plugins.WaitForQuit(mgr), plugins.WaitForQuit(mgr),
findingsUI.RefreshCmd(m.database), findingsUI.RefreshCmd(m.database),
func() tea.Msg { mgr.RunOnStart(); return nil }, func() tea.Msg { mgr.RunOnStart(); return nil },
) }
if m.logFileErr != nil {
err := m.logFileErr
cmds = append(cmds, func() tea.Msg {
return notificationsUI.NotificationMsg{
Title: "Warning",
Body: "Could not open log file: " + err.Error(),
Kind: notificationsUI.KindWarning,
}
})
}
return tea.Batch(cmds...)
} }
+1 -1
View File
@@ -62,7 +62,7 @@ func (m *Model) renderSidebar() string {
var items strings.Builder var items strings.Builder
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true) badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
for i, entry := range sidebarEntries { for i, entry := range pageRegistry {
selected := entry.id == m.page selected := entry.id == m.page
badgeStyle, textStyle := badgeNormal, textNormal badgeStyle, textStyle := badgeNormal, textNormal
if selected { if selected {
+4 -21
View File
@@ -2,7 +2,6 @@ package app
import ( import (
"log" "log"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -21,6 +20,7 @@ import (
historyUI "github.com/anotherhadi/spilltea/internal/ui/history" historyUI "github.com/anotherhadi/spilltea/internal/ui/history"
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept" interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay" replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
"github.com/anotherhadi/spilltea/internal/util"
) )
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -107,16 +107,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case proxyPkg.ErrMsg: case proxyPkg.ErrMsg:
if msg.Err != nil { if msg.Err != nil {
log.Printf("proxy error: %v", msg.Err) log.Printf("proxy error: %v", msg.Err)
return m, tea.Batch( m.fatalErr = msg.Err
func() tea.Msg { return m, tea.Quit
return notificationsUI.NotificationMsg{
Title: "Proxy Error",
Body: msg.Err.Error(),
Kind: notificationsUI.KindError,
}
},
tea.Quit,
)
} }
return m, nil return m, nil
@@ -170,23 +162,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd return m, cmd
case tea.KeyPressMsg: case tea.KeyPressMsg:
// ctrl+c always quits, even when a textarea is focused.
if msg.String() == "ctrl+c" {
m.pluginManager.RunOnQuit()
return m, tea.Quit
}
if key.Matches(msg, keys.Keys.Global.Quit) && !m.activeIsEditing() { if key.Matches(msg, keys.Keys.Global.Quit) && !m.activeIsEditing() {
m.pluginManager.RunOnQuit() m.pluginManager.RunOnQuit()
return m, tea.Quit return m, tea.Quit
} }
if key.Matches(msg, keys.Keys.Global.OpenLogs) { if key.Matches(msg, keys.Keys.Global.OpenLogs) {
editor := os.Getenv("EDITOR")
if editor == "" {
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(util.ResolveEditor(), logPath), nil) // #nosec G204 G702 -- editor from trusted config/$EDITOR, 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
} }
+1 -1
View File
@@ -16,7 +16,7 @@ type parsedRequest struct {
path string path string
host string host string
scheme string scheme string
headers []header // garder header{key, value} pour compat locale headers []header
body string body string
} }
+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
+2 -2
View File
@@ -25,10 +25,10 @@ func readDoc(name string) string {
var contentMarkdown = strings.Join([]string{ var contentMarkdown = strings.Join([]string{
readDoc("main.md"), readDoc("main.md"),
readDoc("legal-disclaimer.md"),
readDoc("basics.md"),
readDoc("proxy.md"), readDoc("proxy.md"),
readDoc("certificate.md"), readDoc("certificate.md"),
readDoc("legal-disclaimer.md"),
readDoc("basics.md"),
readDoc("history.md"), readDoc("history.md"),
}, "\n") }, "\n")
+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")),
) )
} }
+18 -6
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"
@@ -25,7 +26,10 @@ func LoadEntriesCmd(database *db.DB) tea.Cmd {
if database == nil { if database == nil {
return EntriesLoadedMsg{} return EntriesLoadedMsg{}
} }
entries, _ := database.ListEntries() entries, err := database.ListEntries()
if err != nil {
log.Printf("history: load entries: %v", err)
}
return EntriesLoadedMsg{Entries: entries} return EntriesLoadedMsg{Entries: entries}
} }
} }
@@ -234,7 +238,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 +248,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 +269,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 +358,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(),
+3 -2
View File
@@ -318,13 +318,14 @@ func (m Model) renderHelpLine() string {
} }
var parts []string var parts []string
escKey := keys.Keys.Global.Escape.Help().Key
if fs == list.Filtering { if fs == list.Filtering {
parts = append(parts, item("enter", "apply filter")) parts = append(parts, item("enter", "apply filter"))
parts = append(parts, item("esc", "cancel")) parts = append(parts, item(escKey, "cancel"))
} else { } else {
parts = append(parts, item("↑/↓", "navigate")) parts = append(parts, item("↑/↓", "navigate"))
if fs == list.FilterApplied { if fs == list.FilterApplied {
parts = append(parts, item("esc", "clear filter")) parts = append(parts, item(escKey, "clear filter"))
} else { } else {
parts = append(parts, binding(k.Filter)) parts = append(parts, binding(k.Filter))
} }
+14 -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,8 @@ 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 {
log.Printf("home: create temp dir: %v", err)
return m, nil return m, nil
} }
initProjectFiles(dir) initProjectFiles(dir)
@@ -90,7 +92,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 +117,8 @@ 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 {
log.Printf("home: create project dir %q: %v", name, err)
return m, nil return m, nil
} }
initProjectFiles(dir) initProjectFiles(dir)
@@ -147,14 +152,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 +176,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)
}
} }
} }
} }
+4 -1
View File
@@ -6,6 +6,7 @@ import (
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
ilovetui "github.com/anotherhadi/ilovetui" ilovetui "github.com/anotherhadi/ilovetui"
"github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/ui/components/teapot" "github.com/anotherhadi/spilltea/internal/ui/components/teapot"
) )
@@ -45,6 +46,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
} }
@@ -69,7 +71,8 @@ func (m Model) renderNamingPanel() string {
Width(panelW). Width(panelW).
Render(label + "\n" + inputLine) Render(label + "\n" + inputLine)
hint := ilovetui.S.Faint.Render("[enter] confirm [esc] cancel") escKey := keys.Keys.Global.Escape.Help().Key
hint := ilovetui.S.Faint.Render("[enter] confirm [" + escKey + "] cancel")
var sb strings.Builder var sb strings.Builder
sb.WriteString(center(iw, panel)) sb.WriteString(center(iw, panel))
+3 -10
View File
@@ -2,24 +2,17 @@ package intercept
import ( import (
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
"github.com/anotherhadi/spilltea/internal/icons"
"github.com/anotherhadi/spilltea/internal/keys" "github.com/anotherhadi/spilltea/internal/keys"
) )
type interceptKeyMap struct{ width int } type interceptKeyMap struct{ width int }
func iconBinding(b key.Binding, icon string) key.Binding {
h := b.Help()
return key.NewBinding(key.WithKeys(b.Keys()...), key.WithHelp(h.Key, icon+h.Desc))
}
func (interceptKeyMap) ShortHelp() []key.Binding { func (interceptKeyMap) ShortHelp() []key.Binding {
ic := keys.Keys.Intercept ic := keys.Keys.Intercept
i := icons.I
return []key.Binding{ return []key.Binding{
iconBinding(ic.Forward, i.Forward), ic.Forward,
iconBinding(ic.Drop, i.Drop), ic.Drop,
iconBinding(ic.Edit, i.Edit), ic.Edit,
keys.Keys.Global.Help, keys.Keys.Global.Help,
} }
} }
+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(),
+49 -16
View File
@@ -5,9 +5,14 @@ import (
"compress/gzip" "compress/gzip"
"compress/zlib" "compress/zlib"
"crypto/tls" "crypto/tls"
"crypto/x509"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -79,7 +84,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()
@@ -96,24 +103,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.Button { switch msg.Button {
case tea.MouseWheelUp: case tea.MouseWheelUp:
if msg.Mod.Contains(tea.ModShift) { if msg.Mod.Contains(tea.ModShift) {
m.requestViewport.ScrollLeft(6) m.scrollBothViewports(true)
m.responseViewport.ScrollLeft(6)
} else { } else {
m.scrollFocusedViewportVertical(-1) m.scrollFocusedViewportVertical(-1)
} }
case tea.MouseWheelDown: case tea.MouseWheelDown:
if msg.Mod.Contains(tea.ModShift) { if msg.Mod.Contains(tea.ModShift) {
m.requestViewport.ScrollRight(6) m.scrollBothViewports(false)
m.responseViewport.ScrollRight(6)
} else { } else {
m.scrollFocusedViewportVertical(1) m.scrollFocusedViewportVertical(1)
} }
case tea.MouseWheelLeft: case tea.MouseWheelLeft:
m.requestViewport.ScrollLeft(6) m.scrollBothViewports(true)
m.responseViewport.ScrollLeft(6)
case tea.MouseWheelRight: case tea.MouseWheelRight:
m.requestViewport.ScrollRight(6) m.scrollBothViewports(false)
m.responseViewport.ScrollRight(6)
} }
} }
@@ -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
@@ -320,6 +327,16 @@ func (m *Model) setFocusedViewport(vp viewport.Model) {
} }
} }
func (m *Model) scrollBothViewports(left bool) {
if left {
m.requestViewport.ScrollLeft(6)
m.responseViewport.ScrollLeft(6)
} else {
m.requestViewport.ScrollRight(6)
m.responseViewport.ScrollRight(6)
}
}
func (m *Model) scrollFocusedViewportVertical(delta int) { func (m *Model) scrollFocusedViewportVertical(delta int) {
vp := m.focusedViewport() vp := m.focusedViewport()
vp.SetYOffset(vp.YOffset() + delta) vp.SetYOffset(vp.YOffset() + delta)
@@ -351,11 +368,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)
@@ -396,11 +413,27 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
} }
req.Header = headers req.Header = headers
tlsCfg := &tls.Config{InsecureSkipVerify: config.Global.App.SslInsecure} // #nosec G402 -- controlled by ssl_insecure config
if !config.Global.App.SslInsecure {
if certPool, err := x509.SystemCertPool(); err == nil {
caPath := filepath.Join(config.ExpandPath(config.Global.App.CertDir), "mitmproxy-ca-cert.pem")
if pem, err := os.ReadFile(caPath); err == nil {
certPool.AppendCertsFromPEM(pem)
}
tlsCfg.RootCAs = certPool
}
}
transport := &http.Transport{
TLSClientConfig: tlsCfg,
}
if up := config.Global.App.UpstreamProxy; up != "" {
if proxyURL, err := url.Parse(up); err == nil {
transport.Proxy = http.ProxyURL(proxyURL)
}
}
client := &http.Client{ client := &http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: transport,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
},
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")),
) )
} }
+7 -4
View File
@@ -15,7 +15,7 @@ type EditorFinishedMsg struct {
Err error Err error
} }
func resolveEditor() string { func ResolveEditor() string {
editor := config.Global.App.ExternalEditor editor := config.Global.App.ExternalEditor
if editor == "" { if editor == "" {
editor = os.Getenv("EDITOR") editor = os.Getenv("EDITOR")
@@ -29,14 +29,17 @@ func resolveEditor() string {
func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cmd { func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cmd {
f, err := os.CreateTemp("", "spilltea-*.http") f, err := os.CreateTemp("", "spilltea-*.http")
if err != nil { if err != nil {
log.Printf("editor: create temp file: %v", err)
return nil return nil
} }
tmpPath := f.Name() tmpPath := f.Name()
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 +50,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
+61
View File
@@ -0,0 +1,61 @@
{pkgs}: let
defaultCertDir = "$HOME/.local/share/spilltea";
defaultHost = "127.0.0.1";
defaultPort = "8080";
in
pkgs.writeShellApplication {
name = "spilltea-browser";
runtimeInputs = with pkgs; [nss.tools librewolf netcat-gnu procps];
text = ''
CERT_DIR="${defaultCertDir}"
HOST="${defaultHost}"
PORT="${defaultPort}"
while [[ $# -gt 0 ]]; do
case "$1" in
--cert-dir) CERT_DIR="$2"; shift 2 ;;
--host) HOST="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
done
CERT_FILE="$CERT_DIR/mitmproxy-ca-cert.pem"
if [[ ! -f "$CERT_FILE" ]]; then
echo "Certificate not found at $CERT_FILE"
echo "Launch spilltea first, or use --cert-dir if you changed cert_dir in your config."
exit 1
fi
if ! pgrep -x spilltea > /dev/null 2>&1; then
echo "spilltea is not running. Launch spilltea first."
exit 1
fi
if ! nc -z "$HOST" "$PORT" > /dev/null 2>&1; then
echo "Nothing is listening on $HOST:$PORT."
echo "Is spilltea configured on that port? Use --host and --port if you changed them in your config."
exit 1
fi
PROFILE=$(mktemp -d)
trap 'rm -rf "$PROFILE"' EXIT
certutil -N -d sql:"$PROFILE" --empty-password
certutil -d sql:"$PROFILE" -A -n "spilltea" -t "CT,," -i "$CERT_FILE"
cat > "$PROFILE/user.js" <<EOF
user_pref("network.proxy.type", 1);
user_pref("network.proxy.http", "$HOST");
user_pref("network.proxy.http_port", $PORT);
user_pref("network.proxy.ssl", "$HOST");
user_pref("network.proxy.ssl_port", $PORT);
user_pref("network.proxy.no_proxies_on", "");
user_pref("network.stricttransportsecurity.preloadlist", false);
user_pref("dom.security.https_only_mode", false);
EOF
librewolf --profile "$PROFILE" --no-remote
'';
}
+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'
+11 -1
View File
@@ -2,13 +2,22 @@
pkgs, pkgs,
buildGoApplication, buildGoApplication,
}: let }: let
browser = import ./browser.nix {inherit pkgs;};
pname = "spilltea"; pname = "spilltea";
version = "0.0.6"; version = "0.0.7";
ldflags = ["-s" "-w" "-X main.version=${version}"]; ldflags = ["-s" "-w" "-X main.version=${version}"];
pkg = buildGoApplication { pkg = buildGoApplication {
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";
@@ -18,4 +27,5 @@
in { in {
"${pname}" = pkg; "${pname}" = pkg;
default = pkg; default = pkg;
browser = browser;
} }
+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++