ollie/cmd/olliesrv/server.go

787 lines
18 KiB
Go

// 9P filesystem server for ollie sessions.
package main
import (
"context"
"fmt"
"hash/fnv"
"io"
"net"
"os/user"
"strings"
"sync"
"time"
"ollie/fs"
olog "ollie/log"
"9fans.net/go/plan9"
)
type (
syntheticFileInfo = fs.SyntheticFileInfo
)
const (
QTDir = plan9.QTDIR
QTFile = plan9.QTFILE
)
type fid struct {
path string
qid plan9.Qid
mode uint8
writeBuf []byte
waitBase string
dirCache []byte
}
type connState struct {
mu sync.RWMutex
fids map[uint32]*fid
ctx context.Context
cancel context.CancelFunc
pending map[uint16]context.CancelFunc
uname string
}
type Server struct {
mu sync.RWMutex
conns []*connState
log *olog.Logger
sink *olog.Sink
rootTree *fs.Tree
groups map[string]map[string]bool
toolPrompt func(string) string
serveWG sync.WaitGroup
shutdownCtx context.Context
shutdownCancel context.CancelFunc
connMu sync.Mutex
activeConns map[net.Conn]struct{}
handler map[uint8]func(*connState, *plan9.Fcall, context.Context) *plan9.Fcall
}
type Config struct {
Sink *olog.Sink
RootTree *fs.Tree
ToolPrompt func(string) string
}
func New(cfg Config) *Server {
ctx, cancel := context.WithCancel(context.Background())
s := &Server{
log: cfg.Sink.Logger("9p", olog.LevelDebug),
sink: cfg.Sink,
rootTree: cfg.RootTree,
groups: make(map[string]map[string]bool),
toolPrompt: cfg.ToolPrompt,
shutdownCtx: ctx,
shutdownCancel: cancel,
activeConns: make(map[net.Conn]struct{}),
}
s.handler = map[uint8]func(*connState, *plan9.Fcall, context.Context) *plan9.Fcall{
plan9.Tversion: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
msize := fc.Msize
if msize > 65536 {
msize = 65536
}
return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"}
},
plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return errFcall(fc, "no auth required") },
plan9.Tflush: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return flush(cs, fc) },
plan9.Tattach: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return s.attach(cs, fc) },
plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return walk(cfg.RootTree, cs, fc) },
plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return s.open(cs, fc) },
plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return create(cfg.RootTree, cs, fc) },
plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { return read(cfg.RootTree, cs, fc, ctx) },
plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cfg.RootTree, cs, fc) },
plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return stat(cfg.RootTree, cs, fc) },
plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return s.wstat(cs, fc) },
plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return clunk(cfg.RootTree, cs, fc) },
plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return remove(cfg.RootTree, cs, fc) },
}
return s
}
func (s *Server) AddGroup(group, user string) {
s.mu.Lock()
if s.groups[group] == nil {
s.groups[group] = make(map[string]bool)
}
s.groups[group][user] = true
s.mu.Unlock()
}
func (s *Server) RemoveGroup(group, user string) {
s.mu.Lock()
if m := s.groups[group]; m != nil {
delete(m, user)
if len(m) == 0 {
delete(s.groups, group)
}
}
s.mu.Unlock()
}
func (s *Server) InGroup(group, user string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.groups[group][user]
}
func fileOwnerGroup(root *fs.Tree, path string) (uid, gid string) {
info, err := root.Stat(path[1:])
if err == nil {
if fi, ok := info.(*syntheticFileInfo); ok {
if fi.UID_ != "" || fi.GID_ != "" {
return fi.UID_, fi.GID_
}
}
}
if u, err := user.Current(); err == nil {
return u.Username, "agent"
}
return "ollie", "agent"
}
var permMask = [4]uint32{4, 2, 6, 1} // OREAD, OWRITE, ORDWR, OEXEC
func hasPermBits(mode uint8, bits uint32) bool {
m := permMask[mode&3]
return bits&m == m
}
const readTimeout = 10 * time.Second
func (s *Server) Serve(conn net.Conn) {
defer conn.Close()
s.serveWG.Add(1)
defer s.serveWG.Done()
s.connMu.Lock()
s.activeConns[conn] = struct{}{}
s.connMu.Unlock()
defer func() {
s.connMu.Lock()
delete(s.activeConns, conn)
s.connMu.Unlock()
}()
connCtx, connCancel := context.WithCancel(s.shutdownCtx)
cs := &connState{
fids: make(map[uint32]*fid),
ctx: connCtx,
cancel: connCancel,
pending: make(map[uint16]context.CancelFunc),
}
s.mu.Lock()
s.conns = append(s.conns, cs)
s.mu.Unlock()
defer func() {
connCancel()
s.mu.Lock()
for i, c := range s.conns {
if c == cs {
s.conns = append(s.conns[:i], s.conns[i+1:]...)
break
}
}
s.mu.Unlock()
}()
responses := make(chan *plan9.Fcall, 16)
var wg sync.WaitGroup
go func() {
for resp := range responses {
plan9.WriteFcall(conn, resp)
}
}()
for {
fc, err := plan9.ReadFcall(conn)
if err != nil {
if err != io.EOF {
s.log.Error("read: %v", err)
}
break
}
reqCtx, reqCancel := context.WithCancel(connCtx)
cs.mu.Lock()
cs.pending[fc.Tag] = reqCancel
cs.mu.Unlock()
wg.Add(1)
go func(fc *plan9.Fcall, ctx context.Context) {
defer func() {
reqCancel()
cs.mu.Lock()
delete(cs.pending, fc.Tag)
cs.mu.Unlock()
wg.Done()
}()
responses <- s.handle(cs, fc, ctx)
}(fc, reqCtx)
}
wg.Wait()
close(responses)
}
func (s *Server) handle(cs *connState, fc *plan9.Fcall, ctx context.Context) (resp *plan9.Fcall) {
start := time.Now()
defer func() {
if r := recover(); r != nil {
s.log.Error("panic in handle: %v", r)
resp = errFcall(fc, "internal error")
}
s.log.Debug(" -> %s %dµs", fcallTypeName(fc.Type), time.Since(start).Microseconds())
}()
h := s.handler[fc.Type]
if h == nil {
return errFcall(fc, "unsupported operation")
}
return h(cs, fc, ctx)
}
func flush(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
if cancel, ok := cs.pending[fc.Oldtag]; ok {
cancel()
}
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rflush, Tag: fc.Tag}
}
func fcallTypeName(t uint8) string {
switch t {
case plan9.Tversion:
return "Tversion"
case plan9.Tauth:
return "Tauth"
case plan9.Tattach:
return "Tattach"
case plan9.Twalk:
return "Twalk"
case plan9.Topen:
return "Topen"
case plan9.Tcreate:
return "Tcreate"
case plan9.Tread:
return "Tread"
case plan9.Twrite:
return "Twrite"
case plan9.Tstat:
return "Tstat"
case plan9.Twstat:
return "Twstat"
case plan9.Tflush:
return "Tflush"
case plan9.Tclunk:
return "Tclunk"
case plan9.Tremove:
return "Tremove"
default:
return fmt.Sprintf("T%d", t)
}
}
func errFcall(fc *plan9.Fcall, msg string) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rerror, Tag: fc.Tag, Ename: msg}
}
func qidPath(path string) uint64 {
if path == "/" {
return 0
}
h := fnv.New64a()
h.Write([]byte(path))
return h.Sum64()
}
func pathParent(path string) string {
if path == "/" {
return "/"
}
i := strings.LastIndex(path, "/")
if i == 0 {
return "/"
}
return path[:i]
}
func pathJoin(parent, name string) string {
if parent == "/" {
return "/" + name
}
return parent + "/" + name
}
func pathBase(path string) string {
i := strings.LastIndex(path, "/")
if i < 0 {
return path
}
return path[i+1:]
}
func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.uname = fc.Uname
qid := plan9.Qid{Type: QTDir, Path: 0}
cs.fids[fc.Fid] = &fid{path: "/", qid: qid}
s.log.Debug("Tattach uname=%q", fc.Uname)
if fc.Uname != "" {
s.AddGroup("agent", fc.Uname)
}
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: qid}
}
func walk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
defer cs.mu.Unlock()
f, ok := cs.fids[fc.Fid]
if !ok {
return errFcall(fc, "bad fid")
}
newf := &fid{path: f.path, qid: f.qid}
if len(fc.Wname) == 0 {
cs.fids[fc.Newfid] = newf
return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: []plan9.Qid{}}
}
wqids := make([]plan9.Qid, 0, len(fc.Wname))
cur := f.path
for _, name := range fc.Wname {
var next string
if name == ".." {
next = pathParent(cur)
} else {
next = pathJoin(cur, name)
}
info, err := root.Stat(next[1:])
if err != nil {
if len(wqids) == 0 {
return errFcall(fc, name+": file not found")
}
break
}
q := plan9.Qid{Path: qidPath(next)}
if info.IsDir() {
q.Type = QTDir
}
wqids = append(wqids, q)
cur = next
}
if len(wqids) == len(fc.Wname) {
newf.path = cur
newf.qid = wqids[len(wqids)-1]
cs.fids[fc.Newfid] = newf
}
return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: wqids}
}
func (s *Server) open(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
defer cs.mu.Unlock()
f, ok := cs.fids[fc.Fid]
if !ok {
return errFcall(fc, "bad fid")
}
uid, gid := fileOwnerGroup(s.rootTree, f.path)
perm := uint32(makeStat(s.rootTree, f.path).Mode) & 0777
allowed := false
if cs.uname == uid {
allowed = hasPermBits(fc.Mode, perm>>6)
}
if !allowed && s.InGroup(gid, cs.uname) {
allowed = hasPermBits(fc.Mode, perm>>3)
}
if !allowed {
allowed = hasPermBits(fc.Mode, perm)
}
if !allowed {
return errFcall(fc, "permission denied")
}
f.mode = fc.Mode
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid}
}
func create(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
defer cs.mu.Unlock()
f, ok := cs.fids[fc.Fid]
if !ok {
return errFcall(fc, "bad fid")
}
newPath := pathJoin(f.path, fc.Name)
rel := newPath[1:]
if fc.Perm&plan9.DMDIR != 0 {
if err := root.MkdirAll(rel); err != nil {
return errFcall(fc, err.Error())
}
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
f.path = newPath
f.qid = qid
f.mode = fc.Mode
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
}
if err := root.Create(rel); err != nil {
return errFcall(fc, err.Error())
}
qid := plan9.Qid{Path: qidPath(newPath)}
f.path = newPath
f.qid = qid
f.mode = fc.Mode
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
}
func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall {
cs.mu.RLock()
f, ok := cs.fids[fc.Fid]
if !ok {
cs.mu.RUnlock()
return errFcall(fc, "bad fid")
}
path := f.path
isDir := f.qid.Type&QTDir != 0
cs.mu.RUnlock()
if isDir {
if fc.Offset == 0 || f.dirCache == nil {
f.dirCache = buildDirData(root, path)
}
data := sliceDirData(f.dirCache, fc.Offset, fc.Count)
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
}
entry, err := root.Open(path[1:])
if err != nil {
return errFcall(fc, err.Error())
}
if entry.BlockingReadMode() && fc.Offset > 0 {
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
}
if entry.BlockingReadMode() || entry.StreamMode() {
cs.mu.RLock()
f, fidOK := cs.fids[fc.Fid]
var base string
if fidOK {
base = f.waitBase
}
cs.mu.RUnlock()
var waitCtx context.Context
var waitCancel context.CancelFunc
if entry.StreamMode() {
waitCtx, waitCancel = context.WithCancel(ctx)
} else {
waitCtx, waitCancel = context.WithTimeout(ctx, 5*time.Second)
}
defer waitCancel()
content, nextBase, err := entry.BlockingRead(waitCtx, base)
if err != nil {
return errFcall(fc, err.Error())
}
if nextBase != "" {
cs.mu.Lock()
if f, ok := cs.fids[fc.Fid]; ok {
f.waitBase = nextBase
}
cs.mu.Unlock()
}
if entry.StreamMode() {
count := int(fc.Count)
if count > len(content) {
count = len(content)
}
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(count), Data: content[:count]}
}
return readSlice(fc, content)
}
type result struct {
data []byte
err error
}
ch := make(chan result, 1)
go func() {
data, err := entry.Read()
ch <- result{data, err}
}()
timer := time.NewTimer(readTimeout)
defer timer.Stop()
select {
case r := <-ch:
if r.err != nil {
return errFcall(fc, r.err.Error())
}
return readSlice(fc, r.data)
case <-ctx.Done():
return errFcall(fc, ctx.Err().Error())
case <-timer.C:
return errFcall(fc, "read timeout")
}
}
func readSlice(fc *plan9.Fcall, content []byte) *plan9.Fcall {
var data []byte
off := int(fc.Offset)
if off < len(content) {
end := off + int(fc.Count)
if end > len(content) {
end = len(content)
}
data = content[off:end]
}
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
}
func write(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
if !ok {
cs.mu.Unlock()
return errFcall(fc, "bad fid")
}
if entry, err := root.Open(f.path[1:]); err == nil && entry.RequestRespMode() {
cs.mu.Unlock()
if err := entry.Write(fc.Data); err != nil {
return errFcall(fc, err.Error())
}
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
end := int(fc.Offset) + len(fc.Data)
if end > len(f.writeBuf) {
grown := make([]byte, end)
copy(grown, f.writeBuf)
f.writeBuf = grown
}
copy(f.writeBuf[fc.Offset:], fc.Data)
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
func stat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.RLock()
f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock()
if !ok {
return errFcall(fc, "bad fid")
}
dir := makeStat(root, f.path)
stat, err := dir.Bytes()
if err != nil {
return errFcall(fc, err.Error())
}
return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: stat}
}
func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
cs.mu.Unlock()
if !ok {
return errFcall(fc, "bad fid")
}
newDir, err := plan9.UnmarshalDir(fc.Stat)
if err != nil {
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
}
oldName := pathBase(f.path)
if newDir.Name == "" || newDir.Name == oldName {
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
}
if err := s.rootTree.Rename(f.path[1:], newDir.Name); err != nil {
return errFcall(fc, err.Error())
}
relPath := f.path[1:]
oldPath := f.path
newPath := "/" + relPath[:len(relPath)-len(oldName)] + newDir.Name
s.mu.RLock()
conns := s.conns
s.mu.RUnlock()
for _, c := range conns {
c.mu.Lock()
for _, fid := range c.fids {
if fid.path == oldPath || strings.HasPrefix(fid.path, oldPath+"/") {
fid.path = newPath + fid.path[len(oldPath):]
fid.qid.Path = qidPath(fid.path)
}
}
c.mu.Unlock()
}
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
}
func clunk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
var path string
var data []byte
var writable bool
if ok {
if m := f.mode & 3; m == plan9.OWRITE || m == plan9.ORDWR {
path = f.path
writable = true
data = make([]byte, len(f.writeBuf))
copy(data, f.writeBuf)
}
delete(cs.fids, fc.Fid)
}
cs.mu.Unlock()
if writable {
input := strings.TrimSpace(string(data))
e, err := root.Open(path[1:])
if err != nil {
return errFcall(fc, err.Error())
}
if err := e.Write([]byte(input)); err != nil {
return errFcall(fc, err.Error())
}
}
return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
}
func remove(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
if ok {
delete(cs.fids, fc.Fid)
}
cs.mu.Unlock()
if !ok {
return errFcall(fc, "bad fid")
}
if err := root.Delete(f.path[1:]); err != nil {
return errFcall(fc, err.Error())
}
return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag}
}
func (s *Server) Shutdown() {
s.shutdownCancel()
s.connMu.Lock()
for conn := range s.activeConns {
conn.Close()
}
s.connMu.Unlock()
fs.Shutdown(s.rootTree)
done := make(chan struct{})
go func() {
s.serveWG.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(3 * time.Second):
s.log.Warn("shutdown: timed out waiting for Serve goroutines")
}
}
func buildDirData(root *fs.Tree, path string) []byte {
var dirs []plan9.Dir
if path == "/" {
entries, err := root.List()
if err == nil {
for _, e := range entries {
dirs = append(dirs, makeStat(root, "/"+e.Name()))
}
}
} else {
entries, err := root.Readdir(path[1:])
if err == nil {
for _, e := range entries {
fpath := path + "/" + e.Name()
d := makeStat(root, fpath)
if info, err := e.Info(); err == nil {
d.Atime = uint32(info.ModTime().Unix())
d.Mtime = uint32(info.ModTime().Unix())
}
dirs = append(dirs, d)
}
}
}
var allData []byte
for _, d := range dirs {
b, err := d.Bytes()
if err != nil {
continue
}
allData = append(allData, b...)
}
return allData
}
func sliceDirData(allData []byte, offset uint64, count uint32) []byte {
if offset >= uint64(len(allData)) {
return nil
}
remaining := allData[offset:]
var result []byte
for len(remaining) >= 2 {
entrySize := int(remaining[0]) | int(remaining[1])<<8
total := entrySize + 2
if total > len(remaining) {
break
}
if uint32(len(result)+total) > count {
break
}
result = append(result, remaining[:total]...)
remaining = remaining[total:]
}
return result
}
func makeStat(root *fs.Tree, path string) plan9.Dir {
base := pathBase(path)
if path == "/" {
base = "."
}
info, err := root.Stat(path[1:])
if err != nil {
info = &syntheticFileInfo{Name_: base, Mode_: 0444}
}
isDir := info.IsDir()
qid := plan9.Qid{Path: qidPath(path)}
if isDir {
qid.Type = QTDir
}
uid, gid := fileOwnerGroup(root, path)
dir := plan9.Dir{
Qid: qid,
Mode: plan9.Perm(info.Mode()),
Name: base,
Uid: uid,
Gid: gid,
Muid: uid,
}
if fi, ok := info.(*syntheticFileInfo); ok {
dir.Qid.Vers = fi.Vers_
}
if !isDir {
sz := info.Size()
if sz == 0 {
sz = 4096
}
dir.Length = uint64(sz)
}
return dir
}