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. */ package main import ( "bytes" "crypto/md5" "crypto/tls" "encoding/xml" "encoding/csv" "fmt" "io" "log" "net/http" "os" "strings" "sync" "time" "github.com/go-ldap/ldap/v3" "golang.org/x/text/encoding/charmap" "golang.org/x/text/transform" ) type Config struct { LDAPServer string BindDN string BindPassword string SearchBase string ServerPort string } type AddressBook struct { XMLName xml.Name `xml:"AddressBook"` Version string `xml:"version,attr"` PBGroups []PBGroup `xml:"pbgroup"` Contacts []Contact `xml:"Contact"` } type PBGroup struct { ID int `xml:"id,attr"` Name string `xml:"name"` } type Contact struct { FirstName string `xml:"FirstName"` LastName string `xml:"LastName"` Frequent int `xml:"Frequent"` Phones []Phone `xml:"Phone"` Department string `xml:"Department,omitempty"` Group int `xml:"Group"` } type Phone struct { Type string `xml:"type,attr"` PhoneNumber string `xml:"phonenumber"` AccountIndex int `xml:"accountindex"` } type YealinkPhoneBook struct { XMLName xml.Name `xml:"YealinkIPPhoneDirectory"` Entries []YealinkEntry `xml:"DirectoryEntry"` } type YealinkEntry struct { Name string `xml:"Name"` Telephone string `xml:"Telephone"` Mobile string `xml:"Mobile,omitempty"` OtherMobile string `xml:"Other,omitempty"` } // Neue Struktur für das Contact-Format type ContactXML struct { XMLName xml.Name `xml:"contacts"` Contacts []ContactEntry `xml:"contact"` } type ContactEntry struct { Name string `xml:"name,attr"` Number string `xml:"number,attr"` FirstName string `xml:"firstname,attr"` LastName string `xml:"lastname,attr"` Phone string `xml:"phone,attr"` Mobile string `xml:"mobile,attr"` Email string `xml:"email,attr"` Address string `xml:"address,attr"` City string `xml:"city,attr"` State string `xml:"state,attr"` Zip string `xml:"zip,attr"` Comment string `xml:"comment,attr"` ID string `xml:"id,attr"` Info string `xml:"info,attr"` Presence string `xml:"presence,attr"` Starred string `xml:"starred,attr"` Directory string `xml:"directory,attr"` } var ( err error config Config cache struct { grandstreamFile *os.File yealinkFile *os.File contactFile *os.File hash []byte lastUpdate time.Time mutex sync.RWMutex } ) func main() { config = Config{ LDAPServer: os.Getenv("LDAP_SERVER"), BindDN: os.Getenv("BIND_DN"), BindPassword: os.Getenv("BIND_PASSWORD"), SearchBase: os.Getenv("SEARCH_BASE"), ServerPort: os.Getenv("SERVER_PORT"), } if config.ServerPort == "" { config.ServerPort = "8080" } // Initialisiere den Cache vor dem Start des Servers err = initializeCache() if err != nil { log.Fatalf("Fehler beim Initialisieren des Caches: %v", err) } http.HandleFunc("/phonebook.xml", grandstreamPhoneBookHandler) http.HandleFunc("/yphonebook.xml", yealinkPhoneBookHandler) http.HandleFunc("/Contacts.xml", contactXMLHandler) http.HandleFunc("/phonebook.csv", csvExportHandler) log.Printf("Server starting on port %s", config.ServerPort) log.Fatal(http.ListenAndServe(":"+config.ServerPort, nil)) } func grandstreamPhoneBookHandler(w http.ResponseWriter, r *http.Request) { servePhoneBook(w, r, cache.grandstreamFile) } func yealinkPhoneBookHandler(w http.ResponseWriter, r *http.Request) { servePhoneBook(w, r, cache.yealinkFile) } func contactXMLHandler(w http.ResponseWriter, r *http.Request) { servePhoneBook(w, r, cache.contactFile) } func csvExportHandler(w http.ResponseWriter, r *http.Request) { err := updateCachedXMLIfNeeded() if err != nil { http.Error(w, "Fehler beim Aktualisieren der Kontakte", http.StatusInternalServerError) return } contacts, err := getADContacts() if err != nil { http.Error(w, "Fehler beim Abrufen der Kontakte", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/csv; charset=windows-1252") w.Header().Set("Content-Disposition", "attachment; filename=phonebook.csv") if err := exportToCSV(contacts, w); err != nil { http.Error(w, "Fehler beim Exportieren als CSV", http.StatusInternalServerError) return } } func servePhoneBook(w http.ResponseWriter, r *http.Request, file *os.File) { err := updateCachedXMLIfNeeded() if err != nil { http.Error(w, "Fehler beim Aktualisieren der Kontakte", http.StatusInternalServerError) return } cache.mutex.RLock() defer cache.mutex.RUnlock() w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) _, err = file.Seek(0, 0) if err != nil { log.Printf("Fehler beim Zurücksetzen des Datei-Offsets: %v", err) http.Error(w, "Interner Serverfehler", http.StatusInternalServerError) return } _, err = io.Copy(w, file) if err != nil { log.Printf("Fehler beim Kopieren der XML-Datei: %v", err) } } func initializeCache() error { log.Println("Initialisiere Cache...") // Führen Sie updateCachedXMLIfNeeded aus, um die Caching-Dateien zu erstellen err := updateCachedXMLIfNeeded() if err != nil { return fmt.Errorf("Fehler beim Initialisieren des Caches: %v", err) } // Überprüfen Sie, ob alle erforderlichen Dateien erstellt wurden if cache.grandstreamFile == nil || cache.yealinkFile == nil || cache.contactFile == nil { return fmt.Errorf("Nicht alle erforderlichen Cache-Dateien wurden erstellt") } log.Println("Cache erfolgreich initialisiert") return nil } func updateCachedXMLIfNeeded() error { cache.mutex.Lock() defer cache.mutex.Unlock() contacts, err := getADContacts() if err != nil { return err } newHash := calculateHash(contacts) if cache.grandstreamFile != nil && cache.yealinkFile != nil && cache.contactFile != nil && bytes.Equal(newHash, cache.hash) { return nil } addressBook := AddressBook{ Version: "1.0", PBGroups: []PBGroup{ {ID: 1, Name: "Blacklist"}, {ID: 2, Name: "Work"}, {ID: 3, Name: "Friend"}, }, Contacts: contacts, } yealinkPhoneBook := YealinkPhoneBook{ Entries: make([]YealinkEntry, len(contacts)), } for i, c := range contacts { yealinkPhoneBook.Entries[i] = YealinkEntry{ Name: c.FirstName + " " + c.LastName, Telephone: getPhoneByType(c.Phones, "Work"), Mobile: getPhoneByType(c.Phones, "Mobile"), OtherMobile: getPhoneByType(c.Phones, "Home"), } } // Erstellen der Contacts.xml contactXML := ContactXML{ Contacts: make([]ContactEntry, len(contacts)), } for i, c := range contacts { contactXML.Contacts[i] = ContactEntry{ Name: c.FirstName + " " + c.LastName, Number: getPhoneByType(c.Phones, "Work"), FirstName: c.FirstName, LastName: c.LastName, Phone: getPhoneByType(c.Phones, "Work"), Mobile: getPhoneByType(c.Phones, "Mobile"), Presence: "0", Starred: "0", Directory: "0", } } if err := saveToTempFile(&addressBook, &cache.grandstreamFile); err != nil { return fmt.Errorf("Fehler beim Speichern der Grandstream-XML: %v", err) } if err := saveToTempFile(&yealinkPhoneBook, &cache.yealinkFile); err != nil { return fmt.Errorf("Fehler beim Speichern der Yealink-XML: %v", err) } if err := saveToTempFile(&contactXML, &cache.contactFile); err != nil { return fmt.Errorf("Fehler beim Speichern der Contact-XML: %v", err) } csvFile, err := os.OpenFile("phonebook.csv", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("Fehler beim Erstellen der CSV-Datei: %v", err) } defer csvFile.Close() if err := exportToCSV(contacts, csvFile); err != nil { return fmt.Errorf("Fehler beim Exportieren als CSV: %v", err) } cache.hash = newHash cache.lastUpdate = time.Now() return nil } func saveToTempFile(data interface{}, file **os.File) error { tempFile, err := os.CreateTemp("", "phonebook_*.xml") if err != nil { return fmt.Errorf("Fehler beim Erstellen der temporären Datei: %v", err) } enc := xml.NewEncoder(tempFile) enc.Indent("", " ") if err := enc.Encode(data); err != nil { tempFile.Close() os.Remove(tempFile.Name()) return fmt.Errorf("Fehler beim Kodieren der XML: %v", err) } if *file != nil { (*file).Close() os.Remove((*file).Name()) } *file = tempFile return nil } func calculateHash(contacts []Contact) []byte { h := md5.New() for _, contact := range contacts { io.WriteString(h, contact.FirstName) io.WriteString(h, contact.LastName) for _, phone := range contact.Phones { io.WriteString(h, phone.PhoneNumber) } } return h.Sum(nil) } func getADContacts() ([]Contact, error) { l, err := ldap.DialTLS("tcp", config.LDAPServer+":636", &tls.Config{InsecureSkipVerify: true}) if err != nil { return nil, fmt.Errorf("LDAP-Verbindung fehlgeschlagen: %v", err) } defer l.Close() err = l.Bind(config.BindDN, config.BindPassword) if err != nil { return nil, fmt.Errorf("LDAP-Bindung fehlgeschlagen: %v", err) } // Erweiterter LDAP-Filter für Benutzer und Kontakte searchFilter := "(&(|(objectClass=user)(objectClass=contact))(|(telephoneNumber=*)(otherTelephone=*)(mobile=*)(homePhone=*))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" searchRequest := ldap.NewSearchRequest( config.SearchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, searchFilter, []string{"givenName", "sn", "displayName", "telephoneNumber", "otherTelephone", "mobile", "homePhone"}, nil, ) sr, err := l.Search(searchRequest) if err != nil { return nil, fmt.Errorf("LDAP-Suche fehlgeschlagen: %v", err) } var contacts []Contact for _, entry := range sr.Entries { firstName := entry.GetAttributeValue("givenName") lastName := entry.GetAttributeValue("sn") if firstName == "" || lastName == "" { continue } phone := formatPhoneNumber(entry.GetAttributeValue("telephoneNumber")) otherMobile := formatPhoneNumber(entry.GetAttributeValue("otherMobile")) mobile := formatPhoneNumber(entry.GetAttributeValue("mobile")) if isExcludedNumber(phone) || isExcludedNumber(otherMobile) || isExcludedNumber(mobile) { continue } if phone != "" || otherMobile != "" || mobile != "" { contact := Contact{ FirstName: firstName, LastName: lastName, Phones: []Phone{}, Group: 2, // Standardmäßig zur "Work"-Gruppe hinzufügen } if phone != "" { contact.Phones = append(contact.Phones, Phone{Type: "Work", PhoneNumber: phone, AccountIndex: 0}) } if mobile != "" { contact.Phones = append(contact.Phones, Phone{Type: "Mobile", PhoneNumber: mobile, AccountIndex: 0}) } if otherMobile != "" { contact.Phones = append(contact.Phones, Phone{Type: "Home", PhoneNumber: otherMobile, AccountIndex: 0}) } contacts = append(contacts, contact) } } return contacts, nil } func removeDuplicateNumbers(phones []Phone) []Phone { seen := make(map[string]bool) result := []Phone{} for _, phone := range phones { if !seen[phone.PhoneNumber] { seen[phone.PhoneNumber] = true result = append(result, phone) } else if phone.Type == "Work" { // Wenn die Nummer bereits gesehen wurde und dies die "Work" Nummer ist, // ersetzen wir die vorherige (wahrscheinlich "Mobile") mit dieser. for i, p := range result { if p.PhoneNumber == phone.PhoneNumber { result[i] = phone break } } } } return result } func isExcludedNumber(number string) bool { excludedNumbers := []string{ "+49 5331 89", "+49533189", "+ 49 5331 89", } // Zuerst prüfen wir auf die ausgeschlossenen Nummern for _, excluded := range excludedNumbers { if strings.Contains(number, excluded) { return true } } // Dann prüfen wir, ob die Nummer mit "089" endet if len(number) >= 3 && number[len(number)-3:] == "089" { return true } // Wenn keine der obigen Bedingungen zutrifft, ist die Nummer nicht ausgeschlossen return false } func