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