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() }