Steffen Probst 832ebf099a init
2025-03-21 18:47:49 +01:00

468 lines
13 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.
*/
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