2025-03-31 17:45:12 +02:00

381 lines
10 KiB
Go

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 <https://www.gnu.org/licenses/>.
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)
}