mirror of
https://git.mills.io/saltyim/saltyim.git
synced 2024-06-30 18:51:03 +00:00
ddd16c202f
Alternative to #177 The way this works is: Client: - Client creates a normal `net/http.Request{}` object using the `Request()` function in `utils.go`. The `http.Request{}` object is then signed using the Client's Ed25519 private key. - The HTTP Method and Path (_note this is important_) are hashed, as well as the request body (if any) using the FNV128a hashing algorithm. - This hash is then signed by the Client's's Ed25519 private key. - The resulting signature is then encoded to Base64 (_standard encoding_) and added to the HTTP headers as a `Signature:` header. - In addition the Client's Ed25519 public key is added to the HTTP headers as `Signer:` Server: - The server calculates the same FNV128a hash of the HTTP Request Method and Path and the body (if any) - The server decodes the HTTP header `Signature:` - The server then uses the Client's Ed25519 public key in the HTTP header `Signer:` to verify the signature of the `Signature:` HTTP header which gives us back the original FNV128a hash the Client calculated for the request. - The server then compares the Client's hash with the expected hash to see if they compare equally. Co-authored-by: James Mills <1290234+prologic@users.noreply.github.com> Co-authored-by: Jon Lundy <jon@xuu.cc> Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/178 Reviewed-by: xuu <xuu@noreply@mills.io>
163 lines
3.7 KiB
Go
163 lines
3.7 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.mills.io/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 HTPT
|
|
// 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{
|
|
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{
|
|
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:]
|
|
}
|