16 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
45 changed files with 570 additions and 269 deletions
+23 -8
View File
@@ -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)
@@ -40,7 +41,6 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
<img alt="demo" src="./.github/assets/demo.gif" width="700" /> <img alt="demo" src="./.github/assets/demo.gif" width="700" />
<!-- exec: cat ./docs/legal-disclaimer.md --> <!-- exec: cat ./docs/legal-disclaimer.md -->
## Legal Disclaimer ## Legal Disclaimer
**This tool is provided for educational purposes and authorized security testing only.** **This tool is provided for educational purposes and authorized security testing only.**
@@ -48,7 +48,6 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction). Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction).
The author(s) and contributors are not responsible for any misuse, damage, or legal consequences resulting from the use of this software. By using Spilltea, you agree that you are solely responsible for ensuring your usage is lawful and authorized. The author(s) and contributors are not responsible for any misuse, damage, or legal consequences resulting from the use of this software. By using Spilltea, you agree that you are solely responsible for ensuring your usage is lawful and authorized.
<!-- endexec --> <!-- endexec -->
## Features ## Features
@@ -103,16 +102,15 @@ environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ]
</details> </details>
<!-- exec: cat ./docs/basics.md --> <!-- exec: cat ./docs/basics.md -->
## Project Management ## Project Management
Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files. Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files.
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
@@ -121,22 +119,39 @@ 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 -->
## Plugin System ## Plugin System
+140 -126
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,136 +32,150 @@ 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,
home, _ := os.UserHomeDir() RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml") if flagAddDefaultPlugins {
if *flagConfig != "" { home, _ := os.UserHomeDir()
cfgPath = *flagConfig cfgPath := filepath.Join(home, ".config", "spilltea", "config.yaml")
} if flagConfig != "" {
if err := config.Load(cfgPath); err != nil { cfgPath = flagConfig
fmt.Fprintf(os.Stderr, "config: %v\n", err) }
os.Exit(1) if err := config.Load(cfgPath); err != nil {
} return fmt.Errorf("config: %w", err)
dir := config.ExpandPath(config.Global.App.PluginsDir) }
if *flagPluginsDir != "" { dir := config.ExpandPath(config.Global.App.PluginsDir)
dir = *flagPluginsDir if flagPluginsDir != "" {
} dir = flagPluginsDir
n, err := spilltea.InstallDefaultPlugins(dir) }
if err != nil { n, err := spilltea.InstallDefaultPlugins(dir)
fmt.Fprintf(os.Stderr, "add-default-plugins: %v\n", err) if err != nil {
os.Exit(1) return fmt.Errorf("add-default-plugins: %w", err)
} }
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 {
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)
} }
}
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
sslInsecureChanged := cmd.Flags().Changed("ssl-insecure")
applyCLI := func(c *config.Config) {
if flagPluginsDir != "" {
c.App.PluginsDir = flagPluginsDir
}
if flagHost != "" {
c.App.Host = flagHost
}
if flagPort != 0 {
c.App.Port = flagPort
}
if flagUpstreamProxy != "" {
c.App.UpstreamProxy = flagUpstreamProxy
}
if sslInsecureChanged {
c.App.SslInsecure = flagSslInsecure
}
}
applyCLI(config.Global)
config.SetCLIOverrides(applyCLI)
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)
}
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()
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. rootCmd.Flags().StringVarP(&flagConfig, "config", "c", "", "path to config file")
root := rootModel{home: homeUI.New(projectDir)} rootCmd.Flags().StringVar(&flagPluginsDir, "plugins-dir", "", "path to plugins dir (overrides config)")
final, err := tea.NewProgram(root).Run() rootCmd.Flags().StringVar(&flagHost, "host", "", "proxy host (overrides config)")
if err != nil { rootCmd.Flags().IntVarP(&flagPort, "port", "p", 0, "proxy port (overrides config)")
fmt.Fprintf(os.Stderr, "tui: %v\n", err) 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)
} }
if r, ok := final.(rootModel); ok {
if ferr := r.FatalErr(); ferr != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
os.Exit(1)
}
}
} }
+11
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
+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)
+20 -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
@@ -56,6 +53,7 @@ type Model struct {
logFile *os.File logFile *os.File
pluginManager *plugins.Manager pluginManager *plugins.Manager
fatalErr error fatalErr error
logFileErr error
width int width int
height int height int
@@ -97,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)
@@ -131,6 +129,8 @@ 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
@@ -140,7 +140,7 @@ 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(),
@@ -149,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 {
+2 -11
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) {
@@ -162,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(),
+50 -17
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++