WIP: websocket
This commit is contained in:
parent
c91cd6efca
commit
2de97a6936
16 changed files with 261 additions and 56 deletions
7
Makefile
7
Makefile
|
@ -1,11 +1,14 @@
|
||||||
FE=pkg/frontend/frontend
|
FE=pkg/frontend/frontend
|
||||||
|
VER=$(shell git describe --always --tags)
|
||||||
|
|
||||||
|
LDFLAGS=-ldflags='-X dynatron.me/x/blasphem/internal/common.Version=${VER}'
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
build:
|
build:
|
||||||
go build -o blas ./cmd/blas/
|
go build ${LDFLAGS} -o blas ./cmd/blas/
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
go run ./cmd/blas/ serve ${BLAS_ARGS}
|
go run ${LDFLAGS} ./cmd/blas/ serve ${BLAS_ARGS}
|
||||||
|
|
||||||
# pkg/frontend/frontend/hass_frontend:
|
# pkg/frontend/frontend/hass_frontend:
|
||||||
frontend:
|
frontend:
|
||||||
|
|
|
@ -6,6 +6,9 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// this symbol overriden by linker args
|
||||||
|
var Version = "undefined"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// AppName is the name of the application.
|
// AppName is the name of the application.
|
||||||
AppName = "blasphem"
|
AppName = "blasphem"
|
||||||
|
|
|
@ -25,7 +25,7 @@ var (
|
||||||
ErrUserAuthRemote = errors.New("user cannot authenticate remotely")
|
ErrUserAuthRemote = errors.New("user cannot authenticate remotely")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator struct {
|
type authenticator struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
store AuthStore
|
store AuthStore
|
||||||
flows *AuthFlowManager
|
flows *AuthFlowManager
|
||||||
|
@ -33,12 +33,16 @@ type Authenticator struct {
|
||||||
providers map[string]provider.AuthProvider
|
providers map[string]provider.AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Authenticator interface {
|
||||||
|
ValidateAccessToken(token AccessToken) *RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
type AuthError struct {
|
type AuthError struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Description string `json:"error_description"`
|
Description string `json:"error_description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) InstallRoutes(e *echo.Echo) {
|
func (a *authenticator) installRoutes(e *echo.Echo) {
|
||||||
authG := e.Group("/auth")
|
authG := e.Group("/auth")
|
||||||
authG.GET("/authorize", frontend.AliasHandler("authorize.html"))
|
authG.GET("/authorize", frontend.AliasHandler("authorize.html"))
|
||||||
authG.GET("/providers", a.ProvidersHandler)
|
authG.GET("/providers", a.ProvidersHandler)
|
||||||
|
@ -51,12 +55,15 @@ func (a *Authenticator) InstallRoutes(e *echo.Echo) {
|
||||||
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
|
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) InitAuth(s storage.Store) error {
|
func New(e *echo.Echo, s storage.Store) (Authenticator, error) {
|
||||||
a.providers = make(map[string]provider.AuthProvider)
|
a := &authenticator{
|
||||||
|
providers: make(map[string]provider.AuthProvider),
|
||||||
|
}
|
||||||
|
|
||||||
for _, pI := range provider.Providers {
|
for _, pI := range provider.Providers {
|
||||||
nProv, err := pI(s)
|
nProv, err := pI(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.providers[nProv.ProviderType()] = nProv
|
a.providers[nProv.ProviderType()] = nProv
|
||||||
|
@ -69,13 +76,15 @@ func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
var err error
|
var err error
|
||||||
a.store, err = a.newAuthStore(s)
|
a.store, err = a.newAuthStore(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
a.installRoutes(e)
|
||||||
|
|
||||||
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) Provider(name string) provider.AuthProvider {
|
func (a *authenticator) Provider(name string) provider.AuthProvider {
|
||||||
p, ok := a.providers[name]
|
p, ok := a.providers[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
|
@ -87,7 +96,7 @@ func (a *Authenticator) Provider(name string) provider.AuthProvider {
|
||||||
var HomeAssistant = "homeassistant"
|
var HomeAssistant = "homeassistant"
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
func (a *authenticator) ProvidersHandler(c echo.Context) error {
|
||||||
providers := []provider.AuthProviderBase{
|
providers := []provider.AuthProviderBase{
|
||||||
a.Provider(HomeAssistant).ProviderBase(),
|
a.Provider(HomeAssistant).ProviderBase(),
|
||||||
}
|
}
|
||||||
|
@ -95,7 +104,7 @@ func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, providers)
|
return c.JSON(http.StatusOK, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (user provider.ProviderUser, clientID string, err error) {
|
func (a *authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]interface{}) (user provider.ProviderUser, clientID string, err error) {
|
||||||
cID, hasCID := rm["client_id"]
|
cID, hasCID := rm["client_id"]
|
||||||
clientID, cidIsStr := cID.(string)
|
clientID, cidIsStr := cID.(string)
|
||||||
if !hasCID || !cidIsStr || clientID == "" || clientID != string(f.ClientID) {
|
if !hasCID || !cidIsStr || clientID == "" || clientID != string(f.ClientID) {
|
||||||
|
|
|
@ -62,7 +62,7 @@ func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.Au
|
||||||
return lf
|
return lf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
|
func (a *authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
|
||||||
var prov provider.AuthProvider
|
var prov provider.AuthProvider
|
||||||
|
|
||||||
for _, h := range r.Handler {
|
for _, h := range r.Handler {
|
||||||
|
@ -89,7 +89,7 @@ func (f *LoginFlow) redirect(c echo.Context) {
|
||||||
c.Request().Header.Set("Location", f.RedirectURI)
|
c.Request().Header.Set("Location", f.RedirectURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
func (f *LoginFlow) progress(a *authenticator, c echo.Context) error {
|
||||||
switch f.Step() {
|
switch f.Step() {
|
||||||
case flow.StepInit:
|
case flow.StepInit:
|
||||||
rm := make(map[string]interface{})
|
rm := make(map[string]interface{})
|
||||||
|
@ -136,7 +136,7 @@ func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
func (a *authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ func setJSON(c echo.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
func (a *authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ func (a *Authenticator) BeginLoginFlowHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, resp)
|
return c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
func (a *authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,8 @@ type RefreshToken struct {
|
||||||
LastUsedIP *string `json:"last_used_ip"`
|
LastUsedIP *string `json:"last_used_ip"`
|
||||||
CredentialID *CredID `json:"credential_id"`
|
CredentialID *CredID `json:"credential_id"`
|
||||||
Version *string `json:"version"`
|
Version *string `json:"version"`
|
||||||
|
|
||||||
|
User *User `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rt *RefreshToken) IsValid() bool {
|
func (rt *RefreshToken) IsValid() bool {
|
||||||
|
@ -200,9 +202,9 @@ func WithCredential(c *Credentials) RefreshOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultAccessExpiration = "1800"
|
const DefaultAccessExpiration = "1800" // json 🤮
|
||||||
|
|
||||||
func (a *Authenticator) NewRefreshToken(user *User, opts ...RefreshOption) (*RefreshToken, error) {
|
func (a *authenticator) NewRefreshToken(user *User, opts ...RefreshOption) (*RefreshToken, error) {
|
||||||
e := func(es string, arg ...interface{}) (*RefreshToken, error) {
|
e := func(es string, arg ...interface{}) (*RefreshToken, error) {
|
||||||
return nil, fmt.Errorf(es, arg...)
|
return nil, fmt.Errorf(es, arg...)
|
||||||
}
|
}
|
||||||
|
@ -216,6 +218,7 @@ func (a *Authenticator) NewRefreshToken(user *User, opts ...RefreshOption) (*Ref
|
||||||
JWTKey: generate.Hex(64),
|
JWTKey: generate.Hex(64),
|
||||||
CreatedAt: &now,
|
CreatedAt: &now,
|
||||||
AccessTokenExpiration: DefaultAccessExpiration,
|
AccessTokenExpiration: DefaultAccessExpiration,
|
||||||
|
User: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -272,7 +275,7 @@ func (r *RefreshToken) AccessToken(req *http.Request) (string, error) {
|
||||||
}).SignedString([]byte(r.JWTKey))
|
}).SignedString([]byte(r.JWTKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest) *Credentials {
|
func (a *authenticator) verifyAndGetCredential(tr *TokenRequest) *Credentials {
|
||||||
cred, success := a.authCodes.get(tr)
|
cred, success := a.authCodes.get(tr)
|
||||||
if !success {
|
if !success {
|
||||||
return nil
|
return nil
|
||||||
|
@ -283,7 +286,7 @@ func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest) *Credentials {
|
||||||
|
|
||||||
const defaultExpiration = 15 * time.Minute
|
const defaultExpiration = 15 * time.Minute
|
||||||
|
|
||||||
func (a *Authenticator) NewAuthCode(clientID ClientID, cred *Credentials) string {
|
func (a *authenticator) NewAuthCode(clientID ClientID, cred *Credentials) string {
|
||||||
return a.authCodes.put(clientID, cred)
|
return a.authCodes.put(clientID, cred)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +319,7 @@ type TokenRequest struct {
|
||||||
|
|
||||||
const AuthFailed = "authentication failure"
|
const AuthFailed = "authentication failure"
|
||||||
|
|
||||||
func (a *Authenticator) TokenHandler(c echo.Context) error {
|
func (a *authenticator) TokenHandler(c echo.Context) error {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
|
@ -397,13 +400,7 @@ func (a *Authenticator) TokenHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request"})
|
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request"})
|
||||||
}
|
}
|
||||||
|
|
||||||
user := a.store.User(rt.UserID)
|
if err := rt.User.allowedToAuth(c.Request()); err != nil {
|
||||||
if user == nil {
|
|
||||||
log.Error().Str("userID", string(rt.UserID)).Msg("no such user")
|
|
||||||
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.allowedToAuth(c.Request()); err != nil {
|
|
||||||
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: err.Error()})
|
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,3 +422,5 @@ func (a *Authenticator) TokenHandler(c echo.Context) error {
|
||||||
|
|
||||||
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request"})
|
return c.JSON(http.StatusBadRequest, AuthError{Error: "invalid_request"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccessToken string
|
||||||
|
|
|
@ -86,6 +86,7 @@ func (as *authStore) GetRefreshTokenByToken(token RefreshTokenToken) *RefreshTok
|
||||||
for _, rt := range u.RefreshTokens {
|
for _, rt := range u.RefreshTokens {
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(rt.Token)) == 1 {
|
if subtle.ConstantTimeCompare([]byte(token), []byte(rt.Token)) == 1 {
|
||||||
found = rt
|
found = rt
|
||||||
|
found.User = u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +107,7 @@ func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
func (a *authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
||||||
as = &authStore{
|
as = &authStore{
|
||||||
store: s,
|
store: s,
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (u *User) allowedToAuth(r *http.Request) error {
|
||||||
return ErrUserAuthRemote
|
return ErrUserAuthRemote
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) getOrCreateUser(c *Credentials) (*User, error) {
|
func (a *authenticator) getOrCreateUser(c *Credentials) (*User, error) {
|
||||||
u := a.store.User(c.UserID)
|
u := a.store.User(c.UserID)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return nil, ErrInvalidAuth
|
return nil, ErrInvalidAuth
|
||||||
|
|
|
@ -7,20 +7,44 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
|
"dynatron.me/x/blasphem/pkg/auth"
|
||||||
"dynatron.me/x/blasphem/pkg/bus"
|
"dynatron.me/x/blasphem/pkg/bus"
|
||||||
"dynatron.me/x/blasphem/pkg/config"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Core interface {
|
||||||
|
auth.Authenticator
|
||||||
|
bus.Bus
|
||||||
|
storage.Store
|
||||||
|
config.Configured
|
||||||
|
Shutdowner
|
||||||
|
Versioner
|
||||||
|
}
|
||||||
|
|
||||||
|
type Shutdowner interface {
|
||||||
|
Shutdown(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Versioner interface {
|
||||||
|
Version() string
|
||||||
|
}
|
||||||
|
|
||||||
type Blas struct {
|
type Blas struct {
|
||||||
*bus.Bus
|
bus.Bus
|
||||||
storage.Store
|
storage.Store
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Blas) Shutdown(ctx context.Context) error {
|
func (b *Blas) Version() string {
|
||||||
b.Bus.Shutdown()
|
return common.Version
|
||||||
b.Store.Shutdown()
|
}
|
||||||
|
|
||||||
|
func (b *Blas) Conf() *config.Config { return b.Config }
|
||||||
|
|
||||||
|
func (b *Blas) ShutdownBlas(ctx context.Context) error {
|
||||||
|
b.Bus.ShutdownBus()
|
||||||
|
b.Store.ShutdownStore()
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,22 +12,28 @@ type (
|
||||||
|
|
||||||
listeners []chan<- Event
|
listeners []chan<- Event
|
||||||
|
|
||||||
Bus struct {
|
bus struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
subs map[string]listeners
|
subs map[string]listeners
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bus interface {
|
||||||
|
Sub(topic string, ch chan<- Event)
|
||||||
|
Unsub(topic string, ch chan<- Event)
|
||||||
|
Pub(topic string, data interface{})
|
||||||
|
ShutdownBus()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() *Bus {
|
func New() Bus {
|
||||||
bus := &Bus{
|
bus := &bus{
|
||||||
subs: make(map[string]listeners),
|
subs: make(map[string]listeners),
|
||||||
}
|
}
|
||||||
|
|
||||||
return bus
|
return bus
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Sub(topic string, ch chan<- Event) {
|
func (b *bus) Sub(topic string, ch chan<- Event) {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
|
@ -38,7 +44,7 @@ func (b *Bus) Sub(topic string, ch chan<- Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Unsub(topic string, ch chan<- Event) {
|
func (b *bus) Unsub(topic string, ch chan<- Event) {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
|
@ -51,7 +57,7 @@ func (b *Bus) Unsub(topic string, ch chan<- Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Pub(topic string, data interface{}) {
|
func (b *bus) Pub(topic string, data interface{}) {
|
||||||
b.RLock()
|
b.RLock()
|
||||||
defer b.RUnlock()
|
defer b.RUnlock()
|
||||||
|
|
||||||
|
@ -65,7 +71,7 @@ func (b *Bus) Pub(topic string, data interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Shutdown() {
|
func (b *bus) ShutdownBus() {
|
||||||
for _, v := range b.subs {
|
for _, v := range b.subs {
|
||||||
for _, c := range v {
|
for _, c := range v {
|
||||||
close(c)
|
close(c)
|
||||||
|
|
|
@ -11,6 +11,10 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Configured interface {
|
||||||
|
Conf() *Config
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DataDir *string `yaml:"data_dir,omitempty"`
|
DataDir *string `yaml:"data_dir,omitempty"`
|
||||||
Server *server.Config `yaml:"server"`
|
Server *server.Config `yaml:"server"`
|
||||||
|
|
|
@ -29,8 +29,6 @@ type Server struct {
|
||||||
func (s *Server) installRoutes() {
|
func (s *Server) installRoutes() {
|
||||||
s.GET("/*", frontend.FSHandler)
|
s.GET("/*", frontend.FSHandler)
|
||||||
s.GET("/api/websocket", s.wsHandler)
|
s.GET("/api/websocket", s.wsHandler)
|
||||||
|
|
||||||
s.Authenticator.InstallRoutes(s.Echo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) (s *Server, err error) {
|
func New(cfg *config.Config) (s *Server, err error) {
|
||||||
|
@ -43,7 +41,8 @@ func New(cfg *config.Config) (s *Server, err error) {
|
||||||
Blas: b,
|
Blas: b,
|
||||||
Echo: echo.New(),
|
Echo: echo.New(),
|
||||||
}
|
}
|
||||||
err = s.InitAuth(b.Store)
|
|
||||||
|
s.Authenticator, err = auth.New(s.Echo, b.Store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
@ -80,7 +79,7 @@ func New(cfg *config.Config) (s *Server, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Shutdown(ctx context.Context) error {
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
err := s.Blas.Shutdown(ctx)
|
err := s.Blas.ShutdownBlas(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -91,7 +90,7 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
func (s *Server) Go() error {
|
func (s *Server) Go() error {
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
log.Info().Str("bind", s.Config.Server.Bind).Msg("Server listening")
|
log.Info().Str("version", s.Version()).Str("bind", s.Config.Server.Bind).Msg("Server listening")
|
||||||
err := s.Start(s.Config.Server.Bind)
|
err := s.Start(s.Config.Server.Bind)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
s.Logger.Fatal(err)
|
s.Logger.Fatal(err)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"dynatron.me/x/blasphem/pkg/wsapi"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
|
@ -14,7 +14,14 @@ var upgrader = websocket.Upgrader{
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) wsHandler(c echo.Context) error {
|
func (s *Server) wsHandler(c echo.Context) error {
|
||||||
log.Println("WebSocket")
|
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
||||||
//conn, err := upgrader.Upgrade(w, req, nil)
|
if err != nil {
|
||||||
return errors.New("not handled")
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Debug().Str("remote", c.Request().RemoteAddr).Msg("WS")
|
||||||
|
|
||||||
|
return wsapi.New(s, c, conn).Handle()
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@ func (s *fsStore) FlushAll() []error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fsStore) Shutdown() {
|
func (s *fsStore) ShutdownStore() {
|
||||||
errs := s.FlushAll()
|
errs := s.FlushAll()
|
||||||
if errs != nil {
|
if errs != nil {
|
||||||
log.Error().Errs("errors", errs).Msg("errors persisting store")
|
log.Error().Errs("errors", errs).Msg("errors persisting store")
|
||||||
|
|
|
@ -50,6 +50,6 @@ type Store interface {
|
||||||
// Flush flushes a single key to backing.
|
// Flush flushes a single key to backing.
|
||||||
Flush(key string) error
|
Flush(key string) error
|
||||||
|
|
||||||
// Shutdown is called to quiesce and shutdown the store.
|
// ShutdownStore is called to quiesce and shutdown the store.
|
||||||
Shutdown()
|
ShutdownStore()
|
||||||
}
|
}
|
||||||
|
|
99
pkg/wsapi/api.go
Normal file
99
pkg/wsapi/api.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package wsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/pkg/auth"
|
||||||
|
"dynatron.me/x/blasphem/pkg/blas"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
type MsgBase struct {
|
||||||
|
Type Type `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
wsSession struct {
|
||||||
|
*websocket.Conn
|
||||||
|
b blas.Core
|
||||||
|
ec echo.Context
|
||||||
|
h phaseHandler
|
||||||
|
|
||||||
|
user *auth.User
|
||||||
|
refreshToken *auth.RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
WS interface {
|
||||||
|
Handle() error
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseHandler interface {
|
||||||
|
handleMsg(msg interface{}) error
|
||||||
|
msgSchema() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdHandler struct {
|
||||||
|
*wsSession
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func New(s blas.Core, c echo.Context, conn *websocket.Conn) WS {
|
||||||
|
ws := &wsSession{
|
||||||
|
Conn: conn,
|
||||||
|
b: s,
|
||||||
|
ec: c,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *wsSession) Handle() error {
|
||||||
|
err := ws.sendAuthRequired()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg := ws.h.msgSchema()
|
||||||
|
err := ws.ReadJSON(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("remote", ws.ec.Request().RemoteAddr).Msg("websocket read fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ws.h.handleMsg(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmdMsg struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgType string
|
||||||
|
|
||||||
|
func (cm *cmdMsg) UnmarshalJSON(b []byte) error {
|
||||||
|
var t typeType struct {
|
||||||
|
Type MsgType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(b, &t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *cmdHandler) msgSchema() interface{} {
|
||||||
|
return &cmdMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *cmdHandler) handleMsg(msg interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
51
pkg/wsapi/auth.go
Normal file
51
pkg/wsapi/auth.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package wsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dynatron.me/x/blasphem/pkg/auth"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authPhase struct {
|
||||||
|
*wsSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *wsSession) sendAuthRequired() error {
|
||||||
|
authReq := &struct{
|
||||||
|
MsgBase
|
||||||
|
Version string `json:"version"`
|
||||||
|
}{
|
||||||
|
MsgBase{"auth_required"},
|
||||||
|
ws.b.Version(),
|
||||||
|
}
|
||||||
|
return ws.WriteJSON(&authReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authMsg struct {
|
||||||
|
MsgBase
|
||||||
|
AccessToken auth.AccessToken `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *authPhase) msgSchema() interface{} {
|
||||||
|
return &authMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *authPhase) finishAuth(rt *auth.RefreshToken) {
|
||||||
|
ap.user = rt.User
|
||||||
|
ap.refreshToken = rt
|
||||||
|
ap.h = &cmdHandler{ap.wsSession}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *authPhase) handleMsg(msg interface{}) error {
|
||||||
|
authMsg := msg.(*authMsg)
|
||||||
|
refreshToken := ap.b.ValidateAccessToken(authMsg.AccessToken)
|
||||||
|
if refreshToken != nil {
|
||||||
|
ap.finishAuth(refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Str("remote", ap.ec.Request().RemoteAddr).Msg("websocket auth failed")
|
||||||
|
|
||||||
|
|
||||||
|
return auth.ErrInvalidAuth
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue