move server to server directory.

This commit is contained in:
Miroslav Šedivý
2024-06-23 17:48:14 +02:00
parent da45f62ca8
commit 5b98344205
211 changed files with 18 additions and 10 deletions

View File

@ -0,0 +1,204 @@
package file
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"os"
"github.com/demodesk/neko/pkg/types"
)
func New(config Config) types.MemberProvider {
return &MemberProviderCtx{
config: config,
}
}
type MemberProviderCtx struct {
config Config
}
func (provider *MemberProviderCtx) hash(password string) string {
// if hash is disabled, return password as plain text
if !provider.config.Hash {
return password
}
sha256 := sha256.New()
sha256.Write([]byte(password))
hashedPassword := sha256.Sum(nil)
return base64.StdEncoding.EncodeToString(hashedPassword)
}
func (provider *MemberProviderCtx) Connect() error {
return nil
}
func (provider *MemberProviderCtx) Disconnect() error {
return nil
}
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
// id will be also username
id := username
entry, err := provider.getEntry(id)
if err != nil {
return "", types.MemberProfile{}, err
}
if entry.Password != provider.hash(password) {
return "", types.MemberProfile{}, types.ErrMemberInvalidPassword
}
return id, entry.Profile, nil
}
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
// id will be also username
id := username
entries, err := provider.deserialize()
if err != nil {
return "", err
}
_, ok := entries[id]
if ok {
return "", types.ErrMemberAlreadyExists
}
entries[id] = MemberEntry{
Password: provider.hash(password),
Profile: profile,
}
return id, provider.serialize(entries)
}
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
entries, err := provider.deserialize()
if err != nil {
return err
}
entry, ok := entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
entry.Profile = profile
entries[id] = entry
return provider.serialize(entries)
}
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
entries, err := provider.deserialize()
if err != nil {
return err
}
entry, ok := entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
entry.Password = provider.hash(password)
entries[id] = entry
return provider.serialize(entries)
}
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
entry, err := provider.getEntry(id)
if err != nil {
return types.MemberProfile{}, err
}
return entry.Profile, nil
}
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
profiles := map[string]types.MemberProfile{}
entries, err := provider.deserialize()
if err != nil {
return profiles, err
}
i := 0
for id, entry := range entries {
if i >= offset && (limit == 0 || i < offset+limit) {
profiles[id] = entry.Profile
}
i = i + 1
}
return profiles, nil
}
func (provider *MemberProviderCtx) Delete(id string) error {
entries, err := provider.deserialize()
if err != nil {
return err
}
_, ok := entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
delete(entries, id)
return provider.serialize(entries)
}
func (provider *MemberProviderCtx) deserialize() (map[string]MemberEntry, error) {
file, err := os.OpenFile(provider.config.Path, os.O_RDONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return nil, err
}
raw, err := io.ReadAll(file)
if err != nil {
return nil, err
}
if len(raw) == 0 {
return map[string]MemberEntry{}, nil
}
var entries map[string]MemberEntry
if err := json.Unmarshal([]byte(raw), &entries); err != nil {
return nil, err
}
return entries, nil
}
func (provider *MemberProviderCtx) getEntry(id string) (MemberEntry, error) {
entries, err := provider.deserialize()
if err != nil {
return MemberEntry{}, err
}
entry, ok := entries[id]
if !ok {
return MemberEntry{}, types.ErrMemberDoesNotExist
}
return entry, nil
}
func (provider *MemberProviderCtx) serialize(data map[string]MemberEntry) error {
raw, err := json.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(provider.config.Path, raw, os.ModePerm)
}

View File

@ -0,0 +1,48 @@
package file
import (
"encoding/json"
"testing"
"github.com/demodesk/neko/pkg/utils"
)
// Ensure that hashes are the same after encoding and decoding using json
func TestMemberProviderCtx_hash(t *testing.T) {
provider := &MemberProviderCtx{
config: Config{
Hash: true,
},
}
// generate random strings
passwords := []string{}
for i := 0; i < 10; i++ {
password, err := utils.NewUID(32)
if err != nil {
t.Errorf("utils.NewUID() returned error: %s", err)
}
passwords = append(passwords, password)
}
for _, password := range passwords {
hashedPassword := provider.hash(password)
// json encode password hash
hashedPasswordJSON, err := json.Marshal(hashedPassword)
if err != nil {
t.Errorf("json.Marshal() returned error: %s", err)
}
// json decode password hash json
var hashedPasswordStr string
err = json.Unmarshal(hashedPasswordJSON, &hashedPasswordStr)
if err != nil {
t.Errorf("json.Unmarshal() returned error: %s", err)
}
if hashedPasswordStr != hashedPassword {
t.Errorf("hashedPasswordStr: %s != hashedPassword: %s", hashedPasswordStr, hashedPassword)
}
}
}

View File

@ -0,0 +1,15 @@
package file
import (
"github.com/demodesk/neko/pkg/types"
)
type MemberEntry struct {
Password string `json:"password"`
Profile types.MemberProfile `json:"profile"`
}
type Config struct {
Path string
Hash bool
}

View File

@ -0,0 +1,168 @@
package member
import (
"errors"
"sync"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/demodesk/neko/internal/config"
"github.com/demodesk/neko/internal/member/file"
"github.com/demodesk/neko/internal/member/multiuser"
"github.com/demodesk/neko/internal/member/noauth"
"github.com/demodesk/neko/internal/member/object"
"github.com/demodesk/neko/pkg/types"
)
func New(sessions types.SessionManager, config *config.Member) *MemberManagerCtx {
manager := &MemberManagerCtx{
logger: log.With().Str("module", "member").Logger(),
sessions: sessions,
config: config,
}
switch config.Provider {
case "file":
manager.provider = file.New(config.File)
case "object":
manager.provider = object.New(config.Object)
case "multiuser":
manager.provider = multiuser.New(config.Multiuser)
case "noauth":
fallthrough
default:
manager.provider = noauth.New()
}
return manager
}
type MemberManagerCtx struct {
logger zerolog.Logger
sessions types.SessionManager
config *config.Member
providerMu sync.Mutex
provider types.MemberProvider
loginMu sync.Mutex
}
func (manager *MemberManagerCtx) Connect() error {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.Connect()
}
func (manager *MemberManagerCtx) Disconnect() error {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.Disconnect()
}
func (manager *MemberManagerCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.Authenticate(username, password)
}
func (manager *MemberManagerCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.Insert(username, password, profile)
}
func (manager *MemberManagerCtx) Select(id string) (types.MemberProfile, error) {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
// get primarily from corresponding session, if exists
session, ok := manager.sessions.Get(id)
if ok {
return session.Profile(), nil
}
return manager.provider.Select(id)
}
func (manager *MemberManagerCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.SelectAll(limit, offset)
}
func (manager *MemberManagerCtx) UpdateProfile(id string, profile types.MemberProfile) error {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
// update corresponding session, if exists
err := manager.sessions.Update(id, profile)
if err != nil && !errors.Is(err, types.ErrSessionNotFound) {
manager.logger.Err(err).Msg("error while updating session")
}
return manager.provider.UpdateProfile(id, profile)
}
func (manager *MemberManagerCtx) UpdatePassword(id string, password string) error {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
return manager.provider.UpdatePassword(id, password)
}
func (manager *MemberManagerCtx) Delete(id string) error {
manager.providerMu.Lock()
defer manager.providerMu.Unlock()
// destroy corresponding session, if exists
err := manager.sessions.Delete(id)
if err != nil && !errors.Is(err, types.ErrSessionNotFound) {
manager.logger.Err(err).Msg("error while deleting session")
}
return manager.provider.Delete(id)
}
//
// member -> session
//
func (manager *MemberManagerCtx) Login(username string, password string) (types.Session, string, error) {
manager.loginMu.Lock()
defer manager.loginMu.Unlock()
id, profile, err := manager.provider.Authenticate(username, password)
if err != nil {
return nil, "", err
}
if !profile.IsAdmin && manager.sessions.Settings().LockedLogins {
return nil, "", types.ErrSessionLoginsLocked
}
session, ok := manager.sessions.Get(id)
if ok {
if session.State().IsConnected {
return nil, "", types.ErrSessionAlreadyConnected
}
// TODO: Replace session.
if err := manager.sessions.Delete(id); err != nil {
return nil, "", err
}
}
return manager.sessions.Create(id, profile)
}
func (manager *MemberManagerCtx) Logout(id string) error {
manager.loginMu.Lock()
defer manager.loginMu.Unlock()
return manager.sessions.Delete(id)
}

View File

@ -0,0 +1,82 @@
package multiuser
import (
"errors"
"fmt"
"github.com/demodesk/neko/pkg/types"
"github.com/demodesk/neko/pkg/utils"
)
func New(config Config) types.MemberProvider {
return &MemberProviderCtx{
config: config,
}
}
type MemberProviderCtx struct {
config Config
}
func (provider *MemberProviderCtx) Connect() error {
return nil
}
func (provider *MemberProviderCtx) Disconnect() error {
return nil
}
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
// generate random token
token, err := utils.NewUID(5)
if err != nil {
return "", types.MemberProfile{}, err
}
// id is username with token
id := fmt.Sprintf("%s-%s", username, token)
// if logged in as administrator
if provider.config.AdminPassword == password {
profile := provider.config.AdminProfile
if profile.Name == "" {
profile.Name = username
}
return id, profile, nil
}
// if logged in as user
if provider.config.UserPassword == password {
profile := provider.config.UserProfile
if profile.Name == "" {
profile.Name = username
}
return id, profile, nil
}
return "", types.MemberProfile{}, types.ErrMemberInvalidPassword
}
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
return "", errors.New("new user is created on first login in multiuser mode")
}
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
return nil
}
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
return errors.New("password can only be modified in config while in multiuser mode")
}
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
return types.MemberProfile{}, errors.New("cannot select user in multiuser mode")
}
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
return map[string]types.MemberProfile{}, nil
}
func (provider *MemberProviderCtx) Delete(id string) error {
return errors.New("cannot delete user in multiuser mode")
}

View File

@ -0,0 +1,10 @@
package multiuser
import "github.com/demodesk/neko/pkg/types"
type Config struct {
AdminPassword string
UserPassword string
AdminProfile types.MemberProfile
UserProfile types.MemberProfile
}

View File

@ -0,0 +1,75 @@
package noauth
import (
"errors"
"fmt"
"github.com/demodesk/neko/pkg/types"
"github.com/demodesk/neko/pkg/utils"
)
func New() types.MemberProvider {
return &MemberProviderCtx{
profile: types.MemberProfile{
IsAdmin: true,
CanLogin: true,
CanConnect: true,
CanWatch: true,
CanHost: true,
CanShareMedia: true,
CanAccessClipboard: true,
SendsInactiveCursor: true,
CanSeeInactiveCursors: true,
},
}
}
type MemberProviderCtx struct {
profile types.MemberProfile
}
func (provider *MemberProviderCtx) Connect() error {
return nil
}
func (provider *MemberProviderCtx) Disconnect() error {
return nil
}
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
// generate random token
token, err := utils.NewUID(5)
if err != nil {
return "", types.MemberProfile{}, err
}
// id is username with token
id := fmt.Sprintf("%s-%s", username, token)
provider.profile.Name = username
return id, provider.profile, nil
}
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
return "", errors.New("new user is created on first login in noauth mode")
}
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
return nil
}
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
return errors.New("noauth mode does not have password")
}
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
return types.MemberProfile{}, errors.New("cannot select user in noauth mode")
}
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
return map[string]types.MemberProfile{}, nil
}
func (provider *MemberProviderCtx) Delete(id string) error {
return errors.New("cannot delete user in noauth mode")
}

View File

@ -0,0 +1,124 @@
package object
import (
"github.com/demodesk/neko/pkg/types"
)
func New(config Config) types.MemberProvider {
return &MemberProviderCtx{
config: config,
entries: make(map[string]*memberEntry),
}
}
type MemberProviderCtx struct {
config Config
entries map[string]*memberEntry
}
func (provider *MemberProviderCtx) Connect() error {
var err error
for _, entry := range provider.config.Users {
_, err = provider.Insert(entry.Username, entry.Password, entry.Profile)
}
return err
}
func (provider *MemberProviderCtx) Disconnect() error {
return nil
}
func (provider *MemberProviderCtx) Authenticate(username string, password string) (string, types.MemberProfile, error) {
// id will be also username
id := username
entry, ok := provider.entries[id]
if !ok {
return "", types.MemberProfile{}, types.ErrMemberDoesNotExist
}
// TODO: Use hash function.
if !entry.CheckPassword(password) {
return "", types.MemberProfile{}, types.ErrMemberInvalidPassword
}
return id, entry.profile, nil
}
func (provider *MemberProviderCtx) Insert(username string, password string, profile types.MemberProfile) (string, error) {
// id will be also username
id := username
_, ok := provider.entries[id]
if ok {
return "", types.ErrMemberAlreadyExists
}
provider.entries[id] = &memberEntry{
// TODO: Use hash function.
password: password,
profile: profile,
}
return id, nil
}
func (provider *MemberProviderCtx) UpdateProfile(id string, profile types.MemberProfile) error {
entry, ok := provider.entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
entry.profile = profile
return nil
}
func (provider *MemberProviderCtx) UpdatePassword(id string, password string) error {
entry, ok := provider.entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
// TODO: Use hash function.
entry.password = password
return nil
}
func (provider *MemberProviderCtx) Select(id string) (types.MemberProfile, error) {
entry, ok := provider.entries[id]
if !ok {
return types.MemberProfile{}, types.ErrMemberDoesNotExist
}
return entry.profile, nil
}
func (provider *MemberProviderCtx) SelectAll(limit int, offset int) (map[string]types.MemberProfile, error) {
profiles := make(map[string]types.MemberProfile)
i := 0
for id, entry := range provider.entries {
if i >= offset && (limit == 0 || i < offset+limit) {
profiles[id] = entry.profile
}
i = i + 1
}
return profiles, nil
}
func (provider *MemberProviderCtx) Delete(id string) error {
_, ok := provider.entries[id]
if !ok {
return types.ErrMemberDoesNotExist
}
delete(provider.entries, id)
return nil
}

View File

@ -0,0 +1,24 @@
package object
import (
"github.com/demodesk/neko/pkg/types"
)
type memberEntry struct {
password string
profile types.MemberProfile
}
func (m *memberEntry) CheckPassword(password string) bool {
return m.password == password
}
type User struct {
Username string
Password string
Profile types.MemberProfile
}
type Config struct {
Users []User
}