cmd/hub/api/http.go (339 lines of code) (raw):

// Copyright (c) 2022 EPAM Systems, Inc. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //go:build api package api import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/url" "os" "strings" "time" gosocketio "github.com/arkadijs/golang-socketio" gosocketiotransport "github.com/arkadijs/golang-socketio/transport" "github.com/gorilla/websocket" "github.com/epam/hubctl/cmd/hub/config" "github.com/epam/hubctl/cmd/hub/util" ) const ( requestsBindata = "cmd/hub/api/requests" socketIoResource = "hub/socket.io/" ) var ( MethodsWithJsonBody = []string{"POST", "PUT", "PATCH"} error404 = errors.New("404 HTTP") _hubApi *http.Client _hubApiLongWait *http.Client //lint:ignore U1000 still needed? wsApi = &websocket.Dialer{HandshakeTimeout: 10 * time.Second} ) func Invoke(method, path string, body io.Reader) { method = strings.ToUpper(method) path = strings.TrimPrefix(path, "/") code, resp, err := doWithAuthorization(hubApi(), method, path, body, nil) if err != nil { log.Fatalf("%v", err) } if config.Debug { log.Printf("HTTP %d", code) } if config.Verbose { os.Stdout.Write(resp) } if code >= 300 { os.Exit(code) } } func hubApi() *http.Client { if _hubApi == nil { if config.Trace { log.Printf("API timeout: %ds", config.ApiTimeout) } _hubApi = util.RobustHttpClient(time.Duration(config.ApiTimeout)*time.Second, false) } return _hubApi } func hubApiLongWait() *http.Client { if _hubApiLongWait == nil { if config.Trace { log.Printf("API long timeout: %ds", config.ApiTimeout) } _hubApiLongWait = util.RobustHttpClient(time.Duration(config.ApiTimeout)*time.Second, false) } return _hubApiLongWait } func hubRequestWithBearerToken(method, path string, body io.Reader) (*http.Request, error) { return hubRequest(method, path, bearerToken(), body) } func hubRequest(method, path, token string, body io.Reader) (*http.Request, error) { addr := fmt.Sprintf("%s/%s", config.ApiBaseUrl, path) if config.Trace { log.Printf(">>> %s %s", method, addr) } req, err := http.NewRequest(method, addr, body) if err != nil { return nil, err } if token != "" { req.Header.Add("Authorization", "Bearer "+token) } if body != nil && util.Contains(MethodsWithJsonBody, method) { req.Header.Add("Content-type", "application/json") } return req, nil } //lint:ignore U1000 still needed? func hubWs() (*websocket.Conn, *http.Response, error) { if !strings.HasPrefix(config.ApiBaseUrl, "http://") && !strings.HasPrefix(config.ApiBaseUrl, "https://") { log.Fatalf("Unable to construct Hub WebSocket URL from `%s`", config.ApiBaseUrl) } token := bearerToken() addr := fmt.Sprintf("%s/%s?accessToken=%s&EIO=3&transport=websocket", "ws"+config.ApiBaseUrl[4:], socketIoResource, url.QueryEscape(token)) if config.Trace { log.Printf(">>> WS %s", addr) } ws, resp, err := wsApi.Dial(addr, nil) if config.Trace { if resp != nil { log.Printf("<<< WS %s", resp.Status) } } return ws, resp, err } func hubWsSocketIo(connect, disconnect, ex func()) (*gosocketio.Client, error) { if !strings.HasPrefix(config.ApiBaseUrl, "http://") && !strings.HasPrefix(config.ApiBaseUrl, "https://") { log.Fatalf("Unable to construct Hub WebSocket URL from `%s`", config.ApiBaseUrl) } token := bearerToken() addr := fmt.Sprintf("%s/%s?accessToken=%s&EIO=3&transport=websocket", "ws"+config.ApiBaseUrl[4:], socketIoResource, url.QueryEscape(token)) if config.Trace { log.Printf(">>> WS %s", addr) } transport := gosocketiotransport.GetDefaultWebsocketTransport() transport.PingInterval = 25 * time.Second transport.PingTimeout = 5 * time.Second ws, err := gosocketio.Dial(addr, transport) if err != nil { return ws, err } err = ws.On(gosocketio.OnError, func(ch *gosocketio.Channel) { if config.Debug { log.Print("Hub WebSocket error") } if ex != nil { ex() } }) if err != nil { return nil, err } if config.Verbose { err = ws.On(gosocketio.OnConnection, func(ch *gosocketio.Channel) { if config.Debug { log.Print("Hub WebSocket connected") } if connect != nil { connect() } }) if err != nil { return nil, err } err = ws.On(gosocketio.OnDisconnection, func(ch *gosocketio.Channel) { if config.Debug { log.Print("Hub WebSocket disconnected") } if disconnect != nil { disconnect() } }) if err != nil { return nil, err } } return ws, err } func get(client *http.Client, path string, jsResp interface{}) (int, error) { code, _, err := doWithAuthorization(client, "GET", path, nil, jsResp) return code, err } func get2(client *http.Client, path string) (int, []byte, error) { return doWithAuthorization(client, "GET", path, nil, nil) } func delete(client *http.Client, path string) (int, error) { code, _, err := doWithAuthorization(client, "DELETE", path, nil, nil) return code, err } func post(client *http.Client, path string, req interface{}, jsResp interface{}) (int, error) { return ppp(client, "POST", path, req, jsResp) } //lint:ignore U1000 still needed? func put(client *http.Client, path string, req interface{}, jsResp interface{}) (int, error) { return ppp(client, "PUT", path, req, jsResp) } func patch(client *http.Client, path string, req interface{}, jsResp interface{}) (int, error) { code, err := ppp(client, "PATCH", path, req, jsResp) return code, err } func ppp(client *http.Client, method, path string, req interface{}, jsResp interface{}) (int, error) { var body io.Reader if req != nil { reqBody, err := json.Marshal(req) if err != nil { return 0, err } if config.Trace { addr := fmt.Sprintf("%s/%s", config.ApiBaseUrl, path) log.Printf(">>> %s %s\n%s", method, addr, identJson(reqBody)) } body = bytes.NewReader(reqBody) } code, _, err := doWithAuthorization(client, method, path, body, jsResp) return code, err } // post2() trace no request body as it come from CLI user on stdin func post2(client *http.Client, path string, req io.Reader, jsResp interface{}) (int, error) { code, _, err := doWithAuthorization(client, "POST", path, req, jsResp) return code, err } func patch2(client *http.Client, path string, req io.Reader, jsResp interface{}) (int, error) { code, _, err := doWithAuthorization(client, "PATCH", path, req, jsResp) return code, err } func doWithAuthorization(client *http.Client, method, path string, reqBody io.Reader, jsResp interface{}) (int, []byte, error) { req, err := hubRequestWithBearerToken(method, path, reqBody) if err != nil { return 0, nil, fmt.Errorf("Error creating HTTP request: %v", err) } if reqBody != nil && util.Contains(MethodsWithJsonBody, method) { req.Header.Add("Content-type", "application/json") } return do(client, req, jsResp) } func do(client *http.Client, req *http.Request, jsResp interface{}) (int, []byte, error) { resp, err := client.Do(req) if err != nil { return 0, nil, fmt.Errorf("Error during HTTP request: %v", err) } respBody := func() ([]byte, int64, error) { var body bytes.Buffer read, err := body.ReadFrom(resp.Body) resp.Body.Close() bResp := body.Bytes() if config.Trace { pretty := indentIfJson(resp, bResp) if pretty != "" { log.Printf("<<<\n%s", pretty) } } return bResp, read, err } if config.Trace { log.Printf("<<< %s %s: %s", req.Method, req.URL.String(), resp.Status) } if resp.StatusCode == 404 { return resp.StatusCode, nil, error404 } if resp.StatusCode >= 300 { b, _, _ := respBody() maybeErrors := decodeIfApiErrorsAndVerbose(resp, b) maybeJson := "" if maybeErrors == "" { maybeJson = indentIfJsonAndDebug(resp, b) } return resp.StatusCode, nil, fmt.Errorf("%d HTTP%s%s", resp.StatusCode, maybeErrors, maybeJson) } body, read, err := respBody() if err != nil || (read < 2 && resp.StatusCode != 202 && resp.StatusCode != 204 && jsResp != nil) { return resp.StatusCode, body, fmt.Errorf("%d HTTP, error reading response (read %d bytes): %s%s", resp.StatusCode, read, util.Errors2(err), indentIfJsonAndDebug(resp, body)) } if jsResp != nil && read >= 2 { err = json.Unmarshal(body, jsResp) if err != nil { return resp.StatusCode, body, fmt.Errorf("%d HTTP, error unmarshalling response (read %d bytes): %v%s", resp.StatusCode, read, err, indentIfJsonAndDebug(resp, body)) } } return resp.StatusCode, body, nil } func identJson(in []byte) []byte { js := in var pretty bytes.Buffer err := json.Indent(&pretty, in, "", " ") if err != nil { log.Printf("Unable to indent JSON: %v", err) } else { js = pretty.Bytes() } return js } func indentIfJson(resp *http.Response, in []byte) string { ct := resp.Header.Get("Content-type") aj := "application/json" js := in if strings.HasPrefix(ct, aj) { js = identJson(in) } else { if config.Trace && resp.StatusCode != 204 { log.Printf("Response Content-type is not `%s`, but `%s`", aj, ct) } } return string(js) } func indentIfJsonAndDebug(resp *http.Response, b []byte) string { if !config.Debug { return "" } return "\n" + indentIfJson(resp, b) } func decodeIfApiErrorsAndVerbose(resp *http.Response, b []byte) string { if !config.Verbose { return "" } return "\n\t" + strings.Join(unmarshalAndDecodeApiErrors(resp, b), "\n\t") } func unmarshalAndDecodeApiErrors(resp *http.Response, in []byte) []string { ct := resp.Header.Get("Content-type") aj := "application/json" if !strings.HasPrefix(ct, aj) { return nil } var js ApiErrors err := json.Unmarshal(in, &js) if err != nil { if config.Verbose { log.Printf("Error response doesn't look like informative API error: %v", err) } return nil } return decodeApiErrors(js.Errors, "") } func decodeApiErrors(es []ApiError, indent string) []string { errs := make([]string, 0, len(es)) for _, e := range es { errs = append(errs, decodeApiError(e, indent)) } return errs } func decodeApiError(e ApiError, indent string) string { source := "" if e.Source != "" { source = fmt.Sprintf(" at %s", e.Source) } meta := e.Meta nested := "" if len(meta.Data.Errors) > 0 { nested = fmt.Sprintf("\n%s\tNested API error:\n%s\t%s", indent, indent, strings.Join(decodeApiErrors(meta.Data.Errors, indent+"\t"), "\n\t"+indent)) } validation := "" if meta.SchemaPath != "" { validation = fmt.Sprintf("\n%s\t\tValidation: %s %+v", indent, meta.Message, meta.Params) } stack := "" if meta.Stack != "" && config.Debug { frames := strings.Split(meta.Stack, "\n") stack = fmt.Sprintf("\n%s\tStack: %s", indent, strings.Join(frames, "\n\t"+indent)) } return fmt.Sprintf("%s%s%s: %s%s%s%s", indent, e.Type, source, e.Detail, stack, validation, nested) }