Back to godoc.org
aqwari.net/net/styx

Package styx

v0.0.0-...-91faf1a
Latest Go to latest

The latest major version is .

Published: Jun 7, 2020 | License: MIT | Module: aqwari.net/net/styx

Overview

Package styx serves network filesystems using the 9P2000 protocol.

The styx package provides types and routines for implementing 9P servers. The files served may reflect real files on the host operating system, or an in-memory filesystem, or bi-directional RPC endpoints. Regardless, the protocol operations used to access these files are the same.

The ListenAndServe and ListenAndServeTLS functions run 9P servers bound to a TCP port. To create a 9P server, define a type that implements the Serve9P method. The HandlerFunc type allows regular functions to be used:

fs := styx.HandlerFunc(func(s *styx.Session) {
	for s.Next() {
		switch msg := s.Request().(type) {
		case styx.Twalk:
			msg.Rwalk(os.Stat(msg.Path()))
		case styx.Topen:
			msg.Ropen(os.OpenFile(msg.Path(), msg.Flag, 0777))
		case styx.Tstat:
			msg.Rstat(os.Stat(msg.Path()))
		}
	}
})
styx.ListenAndServe(":564", fs)

Multiple handlers can be overlaid using the Stack function.

echo := styx.HandlerFunc(func(s *styx.Session) {
	for s.Next() {
		log.Printf("%s %q %s", s.User, s.Access, s.Request())
	}
	log.Printf("session %s %q ended", s.User, s.Access)
})
styx.ListenAndServe(":564", styx.Stack(echo, handler))

Handlers may pass data downstream using a message's WithContext method:

sessionid := styx.HandlerFunc(func(s *styx.Session) {
	uuid := rand.Int63()
	for s.Next() {
		msg := s.Request()
		ctx := context.WithValue(msg.Context(), "session", uuid)
		s.UpdateRequest(msg.WithContext(ctx))
	}
})
styx.ListenAndServe(":564", styx.Stack(sessionid, echo, fs))
Example

Code:

package main

import (
	"io"
	"log"
	"os"
	"time"

	"aqwari.net/net/styx"
)

type emptyDir struct{}

func (emptyDir) Readdir(n int) ([]os.FileInfo, error) { return nil, io.EOF }
func (emptyDir) Mode() os.FileMode                    { return os.ModeDir | 0777 }
func (emptyDir) IsDir() bool                          { return true }
func (emptyDir) ModTime() time.Time                   { return time.Now() }
func (emptyDir) Name() string                         { return "" }
func (emptyDir) Size() int64                          { return 0 }
func (emptyDir) Sys() interface{}                     { return nil }

func main() {
	// Run a file server that creates directories (and only directories)
	// on-demand, as a client walks to them.

	h := styx.HandlerFunc(func(s *styx.Session) {
		for s.Next() {
			switch t := s.Request().(type) {
			case styx.Tstat:
				t.Rstat(emptyDir{}, nil)
			case styx.Twalk:
				t.Rwalk(emptyDir{}, nil)
			case styx.Topen:
				t.Ropen(emptyDir{}, nil)
			}
		}
	})
	log.Fatal(styx.ListenAndServe(":564", h))
}

Index

Examples

func ListenAndServe

func ListenAndServe(addr string, handler Handler) error

ListenAndServe listens on the specified TCP address, and then calls Serve with handler to handle requests of incoming connections.

func ListenAndServeTLS

func ListenAndServeTLS(addr string, certFile, keyFile string, handler Handler) error

ListenAndServeTLS listens on the specified TCP address for incoming TLS connections. certFile must be a valid x509 certificate in PEM format, concatenated with any intermediate and CA certificates.

type AuthFunc

type AuthFunc func(rwc *Channel, user, access string) error

An AuthFunc is used to authenticate a user to a 9P server. The authentication protocol itself is tunnelled over 9P via read and write operations to a special file, and is outside the scope of the 9P protocol.

An AuthFunc must determine that a client is authorized to start a 9P session to the file tree specified by the access parameter. The Auth method may receive and send data over rwc. Alternatively, additional information can be passed through the Channel's context for external authentication. Notably, the Conn method of the Channel can be used to access the underlying network connection, in order to authenticate based on TLS certificates, unix uid values (on a unix socket), etc.

An AuthFunc must return a non-nil error if authentication fails. The error may be sent to the client and should not contain any sensitive information. If authentication succeeds, an AuthFunc must return nil.

Existing AuthFunc implementations can be found in the styxauth package.

type AuthOpenFunc

type AuthOpenFunc func() (interface{}, error)

type Channel

type Channel struct {
	context.Context
	io.ReadWriteCloser
}

A Channel provides authentication methods with a bidirectional channel between the client and server, along with any contextual information recorded by the server. Of note is the "conn" value, which returns the underlying net.Conn value for the network connection.

func (*Channel) Conn

func (ch *Channel) Conn() interface{}

Conn retrieves the underlying io.ReadWriteCloser for a Channel.

type Directory

type Directory interface {
	Readdir(n int) ([]os.FileInfo, error)
}

In the 9P protocol, a directory is simply a file that returns zero or more styxproto.Stat structures when read. Types that implement the Directory interface can avoid marshalling styxproto.Stat methods in the Read methods. The Readdir method should return up to n os.FileInfo values, based on the contents of the given directory. Further calls to Readdir should pick up where the previous call left off.

If n <= 0, Readdir should return os.FileInfo values for all files in the directory.

type Handler

type Handler interface {
	Serve9P(*Session)
}

Types implementing the Handler interface can receive and respond to 9P requests from clients.

When a client connects to the server and starts a session, a new goroutine is created running the handler's Serve9P method. Each 9P message can be retrieved using the Session's Next and Request methods. Serve9P is expected to last for the duration of the 9P session; if the client ends the session, the Session's Next method will return false. If the Serve9P method exits prematurely, all open files and other resources associated with that session are released, and any further requests for that session will result in an error.

The Serve9P method is not required to answer every type of 9P message. If an existing request is unanswered when Serve9P fetches the next request, the styx package will reply to the client with a default response. The documentation for each request type notes its default response.

In practice, a Handler is usually composed of a for loop and a type switch, like so:

func (srv *Srv) Serve9P(s *styx.Session) {
	for s.Next() {
		switch msg := s.Request().(type) {
		case styx.Twalk:
			if (srv.exists(msg.Path()) {
				msg.Rwalk(srv.filemode(msg.Path())
			} else {
				msg.Rerror("%s does not exist", msg.Path())
			}
		case styx.Topen:
			msg.Ropen(srv.getfile(msg.Path()))
		case styx.Tcreate:
			msg.Rcreate(srv.newfile(msg.Path())
		}
	}
}

Possible message types are listed in the documentation for the Request type.

func Stack

func Stack(handlers ...Handler) Handler

Stack combines multiple handlers into one. When a new message is received from the client, it is passed to each handler, from left to right, until a response is sent. If no response is sent. by any handlers in the stack, the documented default response for that message type will be sent to the client. Handlers may use the UpdateRequest to pass information to downstream handlers.

Example

Code:

package main

import (
	"aqwari.net/net/styx"
	"context"
	"fmt"
	"sync/atomic"
)

func main() {
	// Associate a session ID with each session
	var sessionID int64
	sessionid := styx.HandlerFunc(func(s *styx.Session) {
		id := atomic.AddInt64(&sessionID, 1)
		for s.Next() {
			req := s.Request()
			ctx := context.WithValue(req.Context(), "session", id)
			s.UpdateRequest(req.WithContext(ctx))
		}
	})

	// echo requests to stdout
	echo := styx.HandlerFunc(func(s *styx.Session) {
		for s.Next() {
			req := s.Request()
			id := req.Context().Value("session")
			fmt.Printf("session %v user %q %q %T %s",
				id, s.User, s.Access, req, req.Path())
		}
	})

	// Disallow removal of any files
	blockops := styx.HandlerFunc(func(s *styx.Session) {
		for s.Next() {
			if t, ok := s.Request().(styx.Tremove); ok {
				t.Rerror("permission denied")
			}
		}
	})

	handler := styx.Stack(sessionid, echo, blockops)
	styx.ListenAndServe(":564", handler)
}

type HandlerFunc

type HandlerFunc func(s *Session)

The HandlerFunc provides a convenient adapter type that allows for normal functions to handle 9P sessions.

func (HandlerFunc) Serve9P

func (fn HandlerFunc) Serve9P(s *Session)

Serve9P calls fn(s).

type Logger

type Logger interface {
	Printf(string, ...interface{})
}

Types implementing the Logger interface can receive diagnostic information during a Server's operation. The Logger interface is implemented by *log.Logger.

type OwnerInfo

type OwnerInfo interface {
	Uid() string
	Gid() string
	Muid() string
}

The styx package will attempt to determine the ownership of a file by asking the host operating system, if it is a real file. If a given type implements the OwnerInfo interface, the styx package will use the methods therein to determine file ownership. Uid should return the user name of a file's owner. Gid should return the primary group of the file. Muid, if implemented, should return the name of the user who last modified the file. If Muid is not implemented, the styx package will always return the owner of the file for its Muid.

Usage of this interface is opportunistic; a type can implement all or some of the methods.

type Request

type Request interface {
	// Context is used to implement cancellation and request timeouts. If
	// an operation is going to take a long time to complete, you can
	// allow for the client to cancel the request by receiving on the
	// channel returned by the Context's Done method.
	Context() context.Context

	// WithContext returns a copy of the request with a new Context. It
	// can be used with nested handlers to attach information, deadlines,
	// and cancellations to a request.
	WithContext(context.Context) Request

	// If a request is invalid, not allowed, or cannot be completed properly
	// for some other reason, its Rerror method should be used to respond
	// to it.
	Rerror(format string, args ...interface{})

	// Path returns the Path of the file being operated on.
	Path() string
	// contains filtered or unexported methods
}

A Request is a request by a client to perform an operation on a file or set of files. Types of requests may range from checking if a file exists (Twalk) to opening a file (Topen) to changing a file's name (Twstat).

type Server

type Server struct {
	// Address to listen on, ":9pfs" if empty.
	Addr string

	// maximum wait before timing out write of the response.
	WriteTimeout time.Duration

	// maximum wait before closing an idle connection.
	IdleTimeout time.Duration

	// maximum size of a 9P message, DefaultMsize if unset.
	MaxSize int64

	// optional TLS config, used by ListenAndServeTLS
	TLSConfig *tls.Config

	// Handler to invoke for each session
	Handler Handler

	// Auth is used to authenticate user sessions. If nil,
	// authentication is disabled.
	Auth AuthFunc

	// OpenAuth is used to open file to authentication agent
	OpenAuth AuthOpenFunc

	// If not nil, ErrorLog will be used to log unexpected
	// errors accepting or handling connections. TraceLog,
	// if not nil, will receive detailed protocol tracing
	// information.
	ErrorLog, TraceLog Logger
}

A Server defines parameters for running a 9P server. The zero value of a Server is usable as a 9P server, and will use the defaults set by the styx package.

func (*Server) ListenAndServe

func (srv *Server) ListenAndServe() error

ListenAndServe listens on the TCP network address srv.Addr and calls Serve to handle requests on incoming connections. If srv.Addr is blank, :564 is used.

func (*Server) ListenAndServeTLS

func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error

ListenAndServeTLS listens on the TCP network address srv.Addr for incoming TLS connections.

func (*Server) Serve

func (srv *Server) Serve(l net.Listener) error

Serve accepts connections on the listener l, creating a new service goroutine for each. The service goroutines read requests and relays them to the appropriate Handler goroutines.

type Session

type Session struct {
	// User is the name of the user associated with the session.
	// When establishing a session, the client provides a username, This
	// may or may not be authenticated, depending on the Server in use.
	User string

	// Access is the name of the file tree requested by a client when
	// it establishes a session, in the "aname" field of its "Tattach"
	// request. When the EnableVHost option is used, if a client does
	// not specify one, this is set to the hostname the client used
	// to connect to the server, for non-TLS connections, and the SNI
	// provided by the client, for TLS connections.
	Access string

	// This tracks the number of fids pointing to this session in
	// conn.sessionFid. We need to know when all references are gone
	// so we can properly close any session channels.
	util.RefCount
	// contains filtered or unexported fields
}

A Session is a sequence of related 9P messages from a single client. It begins when a client opens the root of a file tree, and ends when all of its files are closed. Sessions occur over a single connection and are associated with a single user and file tree. Over a single session, a user may perform multiple operations on multiple files. Multiple sessions may be multiplexed over a single connection.

func (*Session) Next

func (s *Session) Next() bool

Next waits for the next Request for a 9P session. The next request for the session can be accessed via the Request method if and only if Next returns true. Any previous messages retrieved for the session should not be modified or responded to after Next is called; if they have not been answered, the styx package will send default responses for them. The default response for a message can be found in the comments for each message type. Next returns false if the session has ended or there was an error receiving the next Request.

func (*Session) Request

func (s *Session) Request() Request

Request returns the last 9P message received by the Session. It is only valid until the next call to Next.

func (*Session) UpdateRequest

func (s *Session) UpdateRequest(r Request)

When multiple Handlers are combined together using Stack, a handler may modify the incoming request using the UpdateRequest method. The current request will be overwritten with r, and reflected in calls to the Request method in he current and all downstream handlers.

type Tchmod

type Tchmod struct {
	Mode os.FileMode
	// contains filtered or unexported fields
}

A Tchmod message is sent by the client to change the permissions of an existing file. The client must have write access to the file's containing directory. Use the Rchmod method to indicate success.

The default response for a Tchmod request is an Rerror saying "permission denied"

func (Tchmod) Rchmod

func (t Tchmod) Rchmod(err error)

Rchmod, when called with a nil error, indicates that the permissions of the file were updated. Future stat requests should reflect the new file mode.

func (Tchmod) Rerror

func (t Tchmod) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Tchmod) WithContext

func (t Tchmod) WithContext(ctx context.Context) Request

type Tchown

type Tchown struct {
	User, Group string

	// These will only be set if using the 9P2000.u or 9P2000.L
	// extensions, and will be -1 otherwise.
	Uid, Gid int
	// contains filtered or unexported fields
}

A Tchown message is sent by the client to change the user and group associated with a file. Use the Rchown method to indicate success.

The default response to a Tchown message is an Rerror message saying "permission denied".

func (Tchown) Rchown

func (t Tchown) Rchown(err error)

Rchown, when called with a nil error, indicates that file and group ownership attributes were updated for the given file. Future stat requests for the same file should reflect the changes.

func (Tchown) Rerror

func (t Tchown) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Tchown) WithContext

func (t Tchown) WithContext(ctx context.Context) Request

type Tcreate

type Tcreate struct {
	Name string      // name of the file to create
	Mode os.FileMode // permissions and file type to create
	Flag int         // flags to open the new file with
	// contains filtered or unexported fields
}

A Tcreate message is sent when a client wants to create a new file and open it with the provided Mode. The Path method of a Tcreate message returns the absolute path of the containing directory. A user must have write permissions in the directory to create a file.

The default response to a Tcreate message is an Rerror message saying "permission denied".

func (Tcreate) Context

func (t Tcreate) Context() context.Context

Context returns the context associated with the request.

func (Tcreate) NewPath

func (t Tcreate) NewPath() string

NewPath joins the path for the Tcreate's containing directory with its Name field, returning the absolute path to the new file.

func (Tcreate) Path

func (t Tcreate) Path() string

Path returns the absolute path to the containing directory of the new file.

func (Tcreate) Rcreate

func (t Tcreate) Rcreate(rwc interface{}, err error)

Rcreate is used to respond to a succesful create request. With 9P, creating a file also opens the file for I/O. Once Rcreate returns, future read and write requests to the file handle will pass through rwc. The value rwc must meet the same criteria listed for the Ropen method of a Topen request.

func (Tcreate) Rerror

func (t Tcreate) Rerror(format string, args ...interface{})

Rerror sends an error to the client.

func (Tcreate) WithContext

func (t Tcreate) WithContext(ctx context.Context) Request

type Topen

type Topen struct {
	// The mode to open the file with. One of the flag constants
	// in the os package, such as O_RDWR, O_APPEND etc.
	Flag int
	// contains filtered or unexported fields
}

A Topen message is sent when a client wants to open a file for I/O Use the Ropen method to provide the opened file.

The default response to a Topen message to send an Rerror message saying "permssion denied".

func (Topen) Context

func (t Topen) Context() context.Context

Context returns the context associated with the request.

func (Topen) Path

func (t Topen) Path() string

Path returns the absolute path of the file being operated on.

func (Topen) Rerror

func (t Topen) Rerror(format string, args ...interface{})

Rerror sends an error to the client.

func (Topen) Ropen

func (t Topen) Ropen(rwc interface{}, err error)

The Ropen method signals to the client that a file has succesfully been opened and is ready for I/O. After Ropen returns, future reads and writes to the opened file handle will pass through rwc.

The value rwc must implement some of the interfaces in the io package for reading and writing. If the type implements io.Seeker or io.ReaderAt and io.WriterAt, clients may read or write at arbitrary offsets within the file. Types that only implement Read or Write operations will return errors on writes and reads, respectively.

If rwc implements the Stat method of os.File, that will be used to answer Tstat requests. Otherwise, the styx package will assemble Rstat responses out of default values merged with any methods rwc provides from the os.FileInfo interface.

If a file does not implement any of the Read or Write interfaces in the io package, A generic error is returned to the client, and a message will be written to the server's ErrorLog.

func (Topen) WithContext

func (t Topen) WithContext(ctx context.Context) Request

type Tremove

type Tremove struct {
	// contains filtered or unexported fields
}

A Tremove message is sent when a client wants to delete a file from the server. The Rremove method should be called once the file has been succesfully deleted.

The default response to a Tremove message is an Rerror message saying "permission denied".

func (Tremove) Context

func (t Tremove) Context() context.Context

Context returns the context associated with the request.

func (Tremove) Path

func (t Tremove) Path() string

Path returns the absolute path of the file being operated on.

func (Tremove) Rerror

func (t Tremove) Rerror(format string, args ...interface{})

Rerror sends an error to the client.

func (Tremove) Rremove

func (t Tremove) Rremove(err error)

Rremove signals to the client that a file has been succesfully removed. The file handle for the file is no longer valid, and may be re-used for other files. Whether or not any other file handles associated with the file continue to be usable for I/O is implementation-defined; many Unix file systems allow a process to continue writing to a file that has been "unlinked", so long as the process has an open file descriptor.

If err is non-nil, an Rerror message is sent to the client. Regardless, the file handle is no longer valid.

func (Tremove) WithContext

func (t Tremove) WithContext(ctx context.Context) Request

type Trename

type Trename struct {
	OldPath, NewPath string
	// contains filtered or unexported fields
}

A Trename message is sent by the client to change the name of an existing file. Use the Rrename method to indicate success.

The default response for a Trename request is an Rerror message saying "permission denied"

func (Trename) Path

func (t Trename) Path() string

The Path method of a Trename request returns the current path to the file, before a rename has taken place.

func (Trename) Rerror

func (t Trename) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Trename) Rrename

func (t Trename) Rrename(err error)

Rrename indicates to the server that the rename was succesful. Once Rrename is called with a nil error, future stat requests should reflect the updated name.

func (Trename) WithContext

func (t Trename) WithContext(ctx context.Context) Request

type Tstat

type Tstat struct {
	// contains filtered or unexported fields
}

A Tstat message is sent when a client wants metadata about a file. A client should have read access to the file's containing directory. Call the Rstat method for a succesful request.

The default response for a Tstat message is an Rerror message saying "permission denied".

func (Tstat) Context

func (t Tstat) Context() context.Context

Context returns the context associated with the request.

func (Tstat) Path

func (t Tstat) Path() string

Path returns the absolute path of the file being operated on.

func (Tstat) Rerror

func (t Tstat) Rerror(format string, args ...interface{})

Rerror sends an error to the client.

func (Tstat) Rstat

func (t Tstat) Rstat(info os.FileInfo, err error)

Rstat responds to a succesful Tstat request. The styx package will translate the os.FileInfo value into the appropriate 9P structure. Rstat will attempt to resolve the names of the file's owner and group. If that cannot be done, an empty string is sent. If err is non-nil, and error is sent to the client instead.

func (Tstat) WithContext

func (t Tstat) WithContext(ctx context.Context) Request

type Tsync

type Tsync struct {
	// contains filtered or unexported fields
}

A Tsync request is made by the client to indicate that the client would like any changes made to the file to be flushed to durable storage. Use the Rsync method to indicate success.

The default response to a Tsync message is an Rerror message saying "not supported".

func (Tsync) Rerror

func (t Tsync) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Tsync) Rsync

func (t Tsync) Rsync(err error)

Rsync, when called with a nil error, indicates that the file has been flushed to durable storage. Note that different servers will have different definitions of what "durable" means, and provide different consistency guarantees.

func (Tsync) WithContext

func (t Tsync) WithContext(ctx context.Context) Request

type Ttruncate

type Ttruncate struct {
	Size int64
	// contains filtered or unexported fields
}

A Ttruncate requests for the size of a file to be changed. Use the Rtruncate method to indicate success.

The default response to a Ttruncate message is an Rerror message saying "permission denied".

func (Ttruncate) Rerror

func (t Ttruncate) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Ttruncate) Rtruncate

func (t Ttruncate) Rtruncate(err error)

Rtruncate, when called with a nil error, indicates that the file has been updated to reflect Size. Future reads, writes and stats should reflect the new file length.

func (Ttruncate) WithContext

func (t Ttruncate) WithContext(ctx context.Context) Request

type Tutimes

type Tutimes struct {
	Atime, Mtime time.Time
	// contains filtered or unexported fields
}

A Tutimes message is sent by the client to change the modification time of a file. Use the Rutime method to indicate success.

The default response to a Tutimes message is an Rerror message saying "permission denied"

func (Tutimes) Rerror

func (t Tutimes) Rerror(format string, args ...interface{})

Call Rerror to provide a descriptive error message explaining why a file attribute could not be updated.

func (Tutimes) Rutimes

func (t Tutimes) Rutimes(err error)

Rutimes, when called with a nil error, indicates that the file times were succesfully updated. Future stat requests should reflect the new access and modification times.

func (Tutimes) WithContext

func (t Tutimes) WithContext(ctx context.Context) Request

type Twalk

type Twalk struct {
	// contains filtered or unexported fields
}

A client sends a Twalk message both to probe if a file exists, and to move a "cursor" within the filesystem hierarchy. In a traditional file system, a Twalk request is similar to using chdir to change the current directory. File servers are free to attach additional meaning to Twalk requests. For instance, a server may create directories on-demand as clients walk to them.

The 9P protocol allows for clients to walk multiple directories with a single 9P message. The styx package translates such requests into multiple Twalk values, providing the following guarantees:

- Path() will return a cleaned, absolute path
- Consecutive, related Twalk requests will differ by at
  most 1 path element.

The default response to a Twalk request is an Rerror message saying "No such file or directory".

func (Twalk) Context

func (t Twalk) Context() context.Context

Context returns the context associated with the request.

func (Twalk) Path

func (t Twalk) Path() string

Path returns the absolute path of the file being operated on.

func (Twalk) Rerror

func (t Twalk) Rerror(format string, args ...interface{})

Rerror signals to the client that the file named by the Twalk's Path method does not exist.

func (Twalk) Rwalk

func (t Twalk) Rwalk(info os.FileInfo, err error)

Rwalk signals to the client that the file named by the Twalk's Path method exists and is of the given mode. The permission bits of mode are ignored, and only the file type bits, such as os.ModeDir, are sent to the client. If err is non-nil, an error response is sent to the client instead.

func (Twalk) WithContext

func (t Twalk) WithContext(ctx context.Context) Request

Package Files

BUGs

  • cancellation of write requests is not yet implemented.

  • renaming a file with one fid will break Twalk requests that attempt to clone another fid pointing to the same file.

Documentation was rendered with GOOS=linux and GOARCH=amd64.

Jump to identifier

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to identifier