Files
hikos/main.go
2025-05-20 11:38:54 +02:00

446 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, // jetztZeit 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 runtimeadjustable 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 URLsafe 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)
}