Compare commits
22 Commits
9025844212
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1978e263d6 | ||
|
|
7726d732d4 | ||
|
|
32bc44c90f | ||
|
|
805f42d828 | ||
|
|
e8349360cc | ||
|
|
4e58df9c9b | ||
|
|
f90421e5db | ||
|
|
c9f2c56d65 | ||
|
|
92b645f34c | ||
|
|
3cd53b3dd9 | ||
|
|
8a528e2b4e | ||
|
|
631796a726 | ||
|
|
066247a08f | ||
|
|
8c9b85d7ca | ||
|
|
1a5502211e | ||
|
|
9d8e6e8e24 | ||
|
|
92b93abb13 | ||
|
|
6f876b2a17 | ||
|
|
17a816e360 | ||
|
|
1e1ae572f2 | ||
|
|
d716aa9455 | ||
|
|
5e37c4bb8f |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/hnctl
|
||||||
|
/router
|
||||||
|
/wrench
|
||||||
81
cli/cli.go
Normal file
81
cli/cli.go
Normal 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
|
||||||
|
}
|
||||||
@@ -4,13 +4,14 @@ import "io"
|
|||||||
import "os"
|
import "os"
|
||||||
import "log"
|
import "log"
|
||||||
import "time"
|
import "time"
|
||||||
import "hnakra/rotate"
|
|
||||||
import "hnakra/router"
|
import "hnakra/router"
|
||||||
|
import "hnakra/rotate"
|
||||||
|
import "hnakra/daemon"
|
||||||
import "hnakra/routines"
|
import "hnakra/routines"
|
||||||
import "hnakra/router/rcon"
|
import "hnakra/router/rcon"
|
||||||
import "hnakra/router/config"
|
import "hnakra/router/config"
|
||||||
import "hnakra/cmd/router/srvhttps"
|
import "hnakra/cmd/hn-router/srvhttps"
|
||||||
import "hnakra/cmd/router/srvhnakra"
|
import "hnakra/cmd/hn-router/srvhnakra"
|
||||||
|
|
||||||
const banner = "\n" +
|
const banner = "\n" +
|
||||||
" -=\\\n" +
|
" -=\\\n" +
|
||||||
@@ -59,10 +60,10 @@ func main () {
|
|||||||
manager := routines.Manager { RestartDeadline: time.Second * 8 }
|
manager := routines.Manager { RestartDeadline: time.Second * 8 }
|
||||||
rout := router.New(conf)
|
rout := router.New(conf)
|
||||||
srvhnakra := &srvhnakra.Server { Config: conf, Router: rout }
|
srvhnakra := &srvhnakra.Server { Config: conf, Router: rout }
|
||||||
manager.Append(srvhnakra.ListenAndServe)
|
manager.Append(srvhnakra)
|
||||||
if conf.HTTPSEnable() {
|
if conf.HTTPSEnable() {
|
||||||
srvhttps := &srvhttps.Server { Config: conf, Handler: rout }
|
srvhttps := &srvhttps.Server { Config: conf, Handler: rout }
|
||||||
manager.Append(srvhttps.ListenAndServe)
|
manager.Append(srvhttps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up rcon
|
// set up rcon
|
||||||
@@ -74,12 +75,19 @@ func main () {
|
|||||||
log.SetOutput(originalWriter)
|
log.SetOutput(originalWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// run servers
|
||||||
err = manager.Run()
|
err = manager.Run()
|
||||||
if err != nil { log.Println("XXX", err) }
|
if err != nil { log.Println("XXX", err) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpsRoutine (server *srvhttps.Server) {
|
|
||||||
err := server.ListenAndServe()
|
|
||||||
if err != nil { log.Println("XXX", err) }
|
|
||||||
}
|
|
||||||
@@ -11,24 +11,34 @@ type Server struct {
|
|||||||
underlying net.Listener
|
underlying net.Listener
|
||||||
Config config.Config
|
Config config.Config
|
||||||
Router *router.Router
|
Router *router.Router
|
||||||
|
|
||||||
|
running bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) ListenAndServe () (err error) {
|
func (server *Server) Run () (err error) {
|
||||||
server.underlying, err = tls.Listen (
|
server.underlying, err = tls.Listen (
|
||||||
"tcp", fmt.Sprint(":", server.Config.RouterPort()),
|
"tcp", fmt.Sprint(":", server.Config.RouterPort()),
|
||||||
config.TLSConfigFor(server.Config))
|
config.TLSConfigFor(server.Config))
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
|
|
||||||
|
server.running = true
|
||||||
log.Println(".// router on", server.underlying.Addr())
|
log.Println(".// router on", server.underlying.Addr())
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := server.underlying.Accept()
|
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())
|
log.Println("-=E incoming connection from", conn.RemoteAddr())
|
||||||
server.Router.Accept(conn)
|
server.Router.Accept(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) Close () error {
|
func (server *Server) Shutdown () error {
|
||||||
|
server.running = false
|
||||||
return server.underlying.Close()
|
return server.underlying.Close()
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,11 @@ type Server struct {
|
|||||||
underlying *http.Server
|
underlying *http.Server
|
||||||
Config config.Config
|
Config config.Config
|
||||||
Handler http.Handler
|
Handler http.Handler
|
||||||
|
|
||||||
|
running bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) ListenAndServe () error {
|
func (server *Server) Run () error {
|
||||||
server.underlying = &http.Server {
|
server.underlying = &http.Server {
|
||||||
Addr: fmt.Sprint(":", server.Config.HTTPSPort()),
|
Addr: fmt.Sprint(":", server.Config.HTTPSPort()),
|
||||||
// ReadHeaderTimeout: timeoutReadHeader * time.Second,
|
// ReadHeaderTimeout: timeoutReadHeader * time.Second,
|
||||||
@@ -22,10 +24,17 @@ func (server *Server) ListenAndServe () error {
|
|||||||
Handler: server.Handler,
|
Handler: server.Handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.running = true
|
||||||
log.Println(".// https on", server.underlying.Addr)
|
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()
|
return server.underlying.Close()
|
||||||
}
|
}
|
||||||
140
cmd/hnctl/main.go
Normal file
140
cmd/hnctl/main.go
Normal 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
113
cmd/hnctl/spawn/spawn.go
Normal 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
270
cmd/wrench/main.go
Normal 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
50
daemon/daemon.go
Normal 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() })
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import "sync"
|
import "sync"
|
||||||
import "path"
|
import "path"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "crypto/tls"
|
|
||||||
import "html/template"
|
import "html/template"
|
||||||
import "hnakra/service"
|
import "hnakra/service"
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ type Post struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Board struct {
|
type Board struct {
|
||||||
service.Service
|
*service.Service
|
||||||
|
|
||||||
root string
|
root string
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
@@ -26,17 +25,11 @@ type Board struct {
|
|||||||
func main () {
|
func main () {
|
||||||
board := Board { root: "/board/" }
|
board := Board { root: "/board/" }
|
||||||
board.mux = http.NewServeMux()
|
board.mux = http.NewServeMux()
|
||||||
board.Service = service.Service {
|
board.Service = service.NewService (
|
||||||
&service.HTTP {
|
"Board",
|
||||||
Mount: service.MountConfig {
|
"A board where you can post things.",
|
||||||
Path: board.root,
|
service.NewHTTP("@", board.root, board.mux))
|
||||||
Name: "Board",
|
|
||||||
Description: "A board where you can post things.",
|
|
||||||
TLSConfig: &tls.Config { InsecureSkipVerify: true },
|
|
||||||
},
|
|
||||||
Handler: board.mux,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
handle := func (pattern string, handler func (http.ResponseWriter, *http.Request)) {
|
handle := func (pattern string, handler func (http.ResponseWriter, *http.Request)) {
|
||||||
board.mux.HandleFunc(pattern, handler)
|
board.mux.HandleFunc(pattern, handler)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "log"
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "crypto/tls"
|
|
||||||
import "hnakra/service"
|
import "hnakra/service"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -11,17 +9,10 @@ func main () {
|
|||||||
|
|
||||||
http.HandleFunc("/gifs/", gifs)
|
http.HandleFunc("/gifs/", gifs)
|
||||||
http.Handle("/gifs/static/", http.StripPrefix("/gifs/static", static))
|
http.Handle("/gifs/static/", http.StripPrefix("/gifs/static", static))
|
||||||
|
|
||||||
err := (&service.HTTP { Mount: service.MountConfig {
|
|
||||||
Path: "/gifs/",
|
|
||||||
Name: "Gifs",
|
|
||||||
Description: "Serves a lot of big gifs on one page.",
|
|
||||||
TLSConfig: &tls.Config { InsecureSkipVerify: true },
|
|
||||||
}}).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
service.NewService (
|
||||||
log.Println("XXX", err)
|
"Gifs", "Serves a lot of big gifs on one page.",
|
||||||
}
|
service.NewHTTP("@", "/gifs/", nil)).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func gifs (res http.ResponseWriter, req *http.Request) {
|
func gifs (res http.ResponseWriter, req *http.Request) {
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "log"
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "crypto/tls"
|
|
||||||
import "hnakra/service"
|
import "hnakra/service"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
http.HandleFunc("/hello/", hellorld)
|
http.HandleFunc("/hello/", hellorld)
|
||||||
err := (&service.HTTP { Mount: service.MountConfig {
|
service.NewService (
|
||||||
Path: "/hello/",
|
"Hellorld", "A test service.",
|
||||||
Name: "Hellorld",
|
service.NewHTTP("@", "/hello/", nil)).Run()
|
||||||
Description: "A test service.",
|
|
||||||
TLSConfig: &tls.Config { InsecureSkipVerify: true },
|
|
||||||
}}).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("XXX", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hellorld (res http.ResponseWriter, req *http.Request) {
|
func hellorld (res http.ResponseWriter, req *http.Request) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "log"
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "crypto/tls"
|
|
||||||
import "hnakra/service"
|
import "hnakra/service"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -14,13 +12,7 @@ func main () {
|
|||||||
// do with how *something* is caching the file.
|
// do with how *something* is caching the file.
|
||||||
http.ServeFile(res, req, "fractal.png")
|
http.ServeFile(res, req, "fractal.png")
|
||||||
})
|
})
|
||||||
|
service.NewService (
|
||||||
err := (&service.HTTP { Mount: service.Mount {
|
"Image", "Displays an image of a fractal.",
|
||||||
Path: "/fractal.png",
|
service.NewHTTP("@", "/fractal.png", nil)).Run()
|
||||||
Name: "Image",
|
|
||||||
Description: "Displays an image of a fractal.",
|
|
||||||
TLSConfig: &tls.Config { InsecureSkipVerify: true },
|
|
||||||
}}).Run()
|
|
||||||
|
|
||||||
if err != nil { log.Println("XXX", err) }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,14 @@ package main
|
|||||||
|
|
||||||
import "log"
|
import "log"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "crypto/tls"
|
|
||||||
import "hnakra/service"
|
import "hnakra/service"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
http.HandleFunc("/kamikaze/", hellorld)
|
http.HandleFunc("/kamikaze/", hellorld)
|
||||||
err := (&service.HTTP { Mount: service.MountConfig {
|
service.NewService (
|
||||||
Path: "/kamikaze/",
|
"Kamikaze",
|
||||||
Name: "Kamikaze",
|
"A service that abrupltly closes upon any request, for testing.",
|
||||||
Description: "A service that abrupltly closes upon any request, for testing",
|
service.NewHTTP("@", "/kamikaze/", nil)).Run()
|
||||||
TLSConfig: &tls.Config { InsecureSkipVerify: true },
|
|
||||||
}}).Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("XXX", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hellorld (res http.ResponseWriter, req *http.Request) {
|
func hellorld (res http.ResponseWriter, req *http.Request) {
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -1,8 +1,5 @@
|
|||||||
module hnakra
|
module hnakra
|
||||||
|
|
||||||
go 1.20
|
go 1.19
|
||||||
|
|
||||||
require (
|
require golang.org/x/crypto v0.9.0
|
||||||
git.tebibyte.media/sashakoshka/ezprof v0.0.0-20230309044548-401cba83602b
|
|
||||||
golang.org/x/crypto v0.9.0
|
|
||||||
)
|
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -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 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
|||||||
@@ -6,10 +6,45 @@ import "fmt"
|
|||||||
import "log"
|
import "log"
|
||||||
import "time"
|
import "time"
|
||||||
import "sync"
|
import "sync"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
// Routine is a long-running function that does not return until it is finished.
|
type routine struct {
|
||||||
// An error is returned if the routine exited due to an error.
|
run, shutdown func () error
|
||||||
type Routine 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 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
|
// Manager is a system capable of managing multiple routines, and restarting
|
||||||
// them if they fail.
|
// them if they fail.
|
||||||
@@ -27,6 +62,9 @@ type Manager struct {
|
|||||||
// is nil, messages will be written to the standard logger. To disable
|
// is nil, messages will be written to the standard logger. To disable
|
||||||
// logging altogether, this can be set to io.Discard.
|
// logging altogether, this can be set to io.Discard.
|
||||||
Logger io.Writer
|
Logger io.Writer
|
||||||
|
|
||||||
|
stoppingMutex sync.Mutex
|
||||||
|
stopping bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run spawns all routines in the Routines slice. If a routine exits with an
|
// Run spawns all routines in the Routines slice. If a routine exits with an
|
||||||
@@ -34,18 +72,31 @@ type Manager struct {
|
|||||||
// Run returns only when all routines have exited.
|
// Run returns only when all routines have exited.
|
||||||
func (manager *Manager) Run () error {
|
func (manager *Manager) Run () error {
|
||||||
var waitGroup sync.WaitGroup
|
var waitGroup sync.WaitGroup
|
||||||
var errExit error
|
|
||||||
|
|
||||||
for _, routine := range manager.Routines {
|
for _, routine := range manager.Routines {
|
||||||
if routine != nil {
|
if routine != nil {
|
||||||
println("yeah")
|
|
||||||
waitGroup.Add(1)
|
waitGroup.Add(1)
|
||||||
go manager.runRoutine(routine, &waitGroup, &errExit)
|
go manager.runRoutine(routine, &waitGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waitGroup.Wait()
|
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
|
// Append adds one or more routines to the Routines slice. This has no effect if
|
||||||
@@ -62,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()
|
defer group.Done()
|
||||||
|
|
||||||
var err error
|
|
||||||
for {
|
for {
|
||||||
// TODO: recover from panics
|
|
||||||
lastStart := time.Now()
|
lastStart := time.Now()
|
||||||
err = routine()
|
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 {
|
if err == nil {
|
||||||
manager.log("(i) routine exited")
|
manager.log("(i) routine exited")
|
||||||
break
|
break
|
||||||
@@ -85,8 +147,15 @@ func (manager *Manager) runRoutine (routine Routine, group *sync.WaitGroup, errE
|
|||||||
manager.log("(i) routine is being restarted")
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func (mount *HTTP) handle (request protocol.MessageHTTPRequest) {
|
|||||||
bodyReader := &bodyReader {
|
bodyReader := &bodyReader {
|
||||||
id: request.ID,
|
id: request.ID,
|
||||||
reader: mount.requests.readerFor(request.ID),
|
reader: mount.requests.readerFor(request.ID),
|
||||||
send: mount.send,
|
send: mount.conn.Send,
|
||||||
close: func () { mount.requests.end(request.ID) },
|
close: func () { mount.requests.end(request.ID) },
|
||||||
}
|
}
|
||||||
defer mount.requests.remove(request.ID)
|
defer mount.requests.remove(request.ID)
|
||||||
@@ -28,10 +28,10 @@ func (mount *HTTP) handle (request protocol.MessageHTTPRequest) {
|
|||||||
handler.ServeHTTP(&responseWriter {
|
handler.ServeHTTP(&responseWriter {
|
||||||
id: request.ID,
|
id: request.ID,
|
||||||
header: make(http.Header),
|
header: make(http.Header),
|
||||||
send: mount.send,
|
send: mount.conn.Send,
|
||||||
}, httpRequest)
|
}, httpRequest)
|
||||||
|
|
||||||
mount.send(protocol.MessageHTTPBodyEnd { ID: request.ID })
|
mount.conn.Send(protocol.MessageHTTPBodyEnd { ID: request.ID })
|
||||||
}
|
}
|
||||||
|
|
||||||
type bodyReader struct {
|
type bodyReader struct {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import "log"
|
import "log"
|
||||||
import "net"
|
|
||||||
import "sync"
|
|
||||||
import "bufio"
|
|
||||||
import "errors"
|
import "errors"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "hnakra/protocol"
|
import "hnakra/protocol"
|
||||||
|
|
||||||
// HTTP is an https:// mount.
|
// HTTP is an https:// mount.
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
// Mount specifies the mount config to use for connecting to the router.
|
MountInfo
|
||||||
Mount MountConfig
|
|
||||||
|
|
||||||
// AllowInsecure allows this mount to respond to plain-text HTTP
|
// AllowInsecure allows this mount to respond to plain-text HTTP
|
||||||
// requests. You can get a TLS cert for free nowadays so there are very
|
// requests. You can get a TLS cert for free nowadays so there are very
|
||||||
@@ -22,82 +18,80 @@ type HTTP struct {
|
|||||||
// http.DefaultServeMux is used.
|
// http.DefaultServeMux is used.
|
||||||
Handler http.Handler
|
Handler http.Handler
|
||||||
|
|
||||||
conn net.Conn
|
conn *Conn
|
||||||
connLock sync.Mutex
|
running bool
|
||||||
connReadWriter *bufio.ReadWriter
|
|
||||||
requests requestManager
|
requests requestManager
|
||||||
idFactory *protocol.IDFactory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the mount abruptly, interrupting any active connections.
|
// Close closes the mount abruptly, interrupting any active connections.
|
||||||
func (mount *HTTP) Close () error {
|
func (htmount *HTTP) Close () error {
|
||||||
mount.connLock.Lock()
|
htmount.running = false
|
||||||
defer mount.connLock.Unlock()
|
return htmount.conn.Close()
|
||||||
return mount.conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the service without interrupting any active
|
// Shutdown gracefully shuts down the service without interrupting any active
|
||||||
// connections.
|
// connections.
|
||||||
func (mount *HTTP) Shutdown () error {
|
func (htmount *HTTP) Shutdown () error {
|
||||||
// TODO
|
// TODO
|
||||||
return mount.Close()
|
return htmount.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run connects to the router, and blocks while fulfilling requests. This method
|
// Run connects to the router, and blocks while fulfilling requests. This method
|
||||||
// will only return when the connection to the router has been closed.
|
// will only return when the connection to the router has been closed.
|
||||||
func (mount *HTTP) Run () (err error) {
|
func (htmount *HTTP) Run (service ServiceInfo) (err error) {
|
||||||
if mount.AllowInsecure {
|
if htmount.AllowInsecure {
|
||||||
mount.Mount.Scheme = "http"
|
htmount.MountInfo.Scheme = "http"
|
||||||
} else {
|
} else {
|
||||||
mount.Mount.Scheme = "https"
|
htmount.MountInfo.Scheme = "https"
|
||||||
}
|
}
|
||||||
mount.conn, mount.idFactory, err = mount.Mount.Connect()
|
htmount.conn, err = Dial(htmount.MountInfo, service)
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
|
|
||||||
mount.connReadWriter = bufio.NewReadWriter (
|
htmount.running = true
|
||||||
bufio.NewReader(mount.conn),
|
htmount.requests.init()
|
||||||
bufio.NewWriter(mount.conn))
|
|
||||||
mount.requests.init()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
message, err := protocol.ReadMessage(mount.connReadWriter)
|
message, err := htmount.conn.Receive()
|
||||||
if err != nil { return err }
|
if err != nil {
|
||||||
|
if htmount.running {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch message.(type) {
|
switch message.(type) {
|
||||||
case protocol.MessageHTTPRequest:
|
case protocol.MessageHTTPRequest:
|
||||||
request := message.(protocol.MessageHTTPRequest)
|
request := message.(protocol.MessageHTTPRequest)
|
||||||
mount.requests.add(request.ID)
|
htmount.requests.add(request.ID)
|
||||||
go mount.handle(request)
|
go htmount.handle(request)
|
||||||
|
|
||||||
case protocol.MessageHTTPBodySegment:
|
case protocol.MessageHTTPBodySegment:
|
||||||
segment := message.(protocol.MessageHTTPBodySegment)
|
segment := message.(protocol.MessageHTTPBodySegment)
|
||||||
mount.requests.feed(segment.ID, segment.Data)
|
htmount.requests.feed(segment.ID, segment.Data)
|
||||||
|
|
||||||
case protocol.MessageHTTPBodyEnd:
|
case protocol.MessageHTTPBodyEnd:
|
||||||
end := message.(protocol.MessageHTTPBodyEnd)
|
end := message.(protocol.MessageHTTPBodyEnd)
|
||||||
mount.requests.end(end.ID)
|
htmount.requests.end(end.ID)
|
||||||
|
|
||||||
case protocol.MessageStatus:
|
case protocol.MessageStatus:
|
||||||
status := message.(protocol.MessageStatus)
|
status := message.(protocol.MessageStatus)
|
||||||
log.Println("router says:", status.Status)
|
log.Println("router says:", status.Status)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
mount.Close()
|
htmount.Close()
|
||||||
return errors.New("router sent unknown type code")
|
return errors.New("router sent unknown type code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTP creates a very basic https:// mount with the specified name and
|
// NewHTTP creates a new HTTPS mount that uses the specified handler.
|
||||||
// description.
|
func NewHTTP (host, path string, handler http.Handler) *HTTP {
|
||||||
func NewHTTP (name, description string) HTTP {
|
return &HTTP {
|
||||||
return HTTP { Mount: M(name, description) }
|
MountInfo: MountInfo {
|
||||||
}
|
Host: host,
|
||||||
|
Path: path,
|
||||||
func (mount *HTTP) send (message protocol.Message) (err error) {
|
},
|
||||||
mount.connLock.Lock()
|
Handler: handler,
|
||||||
defer mount.connLock.Unlock()
|
}
|
||||||
err = message.Send(mount.connReadWriter)
|
|
||||||
if err != nil { return }
|
|
||||||
return mount.connReadWriter.Flush()
|
|
||||||
}
|
}
|
||||||
|
|||||||
236
service/mount.go
236
service/mount.go
@@ -4,37 +4,33 @@ import "os"
|
|||||||
import "log"
|
import "log"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "net"
|
import "net"
|
||||||
|
import "sync"
|
||||||
|
import "bufio"
|
||||||
import "errors"
|
import "errors"
|
||||||
import "strings"
|
import "strings"
|
||||||
import "crypto/tls"
|
import "crypto/tls"
|
||||||
import "encoding/base64"
|
import "encoding/base64"
|
||||||
import "hnakra/protocol"
|
import "hnakra/protocol"
|
||||||
|
|
||||||
// M creates a very basic MountConfig with the specified name and description.
|
|
||||||
func M (name, description string) MountConfig {
|
|
||||||
return MountConfig {
|
|
||||||
Name: name,
|
|
||||||
Description: description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount is an interface satisfied by all mount types.
|
// Mount is an interface satisfied by all mount types.
|
||||||
type Mount interface {
|
type Mount interface {
|
||||||
|
Run (ServiceInfo) error
|
||||||
Close () error
|
Close () error
|
||||||
Shutdown () error
|
Shutdown () error
|
||||||
Run () error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MountConfig contains generic information common to all mounts.
|
// MountInfo contains information about a mount point.
|
||||||
type MountConfig struct {
|
type MountInfo struct {
|
||||||
// Host specifies the host to mount on. If the host is left empty, it
|
// 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
|
// will default to @ (meaning default/any host). The port is entirely up
|
||||||
// to the router. Maximum length for host portion: 255 bytes
|
// to the router.
|
||||||
|
// Maximum length: 255 bytes
|
||||||
Host string
|
Host string
|
||||||
|
|
||||||
// Scheme specifies the protocol to mount on. This will be automatically
|
// Scheme specifies the protocol to mount on. This will be automatically
|
||||||
// set by specialized mount types, so setting it manually shouldn't be
|
// set by specialized mount types, so setting it manually shouldn't be
|
||||||
// needed.
|
// needed.
|
||||||
|
// Maximum length: 255 bytes
|
||||||
Scheme string
|
Scheme string
|
||||||
|
|
||||||
// Path specifies the path to mount on. If the path ends with a /, then
|
// Path specifies the path to mount on. If the path ends with a /, then
|
||||||
@@ -43,7 +39,37 @@ type MountConfig struct {
|
|||||||
// path exactly (when normalized).
|
// path exactly (when normalized).
|
||||||
// Maximum length: 2^16-1 bytes
|
// Maximum length: 2^16-1 bytes
|
||||||
Path string
|
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
|
// Router specifies the host:port of the router to connect to. This
|
||||||
// defaults to $HNAKRA_ROUTER_HOST:$HNAKRA_ROUTER_PORT if left empty.
|
// defaults to $HNAKRA_ROUTER_HOST:$HNAKRA_ROUTER_PORT if left empty.
|
||||||
// The default value of these environment variables (if not set) is
|
// The default value of these environment variables (if not set) is
|
||||||
@@ -78,19 +104,18 @@ type MountConfig struct {
|
|||||||
// Maximum length: 255 bytes
|
// Maximum length: 255 bytes
|
||||||
Key []byte
|
Key []byte
|
||||||
|
|
||||||
// TLSConfig is an optional TLS configuration.
|
// TLSConfig is an optional TLS configuration. If you are looking to
|
||||||
TLSConfig *tls.Config
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect creates a new connection to the router specified in the MountConfig.
|
// FillDefault fills most empty fields with values from environment variables.
|
||||||
func (mount *MountConfig) Connect () (
|
// If an environment variable is blank, it uses a hard-coded default value
|
||||||
conn net.Conn,
|
// instead.
|
||||||
idFactory *protocol.IDFactory,
|
func (service *ServiceInfo) FillDefault () (err error) {
|
||||||
err error,
|
// host
|
||||||
) {
|
|
||||||
log.Println("(i) service", mount.Name)
|
|
||||||
idFactory = protocol.NewServiceIDFactory()
|
|
||||||
|
|
||||||
defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
|
defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
|
||||||
if defaultRouterHost == "" {
|
if defaultRouterHost == "" {
|
||||||
defaultRouterHost = "localhost"
|
defaultRouterHost = "localhost"
|
||||||
@@ -99,111 +124,136 @@ func (mount *MountConfig) Connect () (
|
|||||||
if defaultRouterPort == "" {
|
if defaultRouterPort == "" {
|
||||||
defaultRouterPort = "2048"
|
defaultRouterPort = "2048"
|
||||||
}
|
}
|
||||||
|
routerHost, routerPort, _ := strings.Cut(service.Router, ":")
|
||||||
// parse router host/port
|
|
||||||
routerHost, routerPort, _ := strings.Cut(mount.Router, ":")
|
|
||||||
if routerHost == "" {
|
if routerHost == "" {
|
||||||
routerHost = defaultRouterHost
|
routerHost = defaultRouterHost
|
||||||
}
|
}
|
||||||
if routerPort == "" {
|
if routerPort == "" {
|
||||||
routerPort = defaultRouterPort
|
routerPort = defaultRouterPort
|
||||||
}
|
}
|
||||||
|
service.Router = routerHost + ":" + routerPort
|
||||||
|
|
||||||
// get mount point
|
// user
|
||||||
scheme := mount.Scheme
|
if service.User == "" {
|
||||||
host := mount.Host
|
service.User = os.Getenv("HNAKRA_USER")
|
||||||
if host == "" {
|
|
||||||
host = "@"
|
|
||||||
}
|
|
||||||
if len(host) > 255 {
|
|
||||||
return nil, 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, nil, errors.New(fmt.Sprint (
|
|
||||||
"mount point path cannot be longer than ",
|
|
||||||
protocol.MaxIntOfSize(2), " bytes"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user
|
// key
|
||||||
user := mount.User
|
if service.Key == nil {
|
||||||
if user == "" {
|
|
||||||
user = os.Getenv("HNAKRA_USER")
|
|
||||||
}
|
|
||||||
if len(user) > 255 {
|
|
||||||
return nil, nil, errors.New (
|
|
||||||
"user cannot be longer than 255 bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get key
|
|
||||||
key := mount.Key
|
|
||||||
if key == nil {
|
|
||||||
base64Key := os.Getenv("HNAKRA_KEY")
|
base64Key := os.Getenv("HNAKRA_KEY")
|
||||||
key, err = base64.StdEncoding.DecodeString(base64Key)
|
service.Key, err = base64.StdEncoding.DecodeString(base64Key)
|
||||||
if err != nil { return nil, nil, err }
|
if err != nil { return }
|
||||||
}
|
|
||||||
if len(key) > 255 {
|
|
||||||
return nil, nil, errors.New (
|
|
||||||
"key cannot be longer than 255 bytes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure name/description aren't too big
|
return
|
||||||
if len(mount.Name) > 255 {
|
}
|
||||||
return nil, nil, errors.New (
|
|
||||||
"service name cannot be longer than 255 bytes")
|
// 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
|
||||||
}
|
}
|
||||||
if len(mount.Description) > 255 {
|
}
|
||||||
return nil, nil, errors.New (
|
|
||||||
"service description cannot be longer than 255 bytes")
|
// 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
|
// connect to router
|
||||||
routerAddr := fmt.Sprint(routerHost, ":", routerPort)
|
log.Println("... dialing", service.Router)
|
||||||
log.Println("... dialing", routerAddr)
|
conn.conn, err = tls.Dial("tcp", service.Router, service.TLSConfig)
|
||||||
conn, err = tls.Dial("tcp", routerAddr, mount.TLSConfig)
|
if err != nil { return nil, err }
|
||||||
if err != nil { return nil, nil, err }
|
conn.readWriter = bufio.NewReadWriter (
|
||||||
|
bufio.NewReader(conn.conn),
|
||||||
|
bufio.NewWriter(conn.conn))
|
||||||
|
|
||||||
// log in
|
// log in
|
||||||
log.Println (
|
log.Println("... logging in as", service.User, "on", mount)
|
||||||
"... logging in as", user,
|
err = conn.Send(protocol.MessageLogin {
|
||||||
"on", scheme + "://" + host + path)
|
ID: conn.IDFactory.Next(),
|
||||||
err = protocol.MessageLogin {
|
|
||||||
ID: idFactory.Next(),
|
|
||||||
Version: protocol.Version { Major: 0, Minor: 0 },
|
Version: protocol.Version { Major: 0, Minor: 0 },
|
||||||
User: user,
|
User: service.User,
|
||||||
Key: key,
|
Key: service.Key,
|
||||||
Name: mount.Name,
|
Name: service.Name,
|
||||||
Description: mount.Description,
|
Description: service.Description,
|
||||||
Scheme: scheme,
|
Scheme: mount.Scheme,
|
||||||
Host: host,
|
Host: mount.Host,
|
||||||
Path: path,
|
Path: mount.Path,
|
||||||
}.Send(conn)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// read status
|
// read status
|
||||||
message, err := protocol.ReadMessage(conn)
|
message, err := conn.Receive()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
status, ok := message.(protocol.MessageStatus)
|
status, ok := message.(protocol.MessageStatus)
|
||||||
if !ok {
|
if !ok {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, nil, errors.New(fmt.Sprint (
|
return nil, errors.New(fmt.Sprint (
|
||||||
"router sent unknown type, expecting",
|
"router sent unknown type, expecting",
|
||||||
protocol.TypeStatus))
|
protocol.TypeStatus))
|
||||||
}
|
}
|
||||||
if status.Status != protocol.StatusOk {
|
if status.Status != protocol.StatusOk {
|
||||||
return nil, nil, status
|
return nil, status
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(".// logged in")
|
log.Println(".// logged in")
|
||||||
return conn, idFactory, nil
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,36 @@ import "os"
|
|||||||
import "log"
|
import "log"
|
||||||
import "time"
|
import "time"
|
||||||
import "hnakra/rotate"
|
import "hnakra/rotate"
|
||||||
|
import "hnakra/daemon"
|
||||||
import "hnakra/routines"
|
import "hnakra/routines"
|
||||||
|
|
||||||
// Service is capable of managing multiple mounts. It also sets up logging
|
// Service is capable of managing multiple mounts. It also sets up logging
|
||||||
// automatically.
|
// automatically.
|
||||||
type Service []Mount
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run runs the mounts within the service, and only exits when all of them have
|
// 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
|
// 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.
|
// $HNAKRA_LOG_DIR. If that variable is unset, it will just log to stdout.
|
||||||
func (service Service) Run () error {
|
// 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
|
// set up logging
|
||||||
logDir := os.Getenv("HNAKRA_LOG_DIR")
|
logDir := os.Getenv("HNAKRA_LOG_DIR")
|
||||||
if logDir != "" {
|
if logDir != "" {
|
||||||
@@ -22,24 +42,40 @@ func (service Service) Run () error {
|
|||||||
if err != nil { log.Fatal("cannot access log dir:", err) }
|
if err != nil { log.Fatal("cannot access log dir:", err) }
|
||||||
log.SetOutput(logger)
|
log.SetOutput(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("... starting service", service.Name)
|
||||||
|
|
||||||
// set up routine manager
|
// set up routine manager
|
||||||
manager := routines.Manager { RestartDeadline: time.Second * 8 }
|
service.manager = routines.Manager { RestartDeadline: time.Second * 8 }
|
||||||
manager.Routines = make([]routines.Routine, len(service))
|
service.manager.Routines = make([]routines.Routine, len(service.Mounts))
|
||||||
for index, mount := range service {
|
for index, mount := range service.Mounts {
|
||||||
manager.Routines[index] = mount.Run
|
service.manager.Routines[index] = routines.From (func () error {
|
||||||
|
return mount.Run(service.ServiceInfo)
|
||||||
|
}, mount.Shutdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) }
|
||||||
|
} ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// send it
|
// send it
|
||||||
err := manager.Run()
|
err := service.manager.Run()
|
||||||
if err != nil { log.Println("XXX", err) }
|
if err != nil { log.Println("XXX", err) }
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close abruptly closes all mounts in the service. This will cause Run() to
|
// Close abruptly closes all mounts in the service. This will cause Run() to
|
||||||
// exit.
|
// exit.
|
||||||
func (service Service) Close () (err error) {
|
func (service *Service) Close () (err error) {
|
||||||
for _, mount := range service {
|
for _, mount := range service.Mounts {
|
||||||
singleErr := mount.Close()
|
singleErr := mount.Close()
|
||||||
if singleErr != nil {
|
if singleErr != nil {
|
||||||
err = singleErr
|
err = singleErr
|
||||||
@@ -50,12 +86,6 @@ func (service Service) Close () (err error) {
|
|||||||
|
|
||||||
// Shutdown gracefully shuts down each mount in the service. This will cause
|
// Shutdown gracefully shuts down each mount in the service. This will cause
|
||||||
// Run() to exit.
|
// Run() to exit.
|
||||||
func (service Service) Shutdown () (err error) {
|
func (service *Service) Shutdown () (err error) {
|
||||||
for _, mount := range service {
|
return service.manager.Shutdown()
|
||||||
singleErr := mount.Shutdown()
|
|
||||||
if singleErr != nil {
|
|
||||||
err = singleErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user