517 lines
11 KiB
Go
517 lines
11 KiB
Go
// server.go - 9P filesystem server for toolsrv.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
|
|
"ollie/cmd/toolsrv/internal/fs"
|
|
"ollie/fsedsl"
|
|
|
|
"9fans.net/go/plan9"
|
|
)
|
|
|
|
// fid represents an open file handle.
|
|
type fid struct {
|
|
path string
|
|
qid plan9.Qid
|
|
mode uint8
|
|
opened bool
|
|
entry fsedsl.File
|
|
readBuf []byte // buffer for read data
|
|
readDone bool // true if file has been read
|
|
dirCache []byte
|
|
}
|
|
|
|
// authFid represents an auth file handle for Tauth.
|
|
type authFid struct {
|
|
uname string
|
|
aname string
|
|
authenticated bool
|
|
token string
|
|
}
|
|
|
|
// connState holds per-connection state.
|
|
type connState struct {
|
|
mu sync.RWMutex
|
|
fids map[uint32]*fid
|
|
authFids map[uint32]*authFid
|
|
ctx context.Context
|
|
uname string
|
|
srv *fs.Server
|
|
noAuth bool // skip authentication (for debugging)
|
|
}
|
|
|
|
// serve9P handles a single 9P connection using the fsedsl tree.
|
|
func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree, srv *fs.Server, noAuth bool) {
|
|
defer conn.Close()
|
|
|
|
cs := &connState{
|
|
fids: make(map[uint32]*fid),
|
|
authFids: make(map[uint32]*authFid),
|
|
ctx: ctx,
|
|
noAuth: noAuth,
|
|
srv: srv,
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
fc, err := plan9.ReadFcall(conn)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
logger.Warn("read error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
resp := handleFcall(cs, fc, tree)
|
|
if err := plan9.WriteFcall(conn, resp); err != nil {
|
|
logger.Warn("write error: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleFcall(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
switch fc.Type {
|
|
case plan9.Tversion:
|
|
return handleVersion(fc)
|
|
case plan9.Tauth:
|
|
return handleAuth(cs, fc)
|
|
case plan9.Tattach:
|
|
return handleAttach(cs, fc, tree)
|
|
case plan9.Twalk:
|
|
return handleWalk(cs, fc, tree)
|
|
case plan9.Topen:
|
|
return handleOpen(cs, fc, tree)
|
|
case plan9.Tread:
|
|
return handleRead(cs, fc, tree)
|
|
case plan9.Twrite:
|
|
return handleWrite(cs, fc, tree)
|
|
case plan9.Tclunk:
|
|
return handleClunk(cs, fc)
|
|
case plan9.Tstat:
|
|
return handleStat(cs, fc, tree)
|
|
default:
|
|
return errFcall(fc, fmt.Sprintf("unsupported: %d", fc.Type))
|
|
}
|
|
}
|
|
|
|
func errFcall(fc *plan9.Fcall, msg string) *plan9.Fcall {
|
|
return &plan9.Fcall{Type: plan9.Rerror, Tag: fc.Tag, Ename: msg}
|
|
}
|
|
|
|
func handleVersion(fc *plan9.Fcall) *plan9.Fcall {
|
|
msize := fc.Msize
|
|
if msize > 65536 {
|
|
msize = 65536
|
|
}
|
|
return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"}
|
|
}
|
|
|
|
func handleAuth(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|
cs.mu.Lock()
|
|
cs.authFids[fc.Afid] = &authFid{
|
|
uname: fc.Uname,
|
|
aname: fc.Aname,
|
|
}
|
|
cs.mu.Unlock()
|
|
|
|
return &plan9.Fcall{
|
|
Type: plan9.Rauth,
|
|
Tag: fc.Tag,
|
|
Aqid: plan9.Qid{Type: plan9.QTAUTH, Vers: 0, Path: uint64(fc.Afid)},
|
|
}
|
|
}
|
|
|
|
func handleAttach(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
// Skip auth check if noAuth is set (for debugging)
|
|
if cs.noAuth {
|
|
cs.mu.Lock()
|
|
cs.uname = fc.Uname
|
|
cs.fids[fc.Fid] = &fid{
|
|
path: "/",
|
|
qid: plan9.Qid{Type: plan9.QTDIR, Path: 0},
|
|
}
|
|
cs.mu.Unlock()
|
|
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: plan9.Qid{Type: plan9.QTDIR, Path: 0}}
|
|
}
|
|
|
|
// Check auth - afid must exist and be authenticated
|
|
cs.mu.RLock()
|
|
af, hasAuth := cs.authFids[fc.Afid]
|
|
cs.mu.RUnlock()
|
|
|
|
// NOFID means no auth required (we require auth)
|
|
if fc.Afid == plan9.NOFID {
|
|
return errFcall(fc, "authentication required")
|
|
}
|
|
|
|
if !hasAuth {
|
|
return errFcall(fc, "invalid afid")
|
|
}
|
|
|
|
if !af.authenticated {
|
|
return errFcall(fc, "not authenticated")
|
|
}
|
|
|
|
cs.mu.Lock()
|
|
cs.uname = fc.Uname
|
|
cs.fids[fc.Fid] = &fid{
|
|
path: "/",
|
|
qid: plan9.Qid{Type: plan9.QTDIR, Path: 0},
|
|
}
|
|
cs.mu.Unlock()
|
|
|
|
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: plan9.Qid{Type: plan9.QTDIR, Path: 0}}
|
|
}
|
|
|
|
// handleAuthWrite handles writing the secret to an auth fid.
|
|
func handleAuthWrite(cs *connState, fc *plan9.Fcall, af *authFid) *plan9.Fcall {
|
|
clientSecret := string(fc.Data)
|
|
token, err := cs.srv.Authenticate(clientSecret)
|
|
if err != nil {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
|
|
af.authenticated = true
|
|
af.token = token
|
|
|
|
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
|
|
}
|
|
|
|
// handleAuthRead handles reading the token from an auth fid.
|
|
func handleAuthRead(cs *connState, fc *plan9.Fcall, af *authFid) *plan9.Fcall {
|
|
if !af.authenticated {
|
|
return errFcall(fc, "write secret first")
|
|
}
|
|
|
|
data := []byte(af.token + "\n")
|
|
|
|
offset := int(fc.Offset)
|
|
if offset >= len(data) {
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
|
|
}
|
|
|
|
end := offset + int(fc.Count)
|
|
if end > len(data) {
|
|
end = len(data)
|
|
}
|
|
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: data[offset:end]}
|
|
}
|
|
|
|
func handleWalk(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
cs.mu.RLock()
|
|
f, ok := cs.fids[fc.Fid]
|
|
cs.mu.RUnlock()
|
|
if !ok {
|
|
return errFcall(fc, "unknown fid")
|
|
}
|
|
|
|
newPath := f.path
|
|
var qids []plan9.Qid
|
|
|
|
for _, name := range fc.Wname {
|
|
if name == ".." {
|
|
if newPath != "/" {
|
|
lastSlash := len(newPath) - 1
|
|
for lastSlash > 0 && newPath[lastSlash] != '/' {
|
|
lastSlash--
|
|
}
|
|
if lastSlash == 0 {
|
|
newPath = "/"
|
|
} else {
|
|
newPath = newPath[:lastSlash]
|
|
}
|
|
}
|
|
} else {
|
|
if newPath == "/" {
|
|
newPath = "/" + name
|
|
} else {
|
|
newPath = newPath + "/" + name
|
|
}
|
|
}
|
|
|
|
// Check if path exists in tree
|
|
treePath := newPath
|
|
if treePath == "/" {
|
|
treePath = ""
|
|
} else {
|
|
treePath = treePath[1:] // strip leading /
|
|
}
|
|
|
|
info, err := tree.Stat(treePath)
|
|
if err != nil {
|
|
if len(qids) == 0 {
|
|
return errFcall(fc, "not found")
|
|
}
|
|
break
|
|
}
|
|
|
|
var qt uint8 = plan9.QTFILE
|
|
if info.IsDir() {
|
|
qt = plan9.QTDIR
|
|
}
|
|
qids = append(qids, plan9.Qid{Type: qt, Path: pathHash(newPath)})
|
|
}
|
|
|
|
if len(qids) == len(fc.Wname) && fc.Newfid != fc.Fid {
|
|
var lastQid plan9.Qid
|
|
if len(qids) > 0 {
|
|
lastQid = qids[len(qids)-1]
|
|
} else {
|
|
lastQid = f.qid
|
|
}
|
|
cs.mu.Lock()
|
|
cs.fids[fc.Newfid] = &fid{
|
|
path: newPath,
|
|
qid: lastQid,
|
|
}
|
|
cs.mu.Unlock()
|
|
} else if len(qids) == len(fc.Wname) && len(qids) > 0 {
|
|
cs.mu.Lock()
|
|
f.path = newPath
|
|
f.qid = qids[len(qids)-1]
|
|
cs.mu.Unlock()
|
|
}
|
|
|
|
return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: qids}
|
|
}
|
|
|
|
func handleOpen(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
|
|
f, ok := cs.fids[fc.Fid]
|
|
if !ok {
|
|
return errFcall(fc, "unknown fid")
|
|
}
|
|
|
|
// Directories don't need an entry
|
|
if f.qid.Type&plan9.QTDIR != 0 {
|
|
f.opened = true
|
|
f.mode = fc.Mode
|
|
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192}
|
|
}
|
|
|
|
path := f.path
|
|
if path == "/" {
|
|
path = ""
|
|
} else {
|
|
path = path[1:] // strip leading /
|
|
}
|
|
|
|
entry, err := tree.Open(path)
|
|
if err != nil {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
|
|
f.entry = entry
|
|
f.opened = true
|
|
f.mode = fc.Mode
|
|
f.readBuf = nil
|
|
f.readDone = false
|
|
|
|
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192}
|
|
}
|
|
|
|
func handleRead(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
// Check if reading from auth fid
|
|
cs.mu.RLock()
|
|
af, isAuthFid := cs.authFids[fc.Fid]
|
|
f, isFid := cs.fids[fc.Fid]
|
|
cs.mu.RUnlock()
|
|
|
|
if isAuthFid {
|
|
return handleAuthRead(cs, fc, af)
|
|
}
|
|
|
|
if !isFid {
|
|
return errFcall(fc, "unknown fid")
|
|
}
|
|
if !f.opened {
|
|
return errFcall(fc, "not open")
|
|
}
|
|
|
|
if f.qid.Type&plan9.QTDIR != 0 {
|
|
return handleDirRead(cs, f, fc, tree)
|
|
}
|
|
|
|
if f.entry == nil {
|
|
return errFcall(fc, "no entry")
|
|
}
|
|
|
|
// For rdwr files, check if we've already read
|
|
if f.entry.RequestRespMode() && f.readDone {
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
|
|
}
|
|
|
|
// Buffer read data if not already done
|
|
if f.readBuf == nil {
|
|
data, err := f.entry.Read()
|
|
if err != nil && err != io.EOF {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
f.readBuf = data
|
|
}
|
|
|
|
// Handle offset/count
|
|
offset := int(fc.Offset)
|
|
if offset >= len(f.readBuf) {
|
|
f.readDone = true
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
|
|
}
|
|
|
|
end := offset + int(fc.Count)
|
|
if end > len(f.readBuf) {
|
|
end = len(f.readBuf)
|
|
}
|
|
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.readBuf[offset:end]}
|
|
}
|
|
|
|
func handleDirRead(cs *connState, f *fid, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
if fc.Offset == 0 || f.dirCache == nil {
|
|
path := f.path
|
|
if path == "/" {
|
|
path = ""
|
|
} else {
|
|
path = path[1:]
|
|
}
|
|
|
|
entries, err := tree.Readdir(path)
|
|
if err != nil {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
|
|
var buf []byte
|
|
for _, e := range entries {
|
|
var qt uint8 = plan9.QTFILE
|
|
mode := plan9.Perm(0644)
|
|
if e.IsDir() {
|
|
qt = plan9.QTDIR
|
|
mode = plan9.Perm(plan9.DMDIR | 0755)
|
|
}
|
|
dir := plan9.Dir{
|
|
Name: e.Name(),
|
|
Qid: plan9.Qid{Type: qt, Path: pathHash(f.path + "/" + e.Name())},
|
|
Mode: mode,
|
|
Uid: "toolsrv",
|
|
Gid: "toolsrv",
|
|
}
|
|
data, _ := dir.Bytes()
|
|
buf = append(buf, data...)
|
|
}
|
|
f.dirCache = buf
|
|
}
|
|
|
|
offset := int(fc.Offset)
|
|
if offset >= len(f.dirCache) {
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
|
|
}
|
|
|
|
end := offset + int(fc.Count)
|
|
if end > len(f.dirCache) {
|
|
end = len(f.dirCache)
|
|
}
|
|
|
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.dirCache[offset:end]}
|
|
}
|
|
|
|
func handleWrite(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
// Check if writing to auth fid
|
|
cs.mu.Lock()
|
|
af, isAuthFid := cs.authFids[fc.Fid]
|
|
f, isFid := cs.fids[fc.Fid]
|
|
cs.mu.Unlock()
|
|
|
|
if isAuthFid {
|
|
return handleAuthWrite(cs, fc, af)
|
|
}
|
|
|
|
if !isFid {
|
|
return errFcall(fc, "unknown fid")
|
|
}
|
|
if !f.opened {
|
|
return errFcall(fc, "not open")
|
|
}
|
|
if f.entry == nil {
|
|
return errFcall(fc, "no entry")
|
|
}
|
|
|
|
err := f.entry.Write(fc.Data)
|
|
if err != nil {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
|
|
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
|
|
}
|
|
|
|
func handleClunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|
cs.mu.Lock()
|
|
delete(cs.fids, fc.Fid)
|
|
delete(cs.authFids, fc.Fid)
|
|
cs.mu.Unlock()
|
|
return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
|
|
}
|
|
|
|
func handleStat(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
|
|
cs.mu.RLock()
|
|
f, ok := cs.fids[fc.Fid]
|
|
cs.mu.RUnlock()
|
|
if !ok {
|
|
return errFcall(fc, "unknown fid")
|
|
}
|
|
|
|
path := f.path
|
|
if path == "/" {
|
|
path = ""
|
|
} else {
|
|
path = path[1:]
|
|
}
|
|
|
|
info, err := tree.Stat(path)
|
|
if err != nil {
|
|
return errFcall(fc, err.Error())
|
|
}
|
|
|
|
var qt uint8 = plan9.QTFILE
|
|
mode := plan9.Perm(0644)
|
|
if info.IsDir() {
|
|
qt = plan9.QTDIR
|
|
mode = plan9.Perm(plan9.DMDIR | 0755)
|
|
}
|
|
|
|
name := info.Name()
|
|
if name == "" {
|
|
name = "/"
|
|
}
|
|
|
|
dir := plan9.Dir{
|
|
Name: name,
|
|
Qid: plan9.Qid{Type: qt, Path: pathHash(f.path)},
|
|
Mode: mode,
|
|
Uid: "toolsrv",
|
|
Gid: "toolsrv",
|
|
}
|
|
data, _ := dir.Bytes()
|
|
|
|
return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: data}
|
|
}
|
|
|
|
func pathHash(p string) uint64 {
|
|
h := fnv.New64a()
|
|
h.Write([]byte(p))
|
|
return h.Sum64()
|
|
}
|