Compare commits
No commits in common. "initwip" and "flow" have entirely different histories.
28 changed files with 191 additions and 813 deletions
7
Makefile
7
Makefile
|
@ -1,14 +1,11 @@
|
||||||
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 ${LDFLAGS} -o blas ./cmd/blas/
|
go build -o blas ./cmd/blas/
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
go run ${LDFLAGS} ./cmd/blas/ serve ${BLAS_ARGS}
|
go run ./cmd/blas/ serve ${BLAS_ARGS}
|
||||||
|
|
||||||
# pkg/frontend/frontend/hass_frontend:
|
# pkg/frontend/frontend/hass_frontend:
|
||||||
frontend:
|
frontend:
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/pkg/blas"
|
|
||||||
"dynatron.me/x/blasphem/pkg/cmd/serve"
|
"dynatron.me/x/blasphem/pkg/cmd/serve"
|
||||||
"dynatron.me/x/blasphem/pkg/config"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
|
|
||||||
|
@ -26,12 +25,7 @@ func main() {
|
||||||
log.Fatal().Err(err).Msg("Config read failed")
|
log.Fatal().Err(err).Msg("Config read failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
bl, err := blas.New(config)
|
rootCmd.AddCommand(serve.Command(config))
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("Core create failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd.AddCommand(serve.Command(bl))
|
|
||||||
|
|
||||||
err = rootCmd.Execute()
|
err = rootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -21,7 +21,6 @@ require (
|
||||||
github.com/labstack/gommon v0.3.1 // indirect
|
github.com/labstack/gommon v0.3.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -36,8 +36,6 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
|
|
@ -6,9 +6,6 @@ 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"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
|
"dynatron.me/x/blasphem/pkg/frontend"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
|
|
||||||
// providers
|
// providers
|
||||||
|
@ -24,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
|
||||||
|
@ -32,18 +33,14 @@ type authenticator struct {
|
||||||
providers map[string]provider.AuthProvider
|
providers map[string]provider.AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authenticator interface {
|
|
||||||
ValidateAccessToken(token AccessToken) *RefreshToken
|
|
||||||
InstallRoutes(e *echo.Echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
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("/providers", a.ProvidersHandler)
|
authG.GET("/providers", a.ProvidersHandler)
|
||||||
authG.POST("/token", a.TokenHandler)
|
authG.POST("/token", a.TokenHandler)
|
||||||
|
|
||||||
|
@ -54,15 +51,12 @@ func (a *authenticator) InstallRoutes(e *echo.Echo) {
|
||||||
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
|
loginFlow.DELETE("/:flow_id", a.LoginFlowDeleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(s storage.Store) (Authenticator, error) {
|
func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
a := &authenticator{
|
a.providers = make(map[string]provider.AuthProvider)
|
||||||
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 nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.providers[nProv.ProviderType()] = nProv
|
a.providers[nProv.ProviderType()] = nProv
|
||||||
|
@ -75,13 +69,13 @@ func New(s storage.Store) (Authenticator, 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 nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a, nil
|
return 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
|
||||||
|
@ -93,7 +87,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(),
|
||||||
}
|
}
|
||||||
|
@ -101,7 +95,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) {
|
||||||
|
@ -122,3 +116,5 @@ func (a *authenticator) Check(f *LoginFlow, req *http.Request, rm map[string]int
|
||||||
|
|
||||||
return nil, clientID, ErrInvalidAuth
|
return nil, clientID, ErrInvalidAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func (a *Authenticator) GetOrCreateCreds(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -20,7 +19,6 @@ type AuthFlowManager struct {
|
||||||
type LoginFlow struct {
|
type LoginFlow struct {
|
||||||
flow.FlowHandler
|
flow.FlowHandler
|
||||||
|
|
||||||
prov provider.AuthProvider
|
|
||||||
ClientID common.ClientID
|
ClientID common.ClientID
|
||||||
FlowContext
|
FlowContext
|
||||||
}
|
}
|
||||||
|
@ -54,10 +52,9 @@ func NewAuthFlowManager() *AuthFlowManager {
|
||||||
|
|
||||||
func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow {
|
func (afm *AuthFlowManager) NewLoginFlow(req *LoginFlowRequest, prov provider.AuthProvider) *LoginFlow {
|
||||||
lf := &LoginFlow{
|
lf := &LoginFlow{
|
||||||
FlowHandler: flow.NewFlowHandlerBase(prov.ProviderType()),
|
FlowHandler: flow.NewFlowHandlerBase(prov, prov.ProviderType()),
|
||||||
ClientID: req.ClientID,
|
ClientID: req.ClientID,
|
||||||
FlowContext: req.FlowContext(),
|
FlowContext: req.FlowContext(),
|
||||||
prov: prov,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afm.Register(lf)
|
afm.Register(lf)
|
||||||
|
@ -65,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 {
|
||||||
|
@ -83,16 +80,16 @@ func (a *authenticator) NewFlow(r *LoginFlowRequest) *flow.Result {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lf := a.flows.NewLoginFlow(r, prov)
|
flow := a.flows.NewLoginFlow(r, prov)
|
||||||
|
|
||||||
return lf.ShowForm(lf.WithSchema(prov), lf.WithStep(flow.StepInit))
|
return flow.ShowForm(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *LoginFlow) redirect(c echo.Context) {
|
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{})
|
||||||
|
@ -102,19 +99,15 @@ func (f *LoginFlow) progress(a *authenticator, c echo.Context) error {
|
||||||
return c.String(http.StatusBadRequest, err.Error())
|
return c.String(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.prov.FlowSchema().CheckRequired(rm)
|
err = f.Schema.CheckRequired(rm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, f.ShowForm(f.WithErrors([]string{err.Error()})))
|
return c.JSON(http.StatusBadRequest, f.ShowForm([]string{err.Error()}))
|
||||||
}
|
}
|
||||||
|
|
||||||
user, clientID, err := a.Check(f, c.Request(), rm)
|
user, clientID, err := a.Check(f, c.Request(), rm)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
creds := a.store.GetCredential(user)
|
creds := a.store.GetCredential(user)
|
||||||
if creds == nil {
|
|
||||||
return fmt.Errorf("flow progress: no such credential for %v", user.UserData())
|
|
||||||
}
|
|
||||||
|
|
||||||
finishedFlow := flow.Result{}
|
finishedFlow := flow.Result{}
|
||||||
a.flows.Remove(f)
|
a.flows.Remove(f)
|
||||||
copier.Copy(&finishedFlow, f)
|
copier.Copy(&finishedFlow, f)
|
||||||
|
@ -131,19 +124,19 @@ func (f *LoginFlow) progress(a *authenticator, c echo.Context) error {
|
||||||
case ErrInvalidAuth:
|
case ErrInvalidAuth:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return c.JSON(http.StatusOK, f.ShowForm(f.WithErrors(map[string]interface{}{
|
return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
|
||||||
"base": "invalid_auth",
|
"base": "invalid_auth",
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return c.JSON(http.StatusOK, f.ShowForm(f.WithErrors(map[string]interface{}{
|
return c.JSON(http.StatusOK, f.ShowForm(map[string]interface{}{
|
||||||
"base": "unknown_flow_step",
|
"base": "unknown_flow_step",
|
||||||
})))
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -165,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()
|
||||||
|
|
||||||
|
@ -188,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()
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,18 @@ func (hap *HomeAssistantProvider) NewCredData() interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *HomeAssistantProvider) FlowSchema() flow.Schema {
|
func (hap *HomeAssistantProvider) FlowSchema() flow.Schema {
|
||||||
return flow.NewSchema(flow.RequiredString("username"), flow.RequiredString("password"))
|
return []flow.SchemaItem{
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "username",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "password",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -79,7 +79,18 @@ func (hap *TrustedNetworksProvider) NewCredData() interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hap *TrustedNetworksProvider) FlowSchema() flow.Schema {
|
func (hap *TrustedNetworksProvider) FlowSchema() flow.Schema {
|
||||||
return nil
|
return []flow.SchemaItem{
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "username",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "password",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -153,8 +153,6 @@ 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 {
|
||||||
|
@ -202,9 +200,9 @@ func WithCredential(c *Credentials) RefreshOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultAccessExpiration = "1800" // json 🤮
|
const DefaultAccessExpiration = "1800"
|
||||||
|
|
||||||
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...)
|
||||||
}
|
}
|
||||||
|
@ -218,7 +216,6 @@ 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 {
|
||||||
|
@ -275,28 +272,7 @@ func (r *RefreshToken) AccessToken(req *http.Request) (string, error) {
|
||||||
}).SignedString([]byte(r.JWTKey))
|
}).SignedString([]byte(r.JWTKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *authenticator) ValidateAccessToken(token AccessToken) *RefreshToken {
|
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest) *Credentials {
|
||||||
var unverifiedIssRT *RefreshToken
|
|
||||||
claims := &jwt.StandardClaims{}
|
|
||||||
_, err := jwt.ParseWithClaims(string(token), claims, func(jt *jwt.Token) (interface{}, error) {
|
|
||||||
iss := jt.Claims.(*jwt.StandardClaims).Issuer
|
|
||||||
unverifiedIssRT = a.store.GetRefreshToken(RefreshTokenID(iss))
|
|
||||||
if unverifiedIssRT == nil {
|
|
||||||
return nil, fmt.Errorf("bad token")
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(unverifiedIssRT.JWTKey), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("validateAccessToken")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return unverifiedIssRT
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -307,7 +283,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +316,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()
|
||||||
|
|
||||||
|
@ -421,7 +397,13 @@ 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"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rt.User.allowedToAuth(c.Request()); err != nil {
|
user := a.store.User(rt.UserID)
|
||||||
|
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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,5 +425,3 @@ 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
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ type AuthStore interface {
|
||||||
GetCredential(provider.ProviderUser) *Credentials
|
GetCredential(provider.ProviderUser) *Credentials
|
||||||
PutRefreshToken(*RefreshToken) (*RefreshToken, error)
|
PutRefreshToken(*RefreshToken) (*RefreshToken, error)
|
||||||
GetRefreshTokenByToken(token RefreshTokenToken) *RefreshToken
|
GetRefreshTokenByToken(token RefreshTokenToken) *RefreshToken
|
||||||
GetRefreshToken(RefreshTokenID) *RefreshToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type authStore struct {
|
type authStore struct {
|
||||||
|
@ -87,7 +86,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,22 +93,6 @@ func (as *authStore) GetRefreshTokenByToken(token RefreshTokenToken) *RefreshTok
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *authStore) GetRefreshToken(tid RefreshTokenID) *RefreshToken {
|
|
||||||
var found *RefreshToken
|
|
||||||
|
|
||||||
for _, u := range as.Users {
|
|
||||||
for _, rt := range u.RefreshTokens {
|
|
||||||
if subtle.ConstantTimeCompare([]byte(tid), []byte(rt.ID.String())) == 1 {
|
|
||||||
found = rt
|
|
||||||
found.User = u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
||||||
// XXX: probably broken
|
// XXX: probably broken
|
||||||
prov := p.Provider()
|
prov := p.Provider()
|
||||||
|
@ -124,7 +106,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,36 +7,20 @@ 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/blas/core"
|
|
||||||
"dynatron.me/x/blasphem/pkg/bus"
|
"dynatron.me/x/blasphem/pkg/bus"
|
||||||
"dynatron.me/x/blasphem/pkg/components"
|
|
||||||
"dynatron.me/x/blasphem/pkg/config"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
"dynatron.me/x/blasphem/pkg/wsapi"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Blas struct {
|
type Blas struct {
|
||||||
bus.Bus
|
*bus.Bus
|
||||||
storage.Store
|
storage.Store
|
||||||
auth.Authenticator
|
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
core.WebSocketManager
|
|
||||||
|
|
||||||
components components.ComponentStore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Blas) Version() string {
|
func (b *Blas) Shutdown(ctx context.Context) error {
|
||||||
return common.Version
|
b.Bus.Shutdown()
|
||||||
}
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,42 +51,12 @@ func (b *Blas) openStore() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Blas) Component(k components.ComponentKey) components.Component {
|
func New(cfg *config.Config) (*Blas, error) {
|
||||||
c, ok := b.components[k]
|
b := &Blas{
|
||||||
if !ok {
|
Bus: bus.New(),
|
||||||
return nil
|
Config: cfg,
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blas) Components() components.ComponentStore { return b.components }
|
|
||||||
|
|
||||||
func New(cfg *config.Config) (b *Blas, err error) {
|
|
||||||
b = &Blas{
|
|
||||||
Bus: bus.New(),
|
|
||||||
Config: cfg,
|
|
||||||
components: make(components.ComponentStore),
|
|
||||||
WebSocketManager: wsapi.NewManager(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.openStore()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Authenticator, err = auth.New(b.Store)
|
|
||||||
|
|
||||||
for k, v := range Registry {
|
|
||||||
log.Info().Msgf("Setting up component %s", k)
|
|
||||||
c, err := v(b)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("Error setting up component %s", k)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b.components[k] = c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := b.openStore()
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
package blas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/blas/core"
|
|
||||||
"dynatron.me/x/blasphem/pkg/components"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Setup func(core.Blas) (components.Component, error)
|
|
||||||
|
|
||||||
var Registry = make(map[components.ComponentKey]Setup)
|
|
||||||
|
|
||||||
func Register(key components.ComponentKey, c Setup) {
|
|
||||||
_, already := Registry[key]
|
|
||||||
if already {
|
|
||||||
panic(fmt.Sprintf("component %s already exists", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
Registry[key] = c
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth"
|
|
||||||
"dynatron.me/x/blasphem/pkg/bus"
|
|
||||||
"dynatron.me/x/blasphem/pkg/components"
|
|
||||||
"dynatron.me/x/blasphem/pkg/config"
|
|
||||||
"dynatron.me/x/blasphem/pkg/storage"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Blas interface {
|
|
||||||
auth.Authenticator
|
|
||||||
bus.Bus
|
|
||||||
storage.Store
|
|
||||||
config.Configured
|
|
||||||
components.Componenter
|
|
||||||
|
|
||||||
WebSocketManager
|
|
||||||
|
|
||||||
Shutdowner
|
|
||||||
Versioner
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebSocketManager interface {
|
|
||||||
// Register registers a websocket command.
|
|
||||||
// cmd is the first part, before first slash
|
|
||||||
// dataNew is a function to create a new message datatype
|
|
||||||
RegisterWSCommand(cmd string, hnd Handler, dataNew NewData)
|
|
||||||
WSCommandHandler(cmd string, splitCmd []string) (NewData, Handler, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebSocketSession interface {
|
|
||||||
Conn() *websocket.Conn
|
|
||||||
Go(context.Context) error
|
|
||||||
Blas() Blas
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler func(ctx context.Context, wss WebSocketSession, msgID int, cmd []string, msg interface{}) error
|
|
||||||
type NewData func(cmd []string) interface{}
|
|
||||||
|
|
||||||
type Shutdowner interface {
|
|
||||||
ShutdownBlas(context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Versioner interface {
|
|
||||||
Version() string
|
|
||||||
}
|
|
|
@ -12,28 +12,22 @@ 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()
|
||||||
|
|
||||||
|
@ -44,7 +38,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()
|
||||||
|
|
||||||
|
@ -57,7 +51,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()
|
||||||
|
|
||||||
|
@ -71,7 +65,7 @@ func (b *bus) Pub(topic string, data interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bus) ShutdownBus() {
|
func (b *Bus) Shutdown() {
|
||||||
for _, v := range b.subs {
|
for _, v := range b.subs {
|
||||||
for _, c := range v {
|
for _, c := range v {
|
||||||
close(c)
|
close(c)
|
||||||
|
|
|
@ -2,18 +2,18 @@ package serve
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
blas "dynatron.me/x/blasphem/pkg/blas/core"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
"dynatron.me/x/blasphem/pkg/server"
|
"dynatron.me/x/blasphem/pkg/server"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeOptions struct {
|
type ServeOptions struct {
|
||||||
core blas.Blas
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func Command(core blas.Blas) *cobra.Command {
|
func Command(cfg *config.Config) *cobra.Command {
|
||||||
opts := makeOptions(core)
|
opts := makeOptions(cfg)
|
||||||
serveCmd := &cobra.Command{
|
serveCmd := &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "starts the " + common.AppName + " server",
|
Short: "starts the " + common.AppName + " server",
|
||||||
|
@ -23,9 +23,9 @@ func Command(core blas.Blas) *cobra.Command {
|
||||||
return serveCmd
|
return serveCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeOptions(core blas.Blas) *ServeOptions {
|
func makeOptions(cfg *config.Config) *ServeOptions {
|
||||||
return &ServeOptions{
|
return &ServeOptions{
|
||||||
core: core,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ func (o *ServeOptions) Options(_ *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ServeOptions) Execute() error {
|
func (o *ServeOptions) Execute() error {
|
||||||
server, err := server.New(o.core)
|
server, err := server.New(o.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
package components
|
|
||||||
|
|
||||||
import ()
|
|
||||||
|
|
||||||
type Componenter interface {
|
|
||||||
Component(ComponentKey) Component
|
|
||||||
Components() ComponentStore
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
ComponentStore map[ComponentKey]Component
|
|
||||||
ComponentKey string
|
|
||||||
|
|
||||||
Component interface {
|
|
||||||
Shutdown()
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -11,10 +11,6 @@ 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"`
|
||||||
|
|
126
pkg/flow/flow.go
126
pkg/flow/flow.go
|
@ -8,53 +8,71 @@ import (
|
||||||
"dynatron.me/x/blasphem/internal/generate"
|
"dynatron.me/x/blasphem/internal/generate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResultType string
|
type (
|
||||||
type FlowID string
|
ResultType string
|
||||||
type Step string
|
FlowID string
|
||||||
type HandlerKey string
|
Step string
|
||||||
type Errors interface{}
|
HandlerKey string
|
||||||
|
Errors interface{}
|
||||||
|
|
||||||
type FlowStore map[FlowID]Handler
|
Context interface{}
|
||||||
|
|
||||||
type FlowManager struct {
|
FlowStore map[FlowID]Handler
|
||||||
flows FlowStore
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
FlowManager struct {
|
||||||
Type ResultType `json:"type"`
|
flows FlowStore
|
||||||
ID FlowID `json:"flow_id"`
|
}
|
||||||
Handler []*HandlerKey `json:"handler"`
|
|
||||||
Title *string `json:"title,omitempty"`
|
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
|
||||||
StepID *Step `json:"step_id,omitempty"`
|
|
||||||
Schema []SchemaItem `json:"data_schema"`
|
|
||||||
Extra *string `json:"extra,omitempty"`
|
|
||||||
Required *bool `json:"required,omitempty"`
|
|
||||||
Errors interface{} `json:"errors"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
DescPlace *string `json:"description_placeholders"`
|
|
||||||
URL *string `json:"url,omitempty"`
|
|
||||||
Reason *string `json:"reason,omitempty"`
|
|
||||||
Context *string `json:"context,omitempty"`
|
|
||||||
Result interface{} `json:"result,omitempty"`
|
|
||||||
LastStep *string `json:"last_step"`
|
|
||||||
Options map[string]interface{} `json:"options,omitempty"`
|
|
||||||
Version *int `json:"version,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler interface {
|
Result struct {
|
||||||
BaseHandler() FlowHandler
|
Type ResultType `json:"type"`
|
||||||
FlowID() FlowID
|
ID FlowID `json:"flow_id"`
|
||||||
|
Handler []*HandlerKey `json:"handler"`
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
StepID *Step `json:"step_id,omitempty"`
|
||||||
|
Schema []SchemaItem `json:"data_schema"`
|
||||||
|
Extra *string `json:"extra,omitempty"`
|
||||||
|
Required *bool `json:"required,omitempty"`
|
||||||
|
Errors interface{} `json:"errors"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
DescPlace *string `json:"description_placeholders"`
|
||||||
|
URL *string `json:"url,omitempty"`
|
||||||
|
Reason *string `json:"reason,omitempty"`
|
||||||
|
Context *string `json:"context,omitempty"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
LastStep *string `json:"last_step"`
|
||||||
|
Options map[string]interface{} `json:"options,omitempty"`
|
||||||
|
Version *int `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
flowCtime() time.Time
|
SchemaItem struct {
|
||||||
}
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema []SchemaItem
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Schemer interface {
|
||||||
|
FlowSchema() Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler interface {
|
||||||
|
BaseHandler() FlowHandler
|
||||||
|
FlowID() FlowID
|
||||||
|
|
||||||
|
flowCtime() time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StepInit Step = "init"
|
StepInit Step = "init"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (fs Schema) CheckRequired(rm map[string]interface{}) error {
|
func (fs *Schema) CheckRequired(rm map[string]interface{}) error {
|
||||||
for _, si := range fs {
|
for _, si := range *fs {
|
||||||
if si.Required {
|
if si.Required {
|
||||||
if _, ok := rm[si.Name]; !ok {
|
if _, ok := rm[si.Name]; !ok {
|
||||||
return fmt.Errorf("missing required param %s", si.Name)
|
return fmt.Errorf("missing required param %s", si.Name)
|
||||||
|
@ -76,6 +94,8 @@ func stepPtr(s Step) *Step { return &s }
|
||||||
type FlowHandler struct {
|
type FlowHandler struct {
|
||||||
ID FlowID // ID is the FlowID
|
ID FlowID // ID is the FlowID
|
||||||
Handler HandlerKey // Handler key
|
Handler HandlerKey // Handler key
|
||||||
|
Context Context // flow Context
|
||||||
|
Schema Schema
|
||||||
|
|
||||||
// curStep is the current step set by the flow manager
|
// curStep is the current step set by the flow manager
|
||||||
curStep Step
|
curStep Step
|
||||||
|
@ -93,10 +113,11 @@ func (f *FlowHandler) FlowID() FlowID {
|
||||||
|
|
||||||
func (f *FlowHandler) flowCtime() time.Time { return f.ctime }
|
func (f *FlowHandler) flowCtime() time.Time { return f.ctime }
|
||||||
|
|
||||||
func NewFlowHandlerBase(hand string) FlowHandler {
|
func NewFlowHandlerBase(sch Schemer, hand string) FlowHandler {
|
||||||
return FlowHandler{
|
return FlowHandler{
|
||||||
ID: FlowID(generate.UUID()),
|
ID: FlowID(generate.UUID()),
|
||||||
Handler: HandlerKey(hand),
|
Handler: HandlerKey(hand),
|
||||||
|
Schema: sch.FlowSchema(),
|
||||||
|
|
||||||
curStep: StepInit,
|
curStep: StepInit,
|
||||||
ctime: time.Now(),
|
ctime: time.Now(),
|
||||||
|
@ -119,39 +140,16 @@ func resultErrs(e Errors) Errors {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormOption func(*Result)
|
func (fm *FlowHandler) ShowForm(errs Errors) *Result {
|
||||||
|
|
||||||
func (*FlowHandler) WithErrors(e Errors) FormOption {
|
|
||||||
return func(r *Result) {
|
|
||||||
r.Errors = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*FlowHandler) WithStep(s Step) FormOption {
|
|
||||||
return func(r *Result) {
|
|
||||||
r.StepID = stepPtr(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*FlowHandler) WithSchema(sch Schemer) FormOption {
|
|
||||||
return func(r *Result) {
|
|
||||||
r.Schema = sch.FlowSchema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fm *FlowHandler) ShowForm(opts ...FormOption) *Result {
|
|
||||||
res := &Result{
|
res := &Result{
|
||||||
Type: TypeForm,
|
Type: TypeForm,
|
||||||
ID: fm.ID,
|
ID: fm.ID,
|
||||||
StepID: stepPtr(fm.curStep),
|
StepID: stepPtr(fm.curStep),
|
||||||
|
Schema: fm.Schema,
|
||||||
Handler: fm.Handlers(),
|
Handler: fm.Handlers(),
|
||||||
|
Errors: resultErrs(errs),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Errors = resultErrs(res.Errors)
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package flow
|
|
||||||
|
|
||||||
type Type string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TypeString Type = "string"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t Type) IsValid() bool {
|
|
||||||
switch t {
|
|
||||||
case TypeString:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type SchemaItem struct {
|
|
||||||
Type Type `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Schema []SchemaItem
|
|
||||||
|
|
||||||
type Schemer interface {
|
|
||||||
FlowSchema() Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSchema(items ...SchemaItem) Schema {
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequiredString(name string) SchemaItem {
|
|
||||||
return SchemaItem{
|
|
||||||
Type: TypeString,
|
|
||||||
Name: name,
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +1,24 @@
|
||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"embed"
|
"embed"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/blas"
|
|
||||||
"dynatron.me/x/blasphem/pkg/blas/core"
|
|
||||||
"dynatron.me/x/blasphem/pkg/components"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FrontendKey = "frontend"
|
|
||||||
|
|
||||||
//go:embed frontend/hass_frontend
|
//go:embed frontend/hass_frontend
|
||||||
var root embed.FS
|
var root embed.FS
|
||||||
|
|
||||||
type Frontend struct {
|
var RootFS fs.FS
|
||||||
fsHandler echo.HandlerFunc
|
|
||||||
rootFS fs.FS
|
|
||||||
|
|
||||||
routeInstall sync.Once
|
var FSHandler echo.HandlerFunc
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *Frontend) InstallRoutes(e *echo.Echo) {
|
func AliasHandler(toFile string) echo.HandlerFunc {
|
||||||
fe.routeInstall.Do(func() {
|
|
||||||
e.GET("/*", fe.fsHandler)
|
|
||||||
e.GET("/auth/authorize", fe.AliasHandler("authorize.html"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fe *Frontend) AliasHandler(toFile string) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
file, err := fe.rootFS.Open(toFile)
|
file, err := RootFS.Open(toFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -51,32 +33,13 @@ func (fe *Frontend) AliasHandler(toFile string) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Frontend) Shutdown() {}
|
func init() {
|
||||||
|
|
||||||
func newData(_ []string) interface{} {
|
|
||||||
return map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsHand(ctx context.Context, wss core.WebSocketSession, msgID int, cmd []string, msg interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Setup(b core.Blas) (components.Component, error) {
|
|
||||||
fe := &Frontend{}
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
fe.rootFS, err = fs.Sub(root, "frontend/hass_frontend")
|
RootFS, err = fs.Sub(root, "frontend/hass_frontend")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fe.fsHandler = echo.StaticDirectoryHandler(fe.rootFS, false)
|
FSHandler = echo.StaticDirectoryHandler(RootFS, false)
|
||||||
|
|
||||||
b.RegisterWSCommand("frontend", wsHand, newData)
|
|
||||||
|
|
||||||
return fe, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
blas.Register(FrontendKey, Setup)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,48 +12,47 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/ziflex/lecho/v3"
|
"github.com/ziflex/lecho/v3"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/pkg/auth"
|
||||||
"dynatron.me/x/blasphem/pkg/blas"
|
"dynatron.me/x/blasphem/pkg/blas"
|
||||||
"dynatron.me/x/blasphem/pkg/blas/core"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
"dynatron.me/x/blasphem/pkg/frontend"
|
"dynatron.me/x/blasphem/pkg/frontend"
|
||||||
conf "dynatron.me/x/blasphem/pkg/server/config"
|
conf "dynatron.me/x/blasphem/pkg/server/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
core.Blas
|
*blas.Blas
|
||||||
*echo.Echo
|
*echo.Echo
|
||||||
|
auth.Authenticator
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteHaver interface {
|
|
||||||
InstallRoutes(e *echo.Echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) installRoutes() {
|
func (s *Server) installRoutes() {
|
||||||
|
s.GET("/*", frontend.FSHandler)
|
||||||
s.GET("/api/websocket", s.wsHandler)
|
s.GET("/api/websocket", s.wsHandler)
|
||||||
|
|
||||||
s.Component(frontend.FrontendKey).(RouteHaver).InstallRoutes(s.Echo)
|
s.Authenticator.InstallRoutes(s.Echo)
|
||||||
s.Blas.(*blas.Blas).Authenticator.InstallRoutes(s.Echo)
|
|
||||||
|
|
||||||
for _, c := range s.Components() {
|
|
||||||
if rh, ok := c.(RouteHaver); ok {
|
|
||||||
rh.InstallRoutes(s.Echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(core core.Blas) (s *Server, err error) {
|
func New(cfg *config.Config) (s *Server, err error) {
|
||||||
|
b, err := blas.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
s = &Server{
|
s = &Server{
|
||||||
Blas: core,
|
Blas: b,
|
||||||
Echo: echo.New(),
|
Echo: echo.New(),
|
||||||
}
|
}
|
||||||
|
err = s.InitAuth(b.Store)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
s.Echo.Debug = true
|
s.Echo.Debug = true
|
||||||
s.Echo.HideBanner = true
|
s.Echo.HideBanner = true
|
||||||
logger := lecho.From(log.Logger)
|
logger := lecho.From(log.Logger)
|
||||||
s.Echo.Logger = logger
|
s.Echo.Logger = logger
|
||||||
|
|
||||||
cfg := s.Conf()
|
|
||||||
|
|
||||||
if cfg.Server.LogRequestErrors {
|
if cfg.Server.LogRequestErrors {
|
||||||
s.Echo.Use(lecho.Middleware(lecho.Config{
|
s.Echo.Use(lecho.Middleware(lecho.Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
@ -81,7 +80,7 @@ func New(core core.Blas) (s *Server, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Shutdown(ctx context.Context) error {
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
err := s.ShutdownBlas(ctx)
|
err := s.Blas.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -92,8 +91,8 @@ 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("version", s.Version()).Str("bind", s.Conf().Server.Bind).Msg("Server listening")
|
log.Info().Str("bind", s.Config.Server.Bind).Msg("Server listening")
|
||||||
err := s.Start(s.Conf().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,12 +1,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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{
|
||||||
|
@ -15,16 +14,7 @@ var upgrader = websocket.Upgrader{
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) wsHandler(c echo.Context) error {
|
func (s *Server) wsHandler(c echo.Context) error {
|
||||||
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
log.Println("WebSocket")
|
||||||
if err != nil {
|
//conn, err := upgrader.Upgrade(w, req, nil)
|
||||||
return err
|
return errors.New("not handled")
|
||||||
}
|
|
||||||
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
_ = log.Debug
|
|
||||||
ctx, cancel := context.WithCancel(c.Request().Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return wsapi.NewSession(s, c, conn).Go(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@ func (s *fsStore) FlushAll() []error {
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fsStore) ShutdownStore() {
|
func (s *fsStore) Shutdown() {
|
||||||
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
|
||||||
|
|
||||||
// ShutdownStore is called to quiesce and shutdown the store.
|
// Shutdown is called to quiesce and shutdown the store.
|
||||||
ShutdownStore()
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
264
pkg/wsapi/api.go
264
pkg/wsapi/api.go
|
@ -1,264 +0,0 @@
|
||||||
package wsapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/auth"
|
|
||||||
"dynatron.me/x/blasphem/pkg/blas/core"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
NoSuchHandlerErr = errors.New("bad websocket command")
|
|
||||||
NoMessageIDErr = errors.New("no message ID")
|
|
||||||
AuthInvalidErr = errors.New("invalid auth")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Type string
|
|
||||||
type MsgBase struct {
|
|
||||||
ID *int `json:"id,omitempty"`
|
|
||||||
Type Type `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
wsSession struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
b core.Blas
|
|
||||||
ec echo.Context
|
|
||||||
write chan<- interface{}
|
|
||||||
|
|
||||||
user *auth.User
|
|
||||||
refreshToken *auth.RefreshToken
|
|
||||||
}
|
|
||||||
|
|
||||||
wsEntry struct {
|
|
||||||
dataNew core.NewData
|
|
||||||
hnd core.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
wsRegistry map[string]wsEntry
|
|
||||||
|
|
||||||
wsManager struct {
|
|
||||||
r wsRegistry
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (wsm *wsManager) RegisterWSCommand(cmd string, hnd core.Handler, dataNew core.NewData) {
|
|
||||||
wsm.r[cmd] = wsEntry{
|
|
||||||
dataNew: dataNew,
|
|
||||||
hnd: hnd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wsm *wsManager) WSCommandHandler(cmd string, cmdSplit []string) (core.NewData, core.Handler, error) {
|
|
||||||
if wse, ok := wsm.r[cmd]; ok {
|
|
||||||
return wse.dataNew, wse.hnd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if wse, ok := wsm.r[cmdSplit[0]]; ok {
|
|
||||||
return wse.dataNew, wse.hnd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil, NoSuchHandlerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager() core.WebSocketManager {
|
|
||||||
return &wsManager{
|
|
||||||
r: make(wsRegistry),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession(s core.Blas, c echo.Context, conn *websocket.Conn) core.WebSocketSession {
|
|
||||||
ws := &wsSession{
|
|
||||||
conn: conn,
|
|
||||||
b: s,
|
|
||||||
ec: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ws
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *wsSession) Conn() *websocket.Conn {
|
|
||||||
return ws.conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *wsSession) Blas() core.Blas { return ws.b }
|
|
||||||
|
|
||||||
func (ws *wsSession) Go(ctx context.Context) error {
|
|
||||||
authP := &authPhase{ws}
|
|
||||||
err := ws.sendAuthRequired()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, rdr, err := ws.conn.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = authP.handleMsg(ctx, rdr)
|
|
||||||
if err != nil || ws.refreshToken == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// command phase
|
|
||||||
|
|
||||||
msgChan := make(chan map[string]interface{})
|
|
||||||
write := make(chan interface{})
|
|
||||||
ws.write = write
|
|
||||||
defer close(write)
|
|
||||||
|
|
||||||
cCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
go func(ctx context.Context, ch chan<- map[string]interface{}) {
|
|
||||||
defer close(ch)
|
|
||||||
for {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, rdr, err := ws.conn.NextReader()
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
case websocket.IsCloseError(err, websocket.CloseGoingAway):
|
|
||||||
return
|
|
||||||
case err != nil:
|
|
||||||
log.Error().Err(err).Str("remote", ws.ec.Request().RemoteAddr).Msg("websocket read fail")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var msgMap map[string]interface{}
|
|
||||||
err = json.NewDecoder(rdr).Decode(&msgMap)
|
|
||||||
if err != nil {
|
|
||||||
ws.writeError(-1, Error{Code: "invalid_format", Message: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
ch <- msgMap
|
|
||||||
|
|
||||||
}
|
|
||||||
}(cCtx, msgChan)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case msg, ok := <-msgChan:
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ws.handleMsg(ctx, msg)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("handleMsg")
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
close(msgChan) // maybe remove this?
|
|
||||||
return nil
|
|
||||||
case m := <-write:
|
|
||||||
err := ws.conn.WriteJSON(m)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("writeMsg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *wsSession) Write(msg interface{}) {
|
|
||||||
ws.write <- msg
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ResultMsgType MsgType = "result"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WSError struct {
|
|
||||||
ID *int `json:"id,omitempty"`
|
|
||||||
Type MsgType `json:"type"`
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Error Error `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *wsSession) writeError(id int, err Error) error {
|
|
||||||
return ws.conn.WriteJSON(WSError{
|
|
||||||
ID: &id,
|
|
||||||
Type: ResultMsgType,
|
|
||||||
Success: false,
|
|
||||||
Error: err,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *wsSession) handleMsg(ctx context.Context, msgMap map[string]interface{}) error {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
msgType, ok := msgMap["type"].(string)
|
|
||||||
if !ok {
|
|
||||||
return NoSuchHandlerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
idFl, ok := msgMap["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ws.Write(WSError{
|
|
||||||
Type: ResultMsgType,
|
|
||||||
Success: false,
|
|
||||||
Error: Error{
|
|
||||||
Code: "invalid_id",
|
|
||||||
Message: "command has no ID",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id := int(idFl)
|
|
||||||
|
|
||||||
cmd := strings.Split(msgType, "/")
|
|
||||||
|
|
||||||
newData, hand, err := ws.b.WSCommandHandler(cmd[0], cmd)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
case NoSuchHandlerErr:
|
|
||||||
ws.writeError(id, Error{
|
|
||||||
Code: "invalid_type",
|
|
||||||
Message: "no such command",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
log.Error().Err(err).Msg("dispatch")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nd := newData(cmd)
|
|
||||||
if _, ok := nd.(map[string]interface{}); !ok {
|
|
||||||
err := mapstructure.Decode(&msgMap, &nd)
|
|
||||||
if err != nil {
|
|
||||||
ws.writeError(id, Error{
|
|
||||||
Code: "invalid_format",
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = hand(ctx, ws, id, cmd, nd)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("dispatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
package wsapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"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:"ha_version"`
|
|
||||||
}{
|
|
||||||
MsgBase{Type: "auth_required"},
|
|
||||||
ws.b.Version(),
|
|
||||||
}
|
|
||||||
return ws.conn.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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap *authPhase) sendAuthOK() error {
|
|
||||||
return ap.conn.WriteJSON(struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Version string `json:"ha_version"`
|
|
||||||
}{Type: "auth_ok", Version: ap.Blas().Version()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap *authPhase) sendAuthInvalid() error {
|
|
||||||
return ap.conn.WriteJSON(struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{Type: "auth_ok", Message: "invalid auth"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap *authPhase) handleMsg(ctx context.Context, r io.Reader) error {
|
|
||||||
var authMsg authMsg
|
|
||||||
err := json.NewDecoder(r).Decode(&authMsg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshToken := ap.b.ValidateAccessToken(authMsg.AccessToken)
|
|
||||||
if refreshToken != nil {
|
|
||||||
ap.finishAuth(refreshToken)
|
|
||||||
return ap.sendAuthOK()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error().Str("remote", ap.ec.Request().RemoteAddr).Msg("websocket auth failed")
|
|
||||||
|
|
||||||
err = ap.sendAuthInvalid()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthInvalidErr
|
|
||||||
}
|
|
Loading…
Reference in a new issue