From 375d7fc0b7f70115f663b9cb873c39e390215cd6 Mon Sep 17 00:00:00 2001 From: Jarno Rankinen Date: Wed, 27 Mar 2024 08:22:26 +0000 Subject: [PATCH] Podman kube pod - Added YAML for a Podman kube pod - Support configuring with environment variables --- README.md | 41 ++++++-- cmd/hydroxide-push/main.go | 209 ++++++++++++++++++++----------------- hydroxide-push-podman.yaml | 37 +++++++ ntfy/ntfy.go | 16 ++- 4 files changed, 199 insertions(+), 104 deletions(-) create mode 100644 hydroxide-push-podman.yaml diff --git a/README.md b/README.md index 4ce68d6..9260238 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,11 @@ Binary: ``` Container: ```shell -podman run -it --rm -v hydroxide-config:/data ghcr.io/0ranki/hydroxide-push auth your.proton@email.address +podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push auth your.proton@email.address ``` You will be prompted for the Proton account credentials and the details for the push server. Proton credentials are stored encrypted form. -The auth flow generates a separate password for the bridge, which is stored in plaintext -to `$HOME/.config/notify.json`. Unlike upstream `hydroxide`, there is no service listening on any port, -all communications is internal to the program. +The auth flow generates a separate password for the bridge to fake a login to the bridge, which is stored in plaintext to `$HOME/.config/notify.json`. Unlike upstream `hydroxide`, there is no service listening on any port, the password isn't useful for anything else. ### Reconfigure push server Binary: @@ -39,12 +37,13 @@ hydroxide-push setup-ntfy ``` Container: ```shell -podman run -it --rm -v hydroxide-config:/data ghcr.io/0ranki/hydroxide-push setup-ntfy +podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push setup-ntfy ``` -You'll be asked for the base URL of the push server, and the topic. These will probably -be combined to a single string in future versions. +You'll be asked for the base URL of the push server, and the topic. The push endpoint configuration can be changed while the daemon is running. -**NOTE:** Authentication for the push endpoint is not yet supported. +The currently configured values are shown inside braces. Leave input blank to use the current values. + +>**NOTE:** Authentication for the push endpoint is not yet supported. ### Start the service @@ -54,8 +53,32 @@ hydroxide-push notify ``` Container: ```shell -podman run -it --rm -v hydroxide-config:/data ghcr.io/0ranki/hydroxide-push +podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push ``` +## Podman pod + +A Podman kube YAML file is provided in the repo. + +> **Note:** If you're using 2FA or just don't want to put your password to a file, use the manual method above. Make sure the volume name (claimName) in the YAML mathces what you use in the commands. + +- Download/copy `hydroxide-push-podman.yaml` to the machine you intend to run the daemon on +- Edit the config values at the top of the file +- Start the pod: + ```shell + podman kube play ./hydroxide-push-podman.yaml + ``` + - Latest container image is pulled + - A named volume (`hydroxide-push`) will be created for the configuration + - Login to Proton and push URL configuration is handled automatically, after which the daemon starts +- After the initial setup, the ConfigMap (before `---`) can be removed from the YAML.). Optionally to clear the environment variables, run + + ```shell + podman kube play ./hydroxide-push-podman.yaml --replace + ``` + The command can also be used to pull the latest version and restart the pod. +- To reauthenticate or clear data, simply remove the named volume or run the `auth` command + + ## License MIT diff --git a/cmd/hydroxide-push/main.go b/cmd/hydroxide-push/main.go index 2c4b81f..85eabdc 100644 --- a/cmd/hydroxide-push/main.go +++ b/cmd/hydroxide-push/main.go @@ -74,11 +74,117 @@ func listenEventsAndNotify(addr string, debug bool, authManager *auth.Manager, e } } +func authenticate(authCmd *flag.FlagSet) { + var username string + if os.Getenv("PROTON_ACCT") != "" { + username = os.Getenv("PROTON_ACCT") + } else { + username = authCmd.Arg(0) + } + if username == "" { + log.Fatal("usage: hydroxide auth ") + } + + c := newClient() + + var a *protonmail.Auth + /*if cachedAuth, ok := auths[username]; ok { + var err error + a, err = c.AuthRefresh(a) + if err != nil { + // TODO: handle expired token error + log.Fatal(err) + } + }*/ + + var loginPassword string + if a == nil { + if os.Getenv("PROTON_ACCT_PASSWORD") != "" { + loginPassword = os.Getenv("PROTON_ACCT_PASSWORD") + } else if pass, err := askPass("Password"); err != nil { + log.Fatal(err) + } else { + loginPassword = string(pass) + } + + authInfo, err := c.AuthInfo(username) + if err != nil { + log.Fatal(err) + } + + a, err = c.Auth(username, loginPassword, authInfo) + if err != nil { + log.Fatal(err) + } + + if a.TwoFactor.Enabled != 0 { + if a.TwoFactor.TOTP != 1 { + log.Fatal("Only TOTP is supported as a 2FA method") + } + + scanner := bufio.NewScanner(os.Stdin) + fmt.Printf("2FA TOTP code: ") + scanner.Scan() + code := scanner.Text() + + scope, err := c.AuthTOTP(code) + if err != nil { + log.Fatal(err) + } + a.Scope = scope + } + } + + var mailboxPassword string + if a.PasswordMode == protonmail.PasswordSingle { + mailboxPassword = loginPassword + } + if mailboxPassword == "" { + prompt := "Password" + if a.PasswordMode == protonmail.PasswordTwo { + prompt = "Mailbox password" + } + if pass, err := askPass(prompt); err != nil { + log.Fatal(err) + } else { + mailboxPassword = string(pass) + } + } + + keySalts, err := c.ListKeySalts() + if err != nil { + log.Fatal(err) + } + + _, err = c.Unlock(a, keySalts, mailboxPassword) + if err != nil { + log.Fatal(err) + } + + secretKey, bridgePassword, err := auth.GeneratePassword() + if err != nil { + log.Fatal(err) + } + + err = auth.EncryptAndSave(&auth.CachedAuth{ + Auth: *a, + LoginPassword: loginPassword, + MailboxPassword: mailboxPassword, + KeySalts: keySalts, + }, username, secretKey) + if err != nil { + log.Fatal(err) + } + cfg.BridgePw = bridgePassword + cfg.Setup() +} + const usage = `usage: hydroxide-push [options...] Commands: auth Login to ProtonMail via hydroxide status View hydroxide status notify Start the notification daemon + setup-ntfy (Re)configure the push endpoint Global options: -debug @@ -123,101 +229,8 @@ func main() { switch cmd { case "auth": authCmd.Parse(flag.Args()[1:]) - username := authCmd.Arg(0) - if username == "" { - log.Fatal("usage: hydroxide auth ") - } + authenticate(authCmd) - c := newClient() - - var a *protonmail.Auth - /*if cachedAuth, ok := auths[username]; ok { - var err error - a, err = c.AuthRefresh(a) - if err != nil { - // TODO: handle expired token error - log.Fatal(err) - } - }*/ - - var loginPassword string - if a == nil { - if pass, err := askPass("Password"); err != nil { - log.Fatal(err) - } else { - loginPassword = string(pass) - } - - authInfo, err := c.AuthInfo(username) - if err != nil { - log.Fatal(err) - } - - a, err = c.Auth(username, loginPassword, authInfo) - if err != nil { - log.Fatal(err) - } - - if a.TwoFactor.Enabled != 0 { - if a.TwoFactor.TOTP != 1 { - log.Fatal("Only TOTP is supported as a 2FA method") - } - - scanner := bufio.NewScanner(os.Stdin) - fmt.Printf("2FA TOTP code: ") - scanner.Scan() - code := scanner.Text() - - scope, err := c.AuthTOTP(code) - if err != nil { - log.Fatal(err) - } - a.Scope = scope - } - } - - var mailboxPassword string - if a.PasswordMode == protonmail.PasswordSingle { - mailboxPassword = loginPassword - } - if mailboxPassword == "" { - prompt := "Password" - if a.PasswordMode == protonmail.PasswordTwo { - prompt = "Mailbox password" - } - if pass, err := askPass(prompt); err != nil { - log.Fatal(err) - } else { - mailboxPassword = string(pass) - } - } - - keySalts, err := c.ListKeySalts() - if err != nil { - log.Fatal(err) - } - - _, err = c.Unlock(a, keySalts, mailboxPassword) - if err != nil { - log.Fatal(err) - } - - secretKey, bridgePassword, err := auth.GeneratePassword() - if err != nil { - log.Fatal(err) - } - - err = auth.EncryptAndSave(&auth.CachedAuth{ - Auth: *a, - LoginPassword: loginPassword, - MailboxPassword: mailboxPassword, - KeySalts: keySalts, - }, username, secretKey) - if err != nil { - log.Fatal(err) - } - cfg.BridgePw = bridgePassword - cfg.Setup() case "status": usernames, err := auth.ListUsernames() if err != nil { @@ -235,7 +248,15 @@ func main() { case "setup-ntfy": cfg.Setup() + case "notify": + if os.Getenv("PROTON_ACCT_PASSWORD") != "" && os.Getenv("PROTON_ACCT") != "" && os.Getenv("PUSH_URL") != "" && os.Getenv("PUSH_TOPIC") != "" && cfg.BridgePw == "" { + log.Println("Logging in to Proton account using values from environment") + cfg.URL = os.Getenv("PUSH_URL") + cfg.Topic = os.Getenv("PUSH_TOPIC") + cfg.Save() + authenticate(new(flag.FlagSet)) + } authManager := auth.NewManager(newClient) eventsManager := events.NewManager() listenEventsAndNotify("0", debug, authManager, eventsManager, tlsConfig) diff --git a/hydroxide-push-podman.yaml b/hydroxide-push-podman.yaml new file mode 100644 index 0000000..8e785be --- /dev/null +++ b/hydroxide-push-podman.yaml @@ -0,0 +1,37 @@ +## Remove this ConfigMap section after the initial run +apiVersion: v1 +kind: ConfigMap +metadata: + name: hydroxide-push-config +data: + PROTON_ACCT: "my.account@protonmail.com" + PROTON_ACCT_PASSWORD: "myprotonaccountpassword" + PUSH_URL: "http://ntfy.sh" + PUSH_TOPIC: "" +## Remove the above after first run +--- +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2024-03-27T07:02:59Z" + labels: + app: hydroxide-push + name: hydroxide-push +spec: + containers: + - args: + - notify + image: ghcr.io/0ranki/hydroxide-push:latest + name: main + volumeMounts: + - mountPath: /data + name: hydroxide-push-pvc + envFrom: + - configMapRef: + name: hydroxide-push-config + optional: true + volumes: + - name: hydroxide-push-pvc + persistentVolumeClaim: + claimName: hydroxide-push + diff --git a/ntfy/ntfy.go b/ntfy/ntfy.go index 6e6db76..5263f3e 100644 --- a/ntfy/ntfy.go +++ b/ntfy/ntfy.go @@ -63,7 +63,7 @@ func ntfyConfigFile() (string, error) { func Notify() { cfg := NtfyConfig{} if err := cfg.Read(); err != nil { - log.Printf("error reading notification: %v", err) + log.Printf("error reading configuration: %v", err) return } req, _ := http.NewRequest("POST", cfg.URI(), strings.NewReader("New message received")) @@ -148,11 +148,25 @@ func Login(cfg *NtfyConfig, be backend.Backend) { } func (cfg *NtfyConfig) Setup() { + + // Configure using environment + if os.Getenv("PUSH_URL") != "" && os.Getenv("PUSH_TOPIC") != "" { + cfg.URL = os.Getenv("PUSH_URL") + cfg.Topic = os.Getenv("PUSH_TOPIC") + log.Printf("Current push endpoint: %s\n", cfg.URI()) + err := cfg.Save() + if err != nil { + log.Fatal(err) + } + return + } + var n string if cfg.URL != "" && cfg.Topic != "" { fmt.Printf("Current push endpoint: %s\n", cfg.URI()) n = "new " } + // Read push base URL notValid := true scanner := bufio.NewScanner(os.Stdin)