Audit tecnico — Haivision-go-sdk (2026-08-10)

Audit statico + verifica empirica di serializzazione/deserializzazione su main (commit d38bdc7, tag corrente v1.0.0).

🏷️ Questo documento è una fotografia al 2026-08-10 e NON viene aggiornato. La release v1.1.0 ha chiuso 14 item: tutti quelli marcati P1 in questa pagina, più B1/B3/B6 e la sostenibilità non-breaking. Non sono più veri i punti §C su make build rotto, i 5 file non formattati e la copertura a 0% (ora: make build funziona, gofmt pulito, 32 test, 37,9% su ./haivision/...), né il §D su x/net v0.7.0 (ora v0.35.0 — e GO-2026-4918 si è rivelata non raggiungibile, vedi l’item deps-x-net-vuln).

Restano aperti i findings breaking §B2, §B4, §B5, §B7, §B8 e §A6: CreateRoute* invia ancora il modello di risposta e ResponseStartOrRoute non deserializza l’array reale. Per lo stato corrente guarda sempre docs/roadmap.md e docs/backlog.md, non questa pagina.

Sommario

Area Stato
Build (go build ./...) OK
go vet ./... OK (nessun finding)
staticcheck ./... OK (nessun finding)
gofmt -l . 5 file non formattati
Test (go test ./...) Passano, ma copertura di haivision/ = 0%
make build FALLISCE (no Go files in <root>)
govulncheck ./... 1 vuln raggiungibile in dipendenza (golang.org/x/net v0.7.0) + 19 nella stdlib del toolchain
Correttezza contratto API 4 bug bloccanti (create/start/stop route e stats non funzionano come documentato)
                        BuildHaivision(url, debug, user, pass, header, insecure)
                                        |
        +-------------------------------+--------------------------------+
        | 1. resty.New() + SetBaseURL   | insecure != nil -> SkipVerify  |  <-- BUG A2 (false = skip)
        +-------------------------------+--------------------------------+
                                        |
                        2. POST /api/session  {username,password}         <-- credenziali in log se debug
                                        |  (status HTTP NON controllato)  <-- BUG A1
                                        v
                        3. SetCookie sessionID=<resp.Response.SessionID>  <-- vuoto se 401
                                        |
                        4. GET /api/devices  -> (*resp)[0].ID / .Type     <-- BUG A3 (panic se [])
                                        |
                        5. header.GetHeaders() -> SetHeader(...)          <-- BUG A4 (troppo tardi)
                                        v
                              IHaivisionClient
                                        |
        +---------------+---------------+----------------+---------------+
        v               v               v                v               v
  GetRoutes()    CreateRoute*()  StartOrStopRoute()  Get*Statistics()  GetSessionInfo()
  (raw resty)    BUG B1/B2       BUG B3/B4           BUG B5/B6         (scadenza ignorata)

A. Bug nel costruttore / client

A1 — Nessun controllo dello status HTTP (impatto: alto)

restyGet/restyPost in haivision/haivision.go:82-113 ritornano err == nil per qualsiasi risposta ricevuta, incluse 401/404/500. Tutti i metodi fanno json.Unmarshal(resp.Body(), &obj) direttamente.

Conseguenza concreta: con credenziali errate, InitSession deserializza il body d’errore in una BaseResponseInitSession a zero-value e ritorna nil come errore; BuildHaivision prosegue e imposta il cookie sessionID="". Il client risultante sembra valido e fallisce a ogni chiamata successiva senza un errore diagnostico.

Fix: controllare resp.IsError() e restituire un errore tipizzato che includa status e body troncato, in un unico punto (i due helper resty).

A2 — insecure *bool invertito per il valore false (impatto: alto, security)

haivision/builder.go:33-36:

if insecure != nil {
    haivisionClient.restClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}

Il valore puntato non viene mai letto. insecure := false; BuildHaivision(..., &insecure) disabilita la verifica del certificato TLS — l’opposto di quanto chiede il chiamante. L’unico modo di avere TLS verificato è passare nil.

Fix: if insecure != nil && *insecure. Meglio ancora: bool semplice, oppure una struct di opzioni.

A3 — Panic su lista device vuota (impatto: medio)

haivision/builder.go:47-50: (*deviceResponse)[0].ID senza controllo di lunghezza. Un gateway senza device registrati, o un body d’errore che deserializza in slice vuota (vedi A1), produce index out of range invece di un errore. Inoltre DeviceID/HType prendono sempre il primo device: setup multi-gateway non supportato.

A4 — Header custom applicati dopo il login (impatto: medio)

Gli header di HeaderConfigurator vengono impostati in builder.go:52-58, dopo InitSession e GetDeviceInfo. Quindi CreateBasicAuthHeader(...) e qualsiasi header richiesto da un reverse proxy davanti al gateway non vengono inviati sulle due chiamate di bootstrap: dietro un proxy con Basic auth il costruttore fallisce sempre. Gli header vanno impostati subito dopo SetBaseURL.

A5 — HealthCheck() non può fallire (impatto: medio)

haivision/haivision.go:70-76: il ramo d’errore fa return nil. La funzione ritorna nil in ogni caso — inutilizzabile come probe. In più fa GET su o.Url che, essendo già la BaseURL di resty, produce un path concatenato non intenzionale.

A6 — Nessun context.Context, nessun timeout (impatto: medio)

Nessun metodo di IHaivisionClient accetta un context e il client resty non ha SetTimeout. Una chiamata verso un gateway irraggiungibile può bloccarsi a lungo e non è cancellabile — problematico per i consumer che chiamano l’SDK dentro un handler HTTP.

A7 — Credenziali e sessionID nei log con debug: true (impatto: medio, security)

resty.SetDebug(true) logga i body delle richieste: POST /api/session finisce nei log con username e password in chiaro. debugPrint(resp) logga la risposta contenente il sessionID. Inoltre route.go/stats.go fanno log.Println incondizionato (ignorando il flag debug) sul logger globale del consumer, a ogni chiamata.


B. Bug nel contratto con la REST API Haivision

Tutti verificati eseguendo json.Marshal/json.Unmarshal sulle struct del repo con i payload letterali della documentazione Haivision.

B1 — Le struct annidate perdono il tag JSON: body "Fields" / "Parameters" (impatto: bloccante)

In haivision/route/model.go:37 e :99 i campi Fields e Parameters sono struct anonime senza tag JSON. Output reale:

start/stop: {"deviceID":"d1","command":"start-route","Parameters":{"routeID":"r1"}}
create:     {"action":"create","deviceID":"","elementType":"","Fields":{...}}

L’API documenta "parameters" e "fields" minuscoli. Il gateway non trova i campi attesi.

Fix: aggiungere json:"fields" / json:"parameters".

B2 — CreateRoute* invia il modello di risposta, non la richiesta documentata (impatto: bloccante)

Le quattro CreateRoute* in haivision/route.go accettano *route.RouteModel[TS,TD] — che è la forma di risposta (id, state, elapsedTime, summaryStatusCode, pendingUpdates, hasPendingDelete) — e la POSTano così com’è. Il body inviato non contiene né action, né deviceID, né elementType, né il wrapper fields richiesti da POST /api/devices/{id}/updates. La struct corretta, RequestCreateRoute (route/model.go:33), esiste ma è dead code: non è referenziata da nessuna parte.

B3 — StartOrStopRoute chiama l’endpoint sbagliato (impatto: bloccante)

La doc (e il commento nel codice stesso) indicano POST /api/devices/{id}/commands. Il metodo posta su POST_CREATE_ROUTE(deviceId) = /api/devices/%s/updates (haivision/route.go:186). La costante corretta ROUTE_COMMMAND esiste in constants.go:11 (con typo, tre M), non ha helper Sprintf e non è mai usata.

B4 — ResponseStartOrRoute non deserializza la risposta reale (impatto: bloccante)

L’API risponde con un array top-level. La struct wrappa in un campo Response []struct{...} privo di tag JSON. Verificato sul payload della doc:

json: cannot unmarshal array into Go value of type route.ResponseStartOrRoute

Fix: il tipo deve essere uno slice (type ResponseStartOrRoute []struct{...}).

B5 — Le statistiche usano int per campi frazionari (impatto: alto)

bitrate, sendRate, usedBandwidth sono documentati come number in Mbit/s (quindi frazionari) ma sono tipizzati int in haivision/stats/response.go. Verificato:

json: cannot unmarshal number 4.5 into Go struct field SourceStatisticsModel.bitrate of type int

Qualsiasi route con bitrate non intero fa fallire l’intera chiamata Get*Statistics. Fix: float64.

B6 — GetSrtClientStatistics usa il path senza /client (impatto: alto)

La doc indica GET /api/gateway/{id}/statistics/client?...; il metodo usa GET_ROUTES_STATISTICS = /api/gateway/%s/statistics (haivision/stats.go:118). Manca la costante per il sotto-path.

B7 — Validazione validator.v2 troppo stretta / rotta

validate:"nonnil,min=1" è applicato a tutti i campi, inclusi quelli opzionali (ttl, tos, retainHeader, tipizzati string). Verificato su una RouteModel con solo Name valorizzato:

Source.ID: less than min, Source.Address: less than min, Source.Protocol: less than min,
Source.Port: less than min, Source.NetworkInterface: less than min, Source.Name: less than min

Una route SRT legittima senza ttl/tos espliciti viene rifiutata lato client. Inoltre min=1 su un bool (Fields.StartRoute) produce Fields.StartRoute: unsupported type, cioè la validazione di RequestCreateRoute fallisce sempre — verrà a galla appena B2 verrà corretto usando quella struct. Nota: gopkg.in/validator.v2 è di fatto non manutenuto; go-playground/validator/v10 è lo standard corrente.

B8 — Copertura API incompleta rispetto al README

Assenti: update route, delete route, gestione destinazioni singole, logout sessione. GetRoutes/GetRouteConfiguration ritornano un *resty.Response grezzo — il tipo di trasporto è esposto nell’interfaccia pubblica e il chiamante deve deserializzare a mano; i modelli tipizzati (ResponseRoutes[TS,TD]) esistono ma sono commentati. Il README afferma “stop stream, getting stream status, play stream, and more”: sovrastima quello che l’SDK fa.


C. QualitĂ , build, test

  • make build è rotto: go build . sulla root, dove non esistono file Go → no Go files in .... Il target corretto è go build ./... (che è quello che usa la CI, motivo per cui il problema non è mai emerso). Il Makefile manca inoltre di target fmt, vet, lint, cover.
  • gofmt -l .: haivision/device/response.go, haivision/haivision.go, haivision/header_configurator.go, haivision/rtsp/response.go, haivision/stats/response.go.
  • Test praticamente assenti: TestAuth non asserisce nulla; TestDeviceInfo fa log.Println(err) invece di t.Fatalf — un errore di unmarshal lascia il test verde. Copertura del package haivision: 0%. Tutti i bug B1-B6 sarebbero stati intercettati da test tabellari di serializzazione sui payload della doc, senza toccare un gateway reale.
  • File stub vuoti: haivision/stats/request.go, haivision/device/request.go (solo package), haivision/rtsp/response.go (0 byte).
  • Dead code / commentato: blocchi GetRoutes* per protocollo, Response/Route interface in route/response.go, RequestUdpRtpCreateRoute, BaseSource, RequestCreateRoute. Da rimuovere o completare.
  • Naming: ROUTE_COMMMAND (typo), ResponseStartOrRoute (manca “Stop”), PrompegFeclsBlockAligned (ls invece di Is) in udp_rtp/response.go — quest’ultimo cambia anche il campo JSON atteso rispetto a prompegFecIsBlockAligned usato nella request.
  • Nessun CHANGELOG.md nonostante 30+ tag pubblicati e una release automatica su tag.

D. Dipendenze e CI

  • golang.org/x/net v0.7.0 — GO-2026-4918 raggiungibile dal codice (via resty → net/http), fix in v0.53.0. govulncheck segnala inoltre 19 vulnerabilitĂ  della stdlib del toolchain locale (go1.25.0 → aggiornare a ≥1.25.12) e 26 nei moduli richiesti ma non raggiungibili.
  • go-resty/resty/v2 v2.7.0 è del 2022 (v2 è oggi ~2.16). gopkg.in/validator.v2 non manutenuto.
  • go.mod dichiara go 1.18; il README dice “Go 1.13 or later” — falso, il codice usa generics. Il README documenta anche import "github.com/Allan-Nava/Haivision-go-sdk", che non compila: il package è .../haivision.
  • CI: matrice Go 1.18–1.21, tutte EOL (mancano 1.22–1.25). actions/checkout@v3 e actions/setup-go@v4 obsolete. cache-dependency-path: subdir/go.sum è un residuo di template che punta a un path inesistente (inerte solo perchĂ© cache: è commentato — quindi nessuna cache dei moduli). Nessun gate su gofmt/go vet/staticcheck/govulncheck/coverage.
  • tag-autorelease.yml: usa actions/create-release@v1 (archiviato dal 2021, sostituire con softprops/action-gh-release o gh release create), permissions: write-all (eccessivo: basta contents: write), e installa ffmpeg senza che nulla nel repo lo usi.
  • dependabot.yml monitora /tests, directory che non esiste (è test/, e non ha un go.mod proprio): quell’entry è morta. Dependabot e Renovate sono entrambi configurati e si sovrappongono sul gomod della root.

PrioritĂ  di intervento

Ogni finding di questo audit è tracciato come item in docs/backlog.md, con impact semver e versione target. Il piano per milestone — e quale sia la prossima release — è in docs/roadmap.md, generata dal backlog. Qui la sola mappa audit → item:

Priorità Versione target Finding → item di backlog
P0 wire/contratto, non-breaking v1.1.0 B1 route-json-tag-fields-parameters · B3 startstop-commands-endpoint · B6 srt-client-stats-path
P0 contratto, breaking v2.0.0 B2 create-route-request-model · B4 startstop-response-slice · B5 stats-float64 · B7 validator-v10-optional-fields · B8 typed-get-routes, route-update-delete
P1 correttezza & sicurezza client v1.1.0 A1 http-status-check · A2 insecure-flag-inverted · A3 device-list-empty-panic · A4 header-configurator-order · A5 healthcheck-always-nil · A7 debug-logs-credentials, unconditional-log-println
P1 superficie API v2.0.0 A6 context-and-timeout · A2 builder-options-struct
P2 sostenibilità v1.1.0 wire-contract-fixture-tests · makefile-build-and-gofmt · deps-x-net-vuln (sicurezza)
P2 sostenibilità v1.2.0 httptest-client-coverage · readme-import-path-go-version · ci-quality-gates · ci-go-matrix-and-actions · tag-autorelease-modernize · changelog-bootstrap · deps-resty-bump · dead-code-and-stubs-cleanup · dependabot-tests-dir
P2 cleanup breaking v2.0.0 exported-naming-typos

I fix del contratto API si dividono su due versioni per un motivo preciso: B1/B3/B6 correggono il wire format senza toccare tipi esportati (il consumer continua a compilare → v1.1.0), mentre B2/B4/B5 cambiano firme e tipi esportati (→ v2.0.0). Fino a v2.0.0 create-route e start/stop restano quindi parzialmente rotti: v1.1.0 sistema l’endpoint e i nomi dei campi, non la forma del body.