|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"compress/gzip"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
lm "github.com/hrfee/jfa-go/logmessages"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TimeoutHandler recovers from an http timeout or panic.
|
|
|
|
type TimeoutHandler func()
|
|
|
|
|
|
|
|
// NewTimeoutHandler returns a new Timeout handler.
|
|
|
|
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
|
|
|
return func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
out := fmt.Sprintf(lm.FailedAuth, name, addr, 0, lm.TimedOut)
|
|
|
|
if noFail {
|
|
|
|
log.Print(out)
|
|
|
|
} else {
|
|
|
|
log.Fatalf(out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// most 404 errors are from UserNotFound, so this generic error doesn't really need any detail.
|
|
|
|
type ErrNotFound error
|
|
|
|
|
|
|
|
type ErrUnauthorized struct{}
|
|
|
|
|
|
|
|
func (err ErrUnauthorized) Error() string {
|
|
|
|
return lm.Unauthorized
|
|
|
|
}
|
|
|
|
|
|
|
|
type ErrForbidden struct{}
|
|
|
|
|
|
|
|
func (err ErrForbidden) Error() string {
|
|
|
|
return lm.Forbidden
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
NotFound ErrNotFound = errors.New(lm.NotFound)
|
|
|
|
)
|
|
|
|
|
|
|
|
type ErrUnknown struct {
|
|
|
|
code int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (err ErrUnknown) Error() string {
|
|
|
|
msg := fmt.Sprintf(lm.FailedGenericWithCode, err.code)
|
|
|
|
return msg
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenericErr returns an error appropriate to the given HTTP status (or actual error, if given).
|
|
|
|
func GenericErr(status int, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
switch status {
|
|
|
|
case 200, 204, 201:
|
|
|
|
return nil
|
|
|
|
case 401, 400:
|
|
|
|
return ErrUnauthorized{}
|
|
|
|
case 404:
|
|
|
|
return NotFound
|
|
|
|
case 403:
|
|
|
|
return ErrForbidden{}
|
|
|
|
default:
|
|
|
|
return ErrUnknown{code: status}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfigurableTransport interface {
|
|
|
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
|
|
|
SetTransport(t *http.Transport)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stripped down-ish version of rough http request function used in most of the API clients.
|
|
|
|
func Req(httpClient *http.Client, timeoutHandler TimeoutHandler, mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
|
|
|
var params []byte
|
|
|
|
if data != nil {
|
|
|
|
params, _ = json.Marshal(data)
|
|
|
|
}
|
|
|
|
if qp := queryParams.Encode(); qp != "" {
|
|
|
|
uri += "?" + qp
|
|
|
|
}
|
|
|
|
var req *http.Request
|
|
|
|
if data != nil {
|
|
|
|
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
|
|
|
} else {
|
|
|
|
req, _ = http.NewRequest(mode, uri, nil)
|
|
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
for name, value := range headers {
|
|
|
|
req.Header.Add(name, value)
|
|
|
|
}
|
|
|
|
resp, err := httpClient.Do(req)
|
|
|
|
if resp == nil {
|
|
|
|
return "", 0, err
|
|
|
|
}
|
|
|
|
err = GenericErr(resp.StatusCode, err)
|
|
|
|
if timeoutHandler != nil {
|
|
|
|
defer timeoutHandler()
|
|
|
|
}
|
|
|
|
var responseText string
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if response || err != nil {
|
|
|
|
responseText, err = decodeResp(resp)
|
|
|
|
if err != nil {
|
|
|
|
return responseText, resp.StatusCode, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
var msg any
|
|
|
|
err = json.Unmarshal([]byte(responseText), &msg)
|
|
|
|
if err != nil {
|
|
|
|
return responseText, resp.StatusCode, err
|
|
|
|
}
|
|
|
|
if msg != nil {
|
|
|
|
err = fmt.Errorf("got %d: %+v", resp.StatusCode, msg)
|
|
|
|
}
|
|
|
|
return responseText, resp.StatusCode, err
|
|
|
|
}
|
|
|
|
return responseText, resp.StatusCode, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeResp(resp *http.Response) (string, error) {
|
|
|
|
var out io.Reader
|
|
|
|
switch resp.Header.Get("Content-Encoding") {
|
|
|
|
case "gzip":
|
|
|
|
out, _ = gzip.NewReader(resp.Body)
|
|
|
|
default:
|
|
|
|
out = resp.Body
|
|
|
|
}
|
|
|
|
buf := new(strings.Builder)
|
|
|
|
_, err := io.Copy(buf, out)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return buf.String(), nil
|
|
|
|
}
|