mirror of https://github.com/gabehf/sp9rk.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
533 lines
14 KiB
533 lines
14 KiB
package action
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func CreateApplication(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() != 1 {
|
|
return errors.New("create app must have exactly one argument")
|
|
}
|
|
if !valid(ctx.Args().Get(0)) {
|
|
return errors.New("application name must only contain letters, numbers, dashes and underscores")
|
|
}
|
|
if _, err := os.Stat(AppPath(cfgPath, ctx.Args().Get(0))); err == nil {
|
|
return errors.New("application already exists")
|
|
}
|
|
err := WriteAppFiles(cfgPath, &AppInfo{
|
|
Version: "1",
|
|
Name: ctx.Args().Get(0),
|
|
Description: ctx.String("description"),
|
|
Host: ctx.String("host"),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Created application %s\n", ctx.Args().Get(0))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func CreateRequest(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
|
return errors.New("create req must have exactly one argument")
|
|
}
|
|
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqName := ctx.Args().Get(0)
|
|
|
|
if !valid(app) || !appExists(cfgPath, app) {
|
|
return errors.New("application does not exist")
|
|
}
|
|
if !valid(reqName) {
|
|
return errors.New("request name must only contain letters, numbers, dashes and underscores")
|
|
}
|
|
|
|
path := ReqPath(cfgPath, app, reqName)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return errors.New("request already exists")
|
|
}
|
|
|
|
if err := WriteRequestFiles(cfgPath, app, &RequestInfo{
|
|
Version: "1",
|
|
Name: reqName,
|
|
Description: ctx.String("description"),
|
|
Method: ctx.String("method"),
|
|
Path: ctx.String("path"),
|
|
Headers: ctx.StringSlice("header"),
|
|
Body: ctx.String("body"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Created request %s\n", reqName)
|
|
return nil
|
|
}
|
|
}
|
|
func EditApplication(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() != 1 {
|
|
return errors.New("edit app must have exactly one argument")
|
|
}
|
|
if !valid(ctx.Args().Get(0)) {
|
|
return errors.New("application name is invalid")
|
|
}
|
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, ctx.Args().Get(0)))
|
|
if err != nil {
|
|
return errors.New("application does not exist")
|
|
}
|
|
appinfo := new(AppInfo)
|
|
err = yaml.Unmarshal(contents, appinfo)
|
|
if err != nil {
|
|
return errors.New("failed to read application info")
|
|
}
|
|
if ctx.String("description") != "" {
|
|
appinfo.Description = ctx.String("description")
|
|
}
|
|
if ctx.String("host") != "" {
|
|
appinfo.Host = ctx.String("host")
|
|
}
|
|
err = WriteAppFiles(cfgPath, appinfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func EditRequest(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
|
return errors.New("create req must have exactly one argument")
|
|
}
|
|
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqName := ctx.Args().Get(0)
|
|
|
|
if !valid(app) || !appExists(cfgPath, app) {
|
|
return errors.New("application does not exist")
|
|
}
|
|
if !valid(reqName) {
|
|
return errors.New("request name is invalid")
|
|
}
|
|
|
|
path := ReqPath(cfgPath, app, reqName)
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return errors.New("request does not exists")
|
|
}
|
|
reqinfo := new(RequestInfo)
|
|
err = yaml.Unmarshal(contents, reqinfo)
|
|
if err != nil {
|
|
return errors.New("request info is malformed or corrupted")
|
|
}
|
|
|
|
if ctx.String("description") != "" {
|
|
reqinfo.Description = ctx.String("description")
|
|
}
|
|
if ctx.String("method") != "" {
|
|
reqinfo.Method = ctx.String("method")
|
|
}
|
|
if ctx.String("path") != "" {
|
|
reqinfo.Path = ctx.String("path")
|
|
}
|
|
if ctx.String("body") != "" {
|
|
reqinfo.Body = ctx.String("body")
|
|
}
|
|
if ctx.StringSlice("header") != nil {
|
|
reqinfo.Headers = ctx.StringSlice("header")
|
|
}
|
|
|
|
if err := WriteRequestFiles(cfgPath, app, reqinfo); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func DeleteApplication(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
|
return errors.New("delete app must have exactly one argument")
|
|
}
|
|
|
|
app := ctx.Args().Get(0)
|
|
|
|
if !valid(app) {
|
|
return errors.New("application name is invalid")
|
|
}
|
|
|
|
_path := AppPath(cfgPath, app)
|
|
|
|
files, err := os.ReadDir(_path)
|
|
if err != nil {
|
|
return errors.New("application does not exist")
|
|
}
|
|
fmt.Printf(
|
|
"You are about to delete the application %s and %d associated request(s).\nThis action cannot be undone.\n",
|
|
app,
|
|
len(files)-1, // -1 because .appinfo isnt a req
|
|
)
|
|
if ctx.Bool("confirm") || ConfirmPrompt() {
|
|
err := os.RemoveAll(_path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print("application " + app + " has been deleted")
|
|
return nil
|
|
}
|
|
fmt.Println("delete aborted")
|
|
return nil
|
|
}
|
|
}
|
|
func DeleteRequest(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
|
|
if ctx.NArg() < 1 || ctx.NArg() > 1 {
|
|
return errors.New("delete request must have exactly one argument")
|
|
}
|
|
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqName := ctx.Args().Get(0)
|
|
|
|
if !valid(reqName) {
|
|
return errors.New("request name is invalid")
|
|
}
|
|
if !valid(app) || !appExists(cfgPath, app) {
|
|
return errors.New("application does not exist")
|
|
}
|
|
|
|
_path := ReqPath(cfgPath, app, reqName)
|
|
|
|
if _, err := os.Stat(_path); err != nil {
|
|
return errors.New("request does not exist")
|
|
}
|
|
|
|
fmt.Printf(
|
|
"You are about to delete the request %s in application %s.\nThis action cannot be undone.\n",
|
|
reqName,
|
|
app,
|
|
)
|
|
if ctx.Bool("confirm") || ConfirmPrompt() {
|
|
err := os.Remove(_path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print("request " + reqName + " has been deleted")
|
|
return nil
|
|
}
|
|
fmt.Println("delete aborted")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Switch(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 {
|
|
return errors.New("expected argument")
|
|
}
|
|
if ctx.NArg() > 1 {
|
|
return errors.New("expected exactly one argument")
|
|
}
|
|
app := ctx.Args().Get(0)
|
|
if !valid(app) {
|
|
return errors.New("application does not exist")
|
|
}
|
|
if _, err := os.Stat(AppPath(cfgPath, app)); err != nil {
|
|
return errors.New("application does not exist")
|
|
}
|
|
err := os.WriteFile(path.Join(cfgPath, "current_app"), []byte(ctx.Args().First()), 0700)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print(app)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ListApplications(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
_path := AppPath(cfgPath, "")
|
|
dirs, err := os.ReadDir(_path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var apps string
|
|
for _, dir := range dirs {
|
|
appinfo := new(AppInfo)
|
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, dir.Name()))
|
|
if err != nil {
|
|
return errors.New("failed to read application info")
|
|
}
|
|
err = yaml.Unmarshal(contents, appinfo)
|
|
if err != nil {
|
|
return errors.New(".appinfo is malformed or corrupted")
|
|
}
|
|
if appinfo.Description == "" {
|
|
apps += appinfo.Name + "\n"
|
|
} else {
|
|
apps += appinfo.Name + ": " + appinfo.Description + "\n"
|
|
}
|
|
}
|
|
fmt.Print(apps)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ListRequests(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_path := AppPath(cfgPath, app)
|
|
files, err := os.ReadDir(_path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reqs := ""
|
|
for _, file := range files {
|
|
if file.Name() == ".appinfo" {
|
|
continue
|
|
}
|
|
reqinfo := new(RequestInfo)
|
|
contents, err := os.ReadFile(path.Join(AppPath(cfgPath, app), file.Name()))
|
|
if err != nil {
|
|
return errors.New("failed to read request info")
|
|
}
|
|
err = yaml.Unmarshal(contents, reqinfo)
|
|
if err != nil {
|
|
return errors.New("request file is malformed or corrupted")
|
|
}
|
|
if reqinfo.Description == "" {
|
|
reqs += reqinfo.Name + "\n"
|
|
} else {
|
|
reqs += reqinfo.Name + ": " + reqinfo.Description + "\n"
|
|
}
|
|
}
|
|
fmt.Print(reqs)
|
|
return nil
|
|
}
|
|
}
|
|
func ListAll(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
apps, err := os.ReadDir(AppPath(cfgPath, ""))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output := ""
|
|
if len(apps) < 1 {
|
|
fmt.Println("No applications or requests")
|
|
return nil
|
|
}
|
|
for _, app := range apps {
|
|
reqfiles, err := os.ReadDir(AppPath(cfgPath, app.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(reqfiles) <= 1 {
|
|
output += app.Name() + "\n"
|
|
} else {
|
|
output += app.Name() + ":\n"
|
|
}
|
|
for _, reqfile := range reqfiles {
|
|
if reqfile.Name() == ".appinfo" {
|
|
continue
|
|
}
|
|
reqinfo := new(RequestInfo)
|
|
contents, err := os.ReadFile(path.Join(AppPath(cfgPath, app.Name()), reqfile.Name()))
|
|
if err != nil {
|
|
return errors.New("failed to read request info")
|
|
}
|
|
err = yaml.Unmarshal(contents, reqinfo)
|
|
if err != nil {
|
|
return errors.New("request file is malformed or corrupted")
|
|
}
|
|
if reqinfo.Description == "" {
|
|
output += "\t- " + reqinfo.Name + "\n"
|
|
} else {
|
|
output += "\t- " + reqinfo.Name + ": " + reqinfo.Description + "\n"
|
|
}
|
|
}
|
|
}
|
|
fmt.Print(output)
|
|
return nil
|
|
}
|
|
}
|
|
func InfoApplication(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 {
|
|
return errors.New("expected argument")
|
|
}
|
|
if ctx.NArg() > 1 {
|
|
return errors.New("expected exactly one argument")
|
|
}
|
|
if !valid(ctx.Args().Get(0)) {
|
|
return errors.New("application name is invalid")
|
|
}
|
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, ctx.Args().Get(0)))
|
|
if err != nil {
|
|
return errors.New("failed to read app info")
|
|
}
|
|
appinfo := new(AppInfo)
|
|
err = yaml.Unmarshal(contents, appinfo)
|
|
if err != nil {
|
|
return errors.New("failed to read app info")
|
|
}
|
|
fmt.Printf("%s:\n\tDescription: %s\n\tHost: %s\n", appinfo.Name, appinfo.Description, appinfo.Host)
|
|
return nil
|
|
}
|
|
}
|
|
func InfoRequest(cfgPath string) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 {
|
|
return errors.New("expected argument")
|
|
}
|
|
if ctx.NArg() > 1 {
|
|
return errors.New("expected exactly one argument")
|
|
}
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !valid(app) {
|
|
return errors.New("application name is invalid")
|
|
}
|
|
if !valid(ctx.Args().Get(0)) {
|
|
return errors.New("request name is invalid")
|
|
}
|
|
contents, err := os.ReadFile(ReqPath(cfgPath, app, ctx.Args().Get(0)))
|
|
if err != nil {
|
|
return errors.New("failed to read request info")
|
|
}
|
|
fileLines := strings.Split(string(contents), "\n")
|
|
fmt.Println(ctx.Args().Get(0) + ":\n\t" + strings.Join(fileLines[2:len(fileLines)-1], "\n\t"))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type VerboseCallResponse struct {
|
|
Request string `yaml:"Request"`
|
|
RequestBody string `yaml:"RequestBody"`
|
|
Headers map[string]string `yaml:"Headers"`
|
|
Latency string `yaml:"Latency"`
|
|
Status string `yaml:"Status"`
|
|
ResponseBody string `yaml:"ResponseBody"`
|
|
}
|
|
|
|
func Call(cfgPath string, httpClient http.Client) func(ctx *cli.Context) error {
|
|
return func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 1 {
|
|
return errors.New("expected argument")
|
|
}
|
|
if ctx.NArg() > 1 {
|
|
return errors.New("expected exactly one argument")
|
|
}
|
|
app, err := getApp(cfgPath, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !valid(app) {
|
|
return errors.New("application name is invalid")
|
|
}
|
|
if !valid(ctx.Args().Get(0)) {
|
|
return errors.New("request name is invalid")
|
|
}
|
|
// retrieve app information
|
|
contents, err := os.ReadFile(AppInfoFilePath(cfgPath, app))
|
|
if err != nil {
|
|
return errors.New("failed to retrieve app information")
|
|
}
|
|
appinfo := new(AppInfo)
|
|
err = yaml.Unmarshal(contents, appinfo)
|
|
if err != nil {
|
|
return errors.New("appinfo file is malformed or corrupted")
|
|
}
|
|
// retrieve request information
|
|
contents, err = os.ReadFile(ReqPath(cfgPath, app, ctx.Args().Get(0)))
|
|
if err != nil {
|
|
return errors.New("failed to retrieve request information")
|
|
}
|
|
reqinfo := new(RequestInfo)
|
|
err = yaml.Unmarshal(contents, reqinfo)
|
|
if err != nil {
|
|
return errors.New("request file is malformed or corrupted")
|
|
}
|
|
body := bytes.NewBuffer([]byte(reqinfo.Body))
|
|
req, err := http.NewRequest(reqinfo.Method, appinfo.Host+reqinfo.Path, body)
|
|
if err != nil {
|
|
return errors.New("failed to create web request")
|
|
}
|
|
for _, header := range reqinfo.Headers {
|
|
h := strings.Split(header, ": ")
|
|
if len(h) < 2 {
|
|
return errors.New("malformed header(s)")
|
|
}
|
|
req.Header.Add(h[0], h[1])
|
|
}
|
|
if ctx.Bool("no-redirect") {
|
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
|
|
}
|
|
t1 := time.Now()
|
|
resp, err := httpClient.Do(req)
|
|
t2 := time.Now()
|
|
if err != nil {
|
|
return errors.New("failed to send web request")
|
|
}
|
|
defer resp.Body.Close()
|
|
if ctx.Bool("fail") && resp.StatusCode >= 400 {
|
|
return errors.New("")
|
|
}
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return errors.New("failed to read response body")
|
|
}
|
|
if !ctx.Bool("verbose") {
|
|
fmt.Print(string(respBody) + "\n")
|
|
} else if req.Method == "GET" {
|
|
var out VerboseCallResponse
|
|
out.Request = req.Method + " " + req.URL.String()
|
|
out.RequestBody = reqinfo.Body
|
|
out.Headers = make(map[string]string)
|
|
for _, header := range reqinfo.Headers {
|
|
k := strings.Split(header, ": ")[0]
|
|
v := strings.Split(header, ": ")[1]
|
|
out.Headers[k] = v
|
|
}
|
|
out.Latency = fmt.Sprintf("%v", t2.Sub(t1))
|
|
out.Status = resp.Status
|
|
out.ResponseBody = string(respBody)
|
|
o, err := yaml.Marshal(out)
|
|
if err != nil {
|
|
return errors.New("failed to generate command output")
|
|
}
|
|
fmt.Print(string(o))
|
|
}
|
|
return nil
|
|
}
|
|
}
|