diff --git a/.gitignore b/.gitignore index 0c2ad09..fa12b58 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .env +*.swp +go.mod +go.sum +ldap_phonebook_html diff --git a/config.go b/config.go new file mode 100644 index 0000000..7725048 --- /dev/null +++ b/config.go @@ -0,0 +1,72 @@ +package main + +import ( + "html/template" + "log" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +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) + } +} diff --git a/ldap.go b/ldap.go new file mode 100644 index 0000000..e5e3ba7 --- /dev/null +++ b/ldap.go @@ -0,0 +1,125 @@ +package main + +import ( + "crypto/tls" + "fmt" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +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 +} diff --git a/main.go b/main.go index a9522d5..0f6c12d 100644 --- a/main.go +++ b/main.go @@ -20,128 +20,11 @@ Entwickelt mit Unterstützung von Claude.ai, einem KI-Assistenten von Anthropic, */ 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() @@ -166,39 +49,6 @@ func cacheRefresher() { } } -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) @@ -216,165 +66,4 @@ func handler(w http.ResponseWriter, r *http.Request) { 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) -} +} \ No newline at end of file diff --git a/models.go b/models.go new file mode 100644 index 0000000..3838ee3 --- /dev/null +++ b/models.go @@ -0,0 +1,51 @@ +package main + +import ( + "html/template" + "time" +) + +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 +) diff --git a/util.go b/util.go new file mode 100644 index 0000000..ea1c9af --- /dev/null +++ b/util.go @@ -0,0 +1,84 @@ +package main + +import ( + "strings" + "unicode" +) + +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 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) +}