]> git.proxmox.com Git - proxmox.git/blame - proxmox-notify/src/endpoints/gotify.rs
notify: fix #5274: also set 'X-Gotify-Key' header for authentication
[proxmox.git] / proxmox-notify / src / endpoints / gotify.rs
CommitLineData
990fc8ef
LW
1use std::collections::HashMap;
2
990fc8ef 3use serde::{Deserialize, Serialize};
48657113 4use serde_json::json;
990fc8ef
LW
5
6use proxmox_http::client::sync::Client;
d44ce2c7 7use proxmox_http::{HttpClient, HttpOptions, ProxyConfig};
7cb339df 8use proxmox_schema::api_types::COMMENT_SCHEMA;
990fc8ef
LW
9use proxmox_schema::{api, Updater};
10
7cb339df
WB
11use crate::context::context;
12use crate::renderer::TemplateRenderer;
13use crate::schema::ENTITY_NAME_SCHEMA;
9bea76c6 14use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity};
7cb339df 15
990fc8ef
LW
16fn severity_to_priority(level: Severity) -> u32 {
17 match level {
18 Severity::Info => 1,
19 Severity::Notice => 3,
20 Severity::Warning => 5,
21 Severity::Error => 9,
5f7ac875 22 Severity::Unknown => 3,
990fc8ef
LW
23 }
24}
25
26pub(crate) const GOTIFY_TYPENAME: &str = "gotify";
27
28#[api(
29 properties: {
30 name: {
31 schema: ENTITY_NAME_SCHEMA,
32 },
33 comment: {
34 optional: true,
35 schema: COMMENT_SCHEMA,
36 },
37 }
38)]
39#[derive(Serialize, Deserialize, Updater, Default)]
40#[serde(rename_all = "kebab-case")]
41/// Config for Gotify notification endpoints
42pub struct GotifyConfig {
43 /// Name of the endpoint
44 #[updater(skip)]
45 pub name: String,
46 /// Gotify Server URL
47 pub server: String,
48 /// Comment
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub comment: Option<String>,
b421a7ca
LW
51 /// Deprecated.
52 #[serde(skip_serializing)]
53 #[updater(skip)]
ee0ab52b 54 pub filter: Option<String>,
306f4005
LW
55 /// Disable this target.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub disable: Option<bool>,
9bea76c6
LW
58 /// Origin of this config entry.
59 #[serde(skip_serializing_if = "Option::is_none")]
60 #[updater(skip)]
61 pub origin: Option<Origin>,
990fc8ef
LW
62}
63
64#[api()]
65#[derive(Serialize, Deserialize, Clone, Updater)]
66#[serde(rename_all = "kebab-case")]
67/// Private configuration for Gotify notification endpoints.
68/// This config will be saved to a separate configuration file with stricter
69/// permissions (root:root 0600)
70pub struct GotifyPrivateConfig {
71 /// Name of the endpoint
72 #[updater(skip)]
73 pub name: String,
74 /// Authentication token
75 pub token: String,
76}
77
78/// A Gotify notification endpoint.
79pub struct GotifyEndpoint {
80 pub config: GotifyConfig,
81 pub private_config: GotifyPrivateConfig,
82}
83
84#[derive(Serialize, Deserialize)]
85#[serde(rename_all = "kebab-case")]
86pub enum DeleteableGotifyProperty {
87 Comment,
306f4005 88 Disable,
990fc8ef
LW
89}
90
91impl Endpoint for GotifyEndpoint {
92 fn send(&self, notification: &Notification) -> Result<(), Error> {
df4858e9
LW
93 let (title, message) = match &notification.content {
94 Content::Template {
95 title_template,
96 body_template,
b421a7ca 97 data,
df4858e9
LW
98 } => {
99 let rendered_title =
100 renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
101 let rendered_message =
102 renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
103
104 (rendered_title, rendered_message)
105 }
5f7ac875
LW
106 #[cfg(feature = "mail-forwarder")]
107 Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()),
df4858e9 108 };
48657113
LW
109
110 // We don't have a TemplateRenderer::Markdown yet, so simply put everything
111 // in code tags. Otherwise tables etc. are not formatted properly
112 let message = format!("```\n{message}\n```");
113
114 let body = json!({
115 "title": &title,
116 "message": &message,
b421a7ca 117 "priority": severity_to_priority(notification.metadata.severity),
48657113
LW
118 "extras": {
119 "client::display": {
120 "contentType": "text/markdown"
121 }
122 }
123 });
990fc8ef
LW
124
125 let body = serde_json::to_vec(&body)
126 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
6b393ac0
LW
127 let extra_headers = HashMap::from([
128 (
129 "Authorization".into(),
130 format!("Bearer {}", self.private_config.token),
131 ),
132 ("X-Gotify-Key".into(), self.private_config.token.clone()),
133 ]);
990fc8ef 134
d44ce2c7
LW
135 let proxy_config = context()
136 .http_proxy_config()
137 .map(|url| ProxyConfig::parse_proxy_url(&url))
138 .transpose()
139 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
140
141 let options = HttpOptions {
142 proxy_config,
143 ..Default::default()
144 };
145
146 let client = Client::new(options);
147 let uri = format!("{}/message", self.config.server);
148
990fc8ef
LW
149 client
150 .post(
151 &uri,
152 Some(body.as_slice()),
153 Some("application/json"),
154 Some(&extra_headers),
155 )
156 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
157
158 Ok(())
159 }
160
161 fn name(&self) -> &str {
162 &self.config.name
163 }
306f4005
LW
164
165 /// Check if the endpoint is disabled
166 fn disabled(&self) -> bool {
167 self.config.disable.unwrap_or_default()
168 }
990fc8ef 169}