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 sql.NullInt64 Default_contact_write sql.NullInt64 Default_contact_delete sql.NullInt64 Default_keyword_read sql.NullInt64 Default_keyword_write sql.NullInt64 Default_keyword_delete sql.NullInt64 Default_keyword_attach sql.NullInt64 Default_keyword_detach sql.NullInt64 Default_aduser_read sql.NullInt64 Default_aduser_write sql.NullInt64 Default_aduser_delete sql.NullInt64 Default_location_read sql.NullInt64 Default_location_write sql.NullInt64 Default_location_delete sql.NullInt64 Default_department_read sql.NullInt64 Default_department_write sql.NullInt64 Default_department_delete sql.NullInt64 Self_contact_read sql.NullInt64 Self_contact_write sql.NullInt64 Self_keyword_attach sql.NullInt64 Self_keyword_detach sql.NullInt64 Private_contact_read sql.NullInt64 Private_contact_write sql.NullInt64 Private_keyword_add sql.NullInt64 Private_keyword_delete sql.NullInt64 Private_keyword_attach sql.NullInt64 Private_keyword_detach sql.NullInt64 } type ADUser struct { Id int SamAccountName string Sid string RuleSetId sql.NullInt64 } 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 sql.NullInt64 AdUserId sql.NullInt64 DisplayName string Phone string Mobile string Homeoffice string Email string Room string DepartmentId sql.NullInt64 LocationId sql.NullInt64 } type ContactKeywordLink struct { Contact sql.NullInt64 Keyword sql.NullInt64 } type Keyword struct { Id int Owner sql.NullInt64 Name string } type DataPort struct { Contacts []Contact Keywords []Keyword /*Links []ContactKeywordLink*/ } /* ################################################################## */ /* ENDE DER STRUKTUREN */ /* ################################################################## */ // ----- Example handlers ----- func (s *Server) publicHello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hallo an alle - öffentliche Daten") } func (s *Server) privateHello(w http.ResponseWriter, r *http.Request) { user := r.Context().Value(userKey).(string) fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user) } func GetDataReturnDataPort(QueryContact, QueryKeyword string) DataPort { var contList []Contact var keywordList []Keyword if QueryContact != "" { rowsContact, err := DB.Query(QueryContact) if err != nil { fmt.Println("a", err) } for rowsContact.Next() { var c Contact err = rowsContact.Scan(&c.Id, &c.OwnerId, &c.AdUserId, &c.DisplayName, &c.Phone, &c.Mobile, &c.Homeoffice, &c.Email, &c.Room, &c.DepartmentId, &c.LocationId) if err != nil { fmt.Println("b", err) } contList = append(contList, c) } } if QueryKeyword != "" { rowsKeyword, err := DB.Query(QueryKeyword) if err != nil { fmt.Println("c", err) } for rowsKeyword.Next() { var c0 Keyword err = rowsKeyword.Scan(&c0.Id, &c0.Owner, &c0.Name) if err != nil { fmt.Println("d", err) } keywordList = append(keywordList, c0) } } return DataPort{Contacts: contList, Keywords: keywordList} } func (s *Server) ListPublic(w http.ResponseWriter, r *http.Request) { D := GetDataReturnDataPort("SELECT * FROM contact c WHERE c.contact_owner_id = -1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;") funcs := template.FuncMap{"now": time.Now} templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates") 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")) tplFull.ExecuteTemplate(w, "layout", D) /*user := r.Context().Value(userKey).(string) fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user)*/ } func (s *Server) ListPrivate(w http.ResponseWriter, r *http.Request) { D := GetDataReturnDataPort("SELECT * FROM contact c WHERE c.contact_owner_id = -1 OR c.contact_owner_id = 1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1 OR c.keyword_owner = 1;") funcs := template.FuncMap{"now": time.Now} templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates") 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")) tplFull.ExecuteTemplate(w, "layout", D) /*user := r.Context().Value(userKey).(string) fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user)*/ } var CFG Config var DB *sql.DB 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) } DB = db 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")) layoutSSO := template.Must(template.New("sso").Funcs(funcs).ParseFiles(templatesDir + "/login.html")) mux := http.NewServeMux() mux.HandleFunc("/login", srv.loginHandler) //mux.Handle("/protected", srv.withAuth(http.HandlerFunc(srv.protectedHandler))) mux.HandleFunc("/sso", func(w http.ResponseWriter, r *http.Request) { layoutSSO.ExecuteTemplate(w, "sso", nil) }) mux.Handle("/hello", srv.authAware(true, http.HandlerFunc(srv.publicHello), http.HandlerFunc(srv.privateHello))) // Handler für / mux.Handle("/", srv.authAware(false, http.HandlerFunc(srv.ListPublic), http.HandlerFunc(srv.ListPrivate))) /*mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { tplFull.ExecuteTemplate(w, "layout", nil) })*/ mux.HandleFunc("/htmx/contact", func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } sparam := strings.TrimSpace(r.Form.Get("search")) fmt.Println("All_OK", sparam) sqlq := "SELECT * FROM contact c WHERE c.contact_displayname LIKE '%" + sparam + "%' OR c.contact_phone LIKE '%" + sparam + "%' OR c.contact_mobile LIKE '%" + sparam + "%' OR c.contact_homeoffice LIKE '%" + sparam + "%';" D := GetDataReturnDataPort(sqlq, "") tplKontakt.ExecuteTemplate(w, "kontakt", D) }) mux.HandleFunc("/htmx/kontaktbyschlagwort", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/department", func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } sparam := strings.TrimSpace(r.Form.Get("search")) fmt.Println("Department_OK", sparam) sqlq := "SELECT c.* FROM contact c JOIN department d ON c.contact_department_id = d.department_id WHERE d.department_name LIKE '%" + sparam + "%';" D := GetDataReturnDataPort(sqlq, "") tplKontakt.ExecuteTemplate(w, "kontakt", D) }) mux.HandleFunc("/htmx/room", func(w http.ResponseWriter, r *http.Request) { tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) mux.HandleFunc("/htmx/location", 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 1 == 2 { if err := s.auth.Authenticate(user, pass); err != nil { http.Error(w, "invalid credentials", http.StatusUnauthorized) return } } else { if user == "admin" && pass == "admin" { } else { 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}) http.Redirect(w, r, "/", http.StatusMovedPermanently) } 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) }