Compare commits

...

30 Commits

Author SHA1 Message Date
Sasha Koshka
1978e263d6 sahd;ljasl;dkj 2023-06-01 03:44:33 -04:00
Sasha Koshka
7726d732d4 we got some dhrek we got some doneky we got some fienona 2023-06-01 03:37:08 -04:00
Sasha Koshka
32bc44c90f Oops lol 2023-06-01 03:00:15 -04:00
Sasha Koshka
805f42d828 Changed name of router to hn-router 2023-06-01 02:59:18 -04:00
Sasha Koshka
e8349360cc hnctl prints line breaks after errors 2023-06-01 01:01:08 -04:00
Sasha Koshka
4e58df9c9b Fix hnctl 2023-06-01 00:59:03 -04:00
Sasha Koshka
f90421e5db Add gitignore 2023-05-31 22:35:25 -04:00
Sasha Koshka
c9f2c56d65 Services no longer print out errors when they shut down 2023-05-31 22:00:21 -04:00
Sasha Koshka
92b645f34c Routine manager now recovers from panicking goroutines 2023-05-31 18:49:00 -04:00
Sasha Koshka
3cd53b3dd9 Fixed router 2023-05-31 18:09:56 -04:00
Sasha Koshka
8a528e2b4e Services can now write to pidfiles 2023-05-31 18:08:29 -04:00
Sasha Koshka
631796a726 Routine manager can now be shut down 2023-05-31 18:07:38 -04:00
Sasha Koshka
066247a08f Add daemon utilities 2023-05-31 18:07:06 -04:00
Sasha Koshka
8c9b85d7ca Ported hivectl 2023-05-31 15:52:33 -04:00
Sasha Koshka
1a5502211e Updated wrench to use cli 2023-05-30 18:03:26 -04:00
Sasha Koshka
9d8e6e8e24 Added cli package 2023-05-30 18:00:26 -04:00
Sasha Koshka
92b93abb13 Fixed my god awful 3am code 2023-05-30 11:07:57 -04:00
Sasha Koshka
6f876b2a17 Migrated wrench command 2023-05-30 03:43:09 -04:00
Sasha Koshka
17a816e360 Downgrade to go 1.19 2023-05-30 02:15:08 -04:00
Sasha Koshka
1e1ae572f2 Updated examples to match 2023-05-29 21:22:46 -04:00
Sasha Koshka
d716aa9455 Restructured services 2023-05-29 17:03:27 -04:00
Sasha Koshka
5e37c4bb8f No more funny business with InsecureSkipVerify 2023-05-27 03:57:27 -04:00
Sasha Koshka
9025844212 Update examples to match 2023-05-26 20:27:59 -04:00
Sasha Koshka
5d5d58544f Add doc comment to routines package 2023-05-26 20:04:03 -04:00
Sasha Koshka
3d0a69d5a3 Added a convenient Service type 2023-05-26 20:01:27 -04:00
Sasha Koshka
53e7e00f80 Mount is now MountConfig (and fully public) 2023-05-26 19:42:52 -04:00
Sasha Koshka
92911cdab4 Mount is defined in mount.go 2023-05-26 13:08:42 -04:00
Sasha Koshka
3c7b964cfe Use routine.Manager in router 2023-05-26 13:07:05 -04:00
Sasha Koshka
23f7497622 Routine manager now manages functions 2023-05-26 13:06:21 -04:00
Sasha Koshka
b17bad32fe Changed some bad import paths 2023-05-26 13:05:22 -04:00
21 changed files with 1202 additions and 325 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/hnctl
/router
/wrench

81
cli/cli.go Normal file
View File

@@ -0,0 +1,81 @@
// Package cli provides utilities for writing command line utilities that
// interact with services.
package cli
import "os"
import "fmt"
import "flag"
import "os/user"
import "strings"
import "strconv"
import "path/filepath"
// Sayf is like Printf, but prints the program name before the message. This is
// used for printing messages and errors.
func Sayf (format string, values ...any) {
Printf(os.Args[0] + ": " + format, values...)
}
// Printf prints to stderr.
func Printf (format string, values ...any) {
fmt.Fprintf(flag.CommandLine.Output(), format, values...)
}
// ServiceUser returns the system user that corresponds to the given service
// name. This is not necissarily equivalent Hnakra user, although it is good
// practice to have a 1:1 correspondance between them.
func ServiceUser (service string) string {
return "hn-" + strings.ToLower(service)
}
// DataDir returns the standard Hnakra data directory.
func DataDir () string {
return "/var/hnakra"
}
// ServiceDir returns the standard data directory of a service.
func ServiceDir (service string) string {
return filepath.Join(DataDir(), "services", ServiceUser(service))
}
// NeedRoot halts the program and displays an error if it is not being run as
// root. This should be called whenever an operation takes place that requires
// root privelages.
func NeedRoot() {
uid := os.Getuid()
if uid != 0 {
Sayf("this utility must be run as root")
os.Exit(1)
}
}
// MkdirFor makes a directory makes the specified directory (if it doesnt
// already exist) and gives ownership of it to the specified uid and gid.
func MkdirFor (directory string, uid, gid int) error {
err := os.MkdirAll(directory, 0755)
if err != nil { return err }
err = os.Chmod(directory, 0770)
if err != nil { return err }
err = os.Chown(directory, uid, gid)
if err != nil { return err }
return nil
}
// LookupUID returns the uid and gid of the given username, if it exists.
func LookupUID (name string) (uid, gid uint32, err error) {
user, err := user.Lookup(name)
if err != nil {
return 0, 0, err
}
puid, err := strconv.Atoi(user.Uid)
if err != nil {
return 0, 0, err
}
pgid, err := strconv.Atoi(user.Gid)
if err != nil {
return 0, 0, err
}
return uint32(puid), uint32(pgid), nil
}

View File

@@ -3,12 +3,15 @@ package main
import "io"
import "os"
import "log"
import "hnakra/rcon"
import "hnakra/rotate"
import "hnakra/config"
import "time"
import "hnakra/router"
import "hnakra/srvhttps"
import "hnakra/srvhnakra"
import "hnakra/rotate"
import "hnakra/daemon"
import "hnakra/routines"
import "hnakra/router/rcon"
import "hnakra/router/config"
import "hnakra/cmd/hn-router/srvhttps"
import "hnakra/cmd/hn-router/srvhnakra"
const banner = "\n" +
" -=\\\n" +
@@ -21,7 +24,7 @@ const banner = "\n" +
" '/"
func main () {
// setup logging
// set up logging
logDir := os.Getenv("HNAKRA_LOG_DIR")
if logDir != "" {
logger, err := rotate.New(logDir)
@@ -31,7 +34,6 @@ func main () {
rcon := rcon.New("/debug/rcon")
originalWriter := log.Writer()
log.SetOutput(io.MultiWriter(originalWriter, rcon))
log.Println(banner)
// load config
@@ -53,10 +55,18 @@ func main () {
})
rcon.SetConfig(conf)
// start servers
// set up servers
log.Println("... starting up")
manager := routines.Manager { RestartDeadline: time.Second * 8 }
rout := router.New(conf)
srvhnakra := &srvhnakra.Server { Config: conf, Router: rout }
manager.Append(srvhnakra)
if conf.HTTPSEnable() {
srvhttps := &srvhttps.Server { Config: conf, Handler: rout }
manager.Append(srvhttps)
}
// set up rcon
rcon.SetRouter(rout)
if conf.RconEnable() {
err = rout.HTTPMux().Handle("@/debug/rcon/", rcon)
@@ -64,16 +74,20 @@ func main () {
} else {
log.SetOutput(originalWriter)
}
if conf.HTTPSEnable() {
srvhttps := &srvhttps.Server { Config: conf, Handler: rout }
go httpsRoutine(srvhttps)
}
err = srvhnakra.ListenAndServe()
if err != nil { log.Println("XXX", err) }
}
func httpsRoutine (server *srvhttps.Server) {
err := server.ListenAndServe()
// be a daemon
daemon.ShutdownOnSigint(&manager)
pidfile := daemon.PidFile(os.Getenv("HNAKRA_PIDFILE"))
if !pidfile.Empty() {
err := pidfile.Start()
if err != nil { log.Println("!!! could not write pid:", err) }
defer func () {
err := pidfile.Close()
if err != nil { log.Println("!!! could not delete pidfile:", err) }
} ()
}
// run servers
err = manager.Run()
if err != nil { log.Println("XXX", err) }
}

View File

@@ -5,30 +5,40 @@ import "fmt"
import "net"
import "crypto/tls"
import "hnakra/router"
import "hnakra/config"
import "hnakra/router/config"
type Server struct {
underlying net.Listener
Config config.Config
Router *router.Router
running bool
}
func (server *Server) ListenAndServe () (err error) {
func (server *Server) Run () (err error) {
server.underlying, err = tls.Listen (
"tcp", fmt.Sprint(":", server.Config.RouterPort()),
config.TLSConfigFor(server.Config))
if err != nil { return err }
server.running = true
log.Println(".// router on", server.underlying.Addr())
for {
conn, err := server.underlying.Accept()
if err != nil { return err }
if err != nil {
if server.running {
return err
} else {
return nil
}
}
log.Println("-=E incoming connection from", conn.RemoteAddr())
server.Router.Accept(conn)
}
}
func (server *Server) Close () error {
func (server *Server) Shutdown () error {
server.running = false
return server.underlying.Close()
}

View File

@@ -3,15 +3,17 @@ package srvhttps
import "fmt"
import "log"
import "net/http"
import "hnakra/config"
import "hnakra/router/config"
type Server struct {
underlying *http.Server
Config config.Config
Handler http.Handler
running bool
}
func (server *Server) ListenAndServe () error {
func (server *Server) Run () error {
server.underlying = &http.Server {
Addr: fmt.Sprint(":", server.Config.HTTPSPort()),
// ReadHeaderTimeout: timeoutReadHeader * time.Second,
@@ -22,10 +24,17 @@ func (server *Server) ListenAndServe () error {
Handler: server.Handler,
}
server.running = true
log.Println(".// https on", server.underlying.Addr)
return server.underlying.ListenAndServeTLS("", "")
err := server.underlying.ListenAndServeTLS("", "")
if server.running {
return err
} else {
return nil
}
}
func (server *Server) Close () error {
func (server *Server) Shutdown () error {
server.running = false
return server.underlying.Close()
}

140
cmd/hnctl/main.go Normal file
View File

@@ -0,0 +1,140 @@
package main
import "os"
import "fmt"
import "time"
import "flag"
import "os/exec"
import "hnakra/cli"
import "path/filepath"
import "hnakra/cmd/hnctl/spawn"
func main () {
flag.Usage = func () {
out := flag.CommandLine.Output()
fmt.Fprintf(out, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(out, " start\n")
fmt.Fprintf(out, " Start a service\n")
fmt.Fprintf(out, " stop\n")
fmt.Fprintf(out, " Stop a service\n")
fmt.Fprintf(out, " restart\n")
fmt.Fprintf(out, " Start and then stop a service\n")
os.Exit(1)
}
// define commands
startCommand := flag.NewFlagSet("start", flag.ExitOnError)
startService := startCommand.String("s", "router", "Service to start")
stopCommand := flag.NewFlagSet("stop", flag.ExitOnError)
stopService := stopCommand.String("s", "router", "Service to stop")
restartCommand := flag.NewFlagSet("restart", flag.ExitOnError)
restartService := restartCommand.String("s", "router", "Service to restart")
flag.Parse()
// execute correct command
if len(os.Args) < 2 {
flag.Usage()
os.Exit(1)
}
subCommandArgs := os.Args[2:]
switch os.Args[1] {
case "start":
startCommand.Parse(subCommandArgs)
execStart(*startService)
case "stop":
stopCommand.Parse(subCommandArgs)
execStop(*stopService)
case "restart":
restartCommand.Parse(subCommandArgs)
execStop(*restartService)
execStart(*restartService)
}
}
func execStart (service string) {
fullName := cli.ServiceUser(service)
cli.NeedRoot()
pid, err := spawn.PidOf(fullName)
if err == nil && spawn.Running(pid) {
cli.Sayf("service is already running\n")
return
}
uid, gid, err := cli.LookupUID(fullName)
if err != nil {
cli.Sayf("cannot start service: %v\n", err)
os.Exit(1)
}
path, err := exec.LookPath(fullName)
if err != nil {
cli.Sayf("cannot start service: %v\n", err)
os.Exit(1)
}
logDir := filepath.Join("/var/log/", fullName)
env := append(os.Environ(), "HNAKRA_LOG_DIR=" + logDir)
err = cli.MkdirFor(logDir, int(uid), int(gid))
if err != nil {
cli.Sayf("cannot start service: %v\n", err)
os.Exit(1)
}
// prepare pidfile. the service will be responsible for actually writing
// to it
err = ensurePidFile(spawn.PidFile(fullName), int(uid), int(gid))
if err != nil {
cli.Sayf("cannot start service: %v\n", err)
os.Exit(1)
}
// spawn the service
pid, err = spawn.Spawn(path, uid, gid, env)
if err != nil {
cli.Sayf("cannot start service: %v\n", err)
os.Exit(1)
}
fmt.Println(pid)
}
func execStop (service string) {
fullName := cli.ServiceUser(service)
cli.NeedRoot()
pid, err := spawn.PidOf(fullName)
if err != nil || !spawn.Running(pid) {
cli.Sayf("service is not running\n")
return
}
process, err := os.FindProcess(pid)
if err != nil {
cli.Sayf("service is not running\n")
return
}
err = spawn.KillAndWait(process, 16 * time.Second)
if err != nil {
cli.Sayf("could not stop service: %v\n", err)
os.Exit(1)
}
}
func ensurePidFile (file string, uid, gid int) error {
pidFile, err := os.Create(file)
if err != nil { return err }
err = pidFile.Close()
if err != nil { return err }
err = os.Chmod(file, 0660)
if err != nil { return err }
err = os.Chown(file, uid, gid)
if err != nil { return err }
return nil
}

113
cmd/hnctl/spawn/spawn.go Normal file
View File

@@ -0,0 +1,113 @@
// Package spawn provides utilities for daemonizing services.
package spawn
import "os"
import "fmt"
import "time"
import "errors"
import "syscall"
import "strconv"
import "path/filepath"
// Spawn spawns a process in the background and returns its PID.
func Spawn (path string, uid, gid uint32, env []string, args ...string) (pid int, err error) {
cred := &syscall.Credential{
Uid: uid,
Gid: gid,
Groups: []uint32{},
NoSetGroups: false,
}
// the Noctty flag is used to detach the process from parent tty
sysproc := &syscall.SysProcAttr{
Credential: cred,
Noctty: true,
}
attr := os.ProcAttr{
Dir: ".",
Env: os.Environ(),
Files: []*os.File{
os.Stdin,
nil,
nil,
},
Sys: sysproc,
}
process, err := os.StartProcess(path, args, &attr)
if err != nil {
return 0, err
}
// Release() is what actually detatches the process and places it under
// init
return process.Pid, process.Release()
}
// PidFile returns the path of a pidfile under the specified name. More
// specifically, it returns `/run/<name>.pid`.
func PidFile (name string) string {
return filepath.Join("/run/", name + ".pid")
}
// PidOf returns the PID stored in the pidfile of the given name as defined by
// PidFile.
func PidOf (name string) (pid int, err error) {
content, err := os.ReadFile(PidFile(name))
if err != nil {
return 0, err
}
pid, err = strconv.Atoi(string(content))
if err != nil {
return 0, err
}
return pid, nil
}
// Running returns whether or not a process with the given PID is running.
func Running (pid int) bool {
directoryInfo, err := os.Stat("/proc/")
if os.IsNotExist(err) || !directoryInfo.IsDir() {
// if /proc/ does not exist, fallback to sending a signal
process, err := os.FindProcess(pid)
if err != nil {
return false
}
err = process.Signal(syscall.Signal(0))
if err != nil {
return false
}
} else {
// if /proc/ exists, see if the process's directory exists there
_, err = os.Stat("/proc/" + strconv.Itoa(pid))
if err != nil {
return false
}
}
return true
}
// KillAndWait kills a process and waits for it to finish, with a timeout. If
// the timeout is zero, it will wait indefinetly. This function will poll every
// 100 milliseconds to see if the process has finished.
func KillAndWait (process *os.Process, timeout time.Duration) error {
pid := process.Pid
err := process.Kill()
if err != nil {
return err
}
// wait for the process to exit, with a timeout
timeoutPoint := time.Now()
for timeout == 0 || time.Since(timeoutPoint) < 16 * time.Second {
if !Running(pid) {
return nil
}
time.Sleep(100 * time.Millisecond)
}
return errors.New(fmt.Sprintf (
"timeout exceeded while waiting for process %d to finish", pid))
}

270
cmd/wrench/main.go Normal file
View File

@@ -0,0 +1,270 @@
package main
import "os"
import "fmt"
import "flag"
import "strconv"
import "os/exec"
import "os/user"
import "hnakra/cli"
import "path/filepath"
import "golang.org/x/crypto/bcrypt"
func tryCommand (cmd *exec.Cmd, failReason string) {
output, err := cmd.CombinedOutput()
if err != nil {
cli.Sayf("%s: %s\n", failReason, string(output))
os.Exit(1)
}
}
func ownOne (path string, uid, gid int) {
file, err := os.Stat(path)
if err != nil {
cli.Sayf("could not stat %s: %v\n", path, err)
return
}
err = os.Chown(path, uid, gid)
if err != nil {
cli.Sayf("could not change ownership of %s: %v\n", path, err)
return
}
if file.IsDir() {
err = os.Chmod(path, 0770)
} else {
err = os.Chmod(path, 0660)
}
if err != nil {
cli.Sayf("could not change mode of %s: %v\n", path, err)
return
}
}
func main () {
user, err := user.Current()
if err != nil {
cli.Sayf("could not get username %v\n", err)
os.Exit(1)
}
flag.Usage = func () {
cli.Printf("Usage of %s:\n", os.Args[0])
cli.Printf(" hash\n")
cli.Printf(" Generate a bcrypt hash of a key\n")
cli.Printf(" adduser\n")
cli.Printf(" Add a system user to run a service as\n")
cli.Printf(" deluser\n")
cli.Printf(" Remove a user added with adduser\n")
cli.Printf(" auth\n")
cli.Printf(" Authorize a system user to access a service's files\n")
cli.Printf(" own\n")
cli.Printf(" Give ownership of a file to a service\n")
os.Exit(1)
}
// define commands
hashCommand := flag.NewFlagSet("hash", flag.ExitOnError)
hashCost := hashCommand.Uint("cost", uint(bcrypt.DefaultCost), "Cost of the hash")
hashText := hashCommand.String("k", "", "Text content of the key")
addUserCommand := flag.NewFlagSet("adduser", flag.ExitOnError)
addUserService := addUserCommand.String ("s", "router",
"Service to add a user for")
delUserCommand := flag.NewFlagSet("deluser", flag.ExitOnError)
delUserService := delUserCommand.String ("s", "router",
"Service to delete the user for")
delUserRmData := delUserCommand.Bool ("D", false,
"Whether to remove the service's data directory")
authCommand := flag.NewFlagSet("auth", flag.ExitOnError)
authService := authCommand.String ("s", "router",
"Service to authorize the user to access")
authUser := authCommand.String ("u", user.Username,
"User to be given access")
ownCommand := flag.NewFlagSet("own", flag.ExitOnError)
ownService := ownCommand.String ("s", "router",
"Service to give ownership of the file to")
ownFile := ownCommand.String ("f", ".",
"File to take ownership of")
ownRecursive := ownCommand.Bool ("r", false,
"Whether or not to recurse into sub-directories")
flag.Parse()
// execute correct command
if len(os.Args) < 2 {
flag.Usage()
os.Exit(1)
}
subCommandArgs := os.Args[2:]
switch os.Args[1] {
case "hash":
hashCommand.Parse(subCommandArgs)
execHash(int(*hashCost), *hashText)
case "adduser":
addUserCommand.Parse(subCommandArgs)
execAdduser(*addUserService)
case "deluser":
delUserCommand.Parse(subCommandArgs)
execDeluser(*delUserService, *delUserRmData)
case "auth":
authCommand.Parse(subCommandArgs)
execAuth(*authService, *authUser)
case "own":
ownCommand.Parse(subCommandArgs)
execOwn(*ownService, *ownFile, *ownRecursive)
}
}
func execHash (cost int, key string) {
if key == "" {
cli.Sayf("please specify key text content\n")
os.Exit(1)
}
if cost < bcrypt.MinCost {
cli.Sayf("cost is too low, must be at least %v\n", bcrypt.MinCost)
os.Exit(1)
}
if cost > bcrypt.MaxCost {
cli.Sayf("cost is too hight, can be at most %v\n", bcrypt.MaxCost)
os.Exit(1)
}
hash, err := bcrypt.GenerateFromPassword([]byte(key), cost)
if err != nil {
cli.Sayf("could not hash key: %v\n", err)
os.Exit(1)
}
fmt.Println(string(hash))
}
func execAdduser (service string) {
fullName := cli.ServiceUser(service)
dataDir := cli.ServiceDir(service)
if adduser, err := exec.LookPath("adduser"); err == nil {
// BUSYBOX
addgroup, _ := exec.LookPath("addgroup")
tryCommand (exec.Command(addgroup, fullName, "-S"),
"could not add group")
tryCommand (exec.Command (
adduser, fullName, "-SHDG", fullName, "-h", dataDir),
"could not add user")
} else if useradd, err := exec.LookPath("useradd"); err == nil {
// GNU
tryCommand (exec.Command (
useradd, fullName, "-rUM",
"--shell", "/sbin/nologin",
"-d", dataDir), "could not add user")
} else {
cli.Sayf("could not add user: no command adduser or useradd\n")
os.Exit(1)
}
// create data directory
uid, gid, err := cli.LookupUID(fullName)
if err != nil {
cli.Sayf("could not create data dir: %v\n", err)
os.Exit(1)
}
err = cli.MkdirFor(dataDir, int(uid), int(gid))
if err != nil {
cli.Sayf("could not create data dir: %v\n", err)
os.Exit(1)
}
}
func execDeluser (service string, rmData bool) {
fullName := cli.ServiceUser(service)
dataDir := cli.ServiceDir(service)
if deluser, err := exec.LookPath("deluser"); err == nil {
// BUSYBOX
tryCommand (exec.Command(deluser, fullName, "--remove-home"),
"could not delete user")
} else if userdel, err := exec.LookPath("userdel"); err == nil {
// GNU
tryCommand (exec.Command(userdel, fullName, "-r"),
"could not delete user")
groupdel, _ := exec.LookPath("groupdel")
tryCommand (exec.Command(groupdel, fullName),
"could not delete group")
} else {
cli.Sayf("could not delete user: no command deluser or userdel\n")
os.Exit(1)
}
// delete data directory
if rmData {
err := os.RemoveAll(dataDir)
if err != nil {
cli.Sayf("could not delete data dir: %v\n", err)
os.Exit(1)
}
}
}
func execAuth (service, user string) {
fullName := cli.ServiceUser(service)
adduser, err := exec.LookPath("adduser")
if err == nil {
tryCommand (exec.Command(adduser, user, fullName),
"could not add user to group " + fullName)
return
}
// GNU
useradd, err := exec.LookPath("usermod")
if err == nil {
tryCommand (exec.Command(useradd, "-a", "-g", fullName, user),
"could not add user to group " + fullName)
return
}
cli.Sayf("could not auth user: no command adduser or usermod\n")
os.Exit(1)
}
func execOwn (service, file string, recurse bool) {
fullName := cli.ServiceUser(service)
userInfo, err := user.Lookup(fullName)
uid, _ := strconv.Atoi(userInfo.Uid)
gid, _ := strconv.Atoi(userInfo.Gid)
if err != nil {
cli.Sayf("could not get user info: %v\n", err)
os.Exit(1)
}
if !recurse {
ownOne(file, uid, gid)
return
}
err = filepath.Walk(file, func(
filePath string,
file os.FileInfo,
err error,
) error {
if err != nil {
cli.Sayf("could not traverse filesystem: %v\n", err)
return nil
}
ownOne(filePath, uid, gid)
return nil
})
if err != nil {
cli.Sayf("could not traverse filesystem: %v\n", err)
os.Exit(1)
}
}

50
daemon/daemon.go Normal file
View File

@@ -0,0 +1,50 @@
// Package daemon provides utilities for daemons.
package daemon
import "io"
import "os"
import "syscall"
import "strconv"
import "os/signal"
// PidFile is a string that contains a path to a pidfile.
type PidFile string
// Start writes to the pidfile.
func (pidfile PidFile) Start () error {
return os.WriteFile(string(pidfile), []byte(strconv.Itoa(os.Getpid())), 0644)
}
// Close deletes the pidfile.
func (pidfile PidFile) Close () error {
return os.Remove(string(pidfile))
}
// Empty returns true if the object is zero value (an empty string).
func (pidfile PidFile) Empty () bool {
return pidfile == ""
}
// OnSigint calls the specified function once sigint is recieved. This function
// does not block, and spawns a goroutine that waits. For this reason, the
// callback must be safe to call concurrently.
func OnSigint (callback func ()) {
go func () {
sigintNotify := make(chan os.Signal, 1)
signal.Notify(sigintNotify, os.Interrupt, syscall.SIGTERM)
<-sigintNotify
callback()
} ()
}
// CloseOnSigint is like OnSigint, but takes an io.Closer.
func CloseOnSigint (closer io.Closer) {
OnSigint(func () { closer.Close() })
}
// ShutdownOnSigint is like OnSigint, but takes an object with a Shutdown()
// method.
func ShutdownOnSigint (shutdowner interface { Shutdown() error }) {
OnSigint(func () { shutdowner.Shutdown() })
}

View File

@@ -1,10 +1,8 @@
package main
import "log"
import "sync"
import "path"
import "net/http"
import "crypto/tls"
import "html/template"
import "hnakra/service"
@@ -14,8 +12,9 @@ type Post struct {
}
type Board struct {
*service.Service
root string
mount *service.HTTP
mux *http.ServeMux
template *template.Template
@@ -26,15 +25,11 @@ type Board struct {
func main () {
board := Board { root: "/board/" }
board.mux = http.NewServeMux()
board.mount = &service.HTTP {
Mount: service.Mount {
Path: board.root,
Name: "Board",
Description: "A board where you can post things.",
TLSConfig: &tls.Config { InsecureSkipVerify: true },
},
Handler: board.mux,
}
board.Service = service.NewService (
"Board",
"A board where you can post things.",
service.NewHTTP("@", board.root, board.mux))
handle := func (pattern string, handler func (http.ResponseWriter, *http.Request)) {
board.mux.HandleFunc(pattern, handler)
}
@@ -42,10 +37,7 @@ func main () {
handle(path.Join(board.root, "actionPost"), board.serveActionPost)
board.template = template.Must(template.New("board").Parse(tem))
err := board.mount.Run()
if err != nil {
log.Println("XXX", err)
}
board.Run()
}
func (board *Board) getPosts (max int) []*Post {

View File

@@ -1,9 +1,7 @@
package main
import "fmt"
import "log"
import "net/http"
import "crypto/tls"
import "hnakra/service"
func main () {
@@ -11,17 +9,10 @@ func main () {
http.HandleFunc("/gifs/", gifs)
http.Handle("/gifs/static/", http.StripPrefix("/gifs/static", static))
err := (&service.HTTP { Mount: service.Mount {
Path: "/gifs/",
Name: "Gifs",
Description: "Serves a lot of big gifs on one page.",
TLSConfig: &tls.Config { InsecureSkipVerify: true },
}}).Run()
if err != nil {
log.Println("XXX", err)
}
service.NewService (
"Gifs", "Serves a lot of big gifs on one page.",
service.NewHTTP("@", "/gifs/", nil)).Run()
}
func gifs (res http.ResponseWriter, req *http.Request) {

View File

@@ -1,22 +1,13 @@
package main
import "log"
import "net/http"
import "crypto/tls"
import "hnakra/service"
func main () {
http.HandleFunc("/hello/", hellorld)
err := (&service.HTTP { Mount: service.Mount {
Path: "/hello/",
Name: "Hellorld",
Description: "A test service.",
TLSConfig: &tls.Config { InsecureSkipVerify: true },
}}).Run()
if err != nil {
log.Println("XXX", err)
}
service.NewService (
"Hellorld", "A test service.",
service.NewHTTP("@", "/hello/", nil)).Run()
}
func hellorld (res http.ResponseWriter, req *http.Request) {

View File

@@ -1,8 +1,6 @@
package main
import "log"
import "net/http"
import "crypto/tls"
import "hnakra/service"
func main () {
@@ -14,13 +12,7 @@ func main () {
// do with how *something* is caching the file.
http.ServeFile(res, req, "fractal.png")
})
err := (&service.HTTP { Mount: service.Mount {
Path: "/fractal.png",
Name: "Image",
Description: "Displays an image of a fractal.",
TLSConfig: &tls.Config { InsecureSkipVerify: true },
}}).Run()
if err != nil { log.Println("XXX", err) }
service.NewService (
"Image", "Displays an image of a fractal.",
service.NewHTTP("@", "/fractal.png", nil)).Run()
}

View File

@@ -2,21 +2,14 @@ package main
import "log"
import "net/http"
import "crypto/tls"
import "hnakra/service"
func main () {
http.HandleFunc("/kamikaze/", hellorld)
err := (&service.HTTP { Mount: service.Mount {
Path: "/kamikaze/",
Name: "Kamikaze",
Description: "A service that abrupltly closes upon any request, for testing",
TLSConfig: &tls.Config { InsecureSkipVerify: true },
}}).Run()
if err != nil {
log.Println("XXX", err)
}
service.NewService (
"Kamikaze",
"A service that abrupltly closes upon any request, for testing.",
service.NewHTTP("@", "/kamikaze/", nil)).Run()
}
func hellorld (res http.ResponseWriter, req *http.Request) {

7
go.mod
View File

@@ -1,8 +1,5 @@
module hnakra
go 1.20
go 1.19
require (
git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b
golang.org/x/crypto v0.9.0
)
require golang.org/x/crypto v0.9.0

2
go.sum
View File

@@ -1,4 +1,2 @@
git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b h1:vPFKR7vjN1VrMdMtpATMrKQobz/cqbPiRrA1EbtG6PM=
git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b/go.mod h1:cpXX8SAUDEvZX5m7scoyruavUhEqQ1SByfWzPFHkTbg=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=

View File

@@ -1,3 +1,4 @@
// Package routines provides utilities for managing long-running goroutines.
package routines
import "io"
@@ -5,12 +6,44 @@ import "fmt"
import "log"
import "time"
import "sync"
import "errors"
// Routine represents an object that can be run.
type routine struct {
run, shutdown func () error
}
func (routine routine) Run () error {
if routine.run == nil {
return nil
} else {
return routine.run()
}
}
func (routine routine) Shutdown () error {
if routine.shutdown == nil {
return nil
} else {
return routine.shutdown()
}
}
// From creates a routine from a separate run and shutdown function.
func From (run, shutdown func () error) Routine {
return routine {
run: run,
shutdown: shutdown,
}
}
// Routine is an object that can be run and stopped.
type Routine interface {
// Run starts the routine and does not return until it is finished. An
// error is returned if the routine exited due to an error.
// Run is a long-running function that does not return until it is
// finished. An error is returned if the routine exited due to an error.
Run () error
// Shutdown stops Run.
Shutdown () error
}
// Manager is a system capable of managing multiple routines, and restarting
@@ -29,6 +62,9 @@ type Manager struct {
// is nil, messages will be written to the standard logger. To disable
// logging altogether, this can be set to io.Discard.
Logger io.Writer
stoppingMutex sync.Mutex
stopping bool
}
// Run spawns all routines in the Routines slice. If a routine exits with an
@@ -36,17 +72,37 @@ type Manager struct {
// Run returns only when all routines have exited.
func (manager *Manager) Run () error {
var waitGroup sync.WaitGroup
var errExit error
for _, routine := range manager.Routines {
if routine != nil {
waitGroup.Add(1)
go manager.runRoutine(routine, &waitGroup, &errExit)
go manager.runRoutine(routine, &waitGroup)
}
}
waitGroup.Wait()
return errExit
return nil
}
// Shutdown shuts down all routines in the manager.
func (manager *Manager) Shutdown () (err error) {
manager.stoppingMutex.Lock()
manager.stopping = true
manager.stoppingMutex.Unlock()
for _, routine := range manager.Routines {
routineErr := routine.Shutdown()
if routineErr != nil {
err = routineErr
}
}
return
}
// Append adds one or more routines to the Routines slice. This has no effect if
// the manager is already running.
func (manager *Manager) Append (routines ...Routine) {
manager.Routines = append(manager.Routines, routines...)
}
func (manager *Manager) log (message ...any) {
@@ -57,15 +113,26 @@ func (manager *Manager) log (message ...any) {
}
}
func (manager *Manager) runRoutine (routine Routine, group *sync.WaitGroup, errExit *error) {
func (manager *Manager) runRoutine (routine Routine, group *sync.WaitGroup) {
defer group.Done()
var err error
for {
// TODO: recover from panics
lastStart := time.Now()
err = routine.Run()
err := panicWrap(routine.Run)
stopping := false
manager.stoppingMutex.Lock()
stopping = manager.stopping
manager.stoppingMutex.Unlock()
if stopping {
if err == nil {
manager.log("(i) stopped routine")
} else {
manager.log("!!! stopped routine, with error:", err)
}
break
}
if err == nil {
manager.log("(i) routine exited")
break
@@ -80,8 +147,15 @@ func (manager *Manager) runRoutine (routine Routine, group *sync.WaitGroup, errE
manager.log("(i) routine is being restarted")
}
}
if err != nil {
*errExit = err
}
}
func panicWrap (f func () error) (err error) {
defer func () {
if pan := recover(); pan != nil {
err = errors.New(fmt.Sprint(pan))
}
} ()
err = f()
return
}

View File

@@ -10,7 +10,7 @@ func (mount *HTTP) handle (request protocol.MessageHTTPRequest) {
bodyReader := &bodyReader {
id: request.ID,
reader: mount.requests.readerFor(request.ID),
send: mount.send,
send: mount.conn.Send,
close: func () { mount.requests.end(request.ID) },
}
defer mount.requests.remove(request.ID)
@@ -28,10 +28,10 @@ func (mount *HTTP) handle (request protocol.MessageHTTPRequest) {
handler.ServeHTTP(&responseWriter {
id: request.ID,
header: make(http.Header),
send: mount.send,
send: mount.conn.Send,
}, httpRequest)
mount.send(protocol.MessageHTTPBodyEnd { ID: request.ID })
mount.conn.Send(protocol.MessageHTTPBodyEnd { ID: request.ID })
}
type bodyReader struct {

View File

@@ -1,16 +1,13 @@
package service
import "log"
import "net"
import "sync"
import "bufio"
import "errors"
import "net/http"
import "hnakra/protocol"
// HTTP is an https:// mount.
type HTTP struct {
Mount
MountInfo
// AllowInsecure allows this mount to respond to plain-text HTTP
// requests. You can get a TLS cert for free nowadays so there are very
@@ -21,80 +18,80 @@ type HTTP struct {
// http.DefaultServeMux is used.
Handler http.Handler
conn net.Conn
connLock sync.Mutex
connReadWriter *bufio.ReadWriter
conn *Conn
running bool
requests requestManager
}
// Close closes the mount abruptly, interrupting any active connections.
func (mount *HTTP) Close () error {
mount.connLock.Lock()
defer mount.connLock.Unlock()
return mount.conn.Close()
func (htmount *HTTP) Close () error {
htmount.running = false
return htmount.conn.Close()
}
// Shutdown gracefully shuts down the service without interrupting any active
// connections.
func (mount *HTTP) Shutdown () error {
func (htmount *HTTP) Shutdown () error {
// TODO
return mount.Close()
return htmount.Close()
}
// Run connects to the router, and blocks while fulfilling requests. This method
// will only return when the connection to the router has been closed.
func (mount *HTTP) Run () (err error) {
if mount.AllowInsecure {
mount.conn, err = mount.connect("http")
func (htmount *HTTP) Run (service ServiceInfo) (err error) {
if htmount.AllowInsecure {
htmount.MountInfo.Scheme = "http"
} else {
mount.conn, err = mount.connect("https")
htmount.MountInfo.Scheme = "https"
}
htmount.conn, err = Dial(htmount.MountInfo, service)
if err != nil { return }
mount.connReadWriter = bufio.NewReadWriter (
bufio.NewReader(mount.conn),
bufio.NewWriter(mount.conn))
mount.requests.init()
htmount.running = true
htmount.requests.init()
for {
message, err := protocol.ReadMessage(mount.connReadWriter)
if err != nil { return err }
message, err := htmount.conn.Receive()
if err != nil {
if htmount.running {
return err
} else {
return nil
}
}
switch message.(type) {
case protocol.MessageHTTPRequest:
request := message.(protocol.MessageHTTPRequest)
mount.requests.add(request.ID)
go mount.handle(request)
htmount.requests.add(request.ID)
go htmount.handle(request)
case protocol.MessageHTTPBodySegment:
segment := message.(protocol.MessageHTTPBodySegment)
mount.requests.feed(segment.ID, segment.Data)
htmount.requests.feed(segment.ID, segment.Data)
case protocol.MessageHTTPBodyEnd:
end := message.(protocol.MessageHTTPBodyEnd)
mount.requests.end(end.ID)
htmount.requests.end(end.ID)
case protocol.MessageStatus:
status := message.(protocol.MessageStatus)
log.Println("router says:", status.Status)
default:
mount.Close()
htmount.Close()
return errors.New("router sent unknown type code")
}
}
}
// NewHTTP creates a very basic https:// mount with the specified name and
// description.
func NewHTTP (name, description string) HTTP {
return HTTP { Mount: M(name, description) }
}
func (mount *HTTP) send (message protocol.Message) (err error) {
mount.connLock.Lock()
defer mount.connLock.Unlock()
err = message.Send(mount.connReadWriter)
if err != nil { return }
return mount.connReadWriter.Flush()
// NewHTTP creates a new HTTPS mount that uses the specified handler.
func NewHTTP (host, path string, handler http.Handler) *HTTP {
return &HTTP {
MountInfo: MountInfo {
Host: host,
Path: path,
},
Handler: handler,
}
}

259
service/mount.go Normal file
View File

@@ -0,0 +1,259 @@
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()
}

View File

@@ -3,186 +3,89 @@ package service
import "os"
import "log"
import "fmt"
import "net"
import "errors"
import "strings"
import "crypto/tls"
import "encoding/base64"
import "hnakra/protocol"
import "time"
import "hnakra/rotate"
import "hnakra/daemon"
import "hnakra/routines"
// M creates a very basic mount with the specified name and description.
func M (name, description string) Mount {
return Mount {
Name: name,
Description: description,
// Service is capable of managing multiple mounts. It also sets up logging
// automatically.
type Service struct {
ServiceInfo
Mounts []Mount
manager routines.Manager
}
// NewService provides a shorthand for creating a new service, leaving most
// values to their default.
func NewService (name, description string, mounts ...Mount) *Service {
return &Service {
ServiceInfo: ServiceInfo {
Name: name,
Description: description,
},
Mounts: mounts,
}
}
// 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
// Run runs the mounts within the service, and only exits when all of them have
// exited. It will automatically start logging to the directory specified by
// $HNAKRA_LOG_DIR. If that variable is unset, it will just log to stdout.
// Additionally, if $HNAKRA_PIDFILE is set, it will write the process PID to the
// file specified by it.
func (service *Service) Run () error {
// set up logging
logDir := os.Getenv("HNAKRA_LOG_DIR")
if logDir != "" {
logger, err := rotate.New(logDir)
if err != nil { log.Fatal("cannot access log dir:", err) }
log.SetOutput(logger)
}
log.Println("... starting service", service.Name)
// 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
// set up routine manager
service.manager = routines.Manager { RestartDeadline: time.Second * 8 }
service.manager.Routines = make([]routines.Routine, len(service.Mounts))
for index, mount := range service.Mounts {
service.manager.Routines[index] = routines.From (func () error {
return mount.Run(service.ServiceInfo)
}, mount.Shutdown)
}
// 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
// be a daemon
daemon.ShutdownOnSigint(service)
pidfile := daemon.PidFile(os.Getenv("HNAKRA_PIDFILE"))
if !pidfile.Empty() {
err := pidfile.Start()
if err != nil { log.Println("!!! could not write pid:", err) }
defer func () {
err := pidfile.Close()
if err != nil { log.Println("!!! could not delete pidfile:", err) }
} ()
}
// 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
// send it
err := service.manager.Run()
if err != nil { log.Println("XXX", err) }
return err
}
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"
// Close abruptly closes all mounts in the service. This will cause Run() to
// exit.
func (service *Service) Close () (err error) {
for _, mount := range service.Mounts {
singleErr := mount.Close()
if singleErr != nil {
err = singleErr
}
}
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
return
}
// TODO: other protocols like gemini, websocket, ftp, etc
// Shutdown gracefully shuts down each mount in the service. This will cause
// Run() to exit.
func (service *Service) Shutdown () (err error) {
return service.manager.Shutdown()
}