From 2192b1085ad2e5f3c7c0a342e5a9f8d67c633893 Mon Sep 17 00:00:00 2001 From: Rico Schulte Date: Fri, 10 Mar 2023 05:23:28 +0100 Subject: [PATCH] initial app service implementation --- go.mod | 9 +- go.sum | 21 +- helper/helper.go | 130 +++++++++ service/epsignal/api.go | 25 ++ service/http.go | 97 +++++++ service/innotools.go | 64 +++++ service/innotools_test.go | 77 ++++++ service/pbxadminapi/api.go | 25 ++ service/pbxapi/api.go | 119 ++++++++ service/pbxapi/structs.go | 213 +++++++++++++++ service/pbximpersonation/api.go | 25 ++ service/pbxmessages/api.go | 25 ++ service/pbxtableusers/api.go | 118 ++++++++ service/pbxtableusers/api_test.go | 43 +++ service/pbxtableusers/structs.go | 244 +++++++++++++++++ service/rcc/api.go | 25 ++ service/service.go | 302 +++++++++++++++++++++ service/services/api.go | 25 ++ service/websockets.go | 436 ++++++++++++++++++++++++++++++ 19 files changed, 2020 insertions(+), 3 deletions(-) create mode 100644 helper/helper.go create mode 100644 service/epsignal/api.go create mode 100644 service/http.go create mode 100644 service/innotools.go create mode 100644 service/innotools_test.go create mode 100644 service/pbxadminapi/api.go create mode 100644 service/pbxapi/api.go create mode 100644 service/pbxapi/structs.go create mode 100644 service/pbximpersonation/api.go create mode 100644 service/pbxmessages/api.go create mode 100644 service/pbxtableusers/api.go create mode 100644 service/pbxtableusers/api_test.go create mode 100644 service/pbxtableusers/structs.go create mode 100644 service/rcc/api.go create mode 100644 service/service.go create mode 100644 service/services/api.go create mode 100644 service/websockets.go diff --git a/go.mod b/go.mod index a0bf25c..2a4618a 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,22 @@ module github.com/ricoschulte/go-myapps go 1.19 require ( + github.com/go-chi/chi v1.5.4 github.com/gorilla/websocket v1.5.0 github.com/stretchr/testify v1.8.1 gotest.tools v2.2.0+incompatible ) -require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +require ( + github.com/go-chi/chi/v5 v5.0.1 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) require ( + github.com/chi-middleware/logrus-logger v0.2.0 github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.9.0 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index dc016dc..bdf2d16 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,48 @@ +github.com/chi-middleware/logrus-logger v0.2.0 h1:Do3vcVSRsLh7zSRKxsVg5Kr5//rTqytwprCR1HzVqT8= +github.com/chi-middleware/logrus-logger v0.2.0/go.mod h1:ie/rvKsXrtqqsnJd3qtSEnLxgCs1I758WYmHdv6CRt0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo= +github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/helper/helper.go b/helper/helper.go new file mode 100644 index 0000000..063285d --- /dev/null +++ b/helper/helper.go @@ -0,0 +1,130 @@ +package helper + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" +) + +// func PrettyPrintJSON(data []map[string]string) string { +func PrettyPrintJSON(data any) string { + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + fmt.Println("error:", err) + } + fmt.Println(string(b)) + return string(b) +} + +/* +splits a string by delimiter but ignores delimiter escaped by escape + +in: aaaaa:bbbb:cccc\:dddd +out: [aaaaa,bbbb,cccc\:dddd] + +splitIgnoreEscape(line2, ':', '\\') +*/ +func SplitIgnoreEscape(str string, delimiter byte, escape byte) []string { + var parts []string + var buffer strings.Builder + + escaped := false + + for i := 0; i < len(str); i++ { + char := str[i] + + if !escaped && char == escape { + escaped = true + continue + } + + if escaped { + if char != delimiter && char != escape { + buffer.WriteByte(escape) + } + buffer.WriteByte(char) + escaped = false + continue + } + + if char == delimiter { + parts = append(parts, buffer.String()) + buffer.Reset() + continue + } + + buffer.WriteByte(char) + } + + parts = append(parts, buffer.String()) + + return parts +} + +/* +splits a string by delimiter and returns a slice of non empty strings + +SplitStringIgnoreEmpty(value, ";") +in "aaaa;bbbb;;cccc" +out ["aaaa", "bbbb", "cccc"] +*/ +func SplitStringIgnoreEmpty(value string, delimiter string) []string { + filteredSlice := make([]string, 0) + for _, str := range strings.Split(value, ";") { + if strings.TrimSpace(str) != "" { + filteredSlice = append(filteredSlice, str) + } + } + return filteredSlice +} + +// sha256 hash from map[string]string +func Sha265HashFromMap(m map[string]string) string { + // Get a sorted slice of the keys + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + + // Concatenate the key-value pairs + var str string + for _, key := range keys { + str += key + m[key] + } + + // Compute the SHA-256 hash + hash := sha256.Sum256([]byte(str)) + + // Return the hexadecimal encoding of the hash + return hex.EncodeToString(hash[:]) +} + +/* +takes two []string slices as input and returns true if all the elements in the first slice (s1) are present in the second slice (s2), regardless of their order, and false otherwise. + +The function first checks if the two slices have the same length, and if not, it immediately returns false. Then, it creates a map (m) and adds each element of the second slice (s2) to the map as a key with a value of true. + +Next, it iterates over the elements of the first slice (s1) and checks if each element is present in the map (m). If any element is not present in the map, the function returns false. If all elements are present in the map, the function returns true. +*/ +func AreEqualSlices(s1, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + + m := make(map[string]bool) + for _, v := range s2 { + m[v] = true + } + + for _, v := range s1 { + if _, ok := m[v]; !ok { + return false + } + } + + return true +} diff --git a/service/epsignal/api.go b/service/epsignal/api.go new file mode 100644 index 0000000..b920224 --- /dev/null +++ b/service/epsignal/api.go @@ -0,0 +1,25 @@ +package epsignal + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type EpSignal struct { +} + +func (api *EpSignal) GetApiName() string { + return "EpSignal" +} + +func (api *EpSignal) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *EpSignal) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *EpSignal) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/http.go b/service/http.go new file mode 100644 index 0000000..04165da --- /dev/null +++ b/service/http.go @@ -0,0 +1,97 @@ +package service + +import ( + "fmt" + "io" + + "net/http" + + log "github.com/sirupsen/logrus" +) + +func AppIndex(appservice *AppService, w http.ResponseWriter, req *http.Request) { + w.Header().Add("Content-Type", "text/html") + w.Write([]byte("app index")) +} + +// handles both websocket and http GET/POST requests on the same path +func handleConnectionForHttpOrWebsocket(appservice *AppService, w http.ResponseWriter, r *http.Request) { + log.Warnf("serve handleConnectionForHttpOrWebsocket %s", r.URL.Path) + if r.URL.Path != "/test.company.com/testservice/mytestservice" { + return + } + if r.Method != http.MethodGet && r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + if r.Header.Get("Upgrade") == "websocket" { + HandleWebsocket(appservice, w, r) + } else { + AppIndex(appservice, w, r) + } +} + +func ServeFile(fs http.FileSystem, w http.ResponseWriter, r *http.Request, filename string, contenttype string) { + log.Warnf("ServeFile %s", filename) + + file, err := fs.Open(filename) + if err != nil { + fmt.Fprintf(w, "%v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + content, err_file := io.ReadAll(file) + if err_file != nil { + fmt.Fprintf(w, "Error while reading file: %v", err_file) + http.Error(w, err_file.Error(), http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", contenttype) + _, err_write := w.Write(content) + if err_write != nil { + fmt.Fprintf(w, "Error while writing to response: %v", err_write) + http.Error(w, err_write.Error(), http.StatusInternalServerError) + return + } + +} + +// func GetHttpRoutes(appservice *AppService, mux *chi.Mux) error { + +// // Serve static files on app path +// // mux.Get("/static/*", +// // http.StripPrefix("/test.company.com/testservice/mytestservice/static/", +// // http.FileServer(appservice.Fs), +// // ).ServeHTTP, +// // ) + +// // mux.Get("/admin.htm", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "admin.htm", "text/html") +// // }) +// // mux.Get("/user.htm", func(w http.ResponseWriter, r *http.Request) { +// // log.Warnf("serve /user.htm %s", r.URL.Path) +// // ServeFile(appservice.Fs, w, r, "user.htm", "text/html") +// // }) +// // mux.Get("/searchapi.htm", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "searchapi.htm", "text/html") +// // }) + +// // mux.Get("/user.png", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "user.png", "image/png") +// // }) +// // mux.Get("/admin.png", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "admin.png", "image/png") +// // }) +// // mux.Get("/app.css", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "app.css", "text/css; charset=utf-8") +// // }) +// // mux.Get("/app.js", func(w http.ResponseWriter, r *http.Request) { +// // ServeFile(appservice.Fs, w, r, "app.js", "text/javascript; charset=utf-8") +// // }) + +// return nil +// } diff --git a/service/innotools.go b/service/innotools.go new file mode 100644 index 0000000..bab3eef --- /dev/null +++ b/service/innotools.go @@ -0,0 +1,64 @@ +package service + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" + "strings" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type MyAppsUtils struct{} + +func (mu *MyAppsUtils) GetDigestHashForUserLogingToAppService(app string, domain string, sip string, guid string, dn string, info string, challenge string, password string) string { + str := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s", app, domain, sip, guid, dn, info, challenge, password) + bytes := sha256.Sum256([]byte(str)) + return hex.EncodeToString(bytes[:]) +} + +func (mu *MyAppsUtils) GetDigestForAppLoginFromJson(message_json, password, challenge string) (string, error) { + var msgin AppLogin + if err := json.Unmarshal([]byte(message_json), &msgin); err != nil { + return "", err + } + var calculated_digest string + + if msgin.PbxObj != "" { + // user/admin login + log.Trace("GetDigestForAppLoginFromJson user/admin login") + info, _ := msgin.InfoAsUserDigestString() + calculated_digest = mu.GetDigestHashForUserLogingToAppService(msgin.App, msgin.Domain, msgin.Sip, msgin.Guid, msgin.Dn, info, challenge, password) + } else { + // pbxobj login + log.Trace("GetDigestForAppLoginFromJson pbxobj login") + info, _ := msgin.InfoAsPbxobjectDigestString() + calculated_digest = mu.GetDigestHashForUserLogingToAppService(msgin.App, msgin.Domain, msgin.Sip, msgin.Guid, msgin.Dn, info, challenge, password) + } + + return calculated_digest, nil +} + +func (mu *MyAppsUtils) GetRandomHexString(n int) string { + charPool := "abcdef0123456789" + rand.Seed(time.Now().UnixNano()) // does + b := strings.Builder{} + for i := 0; i < n; i++ { + b.WriteByte(charPool[rand.Intn(len(charPool))]) + } + return b.String() +} + +func CheckAppPasswordForMaximumLength(password string) error { + if password == "" { + return errors.Errorf("Error: App password cant be empty") + } + if len(password) > 15 { + return errors.Errorf("Error: App password invalid. The app password cant be longer than 15 chars. This is a limitation of passwords stored in pbx-objects. the currently set password has a length of %v", len(password)) + } + return nil +} diff --git a/service/innotools_test.go b/service/innotools_test.go new file mode 100644 index 0000000..fc87b03 --- /dev/null +++ b/service/innotools_test.go @@ -0,0 +1,77 @@ +package service_test + +import ( + "strings" + "testing" + + "github.com/ricoschulte/go-myapps/service" + "gotest.tools/assert" +) + +func TestGetDigestForAppLoginFromJson(t *testing.T) { + tests := []struct { + name string + message string + password string + challenge string + expected string + }{ + { + "fritz.box go instance pbxobj go-search", + `{"mt":"AppLogin","sip":"go-search","guid":"3954ae7854c96301c9dd009033400109","dn":"go-search","digest":"130f168773ff760701b74e629eca2544c6e62d5354e92e6f821001a982eb8951","domain":"fritz.box","app":"searchapi","info":{"appobj":"go-search","appdn":"go-search","appurl":"http://192.168.178.29:5000/fritz.box/go/instance/searchapi","pbx":"pbx-main","cn":"go-search","unlicensed":true,"apps":[]}}`, + "go", + "16d7dbbcccb63612", + "130f168773ff760701b74e629eca2544c6e62d5354e92e6f821001a982eb8951", + }, + { + "fritz.box go instance pbxobj go", + `{"mt":"AppLogin","sip":"go","guid":"8e7ca6fabbc5630170e9009033400109","dn":"go","digest":"8f802873dcdee7cef6144017a6112289298ad69e77474a4c37cfdb68a83640c0","domain":"fritz.box","app":"admin","info":{"appobj":"go","appdn":"go","appurl":"http://192.168.178.29:5000/fritz.box/go/instance/admin","pbx":"pbx-main","cn":"go","unlicensed":true,"apps":[]}}`, + "go", + "aeaaebe781e80289", + "8f802873dcdee7cef6144017a6112289298ad69e77474a4c37cfdb68a83640c0", + }, + { + "fritz.box go instance user rico admin", + `{"mt":"AppLogin","app":"admin","domain":"fritz.box","sip":"rico","guid":"f48b06484a8a61015853009033400109","dn":"Schulte, Rico","info":{"appobj":"go","appdn":"go","appurl":"http://192.168.178.29:5000/fritz.box/go/instance/admin","pbx":"pbx-main","cn":"rico","unlicensed":true,"groups":[],"apps":[]},"digest":"965b4dd4d4f127c99df796b1ed137b40454801f94f50aaf93b2ce98034f10d9c","pbxObj":"go"}`, + "go", + "277f41a7a5b7b452", + "965b4dd4d4f127c99df796b1ed137b40454801f94f50aaf93b2ce98034f10d9c", + }, + { + "fritz.box go instance user rico searchapi", + `{"mt":"AppLogin","app":"searchapi","domain":"fritz.box","sip":"rico","guid":"f48b06484a8a61015853009033400109","dn":"Schulte, Rico","info":{"appobj":"go-search","appdn":"go-search","appurl":"http://192.168.178.29:5000/fritz.box/go/instance/searchapi","pbx":"pbx-main","cn":"rico","testmode":true,"groups":[],"apps":[]},"digest":"378b82aa7508eb53866e3f4de054cdc0f7b5229e9742593e9359db3845681eb6","pbxObj":"go-search"}`, + "go", + "7c4fc2e477f69233", + "378b82aa7508eb53866e3f4de054cdc0f7b5229e9742593e9359db3845681eb6", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mu := &service.MyAppsUtils{} + result, err := mu.GetDigestForAppLoginFromJson(test.message, test.password, test.challenge) + assert.NilError(t, err) + assert.Equal(t, test.expected, result) + + }) + } +} + +func TestGetRandomHexString(t *testing.T) { + mu := &service.MyAppsUtils{} + length := 100 + result := mu.GetRandomHexString(length) + result2 := mu.GetRandomHexString(length) + println("test result: ", result) + if len(result) != length { + t.Errorf("Expected length %d, got %d", length, len(result)) + } + for i := range result { + if !strings.Contains("abcdef0123456789", string(result[i])) { + t.Errorf("Expected hexadecimal characters, got %s", string(result[i])) + } + } + if result == result2 { + t.Errorf("two equal strings received, when two should be not equal aka random") + } +} diff --git a/service/pbxadminapi/api.go b/service/pbxadminapi/api.go new file mode 100644 index 0000000..8c252d7 --- /dev/null +++ b/service/pbxadminapi/api.go @@ -0,0 +1,25 @@ +package pbxadminapi + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type PbxAdminApi struct { +} + +func (api *PbxAdminApi) GetApiName() string { + return "PbxAdminApi" +} + +func (api *PbxAdminApi) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *PbxAdminApi) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *PbxAdminApi) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/pbxapi/api.go b/service/pbxapi/api.go new file mode 100644 index 0000000..5a823de --- /dev/null +++ b/service/pbxapi/api.go @@ -0,0 +1,119 @@ +package pbxapi + +import ( + "encoding/json" + "sync" + + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type PbxApi struct { + mu sync.Mutex + receivers []chan PbxApiEvent +} + +func NewPbxApi() *PbxApi { + return &PbxApi{} +} + +func (api *PbxApi) GetApiName() string { + return "PbxApi" +} + +func (api *PbxApi) OnConnect(connection *service.AppServicePbxConnection) { + api.sendEvent(PbxApiEvent{Type: PbxApiEventConnect, Connection: connection}) +} + +func (api *PbxApi) OnDisconnect(connection *service.AppServicePbxConnection) { + api.sendEvent(PbxApiEvent{Type: PbxApiEventDisconnect, Connection: connection}) +} + +func (api *PbxApi) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + + switch msg.Mt { + case "SubscribePresenceResult": + msg := SubscribePresenceResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + if msg.Error != 0 { + log.Errorf("SubscribePresenceResult: error %d: %s", msg.Error, msg.Errortext) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventSubscribePresenceResult, SubscribePresenceResult: &msg, Connection: connection}) + case "PresenceState": + msg := PresenceState{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventPresenceState, PresenceState: &msg, Connection: connection}) + case "PresenceUpdate": + msg := PresenceUpdate{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventPresenceUpdate, PresenceUpdate: &msg, Connection: connection}) + case "SetPresenceResult": + msg := SetPresenceResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + if msg.Error != 0 { + log.Errorf("SetPresenceResult: error %d: %s", msg.Error, msg.Errortext) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventSetPresenceResult, SetPresenceResult: &msg, Connection: connection}) + case "GetNodeInfoResult": + msg := GetNodeInfoResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventGetNodeInfoResult, GetNodeInfoResult: &msg, Connection: connection}) + case "AddAlienCallResult": + msg := AddAlienCallResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventAddAlienCallResult, AddAlienCallResult: &msg, Connection: connection}) + case "DelAlienCallResult": + msg := DelAlienCallResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.sendEvent(PbxApiEvent{Type: PbxApiEventDelAlienCallResult, DelAlienCallResult: &msg, Connection: connection}) + default: + log.Warn("unknown message received: %s", msg.Mt) + } +} + +func (api *PbxApi) AddReceiver() chan PbxApiEvent { + api.mu.Lock() + defer api.mu.Unlock() + ch := make(chan PbxApiEvent) + api.receivers = append(api.receivers, ch) + return ch +} + +func (api *PbxApi) RemoveReceiver(ch chan PbxApiEvent) { + api.mu.Lock() + defer api.mu.Unlock() + + for i, c := range api.receivers { + if c == ch { + // Remove the channel from the slice + api.receivers = append(api.receivers[:i], api.receivers[i+1:]...) + // Close the channel to signal the receiver that it should stop listening + close(ch) + return + } + } +} + +func (api *PbxApi) sendEvent(event PbxApiEvent) { + api.mu.Lock() + defer api.mu.Unlock() + + // Send the event to all registered receivers + for _, ch := range api.receivers { + ch <- event + } +} diff --git a/service/pbxapi/structs.go b/service/pbxapi/structs.go new file mode 100644 index 0000000..a9a0d3f --- /dev/null +++ b/service/pbxapi/structs.go @@ -0,0 +1,213 @@ +package pbxapi + +import ( + "fmt" + + "github.com/ricoschulte/go-myapps/service" +) + +const ( + ActivityAvaiable = "" + ActivityAway = "away" + ActivityBusy = "busy" + ActivityDnd = "dnd" + ActivityOnThePhone = "on-the-phone" // can not be set +) + +type SubscribePresence struct { + service.BaseMessage + Num string `json:"num,omitempty"` + Sip string `json:"sip,omitempty"` +} + +func NewSubscribePresenceWithNum(num, src string) *SubscribePresence { + return &SubscribePresence{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "SubscribePresence", + Src: src, + }, + Num: num, + } +} + +func NewSubscribePresenceWithSip(sip, src string) *SubscribePresence { + return &SubscribePresence{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "SubscribePresence", + Src: src, + }, + Sip: sip, + } + +} + +type SubscribePresenceResult struct { + service.BaseMessage + Error int `json:"error,omitempty"` + Errortext string `json:"errorText,omitempty"` +} + +type PresenceState struct { + service.BaseMessage + Sip string `json:"sip"` + Dn string `json:"dn"` + Num string `json:"num"` + Email string `json:"email"` +} + +type Presence struct { + Contact string `json:"contact"` // tel: im: + Status string `json:"status"` // closed open + Note string `json:"note"` +} + +type PresenceUpdate struct { + service.BaseMessage + Presence []Presence `json:"presence"` +} + +func (update *PresenceUpdate) GetPresenceByContact(contact string) (*Presence, error) { + for _, presence := range update.Presence { + if presence.Contact == contact { + return &presence, nil + } + } + return nil, fmt.Errorf("no contact '%s'", contact) +} + +// Sets the presence for a given contact of a user, defined by the SIP URI or GUID. +type SetPresence struct { + service.BaseMessage + Guid string `json:"guid,omitempty"` + Sip string `json:"sip,omitempty"` + Contact string `json:"contact,omitempty"` // tel: im: + Activity string `json:"activity,omitempty"` // busy, dnd ... + Note string `json:"note,omitempty"` +} + +// Message sent back to confirm that setting the presence has been completed or failed. +type SetPresenceResult struct { + service.BaseMessage + Error int `json:"error,omitempty"` + Errortext string `json:"errorText,omitempty"` +} + +// Sets the presence for a given contact of a user, defined by the GUID. +func NewSetPresenceWithGuid(guid, contact, activity, note, src string) *SetPresence { + return &SetPresence{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "SetPresence", + Src: src, + }, + Guid: guid, + Contact: contact, + Activity: activity, + Note: note, + } +} + +// Sets the presence for a given contact of a user, defined by the SIP URI. +func NewSetPresenceWithSip(sip, contact, activity, note, src string) *SetPresence { + return &SetPresence{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "SetPresence", + Src: src, + }, + Sip: sip, + Contact: contact, + Activity: activity, + Note: note, + } +} + +// Requests information about the node of the user that is authenticated on the underlying AppWebsocket connection. +type GetNodeInfo struct { + service.BaseMessage +} + +func NewGetNodeInfo(src string) *GetNodeInfo { + return &GetNodeInfo{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "GetNodeInfo", + Src: src, + }, + } +} + +// Contains information about the node of the user that is authenticated on the underlying AppWebsocket connection. +type GetNodeInfoResult struct { + service.BaseMessage + Name string `json:"name"` // name Name of the node. + PrefixInternational string `json:"prefix_intl"` // prefix_intl Prefix for dialing international numbers. + PrefixNational string `json:"prefix_ntl"` // prefix_ntl Prefix for dialing national numbers. + PrefixSubscriber string `json:"prefix_subs"` // prefix_subs Subscriber number prefix. + CountryCode string `json:"country_code"` // country_code Country code. +} + +// Adds a call to the PBX, which will result in a busy state of the user and also shows up as on the phone presence +type AddAlienCall struct { + service.BaseMessage +} + +func NewAddAlienCall(src string) *AddAlienCall { + return &AddAlienCall{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "AddAlienCall", + Src: src, + }, + } +} + +type AddAlienCallResult struct { + service.BaseMessage + Id int `json:"id"` // The id can be used to delete the call +} + +// Deletes a call added with AddAlienCall. The id identifies the call to be deleted +type DelAlienCall struct { + service.BaseMessage + Id int `json:"id"` // The id can be used to delete the call +} + +func NewDelAlienCall(id int, src string) *DelAlienCall { + return &DelAlienCall{ + BaseMessage: service.BaseMessage{ + Api: "PbxApi", + Mt: "DelAlienCall", + Src: src, + }, + Id: id, + } +} + +type DelAlienCallResult struct { + service.BaseMessage +} + +type PbxApiEvent struct { + Type int + Connection *service.AppServicePbxConnection + PresenceState *PresenceState + PresenceUpdate *PresenceUpdate + SubscribePresenceResult *SubscribePresenceResult + SetPresenceResult *SetPresenceResult + GetNodeInfoResult *GetNodeInfoResult + AddAlienCallResult *AddAlienCallResult + DelAlienCallResult *DelAlienCallResult +} + +const PbxApiEventDisconnect = -20 +const PbxApiEventConnect = -10 +const PbxApiEventSubscribePresenceResult = 10 +const PbxApiEventPresenceState = 20 +const PbxApiEventPresenceUpdate = 30 +const PbxApiEventSetPresenceResult = 40 +const PbxApiEventGetNodeInfoResult = 50 +const PbxApiEventAddAlienCallResult = 60 +const PbxApiEventDelAlienCallResult = 65 diff --git a/service/pbximpersonation/api.go b/service/pbximpersonation/api.go new file mode 100644 index 0000000..50f7d31 --- /dev/null +++ b/service/pbximpersonation/api.go @@ -0,0 +1,25 @@ +package pbximpersonation + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type PbxImpersonation struct { +} + +func (api *PbxImpersonation) GetApiName() string { + return "PbxImpersonation" +} + +func (api *PbxImpersonation) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *PbxImpersonation) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *PbxImpersonation) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/pbxmessages/api.go b/service/pbxmessages/api.go new file mode 100644 index 0000000..bbf8d2a --- /dev/null +++ b/service/pbxmessages/api.go @@ -0,0 +1,25 @@ +package pbxmessages + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type PbxMessages struct { +} + +func (api *PbxMessages) GetApiName() string { + return "PbxMessages" +} + +func (api *PbxMessages) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *PbxMessages) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *PbxMessages) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/pbxtableusers/api.go b/service/pbxtableusers/api.go new file mode 100644 index 0000000..d8da172 --- /dev/null +++ b/service/pbxtableusers/api.go @@ -0,0 +1,118 @@ +package pbxtableusers + +import ( + "encoding/json" + "strconv" + "sync" + "time" + + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type PbxTableUsers struct { + ReplicatedObjects map[string]ReplicatedObject // synced objects + mu sync.Mutex + receivers []chan PbxTableUsersEvent +} + +func NewPbxTableUsers() *PbxTableUsers { + return &PbxTableUsers{ + ReplicatedObjects: map[string]ReplicatedObject{}, + } +} + +func (api *PbxTableUsers) GetApiName() string { + return "PbxTableUsers" +} + +func (api *PbxTableUsers) OnConnect(connection *service.AppServicePbxConnection) { + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventConnect, Connection: connection}) +} + +func (api *PbxTableUsers) OnDisconnect(connection *service.AppServicePbxConnection) { + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventDisconnect, Connection: connection}) +} + +func (api *PbxTableUsers) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + switch msg.Mt { + case "ReplicateStartResult": + msg := ReplicateStartResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + mbytes, _ := json.Marshal(NewReplicateNext("src_" + strconv.FormatInt(time.Now().UnixNano(), 10))) + connection.WriteMessage(mbytes) + case "ReplicateNextResult": + msg := ReplicateNextResult{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + if len(msg.ReplicatedObject.Guid) > 0 { + api.ReplicatedObjects[msg.ReplicatedObject.Guid] = msg.ReplicatedObject + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventInitial, Object: &msg.ReplicatedObject, Connection: connection}) + + mbytes, _ := json.Marshal(NewReplicateNext("src_" + strconv.FormatInt(time.Now().UnixNano(), 10))) + connection.WriteMessage(mbytes) + } else { + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventInitialDone, Connection: connection}) + } + case "ReplicateAdd": + msg := ReplicateAdd{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.ReplicatedObjects[msg.ReplicatedObject.Guid] = msg.ReplicatedObject + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventAdd, Object: &msg.ReplicatedObject, Connection: connection}) + case "ReplicateUpdate": + msg := ReplicateUpdate{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + api.ReplicatedObjects[msg.ReplicatedObject.Guid] = msg.ReplicatedObject + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventUpdate, Object: &msg.ReplicatedObject, Connection: connection}) + case "ReplicateDel": + msg := ReplicateDel{} + if err := json.Unmarshal(message, &msg); err != nil { + log.Errorf("PbxApi: error unmarshalling message: %v", err) + } + delete(api.ReplicatedObjects, msg.ReplicatedObject.Guid) + api.sendEvent(PbxTableUsersEvent{Type: PbxTableUsersEventDelete, Object: &msg.ReplicatedObject, Connection: connection}) + + default: + log.Warn("unknown message received: %s", string(message)) + } +} + +func (api *PbxTableUsers) AddReceiver() chan PbxTableUsersEvent { + api.mu.Lock() + defer api.mu.Unlock() + ch := make(chan PbxTableUsersEvent) + api.receivers = append(api.receivers, ch) + return ch +} + +func (api *PbxTableUsers) RemoveReceiver(ch chan PbxTableUsersEvent) { + api.mu.Lock() + defer api.mu.Unlock() + + for i, c := range api.receivers { + if c == ch { + // Remove the channel from the slice + api.receivers = append(api.receivers[:i], api.receivers[i+1:]...) + // Close the channel to signal the receiver that it should stop listening + close(ch) + return + } + } +} + +func (api *PbxTableUsers) sendEvent(event PbxTableUsersEvent) { + api.mu.Lock() + defer api.mu.Unlock() + + // Send the event to all registered receivers + for _, ch := range api.receivers { + ch <- event + } +} diff --git a/service/pbxtableusers/api_test.go b/service/pbxtableusers/api_test.go new file mode 100644 index 0000000..1deb7ec --- /dev/null +++ b/service/pbxtableusers/api_test.go @@ -0,0 +1,43 @@ +package pbxtableusers_test + +import ( + "encoding/json" + "testing" + + "github.com/ricoschulte/go-myapps/service/pbxtableusers" +) + +func TestPbxTableUsers_ReplicateUpdate(t *testing.T) { + + tests := []struct { + name string + json string + }{ + + { + name: "wakeup", + json: `{"mt":"ReplicateUpdate","src":"src_1677997325518944961","api":"PbxTableUsers","columns":{"apps-my":"users","cn":"Charlotte Maihoff","config":"Config User","dn":"Maihoff, Charlotte","e164":"403","fax":false,"grps":[{"name":"Support"},{"name":"pats_haus1"}],"guid":"66eaaee678464cffb9a3c8ae259fedd2","h323":"charlottemaihoff","hide":false,"loc":"pbx-main","node":"master","pwd":"f8c26f062fedf2ff2980b8b8bdbb01a6df91add1f605e7f5","t-allows":[{"name":"@fritz.box","online":true,"presence":true,"otf":true,"note":true,"visible":true}],"wakeups":[{"h":2,"m":3,"s":4,"name":"44444","num":"44333","retry":2,"mult":true,"to":34,"fallback":"3333","bool":"my_new_boolean","bool-not":true}]}}`, + }, + { + name: "fork", + json: `{"mt":"ReplicateUpdate","src":"src_1677997916352198068","api":"PbxTableUsers","columns":{"apps-my":"users","cn":"Charlotte Maihoff","config":"Config User","dn":"Maihoff, Charlotte","e164":"403","fax":false,"forks":[{"h323":"aaaaaaaaaaaa","bool":"my_new_boolean","bool-not":true,"mobility":"vvvvvvvvvvvvvvv","delay":444,"hw":"vvvvvvvvvvvvvvvv","app":"vvvvvvvvvvvvv","off":true,"cw":true,"min":5,"max":5}],"grps":[{"name":"Support"},{"name":"pats_haus1"}],"guid":"66eaaee678464cffb9a3c8ae259fedd2","h323":"charlottemaihoff","hide":false,"loc":"pbx-main","node":"master","pwd":"f8c26f062fedf2ff2980b8b8bdbb01a6df91add1f605e7f5","t-allows":[{"name":"@fritz.box","online":true,"presence":true,"otf":true,"note":true,"visible":true}],"wakeups":[]}}`, + }, + { + name: "cds", + json: `{"mt":"ReplicateUpdate","src":"src_1677998086155540423","api":"PbxTableUsers","columns":{"apps-my":"users","cds":[{"type":"cfu","bool":"my_new_boolean","e164":"111233","h323":"sdsdsd","src":"","precedence":true},{"type":"cfb","bool":"Business hours","bool-not":true,"e164":"4444","h323":"ffff","src":""},{"type":"cfnr","h323":"44444444","precedence":true}],"cn":"Charlotte Maihoff","config":"Config User","dn":"Maihoff, Charlotte","e164":"403","fax":false,"grps":[{"name":"Support"},{"name":"pats_haus1"}],"guid":"66eaaee678464cffb9a3c8ae259fedd2","h323":"charlottemaihoff","hide":false,"loc":"pbx-main","node":"master","pwd":"f8c26f062fedf2ff2980b8b8bdbb01a6df91add1f605e7f5","t-allows":[{"name":"@fritz.box","online":true,"presence":true,"otf":true,"note":true,"visible":true}],"wakeups":[]}}`, + }, + { + name: "device", + json: `{"mt":"ReplicateUpdate","src":"src_1677998086155540423","api":"PbxTableUsers","columns":{"apps-my":"users","cn":"Charlotte Maihoff","config":"Config User","devices":[{"hw":"hwid","text":"name","app":"app","admin":true,"no-filter":true,"tls":true,"no-mob":true,"trusted":true,"sreg":true,"mr":true}],"dn":"Maihoff, Charlotte","e164":"403","fax":false,"grps":[{"name":"Support"},{"name":"pats_haus1"}],"guid":"66eaaee678464cffb9a3c8ae259fedd2","h323":"charlottemaihoff","hide":false,"loc":"pbx-main","node":"master","pwd":"f8c26f062fedf2ff2980b8b8bdbb01a6df91add1f605e7f5","t-allows":[{"name":"@fritz.box","online":true,"presence":true,"otf":true,"note":true,"visible":true}],"wakeups":[]}}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + msg := pbxtableusers.ReplicateUpdate{} + if err := json.Unmarshal([]byte(test.json), &msg); err != nil { + t.Fatalf("error unmarshalling message: %v", err) + } + + }) + } +} diff --git a/service/pbxtableusers/structs.go b/service/pbxtableusers/structs.go new file mode 100644 index 0000000..469d118 --- /dev/null +++ b/service/pbxtableusers/structs.go @@ -0,0 +1,244 @@ +package pbxtableusers + +import "github.com/ricoschulte/go-myapps/service" + +type Column struct { + Udate bool `json:"update"` +} + +type ReplicateStart struct { + service.BaseMessage + Add bool `json:"add,omitempty"` + Del bool `json:"del,omitempty"` + Columns map[string]Column `json:"columns"` + Pseudo []string `json:"pseudo"` +} + +func NewReplicateStart(add, del bool, columns map[string]Column, pseudo []string, src string) *ReplicateStart { + return &ReplicateStart{ + BaseMessage: service.BaseMessage{ + Api: "PbxTableUsers", + Mt: "ReplicateStart", + Src: src, + }, + Add: add, + Del: del, + Columns: columns, + Pseudo: pseudo, + } +} + +type ReplicateNext struct { + service.BaseMessage +} + +func NewReplicateNext(src string) *ReplicateNext { + return &ReplicateNext{ + BaseMessage: service.BaseMessage{ + Api: "PbxTableUsers", + Mt: "ReplicateNext", + Src: src, + }, + } +} + +type ReplicateStartResult struct { + service.BaseMessage + Guid service.Guid `json:"guid"` + //Columns ReplicatedObject `json:"columns"` +} + +type ReplicateNextResult struct { + service.BaseMessage + ReplicatedObject ReplicatedObject `json:"columns"` +} + +type ReplicateUpdate struct { + service.BaseMessage + ReplicatedObject ReplicatedObject `json:"columns"` +} + +type ReplicateAdd struct { + service.BaseMessage + ReplicatedObject ReplicatedObject `json:"columns"` +} + +type ReplicateDel struct { + service.BaseMessage + ReplicatedObject ReplicatedObject `json:"columns"` +} + +type ReplicatedObject struct { + Guid string `json:"guid"` // ReplicationString guid Globally unique identifier + H323 string `json:"h323"` // ReplicationString h323 Username + Pwd string `json:"pwd"` // ReplicationString pwd Password + Cn string `json:"cn"` // ReplicationString cn Common name + Dn string `json:"dn"` // ReplicationString dn Display name + AppsMy string `json:"apps-my"` // ReplicationString apps-my List of the apps displayed on the home screen + Config string `json:"config"` // ReplicationString config Config template + Node string `json:"node"` // ReplicationString node Node + Loc string `json:"loc"` // ReplicationString loc Location + Hide bool `json:"hide"` // ReplicationBool hide Hide from LDAP + E164 string `json:"e164"` // ReplicationString e164 Phone number + Cfpr bool `json:"cfpr"` // ReplicationTristate cfpr Call forward based on Presence + Tcfpr string `json:"t-cfpr"` // ReplicationTristate t-cfpr Call forward based on Presence inherited from the config template + Pseudo string `json:"pseudo"` // ReplicationString pseudo Pseudo information of the object + H323email bool `json:"h323-email"` // ReplicationBool h323-email If true, the email is the username + Apps string `json:"apps"` // ReplicationString apps List of the apps that the user has rights to access + Fax bool `json:"fax"` // ReplicationBool fax If true, the user has a fax license + Emails []struct { + Email string `json:"email"` // ReplicationString email Email + } `json:"emails"` // emails Table with the emails of the users + Allows []struct { + Name string `json:"name"` // ReplicationString name Filter name + Grp bool `json:"grp"` // ReplicationString grp If true, the name is a group name + Visible bool `json:"visible"` // ReplicationBool visible Visible + Online bool `json:"online"` // ReplicationBool online Online + Presence bool `json:"presence"` // ReplicationBool presence Presence + Otf bool `json:"otf"` // ReplicationBool otf On the phone + Note bool `json:"note"` // ReplicationBool note Presence note + Dialog bool `json:"dialog"` // ReplicationBool dialog Calls + Ids bool `json:"ids"` // ReplicationBool ids Calls with id + } `json:"allows"` // allows Table with the visibility filters defined for the user + Tallows []struct { + Name string `json:"name"` // ReplicationString name Filter name + Grp bool `json:"grp"` // ReplicationString grp If true, the name is a group name + Visible bool `json:"visible"` // ReplicationBool visible Visible + Online bool `json:"online"` // ReplicationBool online Online + Presence bool `json:"presence"` // ReplicationBool presence Presence + Otf bool `json:"otf"` // ReplicationBool otf On the phone + Note bool `json:"note"` // ReplicationBool note Presence note + Dialog bool `json:"dialog"` // ReplicationBool dialog Calls + Ids bool `json:"ids"` // ReplicationBool ids Calls with id + } `json:"t-allows"` // t-allows Table with the visibility filters defined on the config templates + Grps []struct { + Name string `json:"name"` // ReplicationString name Group name + Mode string `json:"mode"` // ReplicationString mode Mode + Dyn string `json:"dyn"` // ReplicationString dyn Dynamic + } `json:"grps"` // grps Table with the users groups + Devices []struct { + Hw string `json:"hw"` // ReplicationString hw Hardware ID + Text string `json:"text"` // ReplicationString text Name + App string `json:"app"` // ReplicationString app App + Admin bool `json:"admin"` // ReplicationBool admin PBX Pwd + Nofilter bool `json:"no-filter"` // ReplicationBool no-filter No IP Filter + Tls bool `json:"tls"` // ReplicationBool tls TLS only + Nomob bool `json:"no-mob"` // ReplicationBool no-mob No Mobility + Trusted bool `json:"trusted"` // ReplicationBool trusted Reverse Proxy + Sreg bool `json:"sreg"` // ReplicationBool sreg Single Reg. + Mr bool `json:"mr"` // ReplicationBool mr Media Relay + Voip string `json:"voip"` // ReplicationString voip Config VOIP + Gkid string `json:"gk-id"` // ReplicationString gk-id Gatekeeper ID + Prim string `json:"prim"` // ReplicationString prim Primary gatekeeper + } `json:"devices"` // devices Table with the users devices + Cds []struct { + Type string `json:"type"` // ReplicationString type Diversion type (cfu` cfb or cfnr) + Bool string `json:"bool"` // ReplicationString bool Boolean object + Boolnot bool `json:"bool-not"` // ReplicationBool bool-not Not flag (boolean object) + E164 string `json:"e164"` // ReplicationString e164 Phone number + H323 string `json:"h323"` // ReplicationString h323 Username + Src string `json:"src"` // ReplicationString src Filters data on XML format + } `json:"cds"` // cds Table with the users call diversions + Forks []struct { + E164 string `json:"e164"` // ReplicationString e164 Phone number + H323 string `json:"h323"` // ReplicationString h323 Username + Bool string `json:"bool"` // ReplicationString bool Boolean object + Boolnot bool `json:"bool-not"` // ReplicationBool bool-not Not flag (boolean object) + Mobility string `json:"mobility"` // ReplicationString mobility Mobility object + App string `json:"app"` // ReplicationString app App + Delay int `json:"delay"` // ReplicationUnsigned delay Delay + Hw string `json:"hw"` // ReplicationString hw Device + Off bool `json:"off"` // ReplicationBool off Disable + Cw bool `json:"cw"` // ReplicationBool cw Call-Waiting + Min int `json:"min"` // ReplicationUnsigned min Min-Alert + Max int `json:"max"` // ReplicationUnsigned max Max-Alert + } `json:"forks"` // forks Table with the users forks + Wakeups []struct { + H int `json:"h"` // ReplicationUnsigned h Hour + M int `json:"m"` // ReplicationUnsigned m Minute + S int `json:"s"` // ReplicationUnsigned s Second + Name string `json:"name"` // ReplicationString name + Num string `json:"num"` // ReplicationString num + Retry int `json:"retry"` // ReplicationUnsigned retry + Mult bool `json:"mult"` // ReplicationBool mult + To int `json:"to"` // ReplicationUnsigned to + Fallback string `json:"fallback"` // ReplicationString fallback + Bool string `json:"bool"` // ReplicationString bool Boolean object + Boolnot bool `json:"bool-not"` // ReplicationBool bool-not Not flag (boolean object) + } `json:"wakeups"` // wakeups Table with the users wakeups +} + +/* +Pseudo Types + +List of known pseudo types of pbx objects +*/ +var PseudoTypes = []string{ + "app", + "gw", + "loc", + "waiting", + "executive", + "bool", + "trunk", + "", // users +} + +var AllFields = []string{} +var AllColumns = map[string]Column{} // all known Columns to subscribe to + +func init() { + + AllFields = append(AllFields, TableUsers...) + AllFields = append(AllFields, "emails") + AllFields = append(AllFields, "allows") + AllFields = append(AllFields, "t-allows") + AllFields = append(AllFields, "grps") + AllFields = append(AllFields, "devices") + AllFields = append(AllFields, "cds") + AllFields = append(AllFields, "forks") + AllFields = append(AllFields, "wakeups") + + for _, col := range AllFields { + AllColumns[col] = Column{Udate: true} + } +} + +/* +Tables +*/ + +// Table with the user data +var TableUsers = []string{ + "guid", // ReplicationString guid Globally unique identifier + "h323", // ReplicationString h323 Username + "pwd", // ReplicationString pwd Password + "cn", // ReplicationString cn Common name + "dn", // ReplicationString dn Display name + "apps-my", // ReplicationString apps-my List of the apps displayed on the home screen + "config", // ReplicationString config Config template + "node", // ReplicationString node Node + "loc", // ReplicationString loc Location + "hide", // ReplicationBool hide Hide from LDAP + "e164", // ReplicationString e164 Phone number + "cfpr", // ReplicationTristate cfpr Call forward based on Presence + "t-cfpr", // ReplicationTristate t-cfpr Call forward based on Presence inherited from the config template + "pseudo", // ReplicationString pseudo Pseudo information of the object + "h323-email", // ReplicationBool h323-email If true, the email is the username + "apps", // ReplicationString apps List of the apps that the user has rights to access + "fax", // ReplicationBool fax If true, the user has a fax license +} + +type PbxTableUsersEvent struct { + Type int + Connection *service.AppServicePbxConnection + Object *ReplicatedObject +} + +const PbxTableUsersEventDisconnect = -20 +const PbxTableUsersEventConnect = -10 +const PbxTableUsersEventInitial = 0 +const PbxTableUsersEventInitialDone = 5 +const PbxTableUsersEventAdd = 10 +const PbxTableUsersEventUpdate = 20 +const PbxTableUsersEventDelete = 30 diff --git a/service/rcc/api.go b/service/rcc/api.go new file mode 100644 index 0000000..da87c5a --- /dev/null +++ b/service/rcc/api.go @@ -0,0 +1,25 @@ +package rcc + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type RCC struct { +} + +func (api *RCC) GetApiName() string { + return "RCC" +} + +func (api *RCC) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *RCC) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *RCC) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..e552714 --- /dev/null +++ b/service/service.go @@ -0,0 +1,302 @@ +package service + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + + logger "github.com/chi-middleware/logrus-logger" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + log "github.com/sirupsen/logrus" +) + +type AppService struct { + ListenIp string + ListenPort int + ListenPortTls int + TlsCertString string + TlsKeyString string + Domain string + Instance string + Name string + Password string + + Fs http.FileSystem + ApiHandler []PbxApiInterface + + Connections []*AppServicePbxConnection // list of current connected websocket connections + ConnectionsMutext sync.Mutex +} + +func NewAppService(ip string, port int, portTls int, tlsCert string, tlsCertKey string, domain, name, instance, password string, fS http.FileSystem) (*AppService, error) { + if portTls != 0 && (tlsCert == "" || tlsCertKey == "") { + return nil, fmt.Errorf("when Tls Port set, a Certificate has to be set too. generate one with 'openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt'") + } + + return &AppService{ + ListenIp: ip, + ListenPort: port, + ListenPortTls: portTls, + TlsCertString: tlsCert, // X509 Certificate + TlsKeyString: tlsCertKey, // X509 Private Key + Domain: domain, + Instance: instance, + Name: name, + Password: password, + Fs: fS, + }, nil +} + +func (s *AppService) Start() error { + rootpath := fmt.Sprintf("/%s/%s/%s/", s.Domain, s.Name, s.Instance) + log.Infof("register service with path: %s", rootpath) + + mux := http.NewServeMux() + + router := chi.NewRouter() + logs := log.New() + + router.Use(logger.Logger("router", logs)) + + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(middleware.Recoverer) + router.Use(middleware.SetHeader("Server", s.Name)) + + mux.HandleFunc(fmt.Sprintf("/%s/%s/%s", s.Domain, s.Name, s.Instance), func(w http.ResponseWriter, r *http.Request) { + handleConnectionForHttpOrWebsocket(s, w, r) + }) + + // the router for the Webpage + router_api := chi.NewRouter() + //GetHttpRoutes(s, router_api) + router_api.Get("/*", + http.StripPrefix(fmt.Sprintf("/%s/%s/%s/", s.Domain, s.Name, s.Instance), + http.FileServer(s.Fs), + ).ServeHTTP, + ) + + router.Mount(rootpath, router_api) + mux.Handle(rootpath, router) + + mux.HandleFunc("/manager/fixcert.htm", s.HandleManagerFixCertResponse) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log. + WithField("path", r.URL.Path). + Info("404 not found") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("not found")) + }) + + go func() { + if s.ListenPort > 0 { + log.Infof("starting Http %s:%v", s.ListenIp, s.ListenPort) + if err := http.ListenAndServe(fmt.Sprintf("%s:%v", s.ListenIp, s.ListenPort), mux); err != nil { + log.Errorf("Error starting Http server: '%s:%d' %v ", s.ListenIp, s.ListenPort, err) + } + } + }() + + go func() { + if s.ListenPortTls > 0 { + log.Infof("starting Http/Tls %s:%v", s.ListenIp, s.ListenPortTls) + err := s.StartTls(mux) + if err != nil { + log.Errorf("Error starting Http/Tls server: '%s:%d' %v ", s.ListenIp, s.ListenPortTls, err) + } + } + }() + + return nil +} + +/* +openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt +*/ +func (s *AppService) StartTls(mux *http.ServeMux) error { + + cert, err := tls.X509KeyPair([]byte(s.TlsCertString), []byte(s.TlsKeyString)) + if err != nil { + log.Errorf("Error loading certificate:", err) + return err + } + + server := &http.Server{ + Addr: fmt.Sprintf("%s:%v", s.ListenIp, s.ListenPortTls), + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + Handler: mux, + } + + return server.ListenAndServeTLS("", "") +} + +func (s *AppService) RegisterHandler(handler PbxApiInterface) error { + s.ApiHandler = append(s.ApiHandler, handler) + return nil +} + +func (s *AppService) HandleApiConnected(connection *AppServicePbxConnection, msg []byte) { + for _, apiName := range connection.PbxInfo.Apis { + for _, handler := range s.ApiHandler { + if handler.GetApiName() == apiName { + handler.OnConnect(connection) + } + } + } +} +func (s *AppService) HandleUserConnected(connection *AppServicePbxConnection, msg []byte) { + for _, handler := range s.ApiHandler { + if handler.GetApiName() == "user" { + handler.OnConnect(connection) + } + } +} +func (s *AppService) HandleAdminConnected(connection *AppServicePbxConnection, msg []byte) { + for _, handler := range s.ApiHandler { + if handler.GetApiName() == "admin" { + handler.OnConnect(connection) + } + } +} + +func (s *AppService) HandleApiDisConnected(connection *AppServicePbxConnection) { + for _, apiName := range connection.PbxInfo.Apis { + for _, handler := range s.ApiHandler { + if handler.GetApiName() == apiName { + handler.OnDisconnect(connection) + } + } + } + + log.Debug("on disconnect of ", connection.AppLogin.App) + if strings.HasSuffix(connection.AppLogin.App, ".htm") { + app := strings.TrimSuffix(connection.AppLogin.App, ".htm") + for _, handler := range s.ApiHandler { + if handler.GetApiName() == app { + handler.OnDisconnect(connection) + } + } + } +} + +func (s *AppService) HandleApiMessage(connection *AppServicePbxConnection, apiName string, message []byte) { + msg := BaseMessage{} + if err := json.Unmarshal(message, &msg); err != nil { + connection.log().Errorf("server: error unmarshalling message: %v", err) + } + for _, handler := range s.ApiHandler { + if handler.GetApiName() == apiName { + handler.HandleMessage(connection, &msg, message) + return + } + } + log.Warnf("no handler for api '%s'", apiName) +} +func (s *AppService) HandleManagerFixCertResponse(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + + body := ` + + + + + + + You can close this page now. + +` + w.Write([]byte(body)) +} + +func (s *AppService) AddConnection(connection *AppServicePbxConnection) { + s.ConnectionsMutext.Lock() + defer s.ConnectionsMutext.Unlock() + s.Connections = append(s.Connections, connection) +} + +func (s *AppService) DeleteConnection(connection *AppServicePbxConnection) { + s.ConnectionsMutext.Lock() + defer s.ConnectionsMutext.Unlock() + + for i, value := range s.Connections { + if value == connection { + // Remove the item from the slice by slicing it + s.Connections = append(s.Connections[:i], s.Connections[i+1:]...) + break + } + } +} + +/* +send the message to all connected Users or Admins +*/ +func (s *AppService) SendToAll(message []byte) { + log.Debug("SendToAll") + s.SendToAllAdmins(message) + s.SendToAllUsers(message) +} + +/* +send the message to all connected Users +*/ +func (s *AppService) SendToAllUsers(message []byte) { + log.Debug("SendToAllUsers") + for _, connection := range s.Connections { + log.Tracef("SendToAllUsers %s", connection.AppLogin.App) + if connection.AppLogin.App == "user.htm" { + log.Tracef("SendToAllUsers %s", string(message)) + connection.WriteMessage(message) + } + } +} + +/* +send the message to all connected Admins +*/ +func (s *AppService) SendToAllAdmins(message []byte) { + log.Debug("SendToAllAdmins") + for _, connection := range s.Connections { + log.Tracef("SendToAllAdmins %s", connection.AppLogin.App) + if connection.AppLogin.App == "admin.htm" { + log.Tracef("SendToAllAdmins %s", string(message)) + connection.WriteMessage(message) + } + } +} + +/* +send the message to all connected clients of a specific user + +sip is a string of the sip/h323 name of the user +*/ +func (s *AppService) SendToAllConnectionsOfSip(message []byte, sip string) { + log.Debug("SendToAllConnectionsOfSip") + + for _, connection := range s.Connections { + if connection.AppLogin.Sip == sip { + log.Tracef("SendToAllConnectionsOfSip ", sip, string(message)) + connection.WriteMessage(message) + } + } +} + +type PbxApiInterface interface { + GetApiName() string + OnConnect(connection *AppServicePbxConnection) + OnDisconnect(connection *AppServicePbxConnection) + HandleMessage(connection *AppServicePbxConnection, msg *BaseMessage, message []byte) +} + +type Guid string diff --git a/service/services/api.go b/service/services/api.go new file mode 100644 index 0000000..a985253 --- /dev/null +++ b/service/services/api.go @@ -0,0 +1,25 @@ +package services + +import ( + "github.com/ricoschulte/go-myapps/service" + log "github.com/sirupsen/logrus" +) + +type Services struct { +} + +func (api *Services) GetApiName() string { + return "Services" +} + +func (api *Services) OnConnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnConnect not implemented ") +} + +func (api *Services) OnDisconnect(connection *service.AppServicePbxConnection) { + log.WithField("api", api.GetApiName()).Warn("OnDisconnect not implemented ") +} + +func (api *Services) HandleMessage(connection *service.AppServicePbxConnection, msg *service.BaseMessage, message []byte) { + log.WithField("api", api.GetApiName()).Warn("HandleMessage not implemented ") +} diff --git a/service/websockets.go b/service/websockets.go new file mode 100644 index 0000000..dff9ede --- /dev/null +++ b/service/websockets.go @@ -0,0 +1,436 @@ +package service + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +var challenges = make(map[*websocket.Conn]string) + +type BaseMessage struct { + Api string `json:"api"` + Mt string `json:"mt"` + Src string `json:"src,omitempty"` +} + +type AppChallengeResult struct { + Mt string `json:"mt"` + Challenge string `json:"challenge"` +} + +type AppLogin struct { + BaseMessage + Sip string `json:"sip"` + Guid string `json:"guid"` + Dn string `json:"dn"` + Digest string `json:"digest"` + Domain string `json:"domain"` + App string `json:"app"` + //Info AppLoginInfo `json:"info"` + Info json.RawMessage `json:"info"` + PbxObj string `json:"pbxObj,omitempty"` +} + +func (al AppLogin) GetInfoObject() (*AppLoginInfo, error) { + obj := AppLoginInfo{} + err := json.Unmarshal(al.Info, &obj) + if err != nil { + return nil, err + } + return &obj, err +} + +func (al AppLogin) InfoAsPbxobjectDigestString() (string, error) { + msg, err := json.Marshal(al.Info) + return string(msg), err +} + +func (al AppLogin) InfoAsUserDigestString() (string, error) { + msg, err := json.Marshal(al.Info) + return string(msg), err +} + +type AppLoginInfo struct { + Appobj string `json:"appobj"` + Appdn string `json:"appdn"` + Appurl string `json:"appurl"` + Pbx string `json:"pbx"` + Cn string `json:"cn"` + Unlicensed bool `json:"unlicensed,omitempty"` + Testmode bool `json:"testmode,omitempty"` + Groups []string `json:"groups"` + Apps []string `json:"apps"` +} + +func (a *AppLoginInfo) MarshalJSON() ([]byte, error) { + type Alias AppLoginInfo + if len(a.Groups) == 0 { + a.Groups = []string{} + } + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(a), + }) +} + +type AppLoginResult struct { + BaseMessage + App string `json:"app"` + Domain string `json:"domain"` + Ok bool `json:"ok"` +} + +type AppInfo struct { + BaseMessage + App string `json:"app"` +} + +type AppInfoInfo struct { + Hidden bool `json:"hidden"` + Apis struct { + ComInnovaphoneSearch map[string]interface{} `json:"com.innovaphone.search"` + } `json:"apis"` +} + +type AppInfoServiceInfo struct { + Apis struct { + ComInnovaphoneReplicator map[string]interface{} `json:"com.innovaphone.replicator"` + } `json:"apis"` +} + +type AppInfoResult struct { + BaseMessage + App string `json:"app"` + Info AppInfoInfo `json:"info"` + Serviceinfo AppInfoServiceInfo `json:"sericeinfo"` +} + +type PbxInfo struct { + BaseMessage + Domain string `json:"domain"` + Pbx string `json:"pbx"` + PbxDns string `json:"pbxDns"` + Apis []string `json:"apis"` + Version string `json:"version"` + Build string `json:"build"` +} + +type AppServicePbxConnection struct { + AppService *AppService + conn *websocket.Conn + PbxInfo PbxInfo + AppLogin AppLogin + Info *AppLoginInfo + Authenticated bool + WriteMutext sync.Mutex +} + +func NewAppServicePbxConnection(appservice *AppService, conn *websocket.Conn) *AppServicePbxConnection { + return &AppServicePbxConnection{ + AppService: appservice, + conn: conn, + } +} +func (connection *AppServicePbxConnection) log() *log.Entry { + return log. + WithField("Domain", connection.AppService.Domain). + WithField("Instance", connection.AppService.Instance). + WithField("name", connection.AppService.Name). + WithField("pbx", connection.PbxInfo.Pbx). + WithField("pbxDns", connection.PbxInfo.PbxDns) +} + +func (connection *AppServicePbxConnection) Loop() { + + for { + // Read message + _, message, err := connection.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + connection.log().Errorf("websocket error: %s", err) + return + } + if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { + connection.log().Debugf("websocket closed: %s", err) + return + } + break + } + + connection.log().Tracef("received message: %s", message) + + // Unmarshal message + var msg map[string]interface{} + if err := json.Unmarshal(message, &msg); err != nil { + connection.log().Errorf("server: error unmarshalling message: %v", err) + continue + } + + // Check message type + mt, ok := msg["mt"].(string) + if !ok { + connection.log().Warning("received a message without 'mt'") + continue + } + var response []byte + var hErr error + + switch mt { + case "AppChallenge": + response, hErr = connection.handleAppChallenge(connection.conn, message) + case "AppLogin": + response, hErr = connection.handleAppLogin(challenges[connection.conn], message) + case "AppInfo": + response, hErr = connection.handleAppInfo(connection.conn, message) + case "PbxInfo": + var pbxInfo PbxInfo + + if err := json.Unmarshal([]byte(message), &pbxInfo); err != nil { + connection.log().Errorf("unmarshal PbxInfo failed: %v", err) + } + connection.PbxInfo = pbxInfo + connection.AppService.HandleApiConnected(connection, message) + + default: + if _, ok := msg["api"]; ok { + // Key "api" exists in the map + if !connection.Authenticated { + log.Warn("message for a api received but connection isnt authenticated. Closing connection.") + connection.conn.Close() + + } else { + // look for a api handler in app service + go func() { + connection.AppService.HandleApiMessage(connection, msg["api"].(string), message) + }() + } + continue + } else { + connection.log().Warnf("unknown mt: %s", string(message)) + response = []byte("{\"mt\":\"Error\",\"text\":\"unknown mt '" + mt + "'\"}") + connection.WriteMessage(response) + continue + } + + } + + // check if handler function returns an error + if hErr != nil { + connection.log().Errorf("error handling message: %v", hErr) + continue + } + + if string(response) == "" { + response = []byte("{\"mt\":\"Error\",\"text\":\"the server has no content to send\"}") + connection.WriteMessage(response) + } else { + connection.log().Tracef("sending Response: %s", response) + // send response + connection.WriteMessage(response) + } + } +} + +func (connection *AppServicePbxConnection) WriteMessage(message []byte) error { + connection.WriteMutext.Lock() + defer connection.WriteMutext.Unlock() + err := connection.conn.WriteMessage(websocket.TextMessage, message) + if err != nil { + log.Errorf("Error writing message:", err) + return err + } + return nil +} + +func (connection *AppServicePbxConnection) handleAppChallenge(conn *websocket.Conn, msg []byte) ([]byte, error) { + response := &AppChallengeResult{ + Mt: "AppChallengeResult", + Challenge: challenges[conn], + } + return json.Marshal(response) +} + +func (connection *AppServicePbxConnection) handleAppLogin(challenge string, msg []byte) ([]byte, error) { + var msgin AppLogin + if err := json.Unmarshal([]byte(msg), &msgin); err != nil { + return nil, err + } + info, err := msgin.GetInfoObject() + if err != nil { + return nil, err + } + connection.AppLogin = msgin + connection.Info = info + mu := &MyAppsUtils{} + calculated_digest, err := mu.GetDigestForAppLoginFromJson(string(msg), connection.AppService.Password, challenge) + if err != nil { + return nil, err + } + + if msgin.Digest == calculated_digest { + log. + WithField("Domain", connection.AppService.Domain). + WithField("Instance", connection.AppService.Instance). + WithField("name", connection.AppService.Name). + WithField("app", msgin.App). + WithField("sip", msgin.Sip). + Debug("appservice login successful") + response := AppLoginResult{ + BaseMessage: BaseMessage{ + Mt: "AppLoginResult", + }, + App: msgin.App, + Domain: msgin.Domain, + Ok: true, + } + connection.Authenticated = true + go func() { + app := strings.TrimSuffix(msgin.App, ".htm") + switch app { + case "user": + + connection.AppService.HandleUserConnected(connection, msg) + case "admin": + connection.AppService.HandleAdminConnected(connection, msg) + default: + log.Warnf("unknown App '%s'", app) + } + }() + return json.Marshal(response) + } else { + log. + WithField("Domain", connection.AppService.Domain). + WithField("Instance", connection.AppService.Instance). + WithField("name", connection.AppService.Name). + WithField("app", msgin.App). + WithField("sip", msgin.Sip). + Warn("appservice login failed: digest not correct") + + response := AppLoginResult{ + BaseMessage: BaseMessage{ + Mt: "AppLoginResult", + }, + App: msgin.App, + Domain: msgin.Domain, + Ok: false, + } + connection.Authenticated = false + return json.Marshal(response) + } + +} + +func (connection *AppServicePbxConnection) handleAppInfo(conn *websocket.Conn, msg []byte) ([]byte, error) { + log. + WithField("msg", string(msg)). + Trace("handleAppInfo") + var msgin AppInfo + if err := json.Unmarshal([]byte(msg), &msgin); err != nil { + log. + WithField("msg", string(msg)). + Errorf("could not unmashal AppInfo: %v", err) + return nil, err + } + resp := `{ + "mt": "AppInfoResult", + "app": "%s", + "info": { + "hidden": %t, + "apis": { + "com.innovaphone.search": {} + } + }, + "serviceInfo": { + "apis": { + "com.innovaphone.replicator": {} + } + } + }` + var jsn string + switch msgin.App { + default: + jsn = fmt.Sprintf(resp, msgin.App, false) + + case "searchapi": + jsn = fmt.Sprintf(resp, msgin.App, true) + + } + + // pasre from json + var respmsg AppInfoResult + if err := json.Unmarshal([]byte(jsn), &respmsg); err != nil { + log. + WithField("msg", string(msg)). + Errorf("could not unmashal AppInfoResult: %v", err) + return nil, err + } + + // to json and return + return json.Marshal(respmsg) +} + +func HandleWebsocket(appservice *AppService, w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + mlog := log. + WithField("Domain", appservice.Domain). + WithField("Instance", appservice.Instance). + WithField("name", appservice.Name) + + defer func() { + mlog.Debug("server: websocket handler ended") + // handling panic "repeated read on failed websocket connection" + // This will recover from the panic and log the error, then close the connection by sending a 500 Internal Server Error response to the client. + // This will prevent the panic from propagating and crashing your server. + if r := recover(); r != nil { + log. + WithField("webserver", handleConnectionForHttpOrWebsocket). + Debugf("Recovered from panic: %v", r) + w.WriteHeader(http.StatusInternalServerError) + + } + }() + mlog.Debug("server: websocket handler started") + + conn, err := upgrader.Upgrade(w, req, nil) + if err != nil { + mlog.Errorf("upgrading websocket failed: %s", err) + return + } + connection := NewAppServicePbxConnection(appservice, conn) + appservice.AddConnection(connection) + defer func() { + conn.Close() + appservice.DeleteConnection(connection) + appservice.HandleApiDisConnected(connection) + }() + + // Generate challenge for the connection + mu := &MyAppsUtils{} + challenge := mu.GetRandomHexString(16) + challenges[conn] = challenge + + select { + case <-ctx.Done(): + err := ctx.Err() + + mlog.Errorf("websocket error: %s", err) + internalError := http.StatusInternalServerError + http.Error(w, err.Error(), internalError) + delete(challenges, conn) + return + default: + connection.Loop() + } +}