summaryrefslogtreecommitdiffstats
path: root/back
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-03-14 11:23:50 -0400
committerSam Anthony <sam@samanthony.xyz>2026-03-14 11:23:50 -0400
commit700096b078f6e16f2c5c967ea140f0b93e799986 (patch)
treee1d9965e9bf09ebe01a96d8faa6d2bc027e6984c /back
parent7d622695ae19e518fded6aa5fbf001dae4652211 (diff)
downloadbuth-700096b078f6e16f2c5c967ea140f0b93e799986.zip
authfs: rewrite with Harvey-OS/ninep, implement Rattachharveyos
Diffstat (limited to 'back')
-rw-r--r--back/auth/auth.go36
-rw-r--r--back/cmd/authfs/authfs.go130
-rw-r--r--back/cmd/authfs/err.go10
-rw-r--r--back/cmd/authfs/main.go65
-rw-r--r--back/cmd/authfs/root.go36
-rw-r--r--back/cmd/authfs/sess.go48
-rw-r--r--back/cmd/authfs/sessdir.go60
-rw-r--r--back/cmd/authfs/session.go187
-rw-r--r--back/cmd/authfs/userdir.go51
9 files changed, 385 insertions, 238 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
+}