package main import ( "bytes" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "os" "strings" "sync" "time" ) // Configuration holds all website configuration type Config struct { CompanyName string KVK string Email string Phone string Street string PostalCode string Village string Domain string Port string TelegramBotToken string TelegramChatID string } // PageData holds data for template rendering type PageData struct { Config Title string CurrentPage string CurrentYear int ErrorMessage string SuccessMessage string FormData ContactForm } // ContactForm holds form data type ContactForm struct { Name string Email string Phone string Computer string Service string Message string } // Server holds the application state type Server struct { config Config templates *template.Template } // Rate limiting var ( mu sync.Mutex lastSubmissionTime = make(map[string]time.Time) submissionCooldown = 5 * time.Minute ) // getEnv returns environment variable or default value func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // NewServer creates a new server instance func NewServer() *Server { // Configuration - can be overridden with environment variables config := Config{ CompanyName: getEnv("COMPANY_NAME", "Hogeland Linux"), KVK: getEnv("KVK", "12345678"), // Replace with actual KVK number Email: getEnv("EMAIL", "info@hogelandlinux.nl"), Phone: getEnv("PHONE", "+31 6 12345678"), Street: getEnv("STREET", "Voorstraat 123"), PostalCode: getEnv("POSTAL_CODE", "9967 AA"), Village: getEnv("VILLAGE", "Eenrum"), Domain: getEnv("DOMAIN", "hogelandlinux.nl"), // Replace with actual domain Port: ":" + getEnv("PORT", "8080"), TelegramBotToken: getEnv("TELEGRAM_BOT_TOKEN", ""), // Set this in environment TelegramChatID: getEnv("TELEGRAM_CHAT_ID", ""), // Set this in environment } // Parse templates with error handling templates, err := template.ParseGlob("templates/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } return &Server{ config: config, templates: templates, } } // createPageData creates PageData with the given title and current page func (s *Server) createPageData(title, currentPage string) PageData { return PageData{ Config: s.config, Title: title, CurrentPage: currentPage, CurrentYear: time.Now().Year(), } } // renderTemplate renders a template with error handling func (s *Server) renderTemplate(w http.ResponseWriter, templateName string, data PageData) { if err := s.templates.ExecuteTemplate(w, templateName, data); err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } // homeHandler handles the home page func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Linux Migratie Service - Uw Computer Nieuw Leven Geven", "home") s.renderTemplate(w, "index.html", data) } // contactHandler handles the contact page func (s *Server) contactHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Contact - "+s.config.CompanyName, "contact") if r.Method == "POST" { s.handleContactForm(w, r, &data) return } s.renderTemplate(w, "contact.html", data) } // handleContactForm processes the contact form submission func (s *Server) handleContactForm(w http.ResponseWriter, r *http.Request, data *PageData) { // Get client IP for rate limiting ip := getClientIP(r) // Check rate limiting mu.Lock() if lastTime, exists := lastSubmissionTime[ip]; exists { if time.Since(lastTime) < submissionCooldown { mu.Unlock() data.ErrorMessage = "U heeft recent al een bericht verstuurd. Probeer het over een paar minuten opnieuw." s.renderTemplate(w, "contact.html", *data) return } } mu.Unlock() // Parse form data err := r.ParseForm() if err != nil { log.Printf("Error parsing form: %v", err) data.ErrorMessage = "Er is een fout opgetreden bij het verwerken van uw bericht." s.renderTemplate(w, "contact.html", *data) return } // Extract form data form := ContactForm{ Name: strings.TrimSpace(r.FormValue("name")), Email: strings.TrimSpace(r.FormValue("email")), Phone: strings.TrimSpace(r.FormValue("phone")), Computer: strings.TrimSpace(r.FormValue("computer")), Service: strings.TrimSpace(r.FormValue("service")), Message: strings.TrimSpace(r.FormValue("message")), } // Store form data for re-rendering on error data.FormData = form // Validate required fields if form.Name == "" || form.Email == "" || form.Message == "" { data.ErrorMessage = "Vul alle verplichte velden in (naam, email, bericht)." s.renderTemplate(w, "contact.html", *data) return } // Send to Telegram if configured if s.config.TelegramBotToken != "" && s.config.TelegramChatID != "" { err := s.sendToTelegram(form) if err != nil { log.Printf("Error sending message to Telegram: %v", err) data.ErrorMessage = "Er is een fout opgetreden bij het versturen van uw bericht. Probeer het later opnieuw." s.renderTemplate(w, "contact.html", *data) return } } // Update last submission time on success mu.Lock() lastSubmissionTime[ip] = time.Now() mu.Unlock() // On success, render success message data.SuccessMessage = "Bedankt voor uw bericht! Wij nemen zo snel mogelijk contact met u op." data.FormData = ContactForm{} // Clear form data s.renderTemplate(w, "contact.html", *data) } // sendToTelegram sends the contact form data to Telegram func (s *Server) sendToTelegram(form ContactForm) error { // Format message message := fmt.Sprintf( "🔔 Nieuw contactformulier\n\n"+ "👤 *Naam:* %s\n"+ "📧 *Email:* %s\n"+ "📞 *Telefoon:* %s\n"+ "💻 *Computer:* %s\n"+ "🛠️ *Service:* %s\n\n"+ "💬 *Bericht:*\n%s", escapeMarkdown(form.Name), escapeMarkdown(form.Email), escapeMarkdown(form.Phone), escapeMarkdown(form.Computer), escapeMarkdown(form.Service), escapeMarkdown(form.Message), ) // Prepare Telegram API request telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", s.config.TelegramBotToken) payload := map[string]interface{}{ "chat_id": s.config.TelegramChatID, "text": message, "parse_mode": "Markdown", } jsonData, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal JSON: %v", err) } // Send HTTP request resp, err := http.Post(telegramURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to send HTTP request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("Telegram API error: %s, Status Code: %d", string(bodyBytes), resp.StatusCode) } return nil } // escapeMarkdown escapes special characters for Telegram Markdown func escapeMarkdown(text string) string { if text == "" { return "N/A" } // Escape special Markdown characters replacer := strings.NewReplacer( "*", "\\*", "_", "\\_", "`", "\\`", "[", "\\[", "]", "\\]", "(", "\\(", ")", "\\)", "~", "\\~", ">", "\\>", "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!", ) return replacer.Replace(text) } // getClientIP extracts the client IP address from the request func getClientIP(r *http.Request) string { // Check X-Forwarded-For header first (for reverse proxies) if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // X-Forwarded-For can contain multiple IPs, take the first one if idx := strings.Index(xff, ","); idx != -1 { return strings.TrimSpace(xff[:idx]) } return strings.TrimSpace(xff) } // Check X-Real-IP header if xri := r.Header.Get("X-Real-IP"); xri != "" { return strings.TrimSpace(xri) } // Fall back to RemoteAddr if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { return r.RemoteAddr[:idx] } return r.RemoteAddr } // healthHandler provides a simple health check endpoint func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"healthy","service":"linuxservice"}`)) } // setupRoutes configures all HTTP routes func (s *Server) setupRoutes() { // Static files fs := http.FileServer(http.Dir("static/")) http.Handle("/static/", http.StripPrefix("/static/", fs)) // Page routes http.HandleFunc("/", s.homeHandler) http.HandleFunc("/contact", s.contactHandler) http.HandleFunc("/health", s.healthHandler) } func main() { server := NewServer() server.setupRoutes() log.Printf("Server starting on %s", server.config.Port) log.Fatal(http.ListenAndServe(server.config.Port, nil)) }