commit 080c1a3d0629e3b9ccecfdd1cae56b45c97c4e8e Author: eater <=@eater.me> Date: Mon Aug 5 23:03:41 2019 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a466fca --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out +examples/* +!examples/service.toml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4298fee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2019 eater + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/config.go b/config.go new file mode 100644 index 0000000..a43987d --- /dev/null +++ b/config.go @@ -0,0 +1,40 @@ +package service + +import ( + "github.com/BurntSushi/toml" + "log" + "os" + "path/filepath" +) + +type ZeroooConfig struct { + Location string `toml:"location"` + Endpoint string `toml:"endpoint"` +} + +type Config struct { + Zerooo ZeroooConfig `toml:"zerooo"` +} + +func LoadConfig(path string) (*Config, error) { + config := Config{} + _, err := toml.DecodeFile(path, &config) + if err != nil { + return nil, err + } + + return &config, nil +} + +func CreateConfig(path string, cfg *Config) { + conf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + Check(err, "Couldn't open %q, err: %s", path, err) + encoder := toml.NewEncoder(conf) + log.Printf("ZeroooConfig: %#v", cfg) + err = encoder.Encode(cfg) + if cfg.Zerooo.Location[0] != '/' { + cfg.Zerooo.Location = filepath.Join(filepath.Dir(path), cfg.Zerooo.Location) + } + + Check(err, "Couldn't parse %q, err: %s", path, err) +} diff --git a/examples/service.toml b/examples/service.toml new file mode 100644 index 0000000..9e6ac95 --- /dev/null +++ b/examples/service.toml @@ -0,0 +1,3 @@ +[zerooo] +location = "/etc/zerooo" +endpoint = "https://zer.ooo" diff --git a/http.go b/http.go new file mode 100644 index 0000000..3c25728 --- /dev/null +++ b/http.go @@ -0,0 +1,217 @@ +package service + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io/ioutil" + "net/http" +) + +import "golang.org/x/crypto/blowfish" + +type Status int + +const ( + OK Status = iota + WAITING + ERROR +) + +type BaseMessage struct { + Signature string +} + +type httpServer struct { + manager *Manager + status Status +} + +type CreateCSRRequest struct { + *BaseMessage + Hostname string +} + +type CreateCSRResponse struct { + *BaseMessage + CSR string `json:"csr"` +} + +type UpdateOpenVPNConfigRequest struct { + *BaseMessage + Config string +} + +type DeliverCertificateRequest struct { + *BaseMessage + Certificate string +} + +func NewHttpServer(manager *Manager) *httpServer { + return &httpServer{manager, WAITING} +} + +func (it *httpServer) Start() { + http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + writer.Write([]byte("Don't")) + }) + + http.HandleFunc("/create-csr", func(writer http.ResponseWriter, request *http.Request) { + req := &CreateCSRRequest{} + err := it.verifyRequest(request, req) + + if err != nil { + writer.WriteHeader(400) + return + } + + csr, err := it.manager.CreateCSR(req.Hostname) + if err != nil { + writer.WriteHeader(500) + return + } + + it.writeResponse(writer, CreateCSRResponse{ + CSR: string(csr), + }) + }) + + http.HandleFunc("/deliver-crt", func(writer http.ResponseWriter, request *http.Request) { + req := &DeliverCertificateRequest{} + err := it.verifyRequest(request, req) + if err != nil { + writer.WriteHeader(400) + return + } + + err = it.manager.UpdateCertificate(req.Certificate) + if err != nil { + writer.WriteHeader(500) + return + } + + it.manager.openVPN.Start() + }) + + http.HandleFunc("/update-openvpn-config", func(writer http.ResponseWriter, request *http.Request) { + req := &UpdateOpenVPNConfigRequest{} + err := it.verifyRequest(request, req) + if err != nil { + writer.WriteHeader(400) + return + } + + err = it.manager.openVPN.UpdateConfig(req.Config) + if err != nil { + writer.WriteHeader(500) + return + } + + it.manager.openVPN.Restart() + }) + + http.ListenAndServe(":7864", nil) +} + +func (it *httpServer) writeResponse(writer http.ResponseWriter, v interface{}) error { + passwordAndIV := make([]byte, 48) + _, err := rand.Read(passwordAndIV) + if err != nil { + return err + } + + encPasswordAndIV, err := rsa.EncryptPKCS1v15(rand.Reader, it.manager.CAPublicKey(), passwordAndIV) + if err != nil { + return err + } + + _, err = writer.Write([]byte(hex.EncodeToString(encPasswordAndIV))) + if err != nil { + return err + } + + password := passwordAndIV[:32] + iv := passwordAndIV[32:] + + cipher, err := blowfish.NewSaltedCipher(password, iv) + if err != nil { + return err + } + + hashedFingerpint := sha256.Sum256(it.manager.GetServerFingerprint()) + plainSignature, err := rsa.SignPKCS1v15(rand.Reader, it.manager.privateKey, 0, hashedFingerpint[:]) + if err != nil { + return err + } + + sign, ok := v.(*BaseMessage) + if !ok { + return errors.New("Given message can't be signed") + } + sign.Signature = hex.EncodeToString(plainSignature) + + body, err := json.Marshal(v) + + if err != nil { + return err + } + + encBody := make([]byte, len(body)) + cipher.Encrypt(encBody, body) + writer.Write([]byte(hex.EncodeToString(encBody))) + + return nil +} + +func (it *httpServer) verifyRequest(r *http.Request, v interface{}) (error) { + all, err := ioutil.ReadAll(hex.NewDecoder(r.Body)) + if err != nil { + return err + } + + encPasswordAndIV := all[:256] + encBody := all[256:] + + passwordAndIV, err := rsa.DecryptPKCS1v15(rand.Reader, it.manager.privateKey, encPasswordAndIV) + + if err != nil { + return err + } + + password := passwordAndIV[:32] + iv := passwordAndIV[32:] + + cipher, err := blowfish.NewSaltedCipher(password, iv) + + if err != nil { + return err + } + + body := make([]byte, len(encBody)) + cipher.Decrypt(body, encBody) + + err = json.Unmarshal(body, v) + + if err != nil { + return err + } + + baseReq, ok := v.(*BaseMessage) + if !ok { + return errors.New("non-base request") + } + + signature, err := hex.DecodeString(baseReq.Signature) + if err != nil { + return err + } + + fingerprint := it.manager.GetServerFingerprint() + fingerprintHashed := sha256.Sum256(fingerprint) + err = rsa.VerifyPKCS1v15(it.manager.CAPublicKey(), 0, fingerprintHashed[:], signature) + return err +} diff --git a/manager.go b/manager.go new file mode 100644 index 0000000..bd3c9a9 --- /dev/null +++ b/manager.go @@ -0,0 +1,339 @@ +package service + +import ( + "bufio" + "crypto/md5" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "strconv" + "strings" + "syscall" +) + +type Manager struct { + Config Config + privateKey *rsa.PrivateKey + openVPN *OpenVPN + CA *x509.Certificate + HasCertificate bool + HasOpenVPNConfig bool +} + +func NewManager(cfg Config) *Manager { + return &Manager{ + Config: cfg, + } +} + +func fileExists(s string) bool { + if _, err := os.Stat(s); err == nil { + return true + } else if os.IsNotExist(err) { + return false + } else { + Check(err, "Failed to check %q, err: %s", s, err) + } + + return false +} + +func (it *Manager) GetOpenVPNConfigLocation() string { + return it.Config.Zerooo.Location + "/server.conf" +} + +func (it *Manager) GetCALocation() string { + return it.Config.Zerooo.Location + "/ca.crt" +} + +func (it *Manager) GetCertificateLocation() string { + return it.Config.Zerooo.Location + "/server.crt" +} + +func (it *Manager) GetKeyLocation() string { + return it.Config.Zerooo.Location + "/server.key" +} + +func (it *Manager) GetDHLocation() string { + return it.Config.Zerooo.Location + "/dh2048.pem" +} + +func (it *Manager) GetCRLLocation() string { + return it.Config.Zerooo.Location + "/crl.pem" +} + +func (it *Manager) EnsureFile(path string, generator func()) bool { + if !fileExists(path) { + generator() + return true + } + + return false +} +func (it *Manager) Init() { + if !fileExists(it.Config.Zerooo.Location) { + os.MkdirAll(it.Config.Zerooo.Location, 0755) + } + + generated := it.EnsureFile(it.GetKeyLocation(), func() { + it.GenerateKey() + }) + + if !generated { + it.LoadKey() + } + + it.EnsureFile(it.GetCALocation(), func() { + it.DownloadCA() + }) + + it.LoadCA() + + it.EnsureFile(it.GetCRLLocation(), func() { + it.DownloadCRL() + }) + + it.EnsureFile(it.GetDHLocation(), func() { + it.DownloadDH() + }) + +} + +func (it *Manager) GetServerFingerprint() []byte { + der := it.ExportPublicKeyDER() + fp := it.GetOpenSSLLikeFingerprint(der) + return []byte(fp) +} + +func (it *Manager) GetOpenSSLLikeFingerprint(input []byte) string { + output := md5.Sum(input) + items := make([]string, 16) + for i := range output { + items[i] = strings.ToLower(hex.EncodeToString([]byte{output[i]})) + } + + return strings.Join(items, ":") +} + +func (it *Manager) Register() { + fp := it.GetServerFingerprint() + + log.Printf("%s\n", fp) + + signed, err := rsa.SignPKCS1v15(rand.Reader, it.privateKey, 0, fp) + Check(err, "Failed signing fingerprint, err: %s", err) + + signedHash := sha256.Sum256(signed) + + publicKey := string(it.ExportPublicKey()) + hexSignature := hex.EncodeToString(signedHash[:]) + + log.Printf("Registering with\n\nsignature: %s\npublic key:\n\n%s", hexSignature, publicKey) + + resp, err := http.PostForm(it.Config.Zerooo.Endpoint+"/server/register", url.Values{ + "publicKey": []string{publicKey}, + "signature": []string{hexSignature}, + }) + + Check(err, "Failed to sent register post to endpoint (%s/server/register), err: %s", it.Config.Zerooo.Endpoint, err) + + if resp.StatusCode != 200 { + Error("Endpoint responded with %d, may not be actual zerooo endpoint", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + Check(err, "Failed to read response, err: %s", err) + + strBody := string(body) + items := strings.SplitN(strBody, "\n", 2) + code, err := strconv.ParseInt(items[0], 10, 64) + Check(err, "Failed reading response success code, err: %s", err) + + if code != 0 { + Error("Register failed, server responded with:\n\n%s", items[1]) + } + + log.Printf("Register succesful, server responded with: %s", items[1]) +} + +func (it *Manager) ExportPublicKeyDER() []byte { + if (it.privateKey == nil) { + Error("No private key is loaded, can't export public key") + } + + der, err := x509.MarshalPKIXPublicKey(&it.privateKey.PublicKey) + Check(err, "Failed converting public key to DER, err: %s", err) + return der +} + +func (it *Manager) ExportPublicKey() []byte { + pemKey := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: it.ExportPublicKeyDER(), + } + + return pem.EncodeToMemory(pemKey) +} + +func (it *Manager) GenerateKey() { + log.Printf("No key exists, generating new 4096 RSA key") + priv, err := rsa.GenerateKey(rand.Reader, 4096) + Check(err, "Couldn't generate key, err: %s", err) + it.privateKey = priv + + pemKey := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + + keyFile, err := os.OpenFile(it.GetKeyLocation(), os.O_EXCL|os.O_CREATE|os.O_RDWR, 0600) + Check(err, "Failed creating file at %q for private key, err: %s", it.GetKeyLocation(), err) + defer keyFile.Close() + + err = pem.Encode(keyFile, pemKey) + Check(err, "Failed encoding private key, err: %s", err) +} + +func (it *Manager) LoadKey() { + log.Printf("Loading generated key") + + keyFile, err := os.Open(it.GetKeyLocation()) + Check(err, "Failed opening private key at %q for reading, err: %s", it.GetKeyLocation(), err) + defer keyFile.Close() + + stat, _ := keyFile.Stat() + var size int64 = stat.Size() + pembytes := make([]byte, size) + buffer := bufio.NewReader(keyFile) + _, err = buffer.Read(pembytes) + Check(err, "Failed to read private key at %q, err: %s", it.GetKeyLocation(), err) + + data, _ := pem.Decode([]byte(pembytes)) + if data == nil { + Error("No valid key found at %q", it.GetKeyLocation()) + } + + importedKey, err := x509.ParsePKCS1PrivateKey(data.Bytes) + Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err) + it.privateKey = importedKey +} + +func (it *Manager) Daemon() { + server := NewHttpServer(it) + go server.Start() + + it.openVPN = NewOpenVPN(it) + + it.openVPN.Start() + + ch := make(chan os.Signal) + + signal.Notify(ch, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT) + + for sig := range ch { + log.Printf("Received signal %s, quitting.", sig) + os.Exit(0) + } +} + +func (it *Manager) LoadCA() { + log.Printf("Loading CA certificate") + + keyFile, err := os.Open(it.GetCALocation()) + Check(err, "Failed opening CA certificate at %q for reading, err: %s", it.GetKeyLocation(), err) + defer keyFile.Close() + + stat, _ := keyFile.Stat() + var size int64 = stat.Size() + pemBytes := make([]byte, size) + buffer := bufio.NewReader(keyFile) + _, err = buffer.Read(pemBytes) + Check(err, "Failed to read CA certificate at %q, err: %s", it.GetKeyLocation(), err) + + data, _ := pem.Decode([]byte(pemBytes)) + if data == nil { + Error("No valid CA certificate found at %q", it.GetKeyLocation()) + } + + ca, err := x509.ParseCertificate(data.Bytes) + Check(err, "Failed importing private key at %q, err: %s", it.GetKeyLocation(), err) + it.CA = ca +} + +func (it *Manager) DownloadCA() { + caUrl := it.Config.Zerooo.Endpoint + "/ca" + download("CA", it.GetCALocation(), caUrl) +} + +func (it *Manager) CAPublicKey() (*rsa.PublicKey) { + key, ok := it.CA.PublicKey.(*rsa.PublicKey) + if !ok { + Error("Can't read CA Certificate as public key") + } + + return key +} + +func (it *Manager) CreateCSR(hostname string) ([]byte, error) { + tpl := x509.CertificateRequest{Subject: pkix.Name{CommonName: hostname}} + csr, err := x509.CreateCertificateRequest(rand.Reader, &tpl, it.privateKey) + + if err != nil { + return nil, err + } + + pemCSR := &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csr, + } + + return pem.EncodeToMemory(pemCSR), nil +} + +func (it *Manager) UpdateCertificate(s string) error { + f, err := os.OpenFile(it.GetCertificateLocation(), os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(s) + return err +} + +func download(name string, path string, url string) { + resp, err := http.Get(url) + Check(err, "Failed fetching %s at %q, err: %s", name, url, err) + cert, err := ioutil.ReadAll(resp.Body) + Check(err, "Failed fetching %s at %q, err: %s", name, url, err) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + Check(err, "Failed open %s for writing at %q, err: %s", name, path, err) + defer f.Close() + _, err = f.Write(cert) + Check(err, "Failed writing %s at %q, err: %s", name, path, err) +} + +func (it *Manager) DownloadCRL() { + crlUrl := it.Config.Zerooo.Endpoint + "/crl" + download("CRL", it.GetCRLLocation(), crlUrl) +} + +func (it *Manager) DownloadDH() { + dhUrl := it.Config.Zerooo.Endpoint + "/dh.pem" + download("DH", it.GetDHLocation(), dhUrl) +} + +func (it *Manager) IsOpenVPNReady() bool { + hasConfig := fileExists(it.GetOpenVPNConfigLocation()) + hasCert := fileExists(it.GetCertificateLocation()) + return hasConfig && hasCert +} diff --git a/openvpn.go b/openvpn.go new file mode 100644 index 0000000..df9bf11 --- /dev/null +++ b/openvpn.go @@ -0,0 +1,73 @@ +package service + +import ( + "log" + "os" + "os/exec" +) + +type OpenVPN struct { + Config *ZeroooConfig + Manager *Manager + shouldStop bool + cmd *exec.Cmd +} + +func NewOpenVPN(manager *Manager) *OpenVPN { + return &OpenVPN{ + Config: &manager.Config.Zerooo, + Manager: manager, + } +} + +func (it *OpenVPN) Stop() { + it.shouldStop = true + if it.cmd != nil && it.cmd.Process != nil { + it.cmd.Process.Kill() + } +} + +func (it *OpenVPN) GetConfigLocation() string { + return it.Config.Location + "/server.conf" +} + +func (it *OpenVPN) Restart() { + if it.cmd != nil && it.cmd.Process != nil { + it.cmd.Process.Kill() + return + } + + it.Start() +} + +func (it *OpenVPN) Start() { + if !it.Manager.IsOpenVPNReady() { + log.Printf("OpenVPN not ready yet.") + return + } + + log.Printf("Starting OpenVPN") + it.cmd = exec.Command("openvpn", "--config", it.GetConfigLocation()) + it.cmd.Start() + + go func() { + it.cmd.Wait() + if it.shouldStop { + log.Printf("OpenVPN has stopped (exit code: %d)", it.cmd.ProcessState.ExitCode()) + return + } + + log.Printf("OpenVPN has stopped (exit code: %d), restarting", it.cmd.ProcessState.ExitCode()) + it.Start() + }() +} + +func (it *OpenVPN) UpdateConfig(s string) error { + f, err := os.OpenFile(it.GetConfigLocation(), os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(s) + return err +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..0650894 --- /dev/null +++ b/util.go @@ -0,0 +1,17 @@ +package service + +import ( + "log" + "os" +) + +func Check(err error, msg string, v ...interface{}) { + if err != nil { + Error(msg, v...) + } +} + +func Error(msg string, v ...interface{}) { + log.Fatalf(msg+"\n", v...) + os.Exit(1) +} diff --git a/zerooo/cmd/daemon.go b/zerooo/cmd/daemon.go new file mode 100644 index 0000000..164ab77 --- /dev/null +++ b/zerooo/cmd/daemon.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "git.cijber.net/zer.ooo/service" + "github.com/spf13/cobra" +) + +var daemonCmd = &cobra.Command{ + Use: "daemon", + Short: "Starts the zer.ooo service daemon", + Long: "Will start running the zer.ooo service daemon, keeping OpenVPN up", + + Run: func(cmd *cobra.Command, args []string) { + config, err := service.LoadConfig(ConfigPath) + service.Check(err, "Failed loading config, err: %s", err) + mgmt := service.NewManager(*config) + mgmt.Init() + mgmt.Daemon() + }, +} + +func init() { + +} diff --git a/zerooo/cmd/init.go b/zerooo/cmd/init.go new file mode 100644 index 0000000..8cdbb0b --- /dev/null +++ b/zerooo/cmd/init.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "git.cijber.net/zer.ooo/service" + "github.com/spf13/cobra" + "os" + "path/filepath" +) + +var ( + endpoint string + location string +) + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialises an zer.ooo server install", + Long: "Init (zerooo init) will initialise a server", + + Run: func(cmd *cobra.Command, args []string) { + config, err := service.LoadConfig(ConfigPath) + service.Check(err, "Failed reading config, err: %s", err) + + if (cmd.Flag("endpoint").Changed) { + config.Zerooo.Endpoint = endpoint + } + + if (cmd.Flag("location").Changed) { + if location[0] != '/' { + cwd, err := os.Getwd() + service.Check(err, "Can't get current working directory, err: %s", err) + location = filepath.Join(cwd, location) + } + + config.Zerooo.Location = location + } + + service.CreateConfig(ConfigPath, config) + mgmt := service.NewManager(*config) + mgmt.Init() + mgmt.Register() + }, +} + +func init() { + f := initCmd.Flags() + f.StringVar(&endpoint, "endpoint", "https://zer.ooo", "Endpoint to register this server") + f.StringVar(&location, "location", "/etc/zerooo", "Location of OpenVPN installation") +} diff --git a/zerooo/cmd/root.go b/zerooo/cmd/root.go new file mode 100644 index 0000000..9c82d75 --- /dev/null +++ b/zerooo/cmd/root.go @@ -0,0 +1,54 @@ +/* +Copyright © 2019 eater + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" +) + +var ConfigPath string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "zerooo", + Short: "", + Long: `Zer.ooo service manager`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringVar(&ConfigPath, "config", "/etc/zerooo/service.toml", "config file") + rootCmd.AddCommand( + initCmd, + daemonCmd, + ) +} diff --git a/zerooo/main.go b/zerooo/main.go new file mode 100644 index 0000000..d423de0 --- /dev/null +++ b/zerooo/main.go @@ -0,0 +1,7 @@ +package main + +import "git.cijber.net/zer.ooo/service/zerooo/cmd" + +func main() { + cmd.Execute() +}