package server import ( "encoding/json" "io/ioutil" "github.com/keys-pub/keys" "github.com/keys-pub/keys/dstore" "github.com/keys-pub/keys/dstore/events" "github.com/keys-pub/keys/encoding" "github.com/keys-pub/keys/http" "github.com/keys-pub/keys/tsutil" "github.com/keys-pub/keys/users" "github.com/labstack/echo/v4" "github.com/pkg/errors" ) // TODO: Support If-Modified-Since // TODO: Turn off logging // Server ... type Server struct { fi Fire rds Redis clock tsutil.Clock logger Logger client http.Client // URL (base) of form http(s)://host:port with no trailing slash to help // authorization checks in testing where the host is ambiguous. URL string users *users.Users sigchains *keys.Sigchains tasks Tasks // internalAuth token for authorizing internal services. internalAuth string // admins are key ids that can do admin actions on the server. admins []keys.ID // internalKey for encrypting between internal services. internalKey *[32]byte } // Fire defines interface for remote store (like Firestore). type Fire interface { dstore.Documents events.Events } // New creates a Server. func New(fi Fire, rds Redis, client http.Client, clock tsutil.Clock, logger Logger) *Server { sigchains := keys.NewSigchains(fi) usrs := users.New(fi, sigchains, users.Client(client), users.Clock(clock)) return &Server{ fi: fi, rds: rds, client: client, clock: tsutil.NewClock(), tasks: newUnsetTasks(), sigchains: sigchains, users: usrs, logger: logger, } } // SetInternalAuth for authorizing internal requests, like tasks. func (s *Server) SetInternalAuth(internalAuth string) { s.internalAuth = internalAuth } // SetInternalKey for encrypting between internal services. func (s *Server) SetInternalKey(internalKey string) error { if internalKey == "" { return errors.Errorf("empty secret key") } sk, err := encoding.Decode(internalKey, encoding.Hex) if err != nil { return err } s.internalKey = keys.Bytes32(sk) return nil } // SetAdmins sets authorized admins. func (s *Server) SetAdmins(admins []keys.ID) { s.admins = admins } // SetTasks ... func (s *Server) SetTasks(tasks Tasks) { s.tasks = tasks } // NewHandler returns http.Handler for Server. func NewHandler(s *Server) http.Handler { return newHandler(s) } func newHandler(s *Server) *echo.Echo { e := echo.New() e.HTTPErrorHandler = s.ErrorHandler s.AddRoutes(e) return e } // AddRoutes adds routes to an Echo instance. func (s *Server) AddRoutes(e *echo.Echo) { e.GET("/sigchain/:kid/:seq", s.getSigchainStatement) e.PUT("/sigchain/:kid/:seq", s.putSigchainStatement) e.GET("/sigchain/:kid", s.getSigchain) e.POST("/check", s.check) e.GET("/user/search", s.getUserSearch) e.GET("/user/:user", s.getUser) // Tasks e.POST("/task/check/:kid", s.taskCheck) // Cron e.POST("/cron/check", s.cronCheck) // Vault e.POST("/vault/:kid", s.postVault) e.GET("/vault/:kid", s.listVault) e.DELETE("/vault/:kid", s.deleteVault) e.HEAD("/vault/:kid", s.headVault) // Disco e.PUT("/disco/:kid/:rid/:type", s.putDisco) e.GET("/disco/:kid/:rid/:type", s.getDisco) e.DELETE("/disco/:kid/:rid", s.deleteDisco) // Invite Code e.POST("/invite/code/:kid/:rid", s.postInviteCode) e.GET("/invite/code/:code", s.getInviteCode) // Share e.GET("/share/:kid", s.getShare) e.PUT("/share/:kid", s.putShare) // Sigchain (aliases) e.GET("/:kid", s.getSigchainAliased) e.GET("/:kid/:seq", s.getSigchainStatementAliased) e.PUT("/:kid/:seq", s.putSigchainStatementAliased) // // Experimental // // Channel e.PUT("/channel/:cid", s.putChannel) // Create a channel e.GET("/channel/:cid", s.getChannel) // Get a channel e.POST("/channel/:cid/msgs", s.postMessage) // Send message e.GET("/channel/:cid/msgs", s.getMessages) // List messages e.POST("/channels/status", s.postChannelsStatus) // Get channels status // Batch e.POST("/batch", s.postBatch) // Batch // Direct Messages e.POST("/dm/:sender/:recipient", s.postDirectMessage) // Send direct message e.GET("/dm/:recipient", s.getDirectMessages) // List direct messages e.GET("/dm/token/:recipient", s.getDirectToken) // Direct token // Follow e.PUT("/follow/:sender/:recipient", s.putFollow) // Follow e.GET("/follows/:recipient", s.getFollows) // List follows e.GET("/follow/:sender/:recipient", s.getFollow) // Get follow e.DELETE("/follow/:sender/:recipient", s.deleteFollow) // Unfollow // Twitter e.GET("/twitter/:kid/:name/:id", s.checkTwitter) // Admin e.POST("/admin/check/:kid", s.adminCheck) } // SetClock sets clock. func (s *Server) SetClock(clock tsutil.Clock) { s.clock = clock } // JSON response. func JSON(c echo.Context, status int, i interface{}) error { var b []byte switch v := i.(type) { case []byte: b = v default: mb, err := json.Marshal(i) if err != nil { panic(err) } b = mb } return c.Blob(status, echo.MIMEApplicationJSONCharsetUTF8, b) } func (s *Server) checkInternalAuth(c echo.Context) error { if s.internalAuth == "" { return s.ErrForbidden(c, errors.Errorf("no auth token set on server")) } auth := c.Request().Header.Get("Authorization") if auth == "" { return s.ErrForbidden(c, errors.Errorf("no auth token specified")) } if auth != s.internalAuth { return s.ErrForbidden(c, errors.Errorf("invalid auth token")) } return nil } func readBody(c echo.Context, required bool, maxLength int) ([]byte, int, error) { br := c.Request().Body if br == nil { if !required { return []byte{}, 0, nil } return nil, http.StatusBadRequest, errors.Errorf("missing body") } b, err := ioutil.ReadAll(br) if err != nil { return nil, http.StatusInternalServerError, err } if len(b) > maxLength { // TODO: Check length before reading data return nil, http.StatusRequestEntityTooLarge, errors.Errorf("request too large") } if len(b) == 0 && required { return nil, http.StatusBadRequest, errors.Errorf("no body data") } return b, 0, nil }