diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-03-14 11:23:50 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-03-14 11:23:50 -0400 |
| commit | 700096b078f6e16f2c5c967ea140f0b93e799986 (patch) | |
| tree | e1d9965e9bf09ebe01a96d8faa6d2bc027e6984c | |
| parent | 7d622695ae19e518fded6aa5fbf001dae4652211 (diff) | |
| download | buth-harveyos.zip | |
authfs: rewrite with Harvey-OS/ninep, implement Rattachharveyos
| -rw-r--r-- | back/auth/auth.go | 36 | ||||
| -rw-r--r-- | back/cmd/authfs/authfs.go | 130 | ||||
| -rw-r--r-- | back/cmd/authfs/err.go | 10 | ||||
| -rw-r--r-- | back/cmd/authfs/main.go | 65 | ||||
| -rw-r--r-- | back/cmd/authfs/root.go | 36 | ||||
| -rw-r--r-- | back/cmd/authfs/sess.go | 48 | ||||
| -rw-r--r-- | back/cmd/authfs/sessdir.go | 60 | ||||
| -rw-r--r-- | back/cmd/authfs/session.go | 187 | ||||
| -rw-r--r-- | back/cmd/authfs/userdir.go | 51 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 15 |
11 files changed, 401 insertions, 244 deletions
diff --git a/back/auth/auth.go b/back/auth/auth.go index 5685001..9625cc7 100644 --- a/back/auth/auth.go +++ b/back/auth/auth.go @@ -1,17 +1,37 @@ package auth -import "fmt" +import ( + "fmt" + "time" +) const ( - // Maximum number of bytes in a Username. - MaxUsernameSize = 64 + // Maximum number of bytes in a username. + MaxUnameSize = 64 + + // Number of bytes in a session ID. + SessIdSize = 32 + SessTimeout = 1 * time.Hour ) -type Username string +// User name. +type Uname string + +// Session ID. +type SessId [SessIdSize]byte + +func ParseUname(s string) (Uname, error) { + if len(s) > MaxUnameSize { + return "", fmt.Errorf("username longer than %d bytes: %q", MaxUnameSize, s) + } + return Uname(s), nil +} -func ValidiateUsername(s string) (Username, error) { - if len(s) > MaxUsernameSize { - return "", fmt.Errorf("username longer than %d bytes: %q", MaxUsernameSize, s) +func ParseSessId(s string) (SessId, error) { + if len(s) != SessIdSize { + return SessId{}, fmt.Errorf("wrong session ID size: %d (want %d): %q", len(s), SessIdSize, s) } - return Username(s), nil + var id SessId + copy(id[:], s) + return id, nil } diff --git a/back/cmd/authfs/authfs.go b/back/cmd/authfs/authfs.go new file mode 100644 index 0000000..e2a8bf9 --- /dev/null +++ b/back/cmd/authfs/authfs.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "path" + "sync" + + p9 "github.com/Harvey-OS/ninep/protocol" + + "git.samanthony.xyz/buth/back/auth" +) + +const ( + _ uint64 = iota + rootQidPath + usersQidPath + sessionsQidPath +) + +type File interface { + Qid() (p9.QID, error) + // TODO +} + +type Dir interface { + File + // TODO: walk, create +} + +// Authfs is the root p9.NineServer. +type Authfs struct { + *RootDir + *UsersDir + *SessionsDir + + mu sync.Mutex // guards below + files map[p9.FID]File +} + +func NewAuthfs(rootPath string) (*Authfs, error) { + root := NewRootDir() + users, err := NewUsersDir(rootPath, root.Version) + if err != nil { + return nil, err + } + return &Authfs{ + root, + users, + NewSessionsDir(root.Version), + sync.Mutex{}, + make(map[p9.FID]File), + }, nil +} + +func (fs *Authfs) Close() { + fs.RootDir.Close() + fs.UsersDir.Close() + fs.SessionsDir.Close() +} + +func (fs *Authfs) Rattach(fid, afid p9.FID, uname, aname string) (p9.QID, error) { + if afid != p9.NOFID { + return p9.QID{}, fmt.Errorf("authfs: no authentication required") + } + + var ( + f File + err error + ok bool + ) + + aname = path.Clean(path.Join("/", aname)) + dir, file := path.Split(aname) + switch dir { + case "/": + switch file { + case "": + f = fs.RootDir + case "users": + f = fs.UsersDir + case "sessions": + f = fs.SessionsDir + default: + return p9.QID{}, ErrFileNotExist + } + + case "/users/": + f, err = fs.UsersDir.Walk(file) + if err != nil { + return p9.QID{}, err + } + + case "/sessions/": + sessid, err := auth.ParseSessId(file) + if err != nil { + return p9.QID{}, err + } + f, ok = fs.SessionsDir.Get(sessid) + if !ok { + return p9.QID{}, ErrFileNotExist + } + + default: + return p9.QID{}, ErrFileNotExist + } + if f == nil { + panic("unreachable") + } + + qid, err := f.Qid() + if err != nil { + return p9.QID{}, err + } + if err := fs.attach(fid, f); err != nil { + return p9.QID{}, err + } + return qid, nil +} + +func (fs *Authfs) attach(fid p9.FID, f File) error { + fs.mu.Lock() + defer fs.mu.Unlock() + + if _, exists := fs.files[fid]; exists { + return ErrFidExist + } + + fs.files[fid] = f + return nil +} diff --git a/back/cmd/authfs/err.go b/back/cmd/authfs/err.go new file mode 100644 index 0000000..81da2ba --- /dev/null +++ b/back/cmd/authfs/err.go @@ -0,0 +1,10 @@ +package main + +import "errors" + +var ( + ErrFileExist = errors.New("file already exists") + ErrFileNotExist = errors.New("file does not exist") + ErrFidExist = errors.New("fid already exists") + ErrFidNotExist = errors.New("fid does not exist") +) diff --git a/back/cmd/authfs/main.go b/back/cmd/authfs/main.go index f8618df..b705858 100644 --- a/back/cmd/authfs/main.go +++ b/back/cmd/authfs/main.go @@ -1,6 +1,6 @@ /* This is the authfs daemon. It stores the user database and client -sessions, and it serves a 9P filesystem for managing them. +sessions, and serves a 9P filesystem for managing them. User data (usernames and password hashes) are stored on disk. Sessions are stored in memory. @@ -8,59 +8,64 @@ are stored in memory. package main import ( - "context" "flag" + "io" "log" "net" "os" - "github.com/docker-archive/go-p9p" -) - -const ( - defaultNetwork = "unix" - defaultAddress = "authfs.sock" + "github.com/Harvey-OS/ninep/pkg/debugfs" + p9 "github.com/Harvey-OS/ninep/protocol" ) var ( - network, address string + root = flag.String("root", ".", "root directory") + ntype = flag.String("net", "unix", "network type: {tcp, unix}") + naddr = flag.String("addr", "authfs.sock", "network address") + debug = flag.Bool("debug", false, "print debug messages") outlog = log.New(os.Stdout, "authfs [info]: ", log.LstdFlags) - errlog = log.New(os.Stderr, "authfs [error]: ", log.LstdFlags|log.Llongfile) + errlog = log.New(os.Stderr, "authfs [error]: ", log.LstdFlags|log.Lshortfile) ) func main() { - flag.StringVar(&network, "net", defaultNetwork, "network transport protocol: {tcp, unix}") - flag.StringVar(&address, "addr", defaultAddress, "IP address or Unix socket path to listen on") flag.Parse() - ln, err := net.Listen(network, address) + sock, err := net.Listen(*ntype, *naddr) if err != nil { errlog.Fatal(err) } - defer logErr(ln.Close()) + defer logClose(sock, errlog) - for { - if conn, err := ln.Accept(); err == nil { - outlog.Println("connected", conn.RemoteAddr()) - go handle(conn) + fs, err := NewAuthfs(*root) + if err != nil { + errlog.Fatal(err) + } + defer fs.Close() + + nsCreator := func() p9.NineServer { + if *debug { + return &debugfs.DebugFileServer{fs} } else { - errlog.Println(err) + return fs } } -} - -func handle(conn net.Conn) { - defer logErr(conn.Close()) - ctx := context.Background() - handler := p9p.Dispatch(NewSession()) - if err := p9p.ServeConn(ctx, conn, handler); err != nil { - errlog.Printf("%v: %v\n", conn.RemoteAddr(), err) + ln, err := p9.NewListener(nsCreator, func(l *p9.Listener) error { + l.Trace = func(format string, a ...any) { + outlog.Printf(format, a...) + } + return nil + }) + if err != nil { + errlog.Fatal(err) + } + if err := ln.Serve(sock); err != nil { + errlog.Fatal(err) } } -func logErr(err error) { - if err != nil { - errlog.Println(err) +func logClose(c io.Closer, l *log.Logger) { + if err := c.Close(); err != nil { + l.Println(err) } } diff --git a/back/cmd/authfs/root.go b/back/cmd/authfs/root.go index 70ca643..5d56045 100644 --- a/back/cmd/authfs/root.go +++ b/back/cmd/authfs/root.go @@ -1,21 +1,31 @@ package main -const rootQid = p9p.Qid{ - Type: QTDIR, - Version: 0x0, - Path: 0x1, +import ( + p9 "github.com/Harvey-OS/ninep/protocol" + + "git.samanthony.xyz/buth/back/qver" +) + +type RootDir struct { + *qver.Version } -// Root is the root Dir which contains /users and /sessions. -type Root struct { - path string +func NewRootDir() *RootDir { + return &RootDir{qver.New()} } -func (r Root) Qid() p9p.Qid { return rootQid } +func (r *RootDir) Close() { + r.Version.Close() +} -func (r Root) Stat() (p9p.Dir, error) { - return p9p.Dir{ - Qid: rootQid, - Mode: DMDIR | DMREAD, - +func (r *RootDir) Qid() (p9.QID, error) { + ver, err := r.Version.Get() + if err != nil { + return p9.QID{}, err + } + return p9.QID{ + Type: p9.QTDIR, + Version: ver, + Path: rootQidPath, + }, nil } diff --git a/back/cmd/authfs/sess.go b/back/cmd/authfs/sess.go new file mode 100644 index 0000000..138ea84 --- /dev/null +++ b/back/cmd/authfs/sess.go @@ -0,0 +1,48 @@ +package main + +import ( + "sync/atomic" + "time" + + "git.samanthony.xyz/buth/back/auth" +) + +type Session struct { + auth.SessId + auth.Uname + timer *time.Timer + dead *atomic.Bool +} + +func NewSession(id auth.SessId, uname auth.Uname) *Session { + dead := new(atomic.Bool) + timer := time.AfterFunc(auth.SessTimeout, func() { dead.Store(true) }) + return &Session{ + id, + uname, + timer, + dead, + } +} + +// Close expires the session immediately. +func (s *Session) Close() { + s.timer.Stop() + s.dead.Store(true) +} + +// IsActive returns true if the session has not yet expired. +func (s *Session) IsActive() bool { + return !s.dead.Load() +} + +// Extend resets the session's timer or returns false if it has +// already expired. +func (s *Session) Extend() bool { + if s.timer.Stop() { + s.timer.Reset(auth.SessTimeout) + return true + } + // already expired + return false +} diff --git a/back/cmd/authfs/sessdir.go b/back/cmd/authfs/sessdir.go new file mode 100644 index 0000000..186484f --- /dev/null +++ b/back/cmd/authfs/sessdir.go @@ -0,0 +1,60 @@ +package main + +import ( + "sync" + + "git.samanthony.xyz/buth/back/auth" + "git.samanthony.xyz/buth/back/qver" +) + +type SessionsDir struct { + *qver.Version + + mu sync.Mutex // guards below + sessions map[auth.SessId]*Session +} + +func NewSessionsDir(rootVer *qver.Version) *SessionsDir { + return &SessionsDir{ + qver.New(qver.Parent(rootVer)), + sync.Mutex{}, + make(map[auth.SessId]*Session), + } +} + +func (dir *SessionsDir) Close() { + dir.mu.Lock() + // Don't unlock + + dir.Version.Close() + for id, sess := range dir.sessions { + sess.Close() + delete(dir.sessions, id) + } +} + +// Get returns the session with the given ID if it exists, or false if it doesn't. +func (dir *SessionsDir) Get(id auth.SessId) (*Session, bool) { + dir.mu.Lock() + defer dir.mu.Unlock() + sess, ok := dir.sessions[id] + return sess, ok +} + +// Owner returns the owner of the given session if it exists, or false if it doesn't. +func (dir *SessionsDir) Owner(id auth.SessId) (auth.Uname, bool) { + if sess, ok := dir.Get(id); ok { + return sess.Uname, ok + } + return "", false +} + +func (dir *SessionsDir) Kill(id auth.SessId) { + dir.mu.Lock() + defer dir.mu.Unlock() + if sess, ok := dir.sessions[id]; ok { + sess.Close() + delete(dir.sessions, id) + dir.Version.Bump() + } +} diff --git a/back/cmd/authfs/session.go b/back/cmd/authfs/session.go deleted file mode 100644 index 2d4eb98..0000000 --- a/back/cmd/authfs/session.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "context" - "sync" - - "github.com/docker-archive/go-p9p" -) - -// Session implements the p9p.Session interface. -type Session struct { - mu sync.Mutex - fids map[p9p.Fid]Fid - *Root -} - -type Fid struct { - File - uname string -} - -type File interface { - Qid() p9p.Qid - Stat() (p9p.Dir, error) - Open(mode p9p.Flag) error - Remove() error - Read(ctx context.Context, p []byte, offset int64) (n int, err error) - Write(ctx context.Context, p []byte, offset int64) (n int, err error) -} - -type Dir interface { - File - Create(ctx context.Context, name string, perm uint32, mode p9p.Flag) (File, error) - Walk(ctx context.Context, name string) (File, error) -} - -func NewSession(root *Root) p9p.Session { - return &Session{ - fids: make(map[p9p.Fid]Fid), - Root: root, - } -} - -func (s *Session) Auth(ctx context.Context, afid p9p.Fid, uname, aname string) (p9p.Qid, error) { - return p9p.Qid{}, p9p.MessageRerror{"authfs: no authentication required"} -} - -func (s *Session) Attach(ctx context.Context, fid, afid p9p.Fid, uname, aname string) (p9p.Qid, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if _, exists := s.fids[fid]; exists { - return p9p.Qid{}, p9p.ErrDupfid - } - f := Fid{s.root, uname} - s.fids[fid] = f - return f.Qid(), nil -} - -func (s *Session) Clunk(ctx context.Context, fid p9p.Fid) error { - s.mu.Lock() - defer s.mu.Unlock() - - if _, ok := s.fids[fid]; ok { - delete(s.fids, fid) - return nil - } else { - return p9p.ErrUnknownfid - } -} - -func (s *Session) Remove(ctx context.Context, fid p9p.Fid) error { - s.mu.Lock() - defer s.mu.Unlock() - - f, ok := s.fids[fid] - if !ok { - return p9p.ErrUnknownfid - } - delete(s.fids, fid) // clunk - return f.Remove() -} - -func (s *Session) Walk(ctx context.Context, fid, newfid p9p.Fid, names ...string) ([]p9p.Qid, error) { - s.mu.Lock() - defer s.mu.Unlock() - - f, ok := s.fids[fid] - if !ok { - return nil, p9p.ErrUnknownfid - } - - if _, exists := s.fids[newfid]; exists { - return nil, p9p.ErrDupfid - } - - var ( - qids []p9p.Qid - err error - dot = f - ) - defer func() { s.fids[newfid] = dot }() - for _, name := range names { - dir, ok := dot.File.(Dir) - if !ok { - return qids, p9p.ErrWalknodir - } - dot.File, err = dir.Walk(ctx, name) - if err != nil { - return qids, err - } - qids = append(qids, dot.File.Qid()) - } - return qids, nil -} - -func (s *Session) Read(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) { - s.mu.Lock() - defer s.mu.Unlock() - - if f, ok := s.fids[fid]; ok { - return f.File.Read(ctx, p, offset) - } else { - return 0, p9p.ErrUnknownfid - } -} - -func (s *Session) Write(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) { - s.mu.Lock() - defer s.mu.Unlock() - - if f, ok := s.fids[fid]; ok { - return f.File.Write(ctx, p, offset) - } else { - return 0, p9p.ErrUnknownfid - } -} - -func (s *Session) Open(ctx context.Context, fid p9p.Fid, mode p9p.Flag) (p9p.Qid, uint32, error) { - s.mu.Lock() - defer s.mu.Unlock() - - f, ok := s.fids[fid] - if !ok { - return p9p.Qid{}, 0, p9p.ErrUnknownfid - } - if err := f.Open(mode); err != nil { - return p9p.Qid{}, 0, err - } - return f.File.Qid(), 0, nil -} - -func (s *Session) Create(ctx context.Context, parent p9p.Fid, name string, perm uint32, mode p9p.Flag) (p9p.Qid, uint32, error) { - s.mu.Lock() - defer s.mu.Unlock() - - f, ok := s.fids[parent] - if !ok { - return p9p.Qid{}, 0, p9p.ErrUnknownfid - } - dir, ok := f.File.(Dir) - if !ok { - return p9p.Qid{}, 0, p9p.ErrCreatenondir - } - - child, err := dir.Create(ctx, name, perm, mode) - return child.Qid(), 0, err -} - -func (s *Session) Stat(ctx context.Context, fid p9p.Fid) (p9p.Dir, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if f, ok := s.fids[fid]; ok { - return f.File.Stat() - } else { - return p9p.Dir{}, p9p.ErrUnknownfid - } -} - -func (s *Session) WStat(ctx context.Context, fid p9p.Fid, dir p9p.Dir) error { - return p9p.ErrPerm -} - -func (s *Session) Version() (msize int, version string) { - return p9p.DefaultMSize, p9p.DefaultVersion -} diff --git a/back/cmd/authfs/userdir.go b/back/cmd/authfs/userdir.go new file mode 100644 index 0000000..f7ef5a7 --- /dev/null +++ b/back/cmd/authfs/userdir.go @@ -0,0 +1,51 @@ +package main + +import ( + "errors" + "fmt" + "io/fs" + "os" + + p9 "github.com/Harvey-OS/ninep/protocol" + + "git.samanthony.xyz/buth/back/qver" +) + +type UsersDir struct { + path string + *qver.Version +} + +func NewUsersDir(path string, rootVer *qver.Version) (*UsersDir, error) { + info, err := os.Stat(path) + if errors.Is(err, fs.ErrNotExist) { + if err := os.Mkdir(path, 0770); err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } else if !info.IsDir() { + return nil, fmt.Errorf("%q is not a directory", path) + } + + return &UsersDir{ + path, + qver.New(qver.Parent(rootVer)), + }, nil +} + +func (dir *UsersDir) Close() { + dir.Version.Close() +} + +func (dir *UsersDir) Qid() (p9.QID, error) { + ver, err := dir.Version.Get() + if err != nil { + return p9.QID{}, err + } + return p9.QID{ + Type: p9.QTDIR, + Version: ver, + Path: usersQidPath, + }, nil +} @@ -3,6 +3,9 @@ module git.samanthony.xyz/buth go 1.25.5 require ( - github.com/docker-archive/go-p9p v0.0.0-20191112112554-37d97cf40d03 // indirect - github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03 // indirect + github.com/Harvey-OS/ninep v0.0.0-20200724082702-d30a6d4f9789 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -1,4 +1,11 @@ -github.com/docker-archive/go-p9p v0.0.0-20191112112554-37d97cf40d03 h1:TjD+kWlpl4Am4fG67bL5qbgQ+O0AkmwaGMs3Fch7vdc= -github.com/docker-archive/go-p9p v0.0.0-20191112112554-37d97cf40d03/go.mod h1:3BkbhyQV/9GwNZlaGozfPD5bqbNNndle48j1laRrqmQ= -github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03 h1:HiIKimWyR71ORJgvm/aWL/cqeYMpOy4eObwJogG8FAw= -github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03/go.mod h1:GDue7j/yh3AtNoUK0ihznL9JiZVn92CV9bUrYaD4NOc= +github.com/Harvey-OS/ninep v0.0.0-20200724082702-d30a6d4f9789 h1:CTx4Ie/zMSBhZ+pjiklxmqF+HxDlXMXRDiUCCo6J0RY= +github.com/Harvey-OS/ninep v0.0.0-20200724082702-d30a6d4f9789/go.mod h1:YmLTajv/R+cjEsJ8/vfGOckeEBbz06tgHlYBSpbe+eo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |