package internal import ( "errors" "io" "net/http" "net/url" "github.com/julienschmidt/httprouter" log "github.com/sirupsen/logrus" "github.com/unrolled/render" "go.salty.im/saltyim" "go.salty.im/saltyim/internal/authreq" ) // API ... type API struct { router *Router config *Config db Store r *render.Render } // NewAPI ... func NewAPI(router *Router, config *Config, db Store) *API { api := &API{router, config, db, render.New()} api.initRoutes() return api } func (a *API) initRoutes() { router := a.router.Group("/api/v1") router.GET("/ping", a.PingEndpoint()) router.POST("/register", a.RegisterEndpoint()) // Blob Service router.DELETE("/blob/:key", authreq.VerifyMiddleware(a.BlobEndpoint())) router.HEAD("/blob/:key", authreq.VerifyMiddleware(a.BlobEndpoint())) router.GET("/blob/:key", authreq.VerifyMiddleware(a.BlobEndpoint())) router.PUT("/blob/:key", authreq.VerifyMiddleware(a.BlobEndpoint())) router.POST("/blob/:key", authreq.VerifyMiddleware(a.BlobEndpoint())) // Lookup and Send support for Web / PWA clients router.GET("/lookup/:addr", a.LookupEndpoint()) router.POST("/send", a.SendEndpoint()) // Avatar Service router.POST("/avatar", a.AvatarEndpoint()) } // PingEndpoint ... func (a *API) PingEndpoint() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{}`)) } } // RegisterEndpoint ... func (a *API) RegisterEndpoint() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { req, signer, err := saltyim.NewRegisterRequest(r.Body) if err != nil { log.WithError(err).Error("error parsing register request") http.Error(w, "Bad Request", http.StatusBadRequest) return } if signer != req.Key { http.Error(w, "Bad Request", http.StatusBadRequest) return } if err := CreateConfig(a.config, req.Hash, req.Key); err != nil { log.WithError(err).Errorf("error creating config for %s", req.Hash) if err == ErrAddressExists { http.Error(w, "Already Exists", http.StatusConflict) } else { http.Error(w, "Error", http.StatusInternalServerError) } return } http.Error(w, "Endpoint Created", http.StatusCreated) } } // 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) } } // 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 { log.WithError(err).Error("error parsing send request") http.Error(w, "Bad Request", http.StatusBadRequest) return } // TODO: Do something with signer? log.Debugf("request signed by %s", signer) u, err := url.Parse(req.Endpoint) if err != nil { log.WithError(err).Errorf("error parsing endpoing %s", req.Endpoint) http.Error(w, "Bad Endpoint", http.StatusBadRequest) return } if !u.IsAbs() { log.Errorf("endpoint %s is not an absolute uri", req.Endpoint) http.Error(w, "Bad Endpoint", 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 { log.WithError(err).Errorf("error sending message to %s", req.Endpoint) http.Error(w, "Send Error", http.StatusInternalServerError) return } http.Error(w, "Message Accepted", http.StatusAccepted) } } // AvatarEndpoint ... func (a *API) AvatarEndpoint() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { req, signer, err := saltyim.NewAvatarRequest(r.Body) if err != nil { log.WithError(err).Error("error parsing avatar request") http.Error(w, "Bad Request", http.StatusBadRequest) return } // TODO: Do something with signer? log.Debugf("request signed by %s", signer) if err := CreateOrUpdateAvatar(a.config, req.Addr, req.Content); err != nil { log.WithError(err).Errorf("error creating/updating avatar for %s", req.Addr) http.Error(w, "Avatar Error", http.StatusInternalServerError) return } http.Error(w, "Avatar Created", http.StatusCreated) } } // BlobEndpoint ... func (a *API) BlobEndpoint() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { claims := authreq.ClaimsFromRequest(r) if claims == nil { log.Warn("no claims") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } signer := claims.Issuer key := p.ByName("key") switch r.Method { case http.MethodDelete: if err := DeleteBlob(a.config, key, signer); err != nil { if errors.Is(err, ErrBlobNotFound) { http.Error(w, "Blob Not Found", http.StatusNotFound) return } log.WithError(err).Errorf("error getting blob %s for %s", key, signer) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } http.Error(w, "Blob Deleted", http.StatusOK) case http.MethodGet, http.MethodHead: blob, err := GetBlob(a.config, key, signer) if err != nil { if errors.Is(err, ErrBlobNotFound) { http.Error(w, "Blob Not Found", http.StatusNotFound) return } log.WithError(err).Errorf("error getting blob %s for %s", key, signer) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } defer blob.Close() blob.SetHeaders(r) if r.Method == http.MethodGet { _, _ = io.Copy(w, blob) } case http.MethodPut: data, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } defer r.Body.Close() if err := CreateOrUpdateBlob(a.config, key, data, signer); err != nil { log.WithError(err).Errorf("error creating/updating blob for %s for %s", key, signer) http.Error(w, "Avatar Error", http.StatusInternalServerError) return } http.Error(w, "Blob Created", http.StatusCreated) default: w.Header().Add("Allow", "GET, HEAD, PUT, DELETE") http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) } } }