2023-05-25 23:18:16 -06:00
|
|
|
// Package service provides a toolkit for creating Hnakra services.
|
2023-05-25 16:08:56 -06:00
|
|
|
package service
|
|
|
|
|
|
|
|
import "os"
|
|
|
|
import "log"
|
|
|
|
import "fmt"
|
|
|
|
import "net"
|
|
|
|
import "errors"
|
|
|
|
import "strings"
|
|
|
|
import "crypto/tls"
|
|
|
|
import "encoding/base64"
|
|
|
|
import "hnakra/protocol"
|
|
|
|
|
|
|
|
// M creates a very basic mount with the specified name and description.
|
|
|
|
func M (name, description string) Mount {
|
|
|
|
return Mount {
|
|
|
|
Name: name,
|
|
|
|
Description: description,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mount contains generic information common to all mounts.
|
|
|
|
type Mount 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 for host portion: 255 bytes
|
|
|
|
Host 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
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
TLSConfig *tls.Config
|
|
|
|
|
|
|
|
idFactory *protocol.IDFactory
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mount *Mount) connect (scheme string) (conn net.Conn, err error) {
|
|
|
|
log.Println("(i) service", mount.Name)
|
|
|
|
mount.idFactory = protocol.NewServiceIDFactory()
|
|
|
|
|
|
|
|
defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
|
|
|
|
if defaultRouterHost == "" {
|
|
|
|
defaultRouterHost = "localhost"
|
|
|
|
}
|
|
|
|
defaultRouterPort := os.Getenv("HNAKRA_ROUTER_PORT")
|
|
|
|
if defaultRouterPort == "" {
|
|
|
|
defaultRouterPort = "2048"
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse router host/port
|
|
|
|
routerHost, routerPort, _ := strings.Cut(mount.Router, ":")
|
|
|
|
if routerHost == "" {
|
|
|
|
routerHost = defaultRouterHost
|
|
|
|
}
|
|
|
|
if routerPort == "" {
|
|
|
|
routerPort = defaultRouterPort
|
|
|
|
}
|
|
|
|
|
|
|
|
// get mount point
|
|
|
|
host := mount.Host
|
|
|
|
if host == "" {
|
|
|
|
host = "@"
|
|
|
|
}
|
|
|
|
if len(host) > 255 {
|
|
|
|
return nil, errors.New("mount point host cannot be longer than 255 bytes")
|
|
|
|
}
|
|
|
|
path := mount.Path
|
|
|
|
if path == "" {
|
|
|
|
path = "/"
|
|
|
|
}
|
|
|
|
if len(path) > int(protocol.MaxIntOfSize(2)) {
|
|
|
|
return nil, errors.New(fmt.Sprint (
|
|
|
|
"mount point path cannot be longer than ",
|
|
|
|
protocol.MaxIntOfSize(2), " bytes"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// get user
|
|
|
|
user := mount.User
|
|
|
|
if user == "" {
|
|
|
|
user = os.Getenv("HNAKRA_USER")
|
|
|
|
}
|
|
|
|
if len(user) > 255 {
|
|
|
|
return nil, errors.New("user cannot be longer than 255 bytes")
|
|
|
|
}
|
|
|
|
|
|
|
|
// get key
|
|
|
|
key := mount.Key
|
|
|
|
if key == nil {
|
|
|
|
key, err = base64.StdEncoding.DecodeString(os.Getenv("HNAKRA_KEY"))
|
|
|
|
if err != nil { return nil, err }
|
|
|
|
}
|
|
|
|
if len(key) > 255 {
|
|
|
|
return nil, errors.New("key cannot be longer than 255 bytes")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure name/description aren't too big
|
|
|
|
if len(mount.Name) > 255 {
|
|
|
|
return nil, errors.New("service name cannot be longer than 255 bytes")
|
|
|
|
}
|
|
|
|
if len(mount.Description) > 255 {
|
|
|
|
return nil, errors.New("service description cannot be longer than 255 bytes")
|
|
|
|
}
|
|
|
|
|
|
|
|
// connect to router
|
|
|
|
routerAddr := fmt.Sprint(routerHost, ":", routerPort)
|
|
|
|
log.Println("... dialing", routerAddr)
|
|
|
|
conn, err = tls.Dial("tcp", routerAddr, mount.TLSConfig)
|
|
|
|
if err != nil { return nil, err }
|
|
|
|
|
|
|
|
// log in
|
|
|
|
log.Println (
|
|
|
|
"... logging in as", user,
|
|
|
|
"on", scheme + "://" + host + path)
|
|
|
|
err = protocol.MessageLogin {
|
|
|
|
ID: mount.idFactory.Next(),
|
|
|
|
Version: protocol.Version { Major: 0, Minor: 0 },
|
|
|
|
User: user,
|
|
|
|
Key: key,
|
|
|
|
Name: mount.Name,
|
|
|
|
Description: mount.Description,
|
|
|
|
Scheme: scheme,
|
|
|
|
Host: host,
|
|
|
|
Path: path,
|
|
|
|
}.Send(conn)
|
|
|
|
if err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// read status
|
|
|
|
message, err := protocol.ReadMessage(conn)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: other protocols like gemini, websocket, ftp, etc
|