Files
hikos/main.go
2025-05-22 07:04:31 +02:00

559 lines
16 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 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, // 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"))
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 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 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)
}