├── http-client.png ├── .gitignore ├── go.mod ├── internal ├── httpmethods │ ├── execute.go │ └── client.go └── utilities │ └── input_util.go ├── main.go ├── LICENSE ├── utils.go ├── README.md └── go.sum /http-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knbr13/http-client/HEAD/http-client.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build file: 2 | *.exe 3 | *.o 4 | *.out 5 | 6 | *.app 7 | 8 | *.elf 9 | 10 | http-client 11 | 12 | # Dependeny Directory 13 | vendor/ -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knbr13/http-client 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.4.3 7 | github.com/gookit/color v1.5.3 8 | ) 9 | 10 | require ( 11 | github.com/alexflint/go-scalar v1.2.0 // indirect 12 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 13 | golang.org/x/sys v0.6.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /internal/httpmethods/execute.go: -------------------------------------------------------------------------------- 1 | package httpmethods 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/knbr13/http-client/internal/utilities" 8 | ) 9 | 10 | func exec(input Input) (*http.Response, error) { 11 | reqBody, err := utilities.ParseBody(input.Body) 12 | if err != nil { 13 | return nil, err 14 | } 15 | req, err := http.NewRequest(input.HTTPMethod, input.URL, bytes.NewReader(reqBody)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | headers, err := utilities.ParseHeaders(input.Header) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | for k, v := range headers { 26 | req.Header.Set(k, v) 27 | } 28 | 29 | resp, err := httpClient.Do(req) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return resp, err 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "strings" 7 | 8 | "github.com/alexflint/go-arg" 9 | 10 | "github.com/knbr13/http-client/internal/httpmethods" 11 | ) 12 | 13 | func main() { 14 | input := httpmethods.Input{} 15 | arg.MustParse(&input) 16 | 17 | httpResponse, err := httpmethods.RunHttpMethod(input) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | defer httpResponse.Body.Close() 22 | 23 | switch strings.ToLower(input.HTTPMethod) { 24 | case "head": 25 | // For HEAD request, only print the response status 26 | printColoredHeaders(httpResponse.Header) 27 | default: 28 | // For other requests, print the response body 29 | body, err := io.ReadAll(httpResponse.Body) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | printColoredBody(body) 34 | if input.Output != "" { 35 | err = writeToFile(input.Output, body) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/httpmethods/client.go: -------------------------------------------------------------------------------- 1 | package httpmethods 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "slices" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type Command struct { 12 | Name string 13 | Short string 14 | Run func(Input) (*http.Response, error) 15 | } 16 | 17 | var AvailableHttpMethods = []string{ 18 | "GET", 19 | "POST", 20 | "PUT", 21 | "PATCH", 22 | "DELETE", 23 | "HEAD", 24 | "OPTIONS", 25 | } 26 | 27 | type Input struct { 28 | HTTPMethod string `arg:"-m,--http-method,required"` 29 | URL string `arg:"-u,--url,required"` 30 | Body string `arg:"-b,--body"` 31 | Header string `arg:"-H,--header"` 32 | Output string `arg:"-o"` 33 | } 34 | 35 | var httpClient *http.Client = &http.Client{ 36 | Timeout: 30 * time.Second, 37 | } 38 | 39 | func RunHttpMethod(input Input) (*http.Response, error) { 40 | 41 | if ok := slices.Contains(AvailableHttpMethods, strings.ToUpper(input.HTTPMethod)); !ok { 42 | return nil, fmt.Errorf("unknown http method: %v", input.HTTPMethod) 43 | } 44 | 45 | return exec(input) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 knbr13 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/utilities/input_util.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ParseHeaders parses the input headers string into a map. 10 | func ParseHeaders(headersStr string) (map[string]string, error) { 11 | headers := make(map[string]string) 12 | headersStr = strings.TrimSpace(headersStr) 13 | 14 | if len(headersStr) == 0 { 15 | return headers, nil 16 | } 17 | 18 | var headerMap map[string]string 19 | err := json.Unmarshal([]byte(headersStr), &headerMap) 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to parse headers: %v", err) 22 | } 23 | 24 | return headerMap, nil 25 | } 26 | 27 | // ParseBody parses the input body string into a JSON byte slice. 28 | func ParseBody(bodyStr string) ([]byte, error) { 29 | bodyStr = strings.TrimSpace(bodyStr) 30 | 31 | if len(bodyStr) == 0 { 32 | return []byte{}, nil 33 | } 34 | 35 | if !strings.HasPrefix(bodyStr, "{") || !strings.HasSuffix(bodyStr, "}") { 36 | return nil, fmt.Errorf("invalid body format: %s", bodyStr) 37 | } 38 | 39 | bodyStr = strings.TrimPrefix(bodyStr, "{") 40 | bodyStr = strings.TrimSuffix(bodyStr, "}") 41 | 42 | keyValuePairs := strings.Split(bodyStr, ",") 43 | 44 | data := make(map[string]interface{}) 45 | 46 | for _, kvPair := range keyValuePairs { 47 | parts := strings.Split(kvPair, ":") 48 | if len(parts) != 2 { 49 | return nil, fmt.Errorf("invalid body format") 50 | } 51 | 52 | key := strings.TrimSpace(parts[0]) 53 | value := strings.TrimSpace(parts[1]) 54 | 55 | data[key] = value 56 | } 57 | 58 | jsonData, err := json.Marshal(data) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | return jsonData, nil 64 | } 65 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/gookit/color" 11 | ) 12 | 13 | func printColoredHeaders(headers map[string][]string) { 14 | for key, values := range headers { 15 | for _, value := range values { 16 | color.Bold.Printf("%s: ", key) 17 | color.Cyan.Printf("%s\n", value) 18 | } 19 | } 20 | } 21 | 22 | func printColoredBody(body []byte) { 23 | // Try parsing the body as a single JSON object 24 | var data map[string]interface{} 25 | err := json.Unmarshal(body, &data) 26 | if err == nil { 27 | printColoredJSONObject(data) 28 | return 29 | } 30 | 31 | // Try parsing the body as an array of JSON objects 32 | var arrayData []map[string]interface{} 33 | err = json.Unmarshal(body, &arrayData) 34 | if err == nil { 35 | for _, obj := range arrayData { 36 | printColoredJSONObject(obj) 37 | fmt.Println() 38 | } 39 | return 40 | } 41 | 42 | // If parsing fails, print the raw body as plain text 43 | fmt.Println(string(body)) 44 | } 45 | 46 | func printColoredJSONObject(obj map[string]interface{}) { 47 | keyColor := color.New(color.FgLightGreen) 48 | valueColor := color.New(color.FgLightCyan) 49 | 50 | for key, value := range obj { 51 | keyColor.Printf("%s: ", key) 52 | valueColor.Println(value) 53 | } 54 | } 55 | 56 | func writeToFile(fileName string, data []byte) error { 57 | if fileName == "" { 58 | return errors.New("missing file name") 59 | } 60 | 61 | file, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | defer file.Close() 67 | 68 | dataWriter := bufio.NewWriter(file) 69 | 70 | _, err = dataWriter.WriteString(string(data)) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | color.Greenp("Saved to ", fileName, "\n") 76 | dataWriter.Flush() 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Client 2 | 3 | HTTP Client is a command-line tool for executing HTTP requests. It supports various HTTP methods such as GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. 4 | 5 | ## Screenshots: 6 | 7 | ![http-client](./http-client.png) 8 | 9 | ## Installation 10 | 11 | 1. Clone the repository: 12 | 13 | ```shell 14 | git clone https://github.com/knbr13/http-client.git 15 | ``` 16 | 2. Build the project: 17 | ```shell 18 | cd http-client 19 | go build 20 | ``` 21 | ## Usage 22 | 23 | The general format of the command is: 24 | ```shell 25 | ./http-client -m [HTTP_Method] -b [body] -h [headers] -u [URL] -o [OUTPUT] 26 | ``` 27 | ## Examples 28 | 29 | 1- Send an HTTP GET request: 30 | 31 | ```bash 32 | ./http-client -m GET --url http://example.com 33 | ``` 34 | 35 | 2- Send an HTTP POST request: 36 | ```bash 37 | ./http-client -m POST -u http://example.com -b '{"key1","value","key2":"value"}' 38 | ``` 39 | 40 | 3- Send an HTTP DELETE request: 41 | 42 | ```bash 43 | ./http-client -m DELETE -u http://example.com 44 | ``` 45 | 46 | 4- Send an HTTP GET request and extract results to output file: 47 | 48 | ```bash 49 | ./http-client -m GET --url http://example.com -o data.json 50 | ``` 51 | 52 | 5- Run the tool with `--help` arg to get a helpful message explaining how to use this tool. 53 | 54 | 55 | ## Contributions 56 | 57 | Contributions are welcome! If you would like to contribute to this project, please follow these steps: 58 | 59 | 1- Fork the repository. 60 | 61 | 2- Create a new branch for your feature or bug fix. 62 | 63 | 3- Make the necessary changes and commit them. 64 | 65 | 4- Push your changes to your fork. 66 | 67 | 5- Submit a pull request describing your changes. 68 | 69 | ## License 70 | 71 | This project is licensed under the [MIT License](https://github.com/knbr13/http-client/blob/main/LICENSE). See the [LICENSE](https://github.com/knbr13/http-client/blob/main/LICENSE) file for details. 72 | 73 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= 2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= 3 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 4 | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= 5 | github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= 10 | github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 17 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 18 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 19 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 20 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | --------------------------------------------------------------------------------