You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wuzz/wuzz.go

1151 lines
27 KiB
Go

package main
import (
"bytes"
"compress/gzip"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/asciimoo/wuzz/config"
"crypto/tls"
"github.com/jroimartin/gocui"
"github.com/mattn/go-runewidth"
"github.com/nwidger/jsoncolor"
)
const VERSION = "0.1.0"
var METHODS []string = []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
http.MethodPatch,
http.MethodOptions,
http.MethodTrace,
http.MethodConnect,
http.MethodHead,
}
var CLIENT *http.Client = &http.Client{
Timeout: time.Duration(5 * time.Second),
}
var TRANSPORT *http.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
}
var VIEWS []string = []string{
"url",
"get",
"method",
"data",
"headers",
"search",
"response-headers",
"response-body",
}
var defaultEditor ViewEditor
const MIN_WIDTH = 60
const MIN_HEIGHT = 20
type Request struct {
Url string
Method string
GetParams string
Data string
Headers string
ResponseHeaders string
RawResponseBody []byte
ContentType string
}
type App struct {
viewIndex int
historyIndex int
currentPopup string
history []*Request
config *config.Config
}
type ViewEditor struct {
app *App
g *gocui.Gui
backTabEscape bool
origEditor gocui.Editor
}
type SearchEditor struct {
wuzzEditor *ViewEditor
}
// The singlelineEditor removes multilines capabilities
type singlelineEditor struct {
wuzzEditor gocui.Editor
}
func init() {
TRANSPORT.DisableCompression = true
CLIENT.Transport = TRANSPORT
}
// Editor funcs
func (e *ViewEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
// handle back-tab (\033[Z) sequence
if e.backTabEscape {
if ch == 'Z' {
e.app.PrevView(e.g, nil)
e.backTabEscape = false
return
} else {
e.origEditor.Edit(v, 0, '[', gocui.ModAlt)
}
}
if ch == '[' && mod == gocui.ModAlt {
e.backTabEscape = true
return
}
e.origEditor.Edit(v, key, ch, mod)
}
func (e *SearchEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
e.wuzzEditor.Edit(v, key, ch, mod)
e.wuzzEditor.g.Execute(func(g *gocui.Gui) error {
e.wuzzEditor.app.PrintBody(g)
return nil
})
}
// The singlelineEditor removes multilines capabilities
func (e singlelineEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case (ch != 0 || key == gocui.KeySpace) && mod == 0:
e.wuzzEditor.Edit(v, key, ch, mod)
// At the end of the line the default gcui editor adds a whitespace
// Force him to remove
ox, _ := v.Cursor()
if ox > 1 && ox >= len(v.Buffer())-2 {
v.EditDelete(false)
}
return
case key == gocui.KeyEnter:
return
case key == gocui.KeyArrowRight:
ox, _ := v.Cursor()
if ox >= len(v.Buffer())-1 {
return
}
case key == gocui.KeyHome || key == gocui.KeyArrowUp:
v.SetCursor(0, 0)
return
case key == gocui.KeyEnd || key == gocui.KeyArrowDown:
v.SetCursor(len(v.Buffer())-1, 0)
return
}
e.wuzzEditor.Edit(v, key, ch, mod)
}
//
func (a *App) Layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if maxX < MIN_WIDTH || maxY < MIN_HEIGHT {
if v, err := g.SetView("error", 0, 0, maxX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Title = "Error"
g.Cursor = false
fmt.Fprintln(v, "Terminal is too small")
}
return nil
}
if _, err := g.View("error"); err == nil {
g.DeleteView("error")
g.Cursor = true
a.setView(g)
}
splitX := int(0.3 * float32(maxX))
splitY := int(0.25 * float32(maxY-3))
if v, err := g.SetView("url", 0, 0, maxX-1, 3); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Title = "URL - press F1 for help"
v.Editable = true
v.Overwrite = false
v.Editor = &singlelineEditor{&defaultEditor}
setViewTextAndCursor(v, a.config.General.DefaultURLScheme+"://")
}
if v, err := g.SetView("get", 0, 3, splitX, splitY+1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Editable = true
v.Title = "URL params"
v.Editor = &defaultEditor
}
if v, err := g.SetView("method", 0, splitY+1, splitX, splitY+3); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Editable = true
v.Title = "Method"
v.Editor = &singlelineEditor{&defaultEditor}
setViewTextAndCursor(v, "GET")
}
if v, err := g.SetView("data", 0, 3+splitY, splitX, 2*splitY+3); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Editable = true
v.Title = "Request data (POST/PUT)"
v.Editor = &defaultEditor
}
if v, err := g.SetView("headers", 0, 3+(splitY*2), splitX, maxY-2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Wrap = false
v.Editable = true
v.Title = "Request headers"
v.Editor = &defaultEditor
}
if v, err := g.SetView("response-headers", splitX, 3, maxX-1, splitY+3); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Title = "Response headers"
v.Editable = true
v.Editor = &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
return
})}
}
if v, err := g.SetView("response-body", splitX, 3+splitY, maxX-1, maxY-2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
setViewDefaults(v)
v.Title = "Response body"
v.Editable = true
v.Editor = &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
return
})}
}
if v, err := g.SetView("prompt", -1, maxY-2, 7, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Wrap = true
setViewTextAndCursor(v, "search> ")
}
if v, err := g.SetView("search", 7, maxY-2, maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Editable = true
v.Editor = &singlelineEditor{&SearchEditor{&defaultEditor}}
v.Wrap = true
}
return nil
}
func (a *App) NextView(g *gocui.Gui, v *gocui.View) error {
a.viewIndex = (a.viewIndex + 1) % len(VIEWS)
return a.setView(g)
}
func (a *App) PrevView(g *gocui.Gui, v *gocui.View) error {
a.viewIndex = (a.viewIndex - 1 + len(VIEWS)) % len(VIEWS)
return a.setView(g)
}
func (a *App) setView(g *gocui.Gui) error {
a.closePopup(g, a.currentPopup)
_, err := g.SetCurrentView(VIEWS[a.viewIndex])
return err
}
func (a *App) setViewByName(g *gocui.Gui, name string) error {
for i, v := range VIEWS {
if v == name {
a.viewIndex = i
return a.setView(g)
}
}
return fmt.Errorf("View not found")
}
func popup(g *gocui.Gui, msg string) {
var popup *gocui.View
var err error
maxX, maxY := g.Size()
if popup, err = g.SetView("popup", maxX/2-len(msg)/2-1, maxY/2-1, maxX/2+len(msg)/2+1, maxY/2+1); err != nil {
if err != gocui.ErrUnknownView {
return
}
setViewDefaults(popup)
popup.Title = "Info"
setViewTextAndCursor(popup, msg)
g.SetViewOnTop("popup")
}
}
func (a *App) SubmitRequest(g *gocui.Gui, _ *gocui.View) error {
vrb, _ := g.View("response-body")
vrb.Clear()
vrh, _ := g.View("response-headers")
vrh.Clear()
popup(g, "Sending request..")
var r *Request = &Request{}
go func(g *gocui.Gui, a *App, r *Request) error {
defer g.DeleteView("popup")
// parse url
r.Url = getViewValue(g, "url")
u, err := url.Parse(r.Url)
if err != nil {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "URL parse error: %v", err)
return nil
})
return nil
}
q, err := url.ParseQuery(strings.Replace(getViewValue(g, "get"), "\n", "&", -1))
if err != nil {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "Invalid GET parameters: %v", err)
return nil
})
return nil
}
originalQuery := u.Query()
for k, v := range q {
originalQuery.Add(k, strings.Join(v, ""))
}
u.RawQuery = originalQuery.Encode()
r.GetParams = u.RawQuery
// parse method
r.Method = getViewValue(g, "method")
// parse POST/PUT data
data := bytes.NewBufferString("")
r.Data = strings.Replace(getViewValue(g, "data"), "\n", "&", -1)
if r.Method == "POST" || r.Method == "PUT" {
data.WriteString(r.Data)
}
// create request
req, err := http.NewRequest(r.Method, u.String(), data)
if err != nil {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "Request error: %v", err)
return nil
})
return nil
}
// set headers
req.Header.Set("User-Agent", "")
r.Headers = getViewValue(g, "headers")
headers := strings.Split(r.Headers, "\n")
for _, header := range headers {
if header != "" {
header_parts := strings.SplitN(header, ": ", 2)
if len(header_parts) != 2 {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "Invalid header: %v", header)
return nil
})
return nil
}
req.Header.Set(header_parts[0], header_parts[1])
}
}
// do request
response, err := CLIENT.Do(req)
if err != nil {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "Response error: %v", err)
return nil
})
return nil
}
defer response.Body.Close()
// extract body
r.ContentType = response.Header.Get("Content-Type")
if response.Header.Get("Content-Encoding") == "gzip" {
reader, err := gzip.NewReader(response.Body)
if err == nil {
defer reader.Close()
response.Body = reader
} else {
g.Execute(func(g *gocui.Gui) error {
vrb, _ := g.View("response-body")
fmt.Fprintf(vrb, "Cannot uncompress response: %v", err)
return nil
})
return nil
}
}
bodyBytes, err := ioutil.ReadAll(response.Body)
if err == nil {
r.RawResponseBody = bodyBytes
}
// add to history
a.history = append(a.history, r)
a.historyIndex = len(a.history) - 1
// render response
g.Execute(func(g *gocui.Gui) error {
vrh, _ := g.View("response-headers")
a.PrintBody(g)
// print status code and sorted headers
hkeys := make([]string, 0, len(response.Header))
for hname, _ := range response.Header {
hkeys = append(hkeys, hname)
}
sort.Strings(hkeys)
status_color := 32
if response.StatusCode != 200 {
status_color = 31
}
header_str := fmt.Sprintf(
"\x1b[0;%dmHTTP/1.1 %v %v\x1b[0;0m\n",
status_color,
response.StatusCode,
http.StatusText(response.StatusCode),
)
for _, hname := range hkeys {
header_str += fmt.Sprintf("\x1b[0;33m%v:\x1b[0;0m %v\n", hname, strings.Join(response.Header[hname], ","))
}
fmt.Fprint(vrh, header_str)
if _, err := vrh.Line(0); err != nil {
vrh.SetOrigin(0, 0)
}
r.ResponseHeaders = header_str
return nil
})
return nil
}(g, a, r)
return nil
}
func (a *App) PrintBody(g *gocui.Gui) {
g.Execute(func(g *gocui.Gui) error {
if len(a.history) == 0 {
return nil
}
req := a.history[a.historyIndex]
if req.RawResponseBody == nil {
return nil
}
vrb, _ := g.View("response-body")
vrb.Clear()
responseBody := req.RawResponseBody
// pretty-print json
if strings.Contains(req.ContentType, "application/json") && a.config.General.FormatJSON {
formatter := jsoncolor.NewFormatter()
buf := bytes.NewBuffer(make([]byte, 0, len(req.RawResponseBody)))
err := formatter.Format(buf, req.RawResponseBody)
if err == nil {
responseBody = buf.Bytes()
}
}
is_binary := strings.Index(req.ContentType, "text") == -1 && strings.Index(req.ContentType, "application") == -1
search_text := getViewValue(g, "search")
if search_text == "" || is_binary {
vrb.Title = "Response body"
if is_binary {
vrb.Title += " [binary content]"
fmt.Fprint(vrb, hex.Dump(req.RawResponseBody))
} else {
vrb.Write(responseBody)
}
if _, err := vrb.Line(0); !a.config.General.PreserveScrollPosition || err != nil {
vrb.SetOrigin(0, 0)
}
return nil
}
vrb.SetOrigin(0, 0)
search_re, err := regexp.Compile(search_text)
if err != nil {
fmt.Fprint(vrb, "Error: invalid search regexp")
return nil
}
results := search_re.FindAll(req.RawResponseBody, 1000)
if len(results) == 0 {
vrb.Title = "No results"
fmt.Fprint(vrb, "Error: no results")
return nil
}
vrb.Title = fmt.Sprintf("%d results", len(results))
for _, result := range results {
fmt.Fprintf(vrb, "-----\n%s\n", result)
}
return nil
})
}
func parseKey(k string) (interface{}, gocui.Modifier, error) {
mod := gocui.ModNone
if strings.Index(k, "Alt") == 0 {
mod = gocui.ModAlt
k = k[3:]
}
switch len(k) {
case 0:
return 0, 0, errors.New("Empty key string")
case 1:
if mod != gocui.ModNone {
k = strings.ToLower(k)
}
return rune(k[0]), mod, nil
}
key, found := KEYS[k]
if !found {
return 0, 0, fmt.Errorf("Unknown key: %v", k)
}
return key, mod, nil
}
func (a *App) setKey(g *gocui.Gui, keyStr, commandStr, viewName string) error {
if commandStr == "" {
return nil
}
key, mod, err := parseKey(keyStr)
if err != nil {
return err
}
commandParts := strings.SplitN(commandStr, " ", 2)
command := commandParts[0]
var commandArgs string
if len(commandParts) == 2 {
commandArgs = commandParts[1]
}
keyFnGen, found := COMMANDS[command]
if !found {
return fmt.Errorf("Unknown command: %v", command)
}
keyFn := keyFnGen(commandArgs, a)
if err := g.SetKeybinding(viewName, key, mod, keyFn); err != nil {
return fmt.Errorf("Failed to set key '%v': %v", keyStr, err)
}
return nil
}
func (a *App) printViewKeybindings(v io.Writer, viewName string) {
keys, found := a.config.Keys[viewName]
if !found {
return
}
mk := make([]string, len(keys))
i := 0
for k, _ := range keys {
mk[i] = k
i++
}
sort.Strings(mk)
fmt.Fprintf(v, "\n %v\n", viewName)
for _, key := range mk {
fmt.Fprintf(v, " %-15v %v\n", key, keys[key])
}
}
func (a *App) SetKeys(g *gocui.Gui) error {
// load config keybindings
for viewName, keys := range a.config.Keys {
if viewName == "global" {
viewName = ""
}
for keyStr, commandStr := range keys {
if err := a.setKey(g, keyStr, commandStr, viewName); err != nil {
return err
}
}
}
g.SetKeybinding("", gocui.KeyF1, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if a.currentPopup == "help" {
a.closePopup(g, "help")
return nil
}
help, err := a.CreatePopupView("help", 60, 40, g)
if err != nil {
return err
}
help.Title = "Help"
help.Highlight = false
fmt.Fprint(help, "Keybindings:\n")
a.printViewKeybindings(help, "global")
for _, viewName := range VIEWS {
if _, found := a.config.Keys[viewName]; !found {
continue
}
a.printViewKeybindings(help, viewName)
}
g.SetViewOnTop("help")
g.SetCurrentView("help")
return nil
})
g.SetKeybinding("method", gocui.KeyEnter, gocui.ModNone, a.ToggleMethodlist)
cursDown := func(g *gocui.Gui, v *gocui.View) error {
cx, cy := v.Cursor()
v.SetCursor(cx, cy+1)
return nil
}
cursUp := func(g *gocui.Gui, v *gocui.View) error {
cx, cy := v.Cursor()
if cy > 0 {
cy -= 1
}
v.SetCursor(cx, cy)
return nil
}
// history keybindings
g.SetKeybinding("history", gocui.KeyArrowDown, gocui.ModNone, cursDown)
g.SetKeybinding("history", gocui.KeyArrowUp, gocui.ModNone, cursUp)
g.SetKeybinding("history", gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
_, cy := v.Cursor()
// TODO error
if len(a.history) <= cy {
return nil
}
a.restoreRequest(g, cy)
return nil
})
// method keybindings
g.SetKeybinding("method", gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
value := strings.TrimSpace(v.Buffer())
for i, val := range METHODS {
if val == value && i != len(METHODS)-1 {
setViewTextAndCursor(v, METHODS[i+1])
}
}
return nil
})
g.SetKeybinding("method", gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
value := strings.TrimSpace(v.Buffer())
for i, val := range METHODS {
if val == value && i != 0 {
setViewTextAndCursor(v, METHODS[i-1])
}
}
return nil
})
g.SetKeybinding("method-list", gocui.KeyArrowDown, gocui.ModNone, cursDown)
g.SetKeybinding("method-list", gocui.KeyArrowUp, gocui.ModNone, cursUp)
g.SetKeybinding("method-list", gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
_, cy := v.Cursor()
v, _ = g.View("method")
setViewTextAndCursor(v, METHODS[cy])
a.closePopup(g, "method-list")
return nil
})
g.SetKeybinding("save-dialog", gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
defer a.closePopup(g, "save-dialog")
saveLocation := getViewValue(g, "save-dialog")
if len(a.history) == 0 {
return nil
}
req := a.history[a.historyIndex]
if req.RawResponseBody == nil {
return nil
}
err := ioutil.WriteFile(saveLocation, req.RawResponseBody, 0644)
var saveResult string
if err == nil {
saveResult = "Response saved successfully."
} else {
saveResult = "Error saving response: " + err.Error()
}
popupTitle := "Save Result (press enter to close)"
saveResHeight := 1
saveResWidth := len(saveResult) + 1
if len(popupTitle)+2 > saveResWidth {
saveResWidth = len(popupTitle) + 2
}
maxX, _ := g.Size()
if saveResWidth > maxX {
saveResHeight = saveResWidth/maxX + 1
saveResWidth = maxX
}
saveResultPopup, err := a.CreatePopupView("save-result", saveResWidth, saveResHeight, g)
saveResultPopup.Title = popupTitle
setViewTextAndCursor(saveResultPopup, saveResult)
g.SetViewOnTop("save-result")
g.SetCurrentView("save-result")
return err
})
g.SetKeybinding("save-dialog", gocui.KeyCtrlQ, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
a.closePopup(g, "save-dialog")
return nil
})
g.SetKeybinding("save-result", gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
a.closePopup(g, "save-result")
return nil
})
return nil
}
func (a *App) closePopup(g *gocui.Gui, viewname string) {
_, err := g.View(viewname)
if err == nil {
a.currentPopup = ""
g.DeleteView(viewname)
g.SetCurrentView(VIEWS[a.viewIndex%len(VIEWS)])
g.Cursor = true
}
}
// CreatePopupView create a popup like view
func (a *App) CreatePopupView(name string, width, height int, g *gocui.Gui) (v *gocui.View, err error) {
// Remove any concurrent popup
a.closePopup(g, a.currentPopup)
g.Cursor = false
maxX, maxY := g.Size()
if height > maxY-4 {
height = maxY - 4
}
if width > maxX-4 {
width = maxX - 4
}
v, err = g.SetView(name, maxX/2-width/2-1, maxY/2-height/2-1, maxX/2+width/2, maxY/2+height/2+1)
if err != nil && err != gocui.ErrUnknownView {
return
}
err = nil
v.Wrap = false
v.Frame = true
v.Highlight = true
v.SelFgColor = gocui.ColorYellow
a.currentPopup = name
return
}
func (a *App) ToggleHistory(g *gocui.Gui, _ *gocui.View) (err error) {
// Destroy if present
if a.currentPopup == "history" {
a.closePopup(g, "history")
return
}
history, err := a.CreatePopupView("history", 100, len(a.history), g)
if err != nil {
return
}
history.Title = "History"
if len(a.history) == 0 {
setViewTextAndCursor(history, "[!] No items in history")
return
}
for i, r := range a.history {
req_str := fmt.Sprintf("[%02d] %v %v", i, r.Method, r.Url)
if r.GetParams != "" {
req_str += fmt.Sprintf("?%v", strings.Replace(r.GetParams, "\n", "&", -1))
}
if r.Data != "" {
req_str += fmt.Sprintf(" %v", strings.Replace(r.Data, "\n", "&", -1))
}
if r.Headers != "" {
req_str += fmt.Sprintf(" %v", strings.Replace(r.Headers, "\n", ";", -1))
}
fmt.Fprintln(history, req_str)
}
g.SetViewOnTop("history")
g.SetCurrentView("history")
history.SetCursor(0, a.historyIndex)
return
}
func (a *App) ToggleMethodlist(g *gocui.Gui, _ *gocui.View) (err error) {
// Destroy if present
if a.currentPopup == "method-list" {
a.closePopup(g, "method-list")
return
}
method, err := a.CreatePopupView("method-list", 50, len(METHODS), g)
if err != nil {
return
}
method.Title = "Methods"
cur := getViewValue(g, "method")
for i, r := range METHODS {
fmt.Fprintln(method, r)
if cur == r {
method.SetCursor(0, i)
}
}
g.SetViewOnTop("method-list")
g.SetCurrentView("method-list")
return
}
func (a *App) OpenSaveDialog(g *gocui.Gui, _ *gocui.View) (err error) {
dialog, err := a.CreatePopupView("save-dialog", 60, 1, g)
if err != nil {
return
}
g.Cursor = true
dialog.Title = "Save Response (enter to submit, ctrl+q to cancel)"
dialog.Editable = true
dialog.Wrap = false
currentDir, err := os.Getwd()
if err != nil {
currentDir = ""
}
currentDir += "/"
setViewTextAndCursor(dialog, currentDir)
g.SetViewOnTop("save-dialog")
g.SetCurrentView("save-dialog")
dialog.SetCursor(0, len(currentDir))
return
}
func (a *App) restoreRequest(g *gocui.Gui, idx int) {
if idx < 0 || idx >= len(a.history) {
return
}
a.closePopup(g, "history")
a.historyIndex = idx
r := a.history[idx]
v, _ := g.View("url")
setViewTextAndCursor(v, r.Url)
v, _ = g.View("method")
setViewTextAndCursor(v, r.Method)
v, _ = g.View("get")
setViewTextAndCursor(v, r.GetParams)
v, _ = g.View("data")
setViewTextAndCursor(v, r.Data)
v, _ = g.View("headers")
setViewTextAndCursor(v, r.Headers)
v, _ = g.View("response-headers")
setViewTextAndCursor(v, r.ResponseHeaders)
a.PrintBody(g)
}
func (a *App) LoadConfig(configPath string) error {
if configPath == "" {
// Load config from default path
configPath = config.GetDefaultConfigLocation()
}
// If the config file doesn't exist, load the default config
if _, err := os.Stat(configPath); os.IsNotExist(err) {
a.config = &config.DefaultConfig
a.config.Keys = config.DefaultKeys
return nil
}
conf, err := config.LoadConfig(configPath)
if err != nil {
a.config = &config.DefaultConfig
a.config.Keys = config.DefaultKeys
return err
}
a.config = conf
return nil
}
func (a *App) ParseArgs(g *gocui.Gui, args []string) error {
a.Layout(g)
g.SetCurrentView(VIEWS[a.viewIndex])
vheader, err := g.View("headers")
if err != nil {
return errors.New("Too small screen")
}
vheader.Clear()
vget, _ := g.View("get")
vget.Clear()
add_content_type := false
arg_index := 1
args_len := len(args)
for arg_index < args_len {
arg := args[arg_index]
switch arg {
case "-H", "--header":
if arg_index == args_len-1 {
return errors.New("No header value specified")
}
arg_index += 1
header := args[arg_index]
fmt.Fprintf(vheader, "%v\n", header)
case "-d", "--data":
if arg_index == args_len-1 {
return errors.New("No POST/PUT value specified")
}
vmethod, _ := g.View("method")
setViewTextAndCursor(vmethod, "POST")
arg_index += 1
add_content_type = true
data, _ := url.QueryUnescape(args[arg_index])
vdata, _ := g.View("data")
setViewTextAndCursor(vdata, data)
case "-X", "--request":
if arg_index == args_len-1 {
return errors.New("No HTTP method specified")
}
arg_index++
method := args[arg_index]
if method == "POST" || method == "PUT" {
add_content_type = true
}
vmethod, _ := g.View("method")
setViewTextAndCursor(vmethod, method)
case "-t", "--timeout":
if arg_index == args_len-1 {
return errors.New("No timeout value specified")
}
arg_index += 1
timeout, err := strconv.Atoi(args[arg_index])
if err != nil || timeout <= 0 {
return errors.New("Invalid timeout value")
}
a.config.General.Timeout = config.Duration{time.Duration(timeout) * time.Millisecond}
case "--compressed":
vh, _ := g.View("headers")
if strings.Index(getViewValue(g, "headers"), "Accept-Encoding") == -1 {
fmt.Fprintln(vh, "Accept-Encoding: gzip, deflate")
}
case "--insecure":
a.config.General.Insecure = true
default:
u := args[arg_index]
if strings.Index(u, "http://") != 0 && strings.Index(u, "https://") != 0 {
u = "http://" + u
}
parsed_url, err := url.Parse(u)
if err != nil || parsed_url.Host == "" {
return errors.New("Invalid url")
}
if parsed_url.Path == "" {
parsed_url.Path = "/"
}
vurl, _ := g.View("url")
vurl.Clear()
for k, v := range parsed_url.Query() {
fmt.Fprintf(vget, "%v=%v\n", k, strings.Join(v, ""))
}
parsed_url.RawQuery = ""
setViewTextAndCursor(vurl, parsed_url.String())
}
arg_index += 1
}
if add_content_type && strings.Index(getViewValue(g, "headers"), "Content-Type") == -1 {
setViewTextAndCursor(vheader, "Content-Type: application/x-www-form-urlencoded")
}
return nil
}
// Apply startup config values. This is run after a.ParseArgs, so that
// args can override the provided config values
func (a *App) InitConfig() {
CLIENT.Timeout = a.config.General.Timeout.Duration
TRANSPORT.TLSClientConfig = &tls.Config{InsecureSkipVerify: a.config.General.Insecure}
}
func initApp(a *App, g *gocui.Gui) {
g.Cursor = true
g.InputEsc = false
g.BgColor = gocui.ColorDefault
g.FgColor = gocui.ColorDefault
g.SetManagerFunc(a.Layout)
}
func getViewValue(g *gocui.Gui, name string) string {
v, err := g.View(name)
if err != nil {
return ""
}
return strings.TrimSpace(v.Buffer())
}
func setViewDefaults(v *gocui.View) {
v.Frame = true
v.Wrap = true
}
func setViewTextAndCursor(v *gocui.View, s string) {
v.Clear()
fmt.Fprint(v, s)
v.SetCursor(len(s), 0)
}
func help() {
fmt.Println(`wuzz - Interactive cli tool for HTTP inspection
Usage: wuzz [-H|--header HEADER]... [-d|--data POST_DATA] [-X|--request METHOD] [-t|--timeout MSECS] [URL]
Other command line options:
-c, --config PATH Specify custom configuration file
-h, --help Show this
-v, --version Display version number
Key bindings:
ctrl+r Send request
ctrl+s Save response
tab, ctrl+j Next window
shift+tab, ctrl+k Previous window
ctrl+h, alt+h Show history
pageUp Scroll up the current window
pageDown Scroll down the current window`,
)
}
func main() {
configPath := ""
args := os.Args
for i, arg := range os.Args {
switch arg {
case "-h", "--help":
help()
return
case "-v", "--version":
fmt.Printf("wuzz %v\n", VERSION)
return
case "-c", "--config":
configPath = os.Args[i+1]
args = append(os.Args[:i], os.Args[i+2:]...)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Fatal("Config file specified but does not exist: \"" + configPath + "\"")
}
}
}
g, err := gocui.NewGui(gocui.Output256)
if err != nil {
log.Panicln(err)
}
if runtime.GOOS == "windows" && runewidth.IsEastAsian() {
g.ASCII = true
}
app := &App{history: make([]*Request, 0, 31)}
// overwrite default editor
defaultEditor = ViewEditor{app, g, false, gocui.DefaultEditor}
initApp(app, g)
// load config (must be done *before* app.ParseArgs, as arguments
// should be able to override config values). An empty string passed
// to LoadConfig results in LoadConfig loading the default config
// location. If there is no config, the values in
// config.DefaultConfig will be used.
err = app.LoadConfig(configPath)
if err != nil {
g.Close()
log.Fatalf("Error loading config file: %v", err)
}
err = app.ParseArgs(g, args)
// Some of the values in the config need to have some startup
// behavior associated with them. This is run after ParseArgs so
// that command-line arguments can override configuration values.
app.InitConfig()
if err != nil {
g.Close()
fmt.Println("Error!", err)
os.Exit(1)
}
err = app.SetKeys(g)
if err != nil {
g.Close()
fmt.Println("Error!", err)
os.Exit(1)
}
defer g.Close()
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}