package main /* Copyright (C) 2023 Steffen Probst This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Entwickelt mit Unterstützung von Claude.ai, einem KI-Assistenten von Anthropic, PBC. */ import ( "crypto/tls" "fmt" "html/template" "log" "net/http" "os" "path/filepath" "strings" "time" "unicode" "github.com/go-ldap/ldap/v3" "gopkg.in/yaml.v2" ) type Person struct { FirstName string LastName string Email string Department string Phones []Phone RawPhoneNumber string InternalPhone string IsContact bool // Neues Feld zur Unterscheidung zwischen Benutzern und Kontakten } type Phone struct { PhoneNumber string Type string } type Config struct { PhoneRules struct { Country struct { Prefix string `yaml:"prefix"` AreaCodes []string `yaml:"area_codes"` InternalPrefix string `yaml:"internal_prefix"` } `yaml:"country"` InvalidNumber string `yaml:"invalid_number"` } `yaml:"phone_rules"` } type LDAPConfig struct { Server string Port string BindDN string BindPassword string BaseDN string Filter string } var ( cachedData []Person lastUpdate time.Time tmpl *template.Template ldapConfig LDAPConfig serverPort string config Config ) func init() { loadEnvConfig() loadYAMLConfig() loadTemplate() } func loadEnvConfig() { requiredEnvVars := []string{ "LDAP_SERVER", "LDAP_PORT", "LDAP_BIND_DN", "LDAP_BIND_PASSWORD", "LDAP_BASE_DN", "LDAP_FILTER", "SERVER_PORT", } for _, envVar := range requiredEnvVars { if value := os.Getenv(envVar); value == "" { log.Fatalf("Required environment variable %s is not set", envVar) } } ldapConfig = LDAPConfig{ Server: os.Getenv("LDAP_SERVER"), Port: os.Getenv("LDAP_PORT"), BindDN: os.Getenv("LDAP_BIND_DN"), BindPassword: os.Getenv("LDAP_BIND_PASSWORD"), BaseDN: os.Getenv("LDAP_BASE_DN"), Filter: os.Getenv("LDAP_FILTER"), } serverPort = os.Getenv("SERVER_PORT") } func loadYAMLConfig() { configPath := filepath.Join("static", "config.yaml") configFile, err := os.ReadFile(configPath) if err != nil { log.Fatalf("Failed to read config file: %v", err) } err = yaml.Unmarshal(configFile, &config) if err != nil { log.Fatalf("Failed to parse config file: %v", err) } validateConfig() } func validateConfig() { if config.PhoneRules.Country.Prefix == "" || len(config.PhoneRules.Country.AreaCodes) == 0 || config.PhoneRules.Country.InternalPrefix == "" || config.PhoneRules.InvalidNumber == "" { log.Fatalf("Missing required configuration in config.yaml") } } func loadTemplate() { var err error tmpl, err = template.ParseFiles(filepath.Join("static", "index.html")) if err != nil { log.Fatalf("Failed to parse template: %v", err) } } func main() { go cacheRefresher() http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) http.HandleFunc("/", handler) log.Printf("Server starting on port %s", serverPort) log.Fatal(http.ListenAndServe(":"+serverPort, nil)) } func cacheRefresher() { for { newData, err := fetchDataFromLDAP() if err != nil { log.Printf("Error refreshing cache: %v", err) } else if !dataEqual(cachedData, newData) { cachedData = newData lastUpdate = time.Now() log.Println("Cache updated") } time.Sleep(5 * time.Minute) } } func dataEqual(a, b []Person) bool { if len(a) != len(b) { return false } for i := range a { if !personEqual(a[i], b[i]) { return false } } return true } func personEqual(a, b Person) bool { return a.FirstName == b.FirstName && a.LastName == b.LastName && a.Email == b.Email && a.InternalPhone == b.InternalPhone && a.Department == b.Department && phonesEqual(a.Phones, b.Phones) } func phonesEqual(a, b []Phone) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func handler(w http.ResponseWriter, r *http.Request) { log.Printf("Received request for: %s", r.URL.Path) if time.Since(lastUpdate) > 5*time.Minute { newData, err := fetchDataFromLDAP() if err != nil { log.Printf("Error refreshing cache: %v", err) } else { cachedData = newData lastUpdate = time.Now() } } err := tmpl.Execute(w, cachedData) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func fetchDataFromLDAP() ([]Person, error) { l, err := ldap.DialTLS("tcp", ldapConfig.Server+":"+ldapConfig.Port, &tls.Config{InsecureSkipVerify: true}) if err != nil { return nil, fmt.Errorf("failed to connect to LDAP server: %v", err) } defer l.Close() err = l.Bind(ldapConfig.BindDN, ldapConfig.BindPassword) if err != nil { return nil, fmt.Errorf("failed to bind to LDAP server: %v", err) } // Erweitere den Filter, um sowohl Benutzer als auch Kontakte einzuschließen combinedFilter := fmt.Sprintf("(|(objectClass=user)(objectClass=contact)%s)", ldapConfig.Filter) searchRequest := ldap.NewSearchRequest( ldapConfig.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, combinedFilter, []string{"objectClass", "givenName", "sn", "mail", "telephoneNumber", "mobile", "otherTelephone", "physicalDeliveryOfficeName"}, nil, ) sr, err := l.Search(searchRequest) if err != nil { return nil, fmt.Errorf("failed to search LDAP server: %v", err) } var people []Person for _, entry := range sr.Entries { if !isValidEntry(entry) { continue } isContact := isContactObject(entry) person := Person{ FirstName: entry.GetAttributeValue("givenName"), LastName: entry.GetAttributeValue("sn"), Email: entry.GetAttributeValue("mail"), Department: entry.GetAttributeValue("physicalDeliveryOfficeName"), IsContact: isContact, } officePhone := entry.GetAttributeValue("telephoneNumber") mobilePhone := entry.GetAttributeValue("mobile") // Normalisiere die Telefonnummern für den Vergleich normalizedOffice := normalizePhoneNumber(officePhone) normalizedMobile := normalizePhoneNumber(mobilePhone) if normalizedOffice == normalizedMobile && mobilePhone != "" { formattedMobile := formatPhoneNumber(mobilePhone) person.Phones = append(person.Phones, Phone{PhoneNumber: formattedMobile, Type: "Mobil"}) person.RawPhoneNumber = formattedMobile person.InternalPhone = "" // Keine interne Rufnummer für Mobiltelefone } else if officePhone != "" { formattedPhone := formatPhoneNumber(officePhone) person.Phones = append(person.Phones, Phone{PhoneNumber: formattedPhone, Type: "Office"}) person.InternalPhone = extractInternalNumber(officePhone) person.RawPhoneNumber = formattedPhone if mobilePhone != "" && normalizedOffice != normalizedMobile { formattedMobile := formatPhoneNumber(mobilePhone) person.Phones = append(person.Phones, Phone{PhoneNumber: formattedMobile, Type: "Mobil"}) } } else if mobilePhone != "" { formattedMobile := formatPhoneNumber(mobilePhone) person.Phones = append(person.Phones, Phone{PhoneNumber: formattedMobile, Type: "Mobil"}) person.RawPhoneNumber = formattedMobile person.InternalPhone = "" // Keine interne Rufnummer für Mobiltelefone } for _, otherPhone := range entry.GetAttributeValues("otherTelephone") { person.Phones = append(person.Phones, Phone{PhoneNumber: formatPhoneNumber(otherPhone), Type: "Other"}) } if (len(person.Phones) > 0 && person.Email != "") || isContact { people = append(people, person) } } return people, nil } func isValidEntry(entry *ldap.Entry) bool { firstName := entry.GetAttributeValue("givenName") lastName := entry.GetAttributeValue("sn") telephoneNumber := entry.GetAttributeValue("telephoneNumber") mobile := entry.GetAttributeValue("mobile") // Für Kontakte erlauben wir auch Einträge ohne Telefonnummer isContact := isContactObject(entry) return (firstName != "" && lastName != "") && (isContact || telephoneNumber != config.PhoneRules.InvalidNumber || mobile != "") } func isContactObject(entry *ldap.Entry) bool { objectClasses := entry.GetAttributeValues("objectClass") for _, class := range objectClasses { if strings.ToLower(class) == "contact" { return true } } return false } func isValidContact(entry *ldap.Entry) bool { firstName := entry.GetAttributeValue("givenName") lastName := entry.GetAttributeValue("sn") telephoneNumber := entry.GetAttributeValue("telephoneNumber") return firstName != "" && lastName != "" && telephoneNumber != config.PhoneRules.InvalidNumber } func formatPhoneNumber(number string) string { // Entferne alle Nicht-Ziffern digits := strings.Map(func(r rune) rune { if unicode.IsDigit(r) { return r } return -1 }, number) // Entferne führende Nullen digits = strings.TrimLeft(digits, "0") // Entferne den Länderprefix, falls vorhanden countryPrefix := config.PhoneRules.Country.Prefix if strings.HasPrefix(digits, countryPrefix) { digits = digits[len(countryPrefix):] } // Füge eine führende "0" hinzu, wenn nicht vorhanden if !strings.HasPrefix(digits, "0") { digits = "0" + digits } return digits } func extractInternalNumber(phoneNumber string) string { digits := formatPhoneNumber(phoneNumber) for _, areaCode := range config.PhoneRules.Country.AreaCodes { if strings.HasPrefix(digits[1:], areaCode) { // Ignoriere die führende 0 remaining := digits[1+len(areaCode):] if strings.HasPrefix(remaining, config.PhoneRules.Country.InternalPrefix) { return remaining[len(config.PhoneRules.Country.InternalPrefix):] } } } return "" } func normalizePhoneNumber(number string) string { return formatPhoneNumber(number) }