WIP: Login "works"
This commit is contained in:
parent
17149f2c58
commit
cb4ccc37b2
10 changed files with 367 additions and 58 deletions
|
@ -25,6 +25,6 @@ func main() {
|
||||||
|
|
||||||
err = rootCmd.Execute()
|
err = rootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -17,8 +17,8 @@ require (
|
||||||
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
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
golang.org/x/crypto v0.1.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
golang.org/x/net v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -30,14 +30,22 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||||
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|
|
@ -1,26 +1,57 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/pkg/frontend"
|
"dynatron.me/x/blasphem/pkg/frontend"
|
||||||
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidAuth = errors.New("invalid auth")
|
||||||
|
ErrInvalidHandler = errors.New("no such handler")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
Flows FlowStore
|
Flows FlowStore
|
||||||
|
Providers map[string]AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) InitAuth() {
|
func (a *Authenticator) Provider(name string) AuthProvider {
|
||||||
|
p, ok := a.Providers[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||||
a.Flows = make(FlowStore)
|
a.Flows = make(FlowStore)
|
||||||
|
hap, err := NewHAProvider(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: yuck
|
||||||
|
a.Providers = map[string]AuthProvider{
|
||||||
|
hap.ProviderType(): hap,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthProvider interface {
|
type AuthProvider interface {
|
||||||
ProviderName() string
|
ProviderName() string
|
||||||
ProviderID() *string
|
ProviderID() *string
|
||||||
ProviderType() string
|
ProviderType() string
|
||||||
|
ProviderBase() AuthProviderBase
|
||||||
|
FlowSchema() []FlowSchemaItem
|
||||||
|
ValidateCreds(reqMap map[string]interface{}) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthProviderBase struct {
|
type AuthProviderBase struct {
|
||||||
|
@ -29,29 +60,17 @@ type AuthProviderBase struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
||||||
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
||||||
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
||||||
|
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
||||||
type LocalProvider struct {
|
|
||||||
AuthProviderBase
|
|
||||||
}
|
|
||||||
|
|
||||||
var HomeAssistant = "homeassistant"
|
var HomeAssistant = "homeassistant"
|
||||||
|
|
||||||
func hassProvider() *LocalProvider {
|
|
||||||
return &LocalProvider{
|
|
||||||
AuthProviderBase: AuthProviderBase{
|
|
||||||
Name: "Home Assistant Local",
|
|
||||||
Type: HomeAssistant,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
func (s *Authenticator) ProvidersHandler(c echo.Context) error {
|
func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||||
providers := []AuthProvider{
|
providers := []AuthProviderBase{
|
||||||
hassProvider(),
|
a.Provider(HomeAssistant).ProviderBase(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, providers)
|
return c.JSON(http.StatusOK, providers)
|
||||||
|
@ -71,3 +90,29 @@ func (s *Authenticator) AuthorizeHandler(c echo.Context) error {
|
||||||
|
|
||||||
return c.HTML(http.StatusOK, string(b))
|
return c.HTML(http.StatusOK, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) error {
|
||||||
|
cID, hasCID := rm["client_id"]
|
||||||
|
if !hasCID || cID != f.request.ClientID {
|
||||||
|
return ErrInvalidAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range f.Handler {
|
||||||
|
if h == nil {
|
||||||
|
return ErrInvalidHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
p := a.Provider(*h)
|
||||||
|
if p == nil {
|
||||||
|
return ErrInvalidAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
success := p.ValidateCreds(rm)
|
||||||
|
|
||||||
|
if success {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrInvalidAuth
|
||||||
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ type Flow struct {
|
||||||
Handler []*string `json:"handler"`
|
Handler []*string `json:"handler"`
|
||||||
StepID Step `json:"step_id"`
|
StepID Step `json:"step_id"`
|
||||||
Schema []FlowSchemaItem `json:"data_schema"`
|
Schema []FlowSchemaItem `json:"data_schema"`
|
||||||
Errors []string `json:"errors"`
|
Errors interface{} `json:"errors"`
|
||||||
DescPlace *string `json:"description_placeholders"`
|
DescPlace *string `json:"description_placeholders"`
|
||||||
LastStep *string `json:"last_step"`
|
LastStep *string `json:"last_step"`
|
||||||
|
|
||||||
|
@ -89,22 +89,28 @@ func (fs FlowStore) Get(id FlowID) *Flow {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
||||||
|
var sch []FlowSchemaItem
|
||||||
|
|
||||||
|
for _, h := range r.Handler {
|
||||||
|
if h == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if hand := a.Provider(*h); hand != nil {
|
||||||
|
sch = hand.FlowSchema()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sch == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
flow := &Flow{
|
flow := &Flow{
|
||||||
Type: TypeForm,
|
Type: TypeForm,
|
||||||
ID: genFlowID(),
|
ID: genFlowID(),
|
||||||
StepID: StepInit,
|
StepID: StepInit,
|
||||||
Schema: []FlowSchemaItem{
|
Schema: sch,
|
||||||
{
|
|
||||||
Type: "string",
|
|
||||||
Name: "username",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "string",
|
|
||||||
Name: "password",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Handler: r.Handler,
|
Handler: r.Handler,
|
||||||
Errors: []string{},
|
Errors: []string{},
|
||||||
request: r,
|
request: r,
|
||||||
|
@ -116,10 +122,10 @@ func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
||||||
return flow
|
return flow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Flow) progress(c echo.Context) error {
|
func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||||
switch f.StepID {
|
switch f.StepID {
|
||||||
case StepInit:
|
case StepInit:
|
||||||
var rm map[string]interface{}
|
rm := make(map[string]interface{})
|
||||||
|
|
||||||
err := c.Bind(&rm)
|
err := c.Bind(&rm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -133,7 +139,20 @@ func (f *Flow) progress(c echo.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: lookup creds, pass, redirect to url
|
err = a.Check(f, rm)
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
return c.String(http.StatusOK, "login success!")
|
||||||
|
case ErrInvalidHandler:
|
||||||
|
return c.String(http.StatusNotFound, err.Error())
|
||||||
|
case ErrInvalidAuth:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
f.Errors = map[string]interface{}{
|
||||||
|
"base": "invalid_auth",
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, f)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return c.String(http.StatusBadRequest, "unknown flow step")
|
return c.String(http.StatusBadRequest, "unknown flow step")
|
||||||
}
|
}
|
||||||
|
@ -156,7 +175,7 @@ func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
||||||
func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
if c.Request().Method == http.MethodPost && strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "text/plain") {
|
if c.Request().Method == http.MethodPost && strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "text/plain") {
|
||||||
// hack around the content-type, Context.JSON refuses to work otherwise
|
// hack around the content-type, Context.JSON refuses to work otherwise
|
||||||
c.Request().Header.Set(echo.HeaderContentType, "application/json")
|
c.Request().Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
}
|
}
|
||||||
|
|
||||||
flowID := c.Param("flow_id")
|
flowID := c.Param("flow_id")
|
||||||
|
@ -171,6 +190,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
|
|
||||||
resp := a.NewFlow(&flowReq)
|
resp := a.NewFlow(&flowReq)
|
||||||
|
|
||||||
|
if resp == nil {
|
||||||
|
return c.String(http.StatusBadRequest, "no such handler")
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, resp)
|
return c.JSON(http.StatusOK, resp)
|
||||||
default:
|
default:
|
||||||
flow := a.Flows.Get(FlowID(flowID))
|
flow := a.Flows.Get(FlowID(flowID))
|
||||||
|
@ -178,6 +201,6 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||||
return c.String(http.StatusNotFound, "no such flow")
|
return c.String(http.StatusNotFound, "no such flow")
|
||||||
}
|
}
|
||||||
|
|
||||||
return flow.progress(c)
|
return flow.progress(a, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
107
pkg/auth/provider.go
Normal file
107
pkg/auth/provider.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HAProviderKey = "auth_provider.homeassistant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
type HomeAssistantProvider struct {
|
||||||
|
AuthProviderBase `json:"-"`
|
||||||
|
Users []User `json:"users"`
|
||||||
|
|
||||||
|
salt string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
|
||||||
|
hap := &HomeAssistantProvider{
|
||||||
|
AuthProviderBase: AuthProviderBase{
|
||||||
|
Name: "Home Assistant Local",
|
||||||
|
Type: HomeAssistant,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Get(HAProviderKey, hap)
|
||||||
|
if err != nil {
|
||||||
|
return hap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genSalt() string {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
rand.Read(b)
|
||||||
|
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
|
||||||
|
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool {
|
||||||
|
usernameE, hasU := rm["username"]
|
||||||
|
passwordE, hasP := rm["password"]
|
||||||
|
username, unStr := usernameE.(string)
|
||||||
|
password, paStr := passwordE.(string)
|
||||||
|
if !hasU || !hasP || !unStr || !paStr || username == "" || password == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var found *User
|
||||||
|
|
||||||
|
const dummyHash = "$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||||
|
|
||||||
|
for _, v := range hap.Users { // iterate all to thwart timing attacks
|
||||||
|
if v.Username == username {
|
||||||
|
found = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found == nil { // one more compare to thwart timing attacks
|
||||||
|
bcrypt.CompareHashAndPassword([]byte("foo"), []byte(dummyHash))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash []byte
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(found.Password)
|
||||||
|
if err != nil {
|
||||||
|
// XXX: probably log this
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem {
|
||||||
|
return []FlowSchemaItem{
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "username",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
Name: "password",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,74 @@
|
||||||
package bus
|
package bus
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
type Bus struct {
|
type (
|
||||||
}
|
Event struct {
|
||||||
|
EvType string
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners []chan<- Event
|
||||||
|
|
||||||
|
Bus struct {
|
||||||
|
sync.RWMutex
|
||||||
|
subs map[string]listeners
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func New() *Bus {
|
func New() *Bus {
|
||||||
bus := &Bus{}
|
bus := &Bus{
|
||||||
|
subs: make(map[string]listeners),
|
||||||
|
}
|
||||||
|
|
||||||
return bus
|
return bus
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Sub(topic string, ch chan<- Event) {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
if prev, ok := b.subs[topic]; ok {
|
||||||
|
b.subs[topic] = append(prev, ch)
|
||||||
|
} else {
|
||||||
|
b.subs[topic] = append(listeners{}, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Unsub(topic string, ch chan<- Event) {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
for i, v := range b.subs[topic] {
|
||||||
|
if v == ch {
|
||||||
|
// we don't care about order, replace and reslice
|
||||||
|
b.subs[topic][i] = b.subs[topic][len(b.subs[topic])-1]
|
||||||
|
b.subs[topic] = b.subs[topic][:len(b.subs[topic])-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Pub(topic string, data interface{}) {
|
||||||
|
b.RLock()
|
||||||
|
defer b.RUnlock()
|
||||||
|
|
||||||
|
tc, ok := b.subs[topic]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range tc {
|
||||||
|
ch <- Event{EvType: topic, Data: data}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Shutdown() {
|
||||||
|
for _, v := range b.subs {
|
||||||
|
for _, c := range v {
|
||||||
|
close(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func ReadConfig() (*Config, error) {
|
func ReadConfig() (*Config, error) {
|
||||||
|
// TODO: make this use an arg or an env, and unify with pkg/storage/'s home determination
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/pkg/auth"
|
"dynatron.me/x/blasphem/pkg/auth"
|
||||||
"dynatron.me/x/blasphem/pkg/blas"
|
"dynatron.me/x/blasphem/pkg/blas"
|
||||||
"dynatron.me/x/blasphem/pkg/config"
|
"dynatron.me/x/blasphem/pkg/config"
|
||||||
"dynatron.me/x/blasphem/pkg/frontend"
|
"dynatron.me/x/blasphem/pkg/frontend"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const LogHeader = `${time_rfc3339} ${level} ${prefix} ${short_file}:${line}`
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*blas.Blas
|
*blas.Blas
|
||||||
*echo.Echo
|
*echo.Echo
|
||||||
|
@ -42,28 +49,54 @@ func New(cfg *config.Config) (s *Server, err error) {
|
||||||
Blas: b,
|
Blas: b,
|
||||||
Echo: echo.New(),
|
Echo: echo.New(),
|
||||||
}
|
}
|
||||||
s.InitAuth()
|
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
|
||||||
|
s.Echo.Logger.SetPrefix(common.AppName)
|
||||||
|
s.Echo.Logger.SetHeader(LogHeader)
|
||||||
|
|
||||||
s.installRoutes()
|
s.installRoutes()
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
err := s.Blas.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Echo.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Go() error {
|
func (s *Server) Go() error {
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
err := s.Start(s.Config.Server.Bind)
|
err := s.Start(s.Config.Server.Bind)
|
||||||
if err != nil {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatal(err)
|
s.Logger.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.wg.Done()
|
s.wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
|
||||||
|
// Use a buffered channel to avoid missing signals as recommended for signal.Notify
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
<-quit
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := s.Shutdown(ctx); err != nil {
|
||||||
|
s.Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
|
s.Logger.Info("shutdown complete")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,15 +16,43 @@ type Item struct {
|
||||||
Data Data `json:"data"`
|
Data Data `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store struct {
|
type store struct {
|
||||||
fs.FS
|
fs.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(dir fs.FS) (*Store, error) {
|
type Store interface {
|
||||||
|
Get(key string, data interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) Get(key string, data interface{}) error {
|
||||||
|
f, err := s.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
item := Item{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
d := json.NewDecoder(f)
|
||||||
|
err = d.Decode(&item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Key != key {
|
||||||
|
return fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(dir fs.FS) (*store, error) {
|
||||||
stor, err := fs.Sub(dir, ".storage")
|
stor, err := fs.Sub(dir, ".storage")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Store{stor}, nil
|
return &store{stor}, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue