260 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package service
 | |
| 
 | |
| import "os"
 | |
| import "log"
 | |
| import "fmt"
 | |
| import "net"
 | |
| import "sync"
 | |
| import "bufio"
 | |
| import "errors"
 | |
| import "strings"
 | |
| import "crypto/tls"
 | |
| import "encoding/base64"
 | |
| import "hnakra/protocol"
 | |
| 
 | |
| // Mount is an interface satisfied by all mount types.
 | |
| type Mount interface {
 | |
| 	Run      (ServiceInfo) error
 | |
| 	Close    () error
 | |
| 	Shutdown () error
 | |
| }
 | |
| 
 | |
| // MountInfo contains information about a mount point.
 | |
| type MountInfo struct {
 | |
| 	// Host specifies the host to mount on. If the host is left empty, it
 | |
| 	// will default to @ (meaning default/any host). The port is entirely up
 | |
| 	// to the router.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	Host string
 | |
| 
 | |
| 	// Scheme specifies the protocol to mount on. This will be automatically
 | |
| 	// set by specialized mount types, so setting it manually shouldn't be
 | |
| 	// needed.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	Scheme string
 | |
| 
 | |
| 	// Path specifies the path to mount on. If the path ends with a /, then
 | |
| 	// all requests under the path will be sent to this service. If there is
 | |
| 	// no trailing /, this service will only recieve requests that match the
 | |
| 	// path exactly (when normalized).
 | |
| 	// Maximum length: 2^16-1 bytes
 | |
| 	Path string
 | |
| }
 | |
| 
 | |
| // String returns a string representation of the mount.
 | |
| func (mount *MountInfo) String () string {
 | |
| 	return mount.Scheme + "://" + mount.Host + mount.Path
 | |
| }
 | |
| 
 | |
| // FillDefault fills most empty fields with a hard-coded default value.
 | |
| func (mount *MountInfo) FillDefault () {
 | |
| 	if mount.Host == "" { mount.Host   = "@" }
 | |
| 	if mount.Path == "" { mount.Scheme = "/" }
 | |
| }
 | |
| 
 | |
| // Fits returns an error if any data is too big to send over the connection.
 | |
| func (mount *MountInfo) Fits () error {
 | |
| 	switch {
 | |
| 	case len(mount.Host) > 255:
 | |
| 		return errors.New("host cannot be longer than 255 bytes")
 | |
| 	case len(mount.Scheme) > 255:
 | |
| 		return errors.New("scheme cannot be longer than 255 bytes")
 | |
| 	case len(mount.Path) > int(protocol.MaxIntOfSize(2)):
 | |
| 		return errors.New(fmt.Sprint (
 | |
| 			"mount point path cannot be longer than ",
 | |
| 			protocol.MaxIntOfSize(2), " bytes"))
 | |
| 	default: return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ServiceInfo contains information about the service as a whole, such as a
 | |
| // human readable description and login credentials.
 | |
| type ServiceInfo struct {
 | |
| 	// Router specifies the host:port of the router to connect to. This
 | |
| 	// defaults to $HNAKRA_ROUTER_HOST:$HNAKRA_ROUTER_PORT if left empty.
 | |
| 	// The default value of these environment variables (if not set) is
 | |
| 	// localhost:2048. If you are giving this service to other people, it is
 | |
| 	// a good idea to leave this empty.
 | |
| 	Router string
 | |
| 
 | |
| 	// Name should be set to a human-readable name of the service.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	Name string
 | |
| 
 | |
| 	// Description should be set to a human-readable description of what the
 | |
| 	// service does.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	Description string
 | |
| 
 | |
| 	// User is an optional string that specifies who is connecting to the
 | |
| 	// router. This can be used by routers in conjunction with the key
 | |
| 	// bytes in order to enforce rules about which service can do what. This
 | |
| 	// defaults to $HNAKRA_USER if left empty. If you are giving this
 | |
| 	// service to other people, it is a good idea to leave this empty.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	User string
 | |
| 
 | |
| 	// Key is an optional byte slice that is sent to the router in order for
 | |
| 	// it to authorize the connection. If nil, this defaults to the contents
 | |
| 	// of $HNAKRA_KEY interpreted as base64. Do not embed a key in your
 | |
| 	// code - you should only use this if your service reads the key from a
 | |
| 	// safe source upon startup. In addition to this, If you choose to set
 | |
| 	// the key via the aforementioned enviroment variable, ensure that it
 | |
| 	// can only be read by the service.
 | |
| 	// Maximum length: 255 bytes
 | |
| 	Key []byte
 | |
| 
 | |
| 	// TLSConfig is an optional TLS configuration. If you are looking to
 | |
| 	// set InsecureSkipVerify to false, consider instead setting the
 | |
| 	// environment variables $SSL_CERT_FILE or $SSL_CERT_DIR to point toward
 | |
| 	// a custom root certificate.
 | |
| 	TLSConfig *tls.Config	
 | |
| }
 | |
| 
 | |
| // FillDefault fills most empty fields with values from environment variables.
 | |
| // If an environment variable is blank, it uses a hard-coded default value
 | |
| // instead.
 | |
| func (service *ServiceInfo) FillDefault () (err error) {
 | |
| 	// host
 | |
| 	defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
 | |
| 	if defaultRouterHost == "" {
 | |
| 		defaultRouterHost = "localhost"
 | |
| 	}
 | |
| 	defaultRouterPort := os.Getenv("HNAKRA_ROUTER_PORT")
 | |
| 	if defaultRouterPort == "" {
 | |
| 		defaultRouterPort = "2048"
 | |
| 	}
 | |
| 	routerHost, routerPort, _ := strings.Cut(service.Router, ":")
 | |
| 	if routerHost == "" {
 | |
| 		routerHost = defaultRouterHost
 | |
| 	}
 | |
| 	if routerPort == "" {
 | |
| 		routerPort = defaultRouterPort
 | |
| 	}
 | |
| 	service.Router = routerHost + ":" + routerPort
 | |
| 
 | |
| 	// user
 | |
| 	if service.User == "" {
 | |
| 		service.User = os.Getenv("HNAKRA_USER")
 | |
| 	}
 | |
| 
 | |
| 	// key
 | |
| 	if service.Key == nil {
 | |
| 		base64Key := os.Getenv("HNAKRA_KEY")
 | |
| 		service.Key, err = base64.StdEncoding.DecodeString(base64Key)
 | |
| 		if err != nil { return }
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // Fits returns an error if any data is too big to send over the connection.
 | |
| func (service *ServiceInfo) Fits () (err error) {
 | |
| 	switch {
 | |
| 	case len(service.Name) > 255:
 | |
| 		return errors.New("name cannot be longer than 255 bytes")
 | |
| 	case len(service.Description) > 255:
 | |
| 		return errors.New("description cannot be longer than 255 bytes")
 | |
| 	case len(service.User) > 255:
 | |
| 		return errors.New("user cannot be longer than 255 bytes")
 | |
| 	case len(service.Key) > 255:
 | |
| 		return errors.New("key cannot be longer than 255 bytes")
 | |
| 	default: return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Conn represents a connection to a router.
 | |
| type Conn struct {
 | |
| 	IDFactory *protocol.IDFactory
 | |
| 	
 | |
| 	conn       net.Conn
 | |
| 	writeLock  sync.Mutex
 | |
| 	readWriter *bufio.ReadWriter
 | |
| }
 | |
| 
 | |
| // Dial connects to a router, returning the resulting connection. It handles
 | |
| // performing the login sequence and sets ID(0) as active automatically.
 | |
| func Dial (mount MountInfo, service ServiceInfo) (conn *Conn, err error) {
 | |
| 	// fill in default values from env variables and such
 | |
| 	mount.FillDefault()
 | |
| 	err = service.FillDefault()
 | |
| 	if err != nil { return nil, err }
 | |
| 
 | |
| 	// sanity check
 | |
| 	err = mount.Fits()
 | |
| 	if err != nil { return nil, err }
 | |
| 	err = service.Fits()
 | |
| 	if err != nil { return nil, err }
 | |
| 
 | |
| 	conn = &Conn {
 | |
| 		IDFactory: protocol.NewServiceIDFactory(),
 | |
| 	}
 | |
| 
 | |
| 	// connect to router
 | |
| 	log.Println("... dialing", service.Router)
 | |
| 	conn.conn, err = tls.Dial("tcp", service.Router, service.TLSConfig)
 | |
| 	if err != nil { return nil, err }
 | |
| 	conn.readWriter = bufio.NewReadWriter (
 | |
| 		bufio.NewReader(conn.conn),
 | |
| 		bufio.NewWriter(conn.conn))
 | |
| 	
 | |
| 	// log in
 | |
| 	log.Println("... logging in as", service.User, "on", mount)
 | |
| 	err = conn.Send(protocol.MessageLogin {
 | |
| 		ID:          conn.IDFactory.Next(),
 | |
| 		Version:     protocol.Version { Major: 0, Minor: 0 },
 | |
| 		User:        service.User,
 | |
| 		Key:         service.Key,
 | |
| 		Name:        service.Name,
 | |
| 		Description: service.Description,
 | |
| 		Scheme:      mount.Scheme,
 | |
| 		Host:        mount.Host,
 | |
| 		Path:        mount.Path,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		conn.Close()
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// read status
 | |
| 	message, err := conn.Receive()
 | |
| 	if err != nil {
 | |
| 		conn.Close()
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	status, ok := message.(protocol.MessageStatus)
 | |
| 	if !ok {
 | |
| 		conn.Close()
 | |
| 		return nil, errors.New(fmt.Sprint (
 | |
| 			"router sent unknown type, expecting",
 | |
| 			protocol.TypeStatus))
 | |
| 	}
 | |
| 	if status.Status != protocol.StatusOk {
 | |
| 		return nil, status
 | |
| 	}
 | |
| 	
 | |
| 	log.Println(".// logged in")
 | |
| 	return conn, nil
 | |
| }
 | |
| 
 | |
| // Send sends a message along the connection, along with its type code. This
 | |
| // method may be called concurrently.
 | |
| func (conn *Conn) Send (message protocol.Message) (err error) {
 | |
| 	conn.writeLock.Lock()
 | |
| 	defer conn.writeLock.Unlock()
 | |
| 	err = message.Send(conn.readWriter)
 | |
| 	if err != nil { return }
 | |
| 	return conn.readWriter.Flush()
 | |
| }
 | |
| 
 | |
| // Receive recieves a message from the connection. This method may not be called
 | |
| // concurrently.
 | |
| func (conn *Conn) Receive () (message protocol.Message, err error) {
 | |
| 	return protocol.ReadMessage(conn.conn)
 | |
| }
 | |
| 
 | |
| // Close closes the connection.
 | |
| func (conn *Conn) Close () error {
 | |
| 	return conn.conn.Close()
 | |
| }
 |