package main import ( "context" "crypto/rand" "crypto/tls" "database/sql" "encoding/base64" "errors" "fmt" "html/template" "log" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "github.com/go-ldap/ldap/v3" _ "github.com/go-sql-driver/mysql" ) func getenv(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func enabled(k string, def bool) bool { b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k))) if err != nil { return def } return b } func cacheControl(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") next.ServeHTTP(w, r) }) } /* ################################################################## */ /* START DER STRUKTUREN */ /* ################################################################## */ type RuleSet struct { Id int Name string Default_contact_read int Default_contact_write int Default_contact_delete int Default_keyword_read int Default_keyword_write int Default_keyword_delete int Default_keyword_attach int Default_keyword_detach int Default_aduser_read int Default_aduser_write int Default_aduser_delete int Default_location_read int Default_location_write int Default_location_delete int Default_department_read int Default_department_write int Default_department_delete int Self_contact_read int Self_contact_write int Self_keyword_attach int Self_keyword_detach int Private_contact_read int Private_contact_write int Private_keyword_add int Private_keyword_delete int Private_keyword_attach int Private_keyword_detach int } type ADUser struct { Id int SamAccountName string Sid string RuleSetId RuleSet } type Department struct { Id int Name string } type Location struct { Id int Name string Address string Zip string City string } type Contact struct { Id int OwnerId int AdUserId ADUser DisplayName string Phone string Mobile string Homeoffice string Email string Room string DepartmentId Department LocationId Location } type ContactKeywordLink struct { Contact int Keyword int } /* ################################################################## */ /* ENDE DER STRUKTUREN */ /* ################################################################## */ func main() { // Signal-Kanal einrichten stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) // Goroutine, die auf Signale wartet go func() { <-stop fmt.Println("Stop Sign...") prepareExit() os.Exit(0) }() // --- Verzeichnisse konfigurierbar machen ------------------------- staticDir := getenv("BLOG_STATIC_DIR", "./static") templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates") cfg := Config{ DSN: "hikos:hikos@tcp(10.10.5.31:3306)/hikos?parseTime=true", LDAPURL: "ldaps://ldaps.example.com:636", LDAPBindPattern: "uid=%s,ou=users,dc=example,dc=com", SessionTTL: 24 * time.Hour, } db, err := sql.Open("mysql", cfg.DSN) if err != nil { log.Fatal(err) } if err := db.Ping(); err != nil { log.Fatal(err) } store := &SessionStore{DB: db} auth := &LDAPAuthenticator{ URL: cfg.LDAPURL, BindPattern: cfg.LDAPBindPattern, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, } srv := &Server{ cfg: cfg, sessions: store, auth: auth, } funcs := template.FuncMap{ "now": time.Now, // jetzt‑Zeit bereitstellen } // Basislayout zuerst parsen layout := template.Must(template.New("base").Funcs(funcs).ParseFiles(templatesDir + "/base.html")) tplFull := template.Must(layout.Clone()) template.Must(tplFull.Funcs(funcs).ParseFiles(templatesDir+"/kontaktliste.html", templatesDir+"/schlagwortliste.html")) tplKontakt := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html")) tplSchlagwort := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html")) mux := http.NewServeMux() mux.HandleFunc("/login", srv.loginHandler) mux.Handle("/protected", srv.withAuth(http.HandlerFunc(srv.protectedHandler))) // Handler für / mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { tplFull.ExecuteTemplate(w, "layout", nil) }) mux.HandleFunc("/htmx/kontakt", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/kontaktbyschlagwort", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/amt", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/raum", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/gebaeude", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/schlagwort", func(w http.ResponseWriter, r *http.Request) { tplSchlagwort.ExecuteTemplate(w, "schlagwort", nil) }) mux.HandleFunc("/htmx/schlagwortbykontakt", func(w http.ResponseWriter, r *http.Request) { tplSchlagwort.ExecuteTemplate(w, "schlagwort", nil) }) mux.Handle("/static/", cacheControl(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))) mux.HandleFunc("/store", func(w http.ResponseWriter, r *http.Request) { }) server := &http.Server{ /*Addr: ":8443",*/ Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, } log.Println("Listening on https://localhost:8080 …") /*if err := server.ListenAndServeTLS("cert.pem", "key.pem"); err != nil { StopServer(err) }*/ if err := server.ListenAndServe(); err != nil { StopServer(err) } //StopServer(http.ListenAndServe(":8080", mux)) } func prepareExit() { fmt.Println("~", "Running exit tasks...") /*if err := SaveTickCatalog(getenv("BLOG_TICKS_DIR", "/ticks") + "/ticks.json"); err != nil { fmt.Println("Fehler beim Speichern:", err) } fmt.Println("Geladener Katalog:", TickCatalog)*/ fmt.Println("~", "Exit completed.") } func StopServer(e error) { fmt.Println("~", "Stopping server...") prepareExit() fmt.Println("~", "Server stopped!") } /* ################################################################## */ /* START LDAP UND GESICHERTE SEITEN */ /* ################################################################## */ // Config bundles all runtime‑adjustable settings. type Config struct { DSN string // MySQL DSN → "user:pass@tcp(127.0.0.1:3306)/app?parseTime=true" LDAPURL string // full URL → "ldaps://ldap.example.com:636" LDAPBindPattern string // DN pattern → "uid=%s,ou=users,dc=example,dc=com" SessionTTL time.Duration // session validity } type contextKey string const userKey contextKey = "username" // ---- SESSION STORE ---- type SessionStore struct { DB *sql.DB } // generateRandomToken returns a URL‑safe base64 random string. func generateRandomToken(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil } func (s *SessionStore) Create(username string, ttl time.Duration) (string, error) { token, err := generateRandomToken(32) if err != nil { return "", err } expires := time.Now().Add(ttl) _, err = s.DB.Exec(`INSERT INTO sessions (username, token, expires_at) VALUES (?, ?, ?)`, username, token, expires) return token, err } func (s *SessionStore) GetUsername(token string) (string, error) { var username string var expires time.Time err := s.DB.QueryRow(`SELECT username, expires_at FROM sessions WHERE token = ?`, token).Scan(&username, &expires) if err != nil { return "", err } if time.Now().After(expires) { s.Delete(token) return "", errors.New("session expired") } return username, nil } func (s *SessionStore) Delete(token string) { s.DB.Exec(`DELETE FROM sessions WHERE token = ?`, token) } // ---- LDAP AUTHENTICATOR ---- type LDAPAuthenticator struct { URL string BindPattern string TLSConfig *tls.Config } func (a *LDAPAuthenticator) Authenticate(username, password string) error { if password == "" { return errors.New("leer Passwort") } conn, err := ldap.DialURL(a.URL, ldap.DialWithTLSConfig(a.TLSConfig)) if err != nil { return fmt.Errorf("ldap dial: %w", err) } defer conn.Close() dn := fmt.Sprintf(a.BindPattern, ldap.EscapeFilter(username)) if err := conn.Bind(dn, password); err != nil { return fmt.Errorf("ldap bind: %w", err) } return nil } // ---- HTTP SERVER ---- type Server struct { cfg Config sessions *SessionStore auth *LDAPAuthenticator } func (s *Server) loginHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } user := strings.TrimSpace(r.Form.Get("username")) pass := r.Form.Get("password") if err := s.auth.Authenticate(user, pass); err != nil { http.Error(w, "invalid credentials", http.StatusUnauthorized) return } token, err := s.sessions.Create(user, s.cfg.SessionTTL) if err != nil { log.Println("cannot create session:", err) http.Error(w, "internal error", http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "session_token", Value: token, Expires: time.Now().Add(s.cfg.SessionTTL), Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) fmt.Fprintln(w, "ok") } func (s *Server) withAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie("session_token") if err != nil { http.Error(w, "unauthenticated", http.StatusUnauthorized) return } user, err := s.sessions.GetUsername(c.Value) if err != nil { http.Error(w, "unauthenticated", http.StatusUnauthorized) http.Redirect(w, r, "/login", http.StatusMovedPermanently) return } ctx := context.WithValue(r.Context(), userKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } // authAware builds a handler that can treat auth as optional or mandatory. // // required == true → redirect to /login if not authenticated. // required == false → choose between unauth (A) and auth (B) branch. func (s *Server) authAware(required bool, unauth, auth http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var username string if c, err := r.Cookie("session_token"); err == nil { if u, err := s.sessions.GetUsername(c.Value); err == nil { username = u } } isAuth := username != "" if isAuth { r = r.WithContext(context.WithValue(r.Context(), userKey, username)) } if required { if !isAuth { http.Redirect(w, r, "/login", http.StatusFound) return } auth.ServeHTTP(w, r) // C return } if isAuth { auth.ServeHTTP(w, r) // B } else { unauth.ServeHTTP(w, r) // A } }) } func (s *Server) protectedHandler(w http.ResponseWriter, r *http.Request) { user := r.Context().Value(userKey).(string) fmt.Fprintf(w, "Hallo %s, du bist authentifiziert!", user) }