mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
578 lines
16 KiB
Go
578 lines
16 KiB
Go
package render
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Proxy (proxy_renderer.py)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const proxyIcon = "<~>"
|
|
|
|
func proxyStatusStyle(code int) lipgloss.Style {
|
|
switch {
|
|
case code >= 200 && code < 300:
|
|
return Col(Green)
|
|
case code >= 300 && code < 400:
|
|
return Col(Status3xx)
|
|
case code >= 400 && code < 500:
|
|
return Col(Status4xx)
|
|
case code >= 500:
|
|
return Col(Red)
|
|
}
|
|
return Dim()
|
|
}
|
|
|
|
func ptrunc(s string, max int) string {
|
|
if len(s) > max {
|
|
return s[:max-3] + "..."
|
|
}
|
|
return s
|
|
}
|
|
|
|
func psanitize(s string, max int) string {
|
|
clean := strings.NewReplacer("\n", " ", "\r", "", "\t", " ").Replace(s)
|
|
return ptrunc(clean, max)
|
|
}
|
|
|
|
func renderProxyTool(name string, args map[string]any, result any, status string) string {
|
|
switch name {
|
|
case "list_requests":
|
|
return renderListRequests(args, result, status)
|
|
case "view_request":
|
|
return renderViewRequest(args, result, status)
|
|
case "repeat_request":
|
|
return renderRepeatRequest(args, result, status)
|
|
case "list_sitemap":
|
|
return renderListSitemap(args, result, status)
|
|
case "view_sitemap_entry":
|
|
return renderViewSitemapEntry(args, result, status)
|
|
case "scope_rules":
|
|
return renderScopeRules(args, result, status)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func resultMapOf(result any) (map[string]any, bool) {
|
|
m, ok := result.(map[string]any)
|
|
return m, ok
|
|
}
|
|
|
|
func renderListRequests(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" listing requests"))
|
|
if f := StringValue(args["httpql_filter"]); f != "" {
|
|
b.WriteString(Dim().Italic(true).Render(" where " + ptrunc(f, 150)))
|
|
}
|
|
var meta []string
|
|
if s := StringValue(args["sort_by"]); s != "" && s != "timestamp" {
|
|
meta = append(meta, "by:"+s)
|
|
}
|
|
if s := StringValue(args["sort_order"]); s != "" && s != "desc" {
|
|
meta = append(meta, s)
|
|
}
|
|
if s := StringValue(args["scope_id"]); s != "" {
|
|
meta = append(meta, "scope:"+truncStr(s, 8))
|
|
}
|
|
if len(meta) > 0 {
|
|
b.WriteString(Dim().Render(" (" + strings.Join(meta, ", ") + ")"))
|
|
}
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
if e, has := m["error"]; has {
|
|
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
|
|
} else {
|
|
entries, _ := m["entries"].([]any)
|
|
suffix := ""
|
|
if pi, ok := m["page_info"].(map[string]any); ok && truthy(pi["has_next_page"]) {
|
|
suffix = "+"
|
|
}
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [%d%s found]", len(entries), suffix)))
|
|
renderRequestEntries(&b, entries)
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderRequestEntries(b *strings.Builder, entries []any) {
|
|
if len(entries) == 0 {
|
|
return
|
|
}
|
|
b.WriteString("\n")
|
|
limit := len(entries)
|
|
if limit > 20 {
|
|
limit = 20
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
entry, ok := entries[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
req, _ := entry["request"].(map[string]any)
|
|
resp, _ := entry["response"].(map[string]any)
|
|
method := StringValue(req["method"])
|
|
if method == "" {
|
|
method = "?"
|
|
}
|
|
host := StringValue(req["host"])
|
|
path := StringValue(req["path"])
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
b.WriteString(" " + Col(Lavender).Render(fmt.Sprintf("%-6s", method)))
|
|
b.WriteString(Dim().Render(" " + ptrunc(host+path, 180)))
|
|
if code, ok := NumericValue(resp["status_code"]); ok && code != 0 {
|
|
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
|
|
}
|
|
if i < limit-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
if len(entries) > 20 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(entries)-20)))
|
|
}
|
|
}
|
|
|
|
func renderViewRequest(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon))
|
|
part := StringValue(args["part"])
|
|
if part == "" {
|
|
part = "request"
|
|
}
|
|
action := "viewing"
|
|
search := StringValue(args["search_pattern"])
|
|
if search != "" {
|
|
action = "searching"
|
|
}
|
|
b.WriteString(Col(Cyan).Render(" " + action + " " + part))
|
|
if rid := StringValue(args["request_id"]); rid != "" {
|
|
b.WriteString(Dim().Render(" #" + rid))
|
|
}
|
|
if search != "" {
|
|
b.WriteString(Dim().Italic(true).Render(" /" + ptrunc(search, 100) + "/"))
|
|
}
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
if e, has := m["error"]; has {
|
|
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
|
|
} else if hits, has := m["hits"].([]any); has {
|
|
total := len(hits)
|
|
if t, ok := NumericValue(m["total_hits"]); ok {
|
|
total = int(t)
|
|
}
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [%d matches]", total)))
|
|
renderSearchHits(&b, hits)
|
|
} else if content, has := m["content"]; has {
|
|
page := 1
|
|
if p, ok := NumericValue(m["page"]); ok {
|
|
page = int(p)
|
|
}
|
|
tl := 0
|
|
if t, ok := NumericValue(m["total_lines"]); ok {
|
|
tl = int(t)
|
|
}
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [page %d, %d lines]", page, tl)))
|
|
renderContentLines(&b, StringValue(content), truthy(m["has_more"]))
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderSearchHits(b *strings.Builder, hits []any) {
|
|
if len(hits) == 0 {
|
|
return
|
|
}
|
|
b.WriteString("\n")
|
|
limit := len(hits)
|
|
if limit > 5 {
|
|
limit = 5
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
m, ok := hits[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
before := lastN(strings.NewReplacer("\n", " ", "\r", "").Replace(StringValue(m["before"])), 100)
|
|
after := firstN(strings.NewReplacer("\n", " ", "\r", "").Replace(StringValue(m["after"])), 100)
|
|
b.WriteString(" ")
|
|
if before != "" {
|
|
b.WriteString(Dim().Render("..." + before))
|
|
}
|
|
b.WriteString(Bold(Green).Render(StringValue(m["match"])))
|
|
if after != "" {
|
|
b.WriteString(Dim().Render(after + "..."))
|
|
}
|
|
if i < limit-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
if len(hits) > 5 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more matches", len(hits)-5)))
|
|
}
|
|
}
|
|
|
|
func renderContentLines(b *strings.Builder, content string, hasMore bool) {
|
|
if content == "" {
|
|
return
|
|
}
|
|
allLines := strings.Split(content, "\n")
|
|
lines := allLines
|
|
if len(lines) > 15 {
|
|
lines = lines[:15]
|
|
}
|
|
b.WriteString("\n")
|
|
for i, line := range lines {
|
|
b.WriteString(" " + Dim().Render(ptrunc(line, maxLineLength)))
|
|
if i < len(lines)-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
if hasMore || len(allLines) > 15 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(" ... more content available"))
|
|
}
|
|
}
|
|
|
|
func renderRepeatRequest(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" repeating request"))
|
|
if rid := StringValue(args["request_id"]); rid != "" {
|
|
b.WriteString(Dim().Render(" #" + rid))
|
|
}
|
|
if mods, ok := args["modifications"].(map[string]any); ok {
|
|
b.WriteString(Dim().Italic(true).Render("\n modifications:"))
|
|
arrow := Col(Blue).Render(" >> ")
|
|
if url, ok := mods["url"]; ok {
|
|
b.WriteString("\n" + arrow + Dim().Render("url: "+ptrunc(StringValue(url), 180)))
|
|
}
|
|
writeKV := func(key, prefix string, valMax int) {
|
|
if kv, ok := mods[key].(map[string]any); ok {
|
|
n := 0
|
|
for k, v := range kv {
|
|
if n >= 5 {
|
|
break
|
|
}
|
|
b.WriteString("\n" + arrow + Dim().Render(fmt.Sprintf(prefix, k, psanitize(StringValue(v), valMax))))
|
|
n++
|
|
}
|
|
}
|
|
}
|
|
writeKV("headers", "%s: %s", 150)
|
|
writeKV("cookies", "cookie %s=%s", 100)
|
|
writeKV("params", "param %s=%s", 100)
|
|
if body, ok := mods["body"].(string); ok {
|
|
b.WriteString("\n" + arrow)
|
|
bodyLines := strings.Split(body, "\n")
|
|
shown := bodyLines
|
|
if len(shown) > 4 {
|
|
shown = shown[:4]
|
|
}
|
|
for i, line := range shown {
|
|
if i > 0 {
|
|
b.WriteString("\n" + Dim().Render(" "))
|
|
}
|
|
b.WriteString(Dim().Render(ptrunc(line, maxLineLength)))
|
|
}
|
|
if len(bodyLines) > 4 {
|
|
b.WriteString(Dim().Italic(true).Render(" ..."))
|
|
}
|
|
}
|
|
} else if mods, ok := args["modifications"].(string); ok && mods != "" {
|
|
b.WriteString(Dim().Italic(true).Render("\n " + ptrunc(mods, 200)))
|
|
}
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
success, hasSuccess := m["success"].(bool)
|
|
if hasSuccess && !success && StringValue(m["error"]) != "" {
|
|
b.WriteString(Col(Red).Render("\n error: " + psanitize(StringValue(m["error"]), 150)))
|
|
} else {
|
|
resp, _ := m["response"].(map[string]any)
|
|
b.WriteString("\n" + Col(Green).Render(" << "))
|
|
if code, ok := NumericValue(resp["status_code"]); ok && code != 0 {
|
|
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf("%d", int(code))))
|
|
} else {
|
|
b.WriteString(Dim().Render("(no response)"))
|
|
}
|
|
if ms, ok := NumericValue(m["elapsed_ms"]); ok && ms != 0 {
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" (%dms)", int(ms))))
|
|
}
|
|
body := StringValue(resp["body"])
|
|
if body != "" {
|
|
allLines := strings.Split(body, "\n")
|
|
lines := allLines
|
|
if len(lines) > 5 {
|
|
lines = lines[:5]
|
|
}
|
|
for _, line := range lines {
|
|
b.WriteString("\n" + Col(Green).Render(" << ") + Dim().Render(ptrunc(line, maxLineLength-5)))
|
|
}
|
|
if truthy(resp["body_truncated"]) || len(allLines) > 5 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(" ..."))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderListSitemap(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" listing sitemap"))
|
|
if pid := StringValue(args["parent_id"]); pid != "" {
|
|
b.WriteString(Dim().Render(" under #" + ptrunc(pid, 20)))
|
|
}
|
|
var meta []string
|
|
if s := StringValue(args["scope_id"]); s != "" {
|
|
meta = append(meta, "scope:"+truncStr(s, 8))
|
|
}
|
|
if d := StringValue(args["depth"]); d != "" && d != "DIRECT" {
|
|
meta = append(meta, strings.ToLower(d))
|
|
}
|
|
if len(meta) > 0 {
|
|
b.WriteString(Dim().Render(" (" + strings.Join(meta, ", ") + ")"))
|
|
}
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
if e, has := m["error"]; has {
|
|
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
|
|
} else {
|
|
total := 0
|
|
if t, ok := NumericValue(m["total_count"]); ok {
|
|
total = int(t)
|
|
}
|
|
entries, _ := m["entries"].([]any)
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [%d entries]", total)))
|
|
renderSitemapEntries(&b, entries)
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
var sitemapKindColors = map[string]lipgloss.Color{
|
|
"DOMAIN": AmberY, "DIRECTORY": Blue, "REQUEST": Green,
|
|
}
|
|
|
|
func renderSitemapEntries(b *strings.Builder, entries []any) {
|
|
if len(entries) == 0 {
|
|
return
|
|
}
|
|
b.WriteString("\n")
|
|
limit := len(entries)
|
|
if limit > 20 {
|
|
limit = 20
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
entry, ok := entries[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
kind := StringValue(entry["kind"])
|
|
if kind == "" {
|
|
kind = "?"
|
|
}
|
|
label := StringValue(entry["label"])
|
|
if label == "" {
|
|
label = "?"
|
|
}
|
|
kindStyle, ok := sitemapKindColors[kind]
|
|
style := Dim()
|
|
if ok {
|
|
style = Col(kindStyle)
|
|
}
|
|
abbr := kind
|
|
if len(abbr) > 3 {
|
|
abbr = abbr[:3]
|
|
}
|
|
b.WriteString(" " + style.Render(fmt.Sprintf("%-3s", abbr)) + Dim().Render(" "+ptrunc(label, 150)))
|
|
if req, ok := entry["request"].(map[string]any); ok {
|
|
if method := StringValue(req["method"]); method != "" {
|
|
b.WriteString(Col(Lavender).Render(" " + method))
|
|
}
|
|
if code, ok := NumericValue(req["status_code"]); ok && code != 0 {
|
|
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
|
|
}
|
|
}
|
|
if truthy(entry["has_descendants"]) {
|
|
b.WriteString(Dim().Italic(true).Render(" +"))
|
|
}
|
|
if i < limit-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
if len(entries) > 20 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(entries)-20)))
|
|
}
|
|
}
|
|
|
|
func renderViewSitemapEntry(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" viewing sitemap"))
|
|
if eid := StringValue(args["entry_id"]); eid != "" {
|
|
b.WriteString(Dim().Render(" #" + ptrunc(eid, 20)))
|
|
}
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
if e, has := m["error"]; has {
|
|
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
|
|
} else if entry, ok := m["entry"].(map[string]any); ok {
|
|
kind, label := StringValue(entry["kind"]), StringValue(entry["label"])
|
|
related, _ := entry["related_requests"].(map[string]any)
|
|
if kind != "" && label != "" {
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" %s: %s", kind, ptrunc(label, 120))))
|
|
}
|
|
total := 0
|
|
if t, ok := NumericValue(related["total_count"]); ok {
|
|
total = int(t)
|
|
}
|
|
if total != 0 {
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [%d requests]", total)))
|
|
}
|
|
reqs, _ := related["requests"].([]any)
|
|
renderRelatedRequests(&b, reqs)
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderRelatedRequests(b *strings.Builder, reqs []any) {
|
|
if len(reqs) == 0 {
|
|
return
|
|
}
|
|
b.WriteString("\n")
|
|
limit := len(reqs)
|
|
if limit > 10 {
|
|
limit = 10
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
req, ok := reqs[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
method := StringValue(req["method"])
|
|
if method == "" {
|
|
method = "?"
|
|
}
|
|
path := StringValue(req["path"])
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
b.WriteString(" " + Col(Lavender).Render(fmt.Sprintf("%-6s", method)) + Dim().Render(" "+ptrunc(path, 180)))
|
|
if code, ok := NumericValue(req["status_code"]); ok && code != 0 {
|
|
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
|
|
}
|
|
if i < limit-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
if len(reqs) > 10 {
|
|
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(reqs)-10)))
|
|
}
|
|
}
|
|
|
|
var scopeActionMap = map[string]string{
|
|
"get": "getting", "list": "listing", "create": "creating", "update": "updating", "delete": "deleting",
|
|
}
|
|
|
|
func renderScopeRules(args map[string]any, result any, status string) string {
|
|
var b strings.Builder
|
|
b.WriteString(Dim().Render(proxyIcon))
|
|
action := StringValue(args["action"])
|
|
actionText, ok := scopeActionMap[action]
|
|
if !ok {
|
|
if action != "" {
|
|
actionText = action + "ing"
|
|
} else {
|
|
actionText = "managing"
|
|
}
|
|
}
|
|
b.WriteString(Col(Cyan).Render(" " + actionText + " proxy scope"))
|
|
if sn := StringValue(args["scope_name"]); sn != "" {
|
|
b.WriteString(Dim().Italic(true).Render(" '" + ptrunc(sn, 50) + "'"))
|
|
}
|
|
if sid := StringValue(args["scope_id"]); sid != "" {
|
|
b.WriteString(Dim().Render(" #" + truncStr(sid, 8)))
|
|
}
|
|
writeList := func(key, label string) {
|
|
if items, ok := args[key].([]any); ok && len(items) > 0 {
|
|
shown := items
|
|
if len(shown) > 4 {
|
|
shown = shown[:4]
|
|
}
|
|
var parts []string
|
|
for _, it := range shown {
|
|
parts = append(parts, ptrunc(StringValue(it), 40))
|
|
}
|
|
b.WriteString("\n " + Dim().Render(label+": "+strings.Join(parts, ", ")))
|
|
if len(items) > 4 {
|
|
b.WriteString(Dim().Italic(true).Render(fmt.Sprintf(" +%d", len(items)-4)))
|
|
}
|
|
}
|
|
}
|
|
writeList("allowlist", "allow")
|
|
writeList("denylist", "deny")
|
|
if status == "completed" {
|
|
if m, ok := resultMapOf(result); ok {
|
|
switch {
|
|
case m["error"] != nil:
|
|
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(m["error"]), 150)))
|
|
case m["scopes"] != nil:
|
|
scopes, _ := m["scopes"].([]any)
|
|
b.WriteString(Dim().Render(fmt.Sprintf(" [%d scopes]", len(scopes))))
|
|
renderScopeList(&b, scopes)
|
|
case m["scope"] != nil:
|
|
if scope, ok := m["scope"].(map[string]any); ok {
|
|
if allow, ok := scope["allowlist"].([]any); ok && len(allow) > 0 {
|
|
b.WriteString("\n " + Dim().Render("allow: "+joinTrunc(allow, 40, 5)))
|
|
}
|
|
if deny, ok := scope["denylist"].([]any); ok && len(deny) > 0 {
|
|
b.WriteString("\n " + Dim().Render("deny: "+joinTrunc(deny, 40, 5)))
|
|
}
|
|
}
|
|
case m["message"] != nil:
|
|
b.WriteString(Col(Green).Render(" " + StringValue(m["message"])))
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderScopeList(b *strings.Builder, scopes []any) {
|
|
if len(scopes) == 0 {
|
|
return
|
|
}
|
|
b.WriteString("\n")
|
|
limit := len(scopes)
|
|
if limit > 5 {
|
|
limit = 5
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
scope, ok := scopes[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
name := StringValue(scope["name"])
|
|
if name == "" {
|
|
name = "?"
|
|
}
|
|
b.WriteString(" " + Col(Green).Render(ptrunc(name, 40)))
|
|
if allow, ok := scope["allowlist"].([]any); ok && len(allow) > 0 {
|
|
b.WriteString(Dim().Render(" " + joinTrunc(allow, 30, 3)))
|
|
if len(allow) > 3 {
|
|
b.WriteString(Dim().Italic(true).Render(fmt.Sprintf(" +%d", len(allow)-3)))
|
|
}
|
|
}
|
|
if i < limit-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
}
|