6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-16 03:48:24 +00:00

Add support for sender interfaces and the pwa to fallback to sending via a broker as fallback (#101)

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/101
Reviewed-by: xuu <xuu@noreply@mills.io>
This commit is contained in:
James Mills 2022-03-31 03:46:12 +00:00
parent 88d46eaf13
commit 95be492208
7 changed files with 162 additions and 36 deletions

@ -6,7 +6,6 @@ import (
"crypto/sha256"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path"
@ -18,7 +17,6 @@ import (
"github.com/avast/retry-go"
"github.com/keys-pub/keys"
log "github.com/sirupsen/logrus"
"github.com/timewasted/go-accept-headers"
"go.mills.io/salty"
"go.mills.io/saltyim/internal/exec"
@ -70,27 +68,6 @@ func PackMessage(me *Addr, msg string) []byte {
)
}
// Send sends the encrypted message `msg` to the Endpoint `endpoint` using a
// `POST` request and returns nil on success or an error on failure.
func Send(endpoint, msg string, cap Capabilities) error {
headers := make(http.Header)
if cap.AcceptEncoding != "" {
ae, err := accept.Negotiate(cap.AcceptEncoding, acceptEncodings...)
if err != nil {
return fmt.Errorf("error publishing message to %s: %w", endpoint, err)
}
headers.Set("Content-Encoding", ae)
}
res, err := Request(http.MethodPost, endpoint, headers, bytes.NewBufferString(msg))
if err != nil {
return fmt.Errorf("error publishing message to %s: %w", endpoint, err)
}
defer res.Body.Close()
return nil
}
// Client is a Salty IM client that handles talking to a Salty IM Broker
// and Sedngina and Receiving messages to/from Salty IM Users.
type Client struct {
@ -100,6 +77,7 @@ type Client struct {
cache addrCache
lookup Lookuper
send Sender
}
// NewClient reeturns a new Salty IM client for sending and receiving
@ -127,11 +105,14 @@ func NewClient(me *Addr, options ...IdentityOption) (*Client, error) {
log.Debugf("Endpoint is %s", me.Endpoint())
return &Client{
me: me,
id: id,
key: id.key,
cache: make(addrCache),
me: me,
id: id,
key: id.key,
cache: make(addrCache),
lookup: &DirectLookup{},
send: &DirectSend{},
}, nil
}
@ -261,6 +242,12 @@ func (cli *Client) SetLookup(lookup Lookuper) {
cli.lookup = lookup
}
// SetSend sets the internal send interface to use (Sender) for sending
// messages to endpoints. By default the DirectSend implementation is used.
func (cli *Client) SetSend(send Sender) {
cli.send = send
}
// Drain drains this user's inbox by simulteneiously reading until empty anda
// subscribing to the inbox for new messages.
func (cli *Client) Drain(ctx context.Context, extraenvs, prehook, posthook string) chan Message {

@ -32,8 +32,10 @@ func (a *API) initRoutes() {
router := a.router.Group("/api/v1")
router.GET("/ping", a.PingEndpoint())
router.POST("/register", a.RegisterEndpoint())
router.GET("/lookup/:addr", a.LookupEndpoint())
router.POST("/send", a.SendEndpoint())
router.POST("/register", a.RegisterEndpoint())
}
// PingEndpoint ...
@ -44,6 +46,18 @@ func (a *API) PingEndpoint() httprouter.Handle {
}
}
// LookupEndpoint ...
func (a *API) LookupEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
addr, err := saltyim.LookupAddr(p.ByName("addr"))
if err != nil {
http.Error(w, "Lookup Error", http.StatusBadRequest)
return
}
a.r.JSON(w, http.StatusOK, addr)
}
}
// RegisterEndpoint ...
func (a *API) RegisterEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -68,14 +82,26 @@ func (a *API) RegisterEndpoint() httprouter.Handle {
}
}
// LookupEndpoint ...
func (a *API) LookupEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
addr, err := saltyim.LookupAddr(p.ByName("addr"))
// SendEndpoint ...
func (a *API) SendEndpoint() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
req, signer, err := saltyim.NewSendRequest(r.Body)
if err != nil {
http.Error(w, "Lookup Error", http.StatusBadRequest)
log.WithError(err).Error("error parsing send request")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
a.r.JSON(w, http.StatusOK, addr)
if signer != r.Header.Get("Signature") {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// TODO: Queue up an internal retry and return immediately on failure?
if err := saltyim.Send(req.Endpoint, req.Message, req.Capabilities); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
http.Error(w, "Message Accepted", http.StatusAccepted)
}
}

@ -107,6 +107,7 @@ func (h *SaltyChat) connect(ctx app.Context) {
h.dialog.ShowDialog("error setting up client", err.Error())
return
}
newClient.SetSend(&saltyim.ProxySend{SendEndpoint: app.Getenv("SendEndpoint")})
newClient.SetLookup(&saltyim.ProxyLookup{LookupEndpoint: app.Getenv("LookupEndpoint")})
client = newClient

@ -295,6 +295,7 @@ func (s *Server) initRoutes() {
Env: map[string]string{
"LookupEndpoint": strings.TrimSuffix(s.config.BaseURL, "/") + "/api/v1/lookup/",
"SendEndpoint": strings.TrimSuffix(s.config.BaseURL, "/") + "/api/v1/send/",
},
Icon: app.Icon{

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6756046e89856a68e31c8dde6a2c1229674064e47dbf815fd87465a193e45e2c
size 28023655
oid sha256:c25a7a58e7780b3b84afc242a8fd8e1cb70f25aa84238cd8478afb645853c28c
size 28036206

87
send.go Normal file

@ -0,0 +1,87 @@
package saltyim
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/timewasted/go-accept-headers"
)
var (
_ Sender = (*DirectSend)(nil)
_ Sender = (*ProxySend)(nil)
)
type Sender interface {
Send(endpoint, msg string, cap Capabilities) error
}
// Send sends the encrypted message `msg` to the Endpoint `endpoint` using a
// `POST` request and returns nil on success or an error on failure.
func Send(endpoint, msg string, cap Capabilities) error {
req := SendRequest{
Endpoint: endpoint,
Message: msg,
Capabilities: cap,
}
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("error serialing send request: %w", err)
}
res, err := Request(http.MethodPost, endpoint, nil, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error publishing message to %s: %w", endpoint, err)
}
defer res.Body.Close()
return nil
}
// DirectSend performs a direct send request
type DirectSend struct{}
// Send posts a message to an endpoint directly
func (s *DirectSend) Send(endpoint, msg string, cap Capabilities) error {
return Send(endpoint, msg, cap)
}
// ProxySend proxies send requests through a Salty Broker's /api/v1/send endpoint
type ProxySend struct {
// SendEndpoint is the uri of the send endpoint of a broker
SendEndpoint string
}
// Send posts a message to an endpoint directly directly and if the request fails for
// whatever reason (usuaully due to Cross-Orogin-Resource-Sharing policies / CORS) it
// uses the Salty Broker the PWA was served from initially to perform the send by
// proxying the send request through the broker. Why? CORS sucks.
func (s *ProxySend) Send(endpoint, msg string, cap Capabilities) error {
err := Send(endpoint, msg, cap)
if err == nil {
return nil
}
// Fallback to proxying the send request through the broker...
headers := make(http.Header)
if cap.AcceptEncoding != "" {
ae, err := accept.Negotiate(cap.AcceptEncoding, acceptEncodings...)
if err != nil {
return fmt.Errorf("error publishing message to %s: %w", endpoint, err)
}
headers.Set("Content-Encoding", ae)
}
res, err := Request(http.MethodGet, s.SendEndpoint+endpoint, nil, nil)
if err != nil {
return err
}
defer res.Body.Close()
return nil
}

@ -30,3 +30,27 @@ func NewRegisterRequest(r io.Reader) (req RegisterRequest, signer string, err er
err = json.Unmarshal(out, &req)
return
}
// SendRequest is the request used by clients to send messages via a broker
type SendRequest struct {
Endpoint string
Message string
Capabilities Capabilities
}
// NewSendRequest reads the signed request body from a client, verifies its signature
// and returns the resulting `SendRequest` and key used to sign the request on success
// otherwise an empty object and en error on failure.
func NewSendRequest(r io.Reader) (req SendRequest, signer string, err error) {
body, err := ioutil.ReadAll(r)
if err != nil {
return
}
out, key, err := salty.Verify(body)
if err != nil {
return
}
signer = key.ID().String()
err = json.Unmarshal(out, &req)
return
}