From 045898a1bb2b4f6c26c2eb749cc57916b45c2bb9 Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Fri, 11 Sep 2026 23:39:22 +0200 Subject: [PATCH 1/3] fix(fanvil): tolerate absent servers Accept both missing-server responses during cleanup so first-time registrations can continue. Return other errors safely when they have no wrapped cause. Assisted-by: Codex:GPT-6-Astra --- providers/fanvil.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/providers/fanvil.go b/providers/fanvil.go index 35bfb07..0b0bb76 100644 --- a/providers/fanvil.go +++ b/providers/fanvil.go @@ -85,8 +85,12 @@ func (d FanvilDevice) Register() error { err = fanvilParseResponse(resp.Body) - if (err != nil) && (errors.Unwrap(err).Error() != "Error:server_not_exist") { - return err + if err != nil { + 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 + } } //Create Server From e409e0a9ed19f746fe3b35cec93401ca04d8325f Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Fri, 11 Sep 2026 23:40:21 +0200 Subject: [PATCH 2/3] fix(fanvil): deregister before replacing servers Fanvil refuses to delete a server while its phone is registered. Deregister the phone before replacing its server to prevent the subsequent creation from failing because the name already exists. Check the deregistration response and allow an absent device. Assisted-by: Codex:GPT-6-Astra --- providers/fanvil.go | 74 +++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/providers/fanvil.go b/providers/fanvil.go index 0b0bb76..0d128e0 100644 --- a/providers/fanvil.go +++ b/providers/fanvil.go @@ -53,13 +53,51 @@ func (d FanvilDevice) Register() error { password := fanvilGetPassword() + // Fanvil refuses to delete a server while a device is still registered to it. + buf, _ := xml.EncodeClientRequest("redirect.deRegisterDevice", + &struct { + Mac string + }{Mac: d.Mac}) + + req, _ := http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, + bytes.NewReader(buf)) + + req.SetBasicAuth(configuration.Config.Providers.Fanvil.User, password) + + req.Header.Set("Content-Type", "text/xml") + req.Header.Set("User-Agent", " Falconieri/1") + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + return models.ProviderError{ + Message: "connection_to_remote_provider_failed", + WrappedError: err, + } + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.New("provider_remote_call_failed") + } + + err = fanvilParseResponse(resp.Body) + 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 + } + } + //Delete old server - buf, _ := xml.EncodeClientRequest("redirect.deleteServer", + buf, _ = xml.EncodeClientRequest("redirect.deleteServer", &struct { GroupName string }{GroupName: d.Mac}) - req, _ := http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, + req, _ = http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, bytes.NewReader(buf)) req.SetBasicAuth(configuration.Config.Providers.Fanvil.User, password) @@ -67,7 +105,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 = http.DefaultClient.Do(req) if err != nil { return models.ProviderError{ @@ -154,36 +192,6 @@ func (d FanvilDevice) Register() error { return err } - //Deregister the device - buf, _ = xml.EncodeClientRequest("redirect.deRegisterDevice", - &struct { - Mac string - }{Mac: d.Mac}) - - req, _ = http.NewRequest("POST", configuration.Config.Providers.Fanvil.RpcUrl, - bytes.NewReader(buf)) - - req.SetBasicAuth(configuration.Config.Providers.Fanvil.User, password) - - req.Header.Set("Content-Type", "text/xml") - req.Header.Set("User-Agent", " Falconieri/1") - - resp, err = http.DefaultClient.Do(req) - - if err != nil { - return models.ProviderError{ - Message: "connection_to_remote_provider_failed", - WrappedError: err, - } - - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return errors.New("provider_remote_call_failed") - } - //Register the device buf, _ = xml.EncodeClientRequest("redirect.registerDevice", &struct { From 4089d16b8843ad49b123b5a7ed09457e82821a74 Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Mon, 14 Sep 2026 14:21:05 +0200 Subject: [PATCH 3/3] fix(fanvil): make replacement retryable Validate the replacement URL before deregistering the phone. Serialize updates for the same MAC and bound every Fanvil request with a timeout. Missing devices and groups remain valid cleanup states, so clients can repeat the request after a partial or transient failure. The phone can remain unregistered until a retry succeeds. Review: https://github.com/nethesis/falconieri/pull/76#discussion_r4004306868 Assisted-by: Codex:GPT-5.6-Sol --- providers/fanvil.go | 91 +++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/providers/fanvil.go b/providers/fanvil.go index 0d128e0..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,11 +48,70 @@ 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() // Fanvil refuses to delete a server while a device is still registered to it. buf, _ := xml.EncodeClientRequest("redirect.deRegisterDevice", @@ -67,7 +127,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{ @@ -105,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{ @@ -131,23 +191,6 @@ func (d FanvilDevice) Register() error { } } - //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" - } - /* Server/Group configuration: * - Disable DHCP provision: cfgDhcpOpt=false * - Disable PnP provision: cfgPnpEnable=false @@ -170,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{ @@ -207,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{ @@ -251,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")