prologic-saltyim/utils.go

165 lines
3.8 KiB
Go

package saltyim
import (
"bytes"
"compress/gzip"
"crypto/rand"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/andybalholm/brotli"
"github.com/keys-pub/keys"
"github.com/oklog/ulid/v2"
log "github.com/sirupsen/logrus"
"go.salty.im/saltyim/internal/authreq"
)
const (
defaultRequestTimeout = time.Second * 30
)
// GenerateULID generates a new unique identifer
func GenerateULID() (string, error) {
entropy := rand.Reader
id, err := ulid.New(ulid.Timestamp(time.Now()), entropy)
if err != nil {
return "", fmt.Errorf("error generating ulid: %w", err)
}
return id.String(), nil
}
// MustGenerateULID generates a new unique identifer or fails
func MustGenerateULID() string {
ulid, err := GenerateULID()
if err != nil {
log.WithError(err).Fatal("error generating ulid")
}
return ulid
}
// Request is a generic request handling function for making artbitrary HTTP
// requests to Salty endpoints for looking up Salty Addresses, Configs and
// publishing encrypted messages.
func Request(method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
if headers == nil {
headers = make(http.Header)
}
if body != nil {
switch headers.Get("Content-Encoding") {
case "br":
buf := &bytes.Buffer{}
br := brotli.NewWriter(buf)
io.Copy(br, body)
br.Close()
body = buf
case "gzip":
buf := &bytes.Buffer{}
gz := gzip.NewWriter(buf)
io.Copy(gz, body)
gz.Close()
body = buf
}
}
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, fmt.Errorf("%s: http.NewRequest fail: %s", uri, err)
}
// Set a default User-Agent (if none set)
if headers.Get("User-Agent") == "" {
headers.Set("User-Agent", fmt.Sprintf("saltyim/%s", FullVersion()))
}
req.Header = headers
client := &http.Client{
Transport: http.DefaultClient.Transport,
}
client.Timeout = defaultRequestTimeout
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: client.Do fail: %s", uri, err)
}
if res.StatusCode/100 != 2 {
return nil, fmt.Errorf("non-2xx response received: %s", res.Status)
}
return res, nil
}
// SignedRequest is a generic request handling function for making artbitrary HTPT
// requests to a Salty broker's API endpoints that require authorization.
func SignedRequest(key *keys.EdX25519Key, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
if headers == nil {
headers = make(http.Header)
}
if body != nil {
switch headers.Get("Content-Encoding") {
case "br":
buf := &bytes.Buffer{}
br := brotli.NewWriter(buf)
io.Copy(br, body)
br.Close()
body = buf
case "gzip":
buf := &bytes.Buffer{}
gz := gzip.NewWriter(buf)
io.Copy(gz, body)
gz.Close()
body = buf
}
}
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, fmt.Errorf("%s: http.NewRequest fail: %s", uri, err)
}
// Set a default User-Agent (if none set)
if headers.Get("User-Agent") == "" {
headers.Set("User-Agent", fmt.Sprintf("saltyim/%s", FullVersion()))
}
req.Header = headers
client := &http.Client{
Transport: http.DefaultClient.Transport,
}
client.Timeout = defaultRequestTimeout
req, err = authreq.Sign(req, key.Private())
if err != nil {
return nil, fmt.Errorf("error signing request: %w", err)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: client.Do fail: %s", uri, err)
}
if res.StatusCode/100 != 2 {
return nil, fmt.Errorf("non-2xx response received: %s", res.Status)
}
return res, nil
}
// SplitInbox splits and endpoint into it's components (inbox, uri)
// where inbox is a topic queue on the Salty broker uri
func SplitInbox(endpoint string) (string, string) {
idx := strings.LastIndex(endpoint, "/")
if idx == -1 {
return "", ""
}
return endpoint[:idx], endpoint[idx+1:]
}