package main import ( "bytes" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "os" "path/filepath" "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 AboutName string BirthDate string SonBirthDate string } // PageData holds data for template rendering type PageData struct { Config Title string CurrentPage string CurrentYear int ErrorMessage string SuccessMessage string FormData ContactForm AboutName string AgeYears int SonAgeYears int } // 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", "91927935"), // Replace with actual KVK number Email: getEnv("EMAIL", "info@chemistry.software"), 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 AboutName: getEnv("ABOUT_NAME", "Bdnugget"), BirthDate: getEnv("BIRTH_DATE", "1990-01-01"), SonBirthDate: getEnv("SON_BIRTH_DATE", "2022-01-01"), } // Template Funcs funcs := template.FuncMap{ "dict": func(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("dict expects even number of args") } m := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { k, ok := values[i].(string) if !ok { return nil, fmt.Errorf("dict keys must be strings") } m[k] = values[i+1] } return m, nil }, } // Parse templates with error handling templates, err := template.New("").Funcs(funcs).ParseGlob("templates/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } // Optionally parse partials if any exist if matches, _ := filepath.Glob("templates/partials/*.html"); len(matches) > 0 { if _, err := templates.ParseFiles(matches...); err != nil { log.Fatalf("Failed to parse partial templates: %v", err) } } return &Server{ config: config, templates: templates, } } // cleanupOldSubmissions periodically purges stale rate-limit entries func cleanupOldSubmissions() { for { time.Sleep(10 * time.Minute) mu.Lock() cutoff := time.Now().Add(-submissionCooldown) for ip, t := range lastSubmissionTime { if t.Before(cutoff) { delete(lastSubmissionTime, ip) } } mu.Unlock() } } // 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-migratieservice - 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 hebt 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, e-mail, 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! Ik neem 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": "MarkdownV2", } 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"}`)) } // aboutHandler handles the About Me page func (s *Server) aboutHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Over mij - "+s.config.CompanyName, "about") // Populate dynamic About fields from config/env data.AboutName = s.config.AboutName now := time.Now() // Parse BirthDate (ISO YYYY-MM-DD) if t, err := time.Parse("2006-01-02", s.config.BirthDate); err == nil { age := now.Year() - t.Year() if now.Month() < t.Month() || (now.Month() == t.Month() && now.Day() < t.Day()) { age-- } data.AgeYears = age } // Parse SonBirthDate (ISO YYYY-MM-DD) if t, err := time.Parse("2006-01-02", s.config.SonBirthDate); err == nil { sonAge := now.Year() - t.Year() if now.Month() < t.Month() || (now.Month() == t.Month() && now.Day() < t.Day()) { sonAge-- } data.SonAgeYears = sonAge } s.renderTemplate(w, "over-mij.html", data) } // dienstenHandler handles the services page func (s *Server) dienstenHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Diensten en tarieven - "+s.config.CompanyName, "diensten") s.renderTemplate(w, "diensten.html", data) } // linuxHandler handles the Linux page (distributions + features) func (s *Server) linuxHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Linux distributies en functies - "+s.config.CompanyName, "linux") s.renderTemplate(w, "linux.html", data) } // setupRoutes configures all HTTP routes func (s *Server) setupRoutes() { // Static files fs := http.FileServer(http.Dir("static/")) http.Handle("/static/", http.StripPrefix("/static/", cacheControlMiddleware(fs))) // Page routes http.HandleFunc("/", s.homeHandler) http.HandleFunc("/contact", s.contactHandler) http.HandleFunc("/over-mij", s.aboutHandler) http.HandleFunc("/diensten", s.dienstenHandler) http.HandleFunc("/linux", s.linuxHandler) http.HandleFunc("/health", s.healthHandler) } func main() { server := NewServer() server.setupRoutes() // Start background cleanup for rate limiting map go cleanupOldSubmissions() log.Printf("Server starting on %s", server.config.Port) log.Fatal(http.ListenAndServe(server.config.Port, nil)) } // cacheControlMiddleware sets Cache-Control headers for static assets func cacheControlMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".png") || strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg") || strings.HasSuffix(path, ".webp") || strings.HasSuffix(path, ".svg") || strings.HasSuffix(path, ".ico") || strings.HasSuffix(path, ".woff2"): w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") default: w.Header().Set("Cache-Control", "public, max-age=300") } next.ServeHTTP(w, r) }) }