diff options
| author | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
|---|---|---|
| committer | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
| commit | 0a477fd288d064d265395c88dbd0f261b723804e (patch) | |
| tree | 9b129f8f41c907285b89ed8385679934bb59c14f /internal/ui/repeater.go | |
Initial release
Diffstat (limited to 'internal/ui/repeater.go')
| -rw-r--r-- | internal/ui/repeater.go | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/internal/ui/repeater.go b/internal/ui/repeater.go new file mode 100644 index 0000000..72c61ae --- /dev/null +++ b/internal/ui/repeater.go @@ -0,0 +1,224 @@ +package ui + +import ( + "bytes" + "fmt" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + + internalhttp "github.com/shiv/internal/http" + "github.com/shiv/internal/events" + "github.com/shiv/internal/logger" + "github.com/shiv/internal/store" +) + +type repeaterTab struct { + projectStore *store.Store + bus *events.Bus + loot *lootTab + tabs *container.DocTabs + win fyne.Window + tabIDs map[*container.TabItem]int64 + sendFns map[*container.TabItem]func() + loadFns map[*container.TabItem]func() +} + +func newRepeaterTab(projectStore *store.Store, bus *events.Bus, win fyne.Window) *repeaterTab { + return &repeaterTab{ + projectStore: projectStore, + bus: bus, + win: win, + tabIDs: make(map[*container.TabItem]int64), + sendFns: make(map[*container.TabItem]func()), + loadFns: make(map[*container.TabItem]func()), + } +} + +func (r *repeaterTab) closeTab(closed *container.TabItem) { + if tabID, ok := r.tabIDs[closed]; ok { + if err := r.projectStore.DeleteRepeaterTab(tabID); err != nil { + logger.Error("repeater: delete tab: %v", err) + } + delete(r.tabIDs, closed) + } + delete(r.sendFns, closed) + delete(r.loadFns, closed) +} + +func (r *repeaterTab) build() fyne.CanvasObject { + r.tabs = container.NewDocTabs() + r.tabs.SetTabLocation(container.TabLocationTop) + + r.tabs.OnSelected = func(item *container.TabItem) { + if fn, ok := r.loadFns[item]; ok { + fn() + delete(r.loadFns, item) + } + } + + r.tabs.OnClosed = r.closeTab + + r.tabs.CreateTab = func() *container.TabItem { + saved := store.RepeaterTab{ + Name: "New Tab", + Port: 443, + TabType: "http", + } + id, err := r.projectStore.SaveRepeaterTab(saved) + if err != nil { + logger.Error("repeater: save new tab: %v", err) + return container.NewTabItem("New Tab", widget.NewLabel("")) + } + saved.ID = id + return r.buildTabItem(saved) + } + + saved, err := r.projectStore.AllRepeaterTabs() + if err != nil { + logger.Error("repeater: load tabs: %v", err) + } + for _, t := range saved { + r.tabs.Append(r.buildTabItem(t)) + } + if selected := r.tabs.Selected(); selected != nil { + if fn, ok := r.loadFns[selected]; ok { + fn() + delete(r.loadFns, selected) + } + } + + return r.tabs +} + +func (r *repeaterTab) AddTab(name, host string, port int, useTLS bool, rawRequest string) { + saved := store.RepeaterTab{ + Name: name, + Host: host, + Port: port, + TLS: useTLS, + TabType: "http", + RawRequest: rawRequest, + } + id, err := r.projectStore.SaveRepeaterTab(saved) + if err != nil { + logger.Error("repeater: save tab: %v", err) + return + } + saved.ID = id + item := r.buildTabItem(saved) + r.tabs.Append(item) + r.tabs.Select(item) +} + +// AddWSTab opens a new WebSocket repeater tab for the given connection. +func (r *repeaterTab) AddWSTab(name, wsURL string, useTLS bool) { + saved := store.RepeaterTab{ + Name: name, + Host: wsURL, + TLS: useTLS, + TabType: "websocket", + RawRequest: wsURL, + } + id, err := r.projectStore.SaveRepeaterTab(saved) + if err != nil { + logger.Error("repeater: save ws tab: %v", err) + return + } + saved.ID = id + item := r.buildTabItem(saved) + r.tabs.Append(item) + r.tabs.Select(item) +} + +func (r *repeaterTab) buildTabItem(tab store.RepeaterTab) *container.TabItem { + if tab.TabType == "websocket" { + return r.buildWSTabItem(tab) + } + return r.buildHTTPTabItem(tab) +} + +// updateTabName updates the DocTab label after an HTTP send. +func (r *repeaterTab) updateTabName(rawReq string, tabID int64) { + firstLine := strings.SplitN(rawReq, "\n", 2)[0] + parts := strings.Fields(firstLine) + if len(parts) < 2 { + return + } + path := parts[1] + if len(path) > 24 { + path = path[:24] + "..." + } + name := fmt.Sprintf("%s %s", parts[0], path) + + for item, id := range r.tabIDs { + if id == tabID { + item.Text = name + r.tabs.Refresh() + break + } + } + + if err := r.projectStore.RenameRepeaterTab(tabID, name); err != nil { + logger.Error("repeater: rename tab: %v", err) + } +} + +// patchRaw applies plugin modifications surgically to the normalized raw +// request string. Only fields the plugin actually changed are rewritten. +// SendRaw owns Content-Length, Connection, and Accept-Encoding — we leave +// those untouched so its internal rewriteHeaders handles them correctly. +func patchRaw(raw, headerSection string, origBody []byte, origMethod, origPath string, result events.RequestResult) string { + lines := strings.Split(headerSection, "\r\n") + if len(lines) == 0 { + return raw + } + + // Rewrite request line only if method or path actually changed. + newMethod := result.Request.Method + newPath := result.Request.URL.RequestURI() + if newMethod == "" { + newMethod = origMethod + } + if newPath == "" || newPath == "?" { + newPath = origPath + } + if newMethod != origMethod || newPath != origPath { + lines[0] = fmt.Sprintf("%s %s HTTP/1.1", newMethod, newPath) + } + + // Apply header changes: update existing headers in-place (preserving + // original casing and order), append new ones at the end. + for pluginKey, pluginVals := range result.Request.Header { + if len(pluginVals) == 0 { + continue + } + pluginVal := strings.Join(pluginVals, ", ") + found := false + for i := 1; i < len(lines); i++ { + if lines[i] == "" { + break + } + parts := strings.SplitN(lines[i], ": ", 2) + if len(parts) == 2 && strings.EqualFold(parts[0], pluginKey) { + lines[i] = parts[0] + ": " + pluginVal + found = true + break + } + } + if !found { + lines = append(lines, pluginKey+": "+pluginVal) + } + } + + // Replace body only if the plugin actually changed it. + _, body := internalhttp.SplitRaw(raw) + finalBody := body + if !bytes.Equal(result.Body, origBody) { + finalBody = result.Body + } + + return strings.Join(lines, "\r\n") + "\r\n\r\n" + string(finalBody) +} |
