diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml new file mode 100644 index 0000000..5f7d908 --- /dev/null +++ b/.github/workflows/release_build.yml @@ -0,0 +1,31 @@ +name: Release Go project + +on: + push: + tags: + - "*" + +jobs: + build: + name: GoReleaser build + runs-on: ubuntu-latest + + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: 1.15 + id: go + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@master + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f5a5af --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +*.conf + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +www/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..81bb0c4 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,24 @@ +env: + - GO111MODULE=on + - GOPROXY=https://goproxy.io + +before: + hooks: + - go mod download + +builds: + - + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + +archives: + - files: + - templates/* + - configs/* + +checksum: + name_template: 'checksums.txt' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..af70b84 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## FreePBX phone book server + +## How it works + +## Requirements: + +* [FreePBX >= 15](https://www.freepbx.org) + +## Flags: + +```bash +Usage of ./freepbx-phonebook-server-mac: + -cron string + Set update time phone books (default "*/5 * * * *") + -freepbx-conf string + Set path to freepbx db connection config file (default "/etc/freepbx.conf") + -listen-port int + Set http server listen port (default 8081) + -server-addr string + Overwrite ip/dns name for template (default "autodetect") + -templates-file-dir string + Set path to templates phonebook files (default "./templates") + -workdir string + Set working directory (default "./www") +``` \ No newline at end of file diff --git a/configs/freepbx-phonebook-server.service b/configs/freepbx-phonebook-server.service new file mode 100644 index 0000000..deca785 --- /dev/null +++ b/configs/freepbx-phonebook-server.service @@ -0,0 +1,14 @@ +[Unit] +Description=FreePBX phone book server +After=network.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=10 + +WorkingDirectory=/opt/freepbx-phonebook-server +ExecStart=/opt/freepbx-phonebook-server/freepbx-phonebook-server + +[Install] +WantedBy=multi-user.target diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d3d6d3 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module freepbx-phonebook-server + +go 1.15 + +require ( + github.com/go-sql-driver/mysql v1.5.0 + github.com/mileusna/crontab v1.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a71b942 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg= +github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..918221e --- /dev/null +++ b/main.go @@ -0,0 +1,381 @@ +package main + +import ( + "bytes" + "database/sql" + "encoding/json" + "flag" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "text/template" + + _ "github.com/go-sql-driver/mysql" + "github.com/mileusna/crontab" +) + +var ( + workdir, templatesFileDir, freepbxConf string + serveraddr, cron string + db *sql.DB + listenport int +) + +//CiscoIPPhoneDirectory struct +type CiscoIPPhoneDirectory struct { + List []DirectoryEntry +} + +//DirectoryEntry struct +type DirectoryEntry struct { + DisplayName, MAC string + PhoneNumber, Company string + FirstName, LastName, Title string + Department string +} + +//CiscoIPPhoneMenu struct +type CiscoIPPhoneMenu struct { + List []MenuItem +} + +//MenuItem struct +type MenuItem struct { + Name, URL string +} + +//Group struct +type Group struct { + ID int + GroupName, Description string + Users string +} + +//Grandsteam struct +type Grandsteam struct { + GroupList []Group + UserList []DirectoryEntry +} + +func ciscoPhonebook(templates *template.Template, groups []Group) error { + var ( + menuitems []MenuItem + menuitem MenuItem + ) + + for _, group := range groups { + filename := fmt.Sprintf("%s.%s", group.GroupName, "xml") + + menuitem.Name = group.Description + menuitem.URL = fmt.Sprintf("http://%s:%d/%s", serveraddr, listenport, filename) + menuitems = append(menuitems, menuitem) + directoryentrys, err := loopUsers(group.Users) + if err != nil { + return err + } + directoryentryfile := filepath.Join(workdir, filename) + + if group.Users != "[]" { + f, err := os.Create(directoryentryfile) + if err != nil { + return err + } + templates.ExecuteTemplate(f, "cisco-ipphonedirectory.xml.tpl", CiscoIPPhoneDirectory{directoryentrys}) + f.Close() + } + } + + f, err := os.Create(filepath.Join(workdir, "directory.xml")) + if err != nil { + return err + } + templates.ExecuteTemplate(f, "cisco-ipphonemenu.xml.tpl", CiscoIPPhoneMenu{menuitems}) + f.Close() + + return nil +} + +func grandstreamPhonebook(templates *template.Template, groups []Group) error { + var directoryentrys []DirectoryEntry + + for _, group := range groups { + directoryentry, err := loopUsers(group.Users) + if err != nil { + return err + } + directoryentrys = append(directoryentrys, directoryentry...) + } + + f, err := os.Create(filepath.Join(workdir, "phonebook.xml")) + if err != nil { + return err + } + templates.ExecuteTemplate(f, "grandstream-phonebook.xml.tpl", Grandsteam{groups, directoryentrys}) + f.Close() + + return nil +} + +func loopUsers(usersarr string) ([]DirectoryEntry, error) { + var ( + err error + directoryentry DirectoryEntry + users []string + directoryentrys []DirectoryEntry + ) + + err = json.Unmarshal([]byte(usersarr), &users) + if err != nil { + return directoryentrys, err + } + + if len(users) > 0 { + usersid := make([]interface{}, len(users)) + for i, id := range users { + usersid[i] = id + } + + query := ` + SELECT default_extension,fname,lname,displayname,title,company,department,fax + FROM userman_users + WHERE id + IN (?` + strings.Repeat(",?", len(usersid)-1) + `) + ` + + rows, err := db.Query(query, usersid...) + if err != nil { + return directoryentrys, err + } + + for rows.Next() { + err := rows.Scan(&directoryentry.PhoneNumber, &directoryentry.FirstName, &directoryentry.LastName, + &directoryentry.DisplayName, &directoryentry.Title, &directoryentry.Company, + &directoryentry.Department, &directoryentry.MAC) + if err != nil { + return directoryentrys, err + } + directoryentrys = append(directoryentrys, directoryentry) + } + } + + return directoryentrys, nil +} + +func getPBXGroups() ([]Group, error) { + var ( + group Group + groups []Group + ) + + query := ` + SELECT id,groupname,description,users + FROM userman_groups + WHERE groupname + REGEXP 'pbx-phonebook.*' + ` + + rows, err := db.Query(query) + if err != nil { + return []Group{}, err + } + + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&group.ID, &group.GroupName, &group.Description, &group.Users) + if err != nil { + return []Group{}, err + } + groups = append(groups, group) + } + + return groups, nil +} + +func getDBConnectionParams() (string, error) { + var con string + + rex := regexp.MustCompile(`\["(.*)"\] = "(.*)";`) + buf := new(bytes.Buffer) + + file, err := os.Open(freepbxConf) + if err != nil { + return con, err + } + defer file.Close() + + buf.ReadFrom(file) + + data := rex.FindAllStringSubmatch(buf.String(), -1) + + res := make(map[string]string) + for _, kv := range data { + k := kv[1] + v := kv[2] + res[k] = v + } + + con = fmt.Sprintf("%s:%s@tcp(%s)/%s", res["AMPDBUSER"], res["AMPDBPASS"], res["AMPDBHOST"], res["AMPDBNAME"]) + + return con, nil +} + +func getIP() (string, error) { + ifaces, err := net.Interfaces() + if err != nil { + return "", err + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue // interface down + } + if iface.Flags&net.FlagLoopback != 0 { + continue // loopback interface + } + addrs, err := iface.Addrs() + if err != nil { + return "", err + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + continue // not an ipv4 address + } + return ip.String(), nil + } + } + return "", nil +} + +func generatePhoneBooks() error { + + fmt.Println("Updating phone books...") + + //Create working directory + if _, err := os.Stat(workdir); os.IsNotExist(err) { + os.Mkdir(workdir, 0755) + } + + groups, err := getPBXGroups() + if err != nil { + return err + } + + increment := template.FuncMap{ + "inc": func(i int) int { + return i + 1 + }, + } + + allTemplates, err := template.New("phonebooks").Funcs(increment).ParseGlob(filepath.Join(templatesFileDir, "*")) + if err != nil { + return err + } + + err = ciscoPhonebook(allTemplates, groups) + if err != nil { + return err + } + + err = grandstreamPhonebook(allTemplates, groups) + if err != nil { + return err + } + + return nil +} + +func logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + }) +} + +func init() { + flag.StringVar(&workdir, "workdir", "./www", "Set working directory") + flag.StringVar(&templatesFileDir, "templates-file-dir", "./templates", "Set path to templates phonebook files") + flag.StringVar(&freepbxConf, "freepbx-conf", "/etc/freepbx.conf", "Set path to freepbx db connection config file") + flag.StringVar(&serveraddr, "server-addr", "", "Overwrite ip/dns name for template") + flag.IntVar(&listenport, "listen-port", 8081, "Set http server listen port") + flag.StringVar(&cron, "cron", "*/5 * * * *", "Set update time phone books") +} + +func main() { + + var err error + + flag.Parse() + + //Detect ip if server-addr not set + if serveraddr == "" { + serveraddr, err = getIP() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get ip address: %v\n", err) + os.Exit(1) + } + } + fmt.Printf("Server address: %s\n", serveraddr) + + //Getting params for db connection + dbConnParams, err := getDBConnectionParams() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + //Connicting to db + db, err = sql.Open("mysql", dbConnParams) + if err != nil { + fmt.Fprintf(os.Stderr, "Error on initializing database connection: %s\n", err) + os.Exit(1) + } + + db.SetMaxIdleConns(10) + + //Checking db connection + err = db.Ping() + if err != nil { + fmt.Fprintf(os.Stderr, "Error on database connection: %s\n", err) + os.Exit(1) + } + + //Create working directory + if _, err := os.Stat(workdir); os.IsNotExist(err) { + os.Mkdir(workdir, 0755) + } + + //Generating phone books + err = generatePhoneBooks() + if err != nil { + fmt.Fprintf(os.Stderr, "Error generating phone books: %s\n", err) + os.Exit(1) + } + + //Create cron table + ctab := crontab.New() + err = ctab.AddJob(cron, generatePhoneBooks) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + + http.Handle("/", http.FileServer(http.Dir(workdir))) + + fmt.Printf("Serving %s on HTTP port: %d\n", workdir, listenport) + http.ListenAndServe(":"+strconv.Itoa(listenport), logRequest(http.DefaultServeMux)) +} diff --git a/templates/cisco-ipphonedirectory.xml.tpl b/templates/cisco-ipphonedirectory.xml.tpl new file mode 100644 index 0000000..a311b2e --- /dev/null +++ b/templates/cisco-ipphonedirectory.xml.tpl @@ -0,0 +1,12 @@ + + + Телефонная книга +{{- with .List }} +{{- range . }} + + {{ .DisplayName }} + {{ .PhoneNumber }} + +{{- end }} +{{- end }} + diff --git a/templates/cisco-ipphonemenu.xml.tpl b/templates/cisco-ipphonemenu.xml.tpl new file mode 100644 index 0000000..ff73e99 --- /dev/null +++ b/templates/cisco-ipphonemenu.xml.tpl @@ -0,0 +1,12 @@ + + + Выберете книгу +{{- with .List }} +{{- range . }} + + {{ .Name }} + {{ .URL }} + +{{- end }} +{{- end }} + diff --git a/templates/grandstream-phonebook.xml.tpl b/templates/grandstream-phonebook.xml.tpl new file mode 100644 index 0000000..e013b64 --- /dev/null +++ b/templates/grandstream-phonebook.xml.tpl @@ -0,0 +1,33 @@ +{{- $i := 0 -}} + + +{{- with .GroupList }} +{{- range $group_index, $group_element := . }} +{{- $group_id := $group_element.ID }} + + {{ $group_id }} + {{ $group_element.Description }} + +{{- with $.UserList }} +{{- range $user_index, $user_element := . }} + + {{- $i = inc $i }} + {{ $i }} + {{ $user_element.FirstName }} + {{ $user_element.LastName }} + 0 + + {{ $user_element.PhoneNumber }} + 0 + + {{ $group_id }} + 0 + {{ $user_element.Department }} + {{ $user_element.Title }} + {{ $user_element.Company }} + +{{- end }} +{{- end }} +{{- end }} +{{- end }} +