diff --git a/providers/fanvil.go b/providers/fanvil.go index 35bfb07..0e22e7d 100644 --- a/providers/fanvil.go +++ b/providers/fanvil.go @@ -28,12 +28,13 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" "regexp" "strconv" "strings" + "sync" + "time" "github.com/divan/gorilla-xmlrpc/xml" @@ -47,17 +48,76 @@ type FanvilDevice struct { Url string } -var fanvilPassword string +type fanvilDeviceLock struct { + sync.Mutex + users int +} + +var ( + fanvilPassword string + fanvilPasswordMutex sync.Mutex + fanvilHTTPClient = &http.Client{Timeout: 30 * time.Second} + fanvilDeviceLocks = struct { + sync.Mutex + devices map[string]*fanvilDeviceLock + }{devices: make(map[string]*fanvilDeviceLock)} +) + +// fanvilLockDevice prevents requests for the same phone from interleaving. +func fanvilLockDevice(mac string) func() { + fanvilDeviceLocks.Lock() + lock := fanvilDeviceLocks.devices[mac] + if lock == nil { + lock = &fanvilDeviceLock{} + fanvilDeviceLocks.devices[mac] = lock + } + lock.users++ + fanvilDeviceLocks.Unlock() + + lock.Lock() + return func() { + lock.Unlock() + fanvilDeviceLocks.Lock() + defer fanvilDeviceLocks.Unlock() + lock.users-- + if lock.users == 0 { + delete(fanvilDeviceLocks.devices, mac) + } + } +} func (d FanvilDevice) Register() error { + Url, err := url.Parse(d.Url) + if err != nil || Url.Hostname() == "" { + return errors.New("malformed_url") + } + + var UrlScheme string + switch Url.Scheme { + case "ftp": + UrlScheme = "1" + case "tftp": + UrlScheme = "2" + case "http": + UrlScheme = "4" + case "https": + UrlScheme = "5" + default: + return errors.New("malformed_url") + } + unlock := fanvilLockDevice(d.Mac) + defer unlock() + + fanvilPasswordMutex.Lock() password := fanvilGetPassword() + fanvilPasswordMutex.Unlock() - //Delete old server - buf, _ := xml.EncodeClientRequest("redirect.deleteServer", + // Fanvil refuses to delete a server while a device is still registered to it. + buf, _ := xml.EncodeClientRequest("redirect.deRegisterDevice", &struct { - GroupName string - }{GroupName: d.Mac}) + Mac string + }{Mac: d.Mac}) req, _ := http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, bytes.NewReader(buf)) @@ -67,14 +127,13 @@ func (d FanvilDevice) Register() error { req.Header.Set("Content-Type", "text/xml") req.Header.Set("User-Agent", " Falconieri/1") - resp, err := http.DefaultClient.Do(req) + resp, err := fanvilHTTPClient.Do(req) if err != nil { return models.ProviderError{ Message: "connection_to_remote_provider_failed", WrappedError: err, } - } defer resp.Body.Close() @@ -84,41 +143,19 @@ func (d FanvilDevice) Register() error { } err = fanvilParseResponse(resp.Body) - - if (err != nil) && (errors.Unwrap(err).Error() != "Error:server_not_exist") { - return err - } - - //Create Server - var Url *url.URL - var UrlScheme string - - Url, err = url.Parse(d.Url) - - switch Url.Scheme { - case "ftp": - UrlScheme = "1" - case "tftp": - UrlScheme = "2" - case "http": - UrlScheme = "4" - case "https": - UrlScheme = "5" + if err != nil { + cause := errors.Unwrap(err) + // A device that has never been registered needs no cleanup. + if cause == nil || cause.Error() != "Error:device_not_exist" { + return err + } } - /* Server/Group configuration: - * - Disable DHCP provision: cfgDhcpOpt=false - * - Disable PnP provision: cfgPnpEnable=false - * - Apply the configuration after the reboot: cfgPfMode=1 - */ - - buf, _ = xml.EncodeClientRequest("redirect.addMaterialServer", + //Delete old server + buf, _ = xml.EncodeClientRequest("redirect.deleteServer", &struct { - ServerConfigs []string - }{ServerConfigs: []string{"cfgName=" + d.Mac, "cfgPfMode=1", - "cfgDhcpOpt=false", "cfgPnpEnable=false", - "cfgPfSrv=" + Url.Host, "cfgPfName=" + strings.TrimPrefix(Url.Path, "/"), - "cfgPfProt=" + UrlScheme}}) + GroupName string + }{GroupName: d.Mac}) req, _ = http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, bytes.NewReader(buf)) @@ -128,7 +165,7 @@ func (d FanvilDevice) Register() error { req.Header.Set("Content-Type", "text/xml") req.Header.Set("User-Agent", " Falconieri/1") - resp, err = http.DefaultClient.Do(req) + resp, err = fanvilHTTPClient.Do(req) if err != nil { return models.ProviderError{ @@ -147,14 +184,26 @@ func (d FanvilDevice) Register() error { err = fanvilParseResponse(resp.Body) if err != nil { - return err + cause := errors.Unwrap(err) + // An absent server is expected when registering a device for the first time. + if cause == nil || (cause.Error() != "Error:server_not_exist" && cause.Error() != "Error:server_not_existed") { + return err + } } - //Deregister the device - buf, _ = xml.EncodeClientRequest("redirect.deRegisterDevice", + /* Server/Group configuration: + * - Disable DHCP provision: cfgDhcpOpt=false + * - Disable PnP provision: cfgPnpEnable=false + * - Apply the configuration after the reboot: cfgPfMode=1 + */ + + buf, _ = xml.EncodeClientRequest("redirect.addMaterialServer", &struct { - Mac string - }{Mac: d.Mac}) + ServerConfigs []string + }{ServerConfigs: []string{"cfgName=" + d.Mac, "cfgPfMode=1", + "cfgDhcpOpt=false", "cfgPnpEnable=false", + "cfgPfSrv=" + Url.Host, "cfgPfName=" + strings.TrimPrefix(Url.Path, "/"), + "cfgPfProt=" + UrlScheme}}) req, _ = http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, bytes.NewReader(buf)) @@ -164,7 +213,7 @@ func (d FanvilDevice) Register() error { req.Header.Set("Content-Type", "text/xml") req.Header.Set("User-Agent", " Falconieri/1") - resp, err = http.DefaultClient.Do(req) + resp, err = fanvilHTTPClient.Do(req) if err != nil { return models.ProviderError{ @@ -180,6 +229,12 @@ func (d FanvilDevice) Register() error { return errors.New("provider_remote_call_failed") } + err = fanvilParseResponse(resp.Body) + + if err != nil { + return err + } + //Register the device buf, _ = xml.EncodeClientRequest("redirect.registerDevice", &struct { @@ -195,7 +250,7 @@ func (d FanvilDevice) Register() error { req.Header.Set("Content-Type", "text/xml") req.Header.Set("User-Agent", " Falconieri/1") - resp, err = http.DefaultClient.Do(req) + resp, err = fanvilHTTPClient.Do(req) if err != nil { return models.ProviderError{ @@ -239,7 +294,7 @@ func fanvilParseResponse(body io.ReadCloser) error { var response_regexp = `(?s).*(.*).*(.*).*` - respBytes, err := ioutil.ReadAll(body) + respBytes, err := io.ReadAll(body) if err != nil { return errors.New("read_remote_response_failed")