Enable TLS by default

This commit is contained in:
2025-09-23 00:40:42 -07:00
parent ce1e878fb7
commit b59fd7dcc4
7 changed files with 176 additions and 36 deletions

130
main.go
View File

@@ -2,18 +2,25 @@ package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"crypto/x509/pkix"
"math/big"
)
// SlackWebhookPayload represents the incoming Slack webhook format
type SlackWebhookPayload struct {
Text string `json:"text"`
Username string `json:"username,omitempty"`
@@ -27,8 +34,6 @@ type SlackWebhookPayload struct {
Timestamp int64 `json:"ts,omitempty"`
} `json:"attachments,omitempty"`
}
// Config holds the middleware configuration
type Config struct {
NtfyBaseURL string
NtfyToken string
@@ -36,9 +41,9 @@ type Config struct {
NtfyPassword string
BindAddress string
BindPort string
TLSCertFile string
TLSKeyFile string
}
// Middleware handles the webhook forwarding
type Middleware struct {
config Config
client *http.Client
@@ -53,14 +58,11 @@ func NewMiddleware(config Config) *Middleware {
}
}
// extractMessage extracts the alert message from various Slack webhook formats
func (m *Middleware) extractMessage(payload *SlackWebhookPayload) string {
// Primary message text
if payload.Text != "" {
return payload.Text
}
// Check attachments for additional content
if len(payload.Attachments) > 0 {
var parts []string
@@ -81,7 +83,6 @@ func (m *Middleware) extractMessage(payload *SlackWebhookPayload) string {
return "Generic Message"
}
// forwardToNtfy sends the message to the ntfy server
func (m *Middleware) forwardToNtfy(topic, message string) error {
ntfyURL := fmt.Sprintf("%s/%s", strings.TrimRight(m.config.NtfyBaseURL, "/"), topic)
@@ -118,28 +119,24 @@ func (m *Middleware) forwardToNtfy(topic, message string) error {
return nil
}
// webhookHandler handles incoming Slack webhook requests
func (m *Middleware) webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract topic from URL path
topic := strings.TrimPrefix(r.URL.Path, "/")
if topic == "" {
http.Error(w, "Topic is required in URL path", http.StatusBadRequest)
return
}
// Clean topic (remove any path traversal attempts)
topic = path.Clean(topic)
if strings.Contains(topic, "..") {
http.Error(w, "Invalid topic name", http.StatusBadRequest)
return
}
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
@@ -149,13 +146,11 @@ func (m *Middleware) webhookHandler(w http.ResponseWriter, r *http.Request) {
var message string
// Try to parse as JSON (Slack webhook format)
var payload SlackWebhookPayload
if err := json.Unmarshal(body, &payload); err == nil {
message = m.extractMessage(&payload)
log.Printf("Received Slack webhook for topic '%s': %s", topic, message)
} else {
// Fallback to raw text
message = string(body)
if message == "" {
message = "Slack to ntfy Alert (no content)"
@@ -163,14 +158,12 @@ func (m *Middleware) webhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Received raw webhook for topic '%s': %s", topic, message)
}
// Forward to ntfy
if err := m.forwardToNtfy(topic, message); err != nil {
log.Printf("Failed to forward to ntfy: %v", err)
http.Error(w, "Failed to forward alert", http.StatusInternalServerError)
return
}
// Return success response
w.Header().Set("Content-Type", "application/json")
response := map[string]string{
"status": "success",
@@ -180,7 +173,6 @@ func (m *Middleware) webhookHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}
// healthHandler provides a health check endpoint
func (m *Middleware) healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
@@ -200,6 +192,8 @@ func loadConfig() Config {
NtfyPassword: getEnv("NTFY_PASSWORD", ""),
BindAddress: getEnv("BIND_ADDRESS", "0.0.0.0"),
BindPort: getEnv("BIND_PORT", "8080"),
TLSCertFile: getEnv("TLS_CERT_FILE", ""),
TLSKeyFile: getEnv("TLS_KEY_FILE", ""),
}
}
@@ -210,15 +204,81 @@ func getEnv(key, defaultValue string) string {
return defaultValue
}
func generateSelfSignedCert(certFile, keyFile string) error {
log.Printf("Generating self-signed TLS certificate: %s and %s", certFile, keyFile)
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("failed to generate private key: %w", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %w", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Slack to ntfy Middleware"},
CommonName: "localhost",
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
}
certDir := filepath.Dir(certFile)
if err := os.MkdirAll(certDir, 0755); err != nil {
return fmt.Errorf("failed to create certs directory %s: %w", certDir, err)
}
certOut, err := os.Create(certFile)
if err != nil {
return fmt.Errorf("failed to open cert.pem for writing: %w", err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return fmt.Errorf("failed to write data to cert.pem: %w", err)
}
if err := certOut.Close(); err != nil {
return fmt.Errorf("error closing cert.pem: %w", err)
}
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open key.pem for writing: %w", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
return fmt.Errorf("failed to write data to key.pem: %w", err)
}
if err := keyOut.Close(); err != nil {
return fmt.Errorf("error closing key.pem: %w", err)
}
log.Println("Self-signed TLS certificate generated successfully.")
return nil
}
func main() {
config := loadConfig()
middleware := NewMiddleware(config)
// Setup routes
http.HandleFunc("/health", middleware.healthHandler)
http.HandleFunc("/", middleware.webhookHandler)
// Start server
bindAddr := fmt.Sprintf("%s:%s", config.BindAddress, config.BindPort)
log.Printf("Starting Slack to ntfy middleware server")
@@ -226,7 +286,33 @@ func main() {
log.Printf("ntfy URL: %s", config.NtfyBaseURL)
log.Printf("Authentication: %t", config.NtfyToken != "" || config.NtfyUsername != "")
if err := http.ListenAndServe(bindAddr, nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
certFile := config.TLSCertFile
keyFile := config.TLSKeyFile
if certFile == "" || keyFile == "" {
log.Println("TLS_CERT_FILE/TLS_KEY_FILE are not set. Attempting to generate self-signed certificates.")
certFile = "/app/certs/generated_server.crt"
keyFile = "/app/certs/generated_server.key"
if err := generateSelfSignedCert(certFile, keyFile); err != nil {
log.Printf("Failed to generate self-signed TLS certificate: %v. Falling back to HTTP.", err)
certFile = ""
keyFile = ""
} else {
log.Printf("Using generated TLS certificates: %s and %s", certFile, keyFile)
}
}
if certFile != "" && keyFile != "" {
log.Printf("TLS enabled: using %s and %s", certFile, keyFile)
if err := http.ListenAndServeTLS(bindAddr, certFile, keyFile, nil); err != nil {
log.Fatalf("Server failed to start with TLS: %v", err)
}
} else {
log.Printf("TLS disabled")
if err := http.ListenAndServe(bindAddr, nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
}