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 buildrotto, i 5 file non formattati e la copertura a 0% (ora:make buildfunziona,gofmtpulito, 32 test, 37,9% su./haivision/...), né il §D sux/netv0.7.0 (ora v0.35.0 — e GO-2026-4918 si è rivelata non raggiungibile, vedi l’itemdeps-x-net-vuln).Restano aperti i findings breaking §B2, §B4, §B5, §B7, §B8 e §A6:
CreateRoute*invia ancora il modello di risposta eResponseStartOrRoutenon deserializza l’array reale. Per lo stato corrente guarda sempredocs/roadmap.mdedocs/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)
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). IlMakefilemanca inoltre di targetfmt,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:
TestAuthnon asserisce nulla;TestDeviceInfofalog.Println(err)invece dit.Fatalf— un errore di unmarshal lascia il test verde. Copertura del packagehaivision: 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(solopackage),haivision/rtsp/response.go(0 byte). - Dead code / commentato: blocchi
GetRoutes*per protocollo,Response/Routeinterface inroute/response.go,RequestUdpRtpCreateRoute,BaseSource,RequestCreateRoute. Da rimuovere o completare. - Naming:
ROUTE_COMMMAND(typo),ResponseStartOrRoute(manca “Stop”),PrompegFeclsBlockAligned(lsinvece diIs) inudp_rtp/response.go— quest’ultimo cambia anche il campo JSON atteso rispetto aprompegFecIsBlockAlignedusato nella request. - Nessun
CHANGELOG.mdnonostante 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 (viaresty→net/http), fix inv0.53.0.govulnchecksegnala 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.v2non manutenuto.go.moddichiarago 1.18; il README dice “Go 1.13 or later” — falso, il codice usa generics. Il README documenta ancheimport "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@v3eactions/setup-go@v4obsolete.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 sugofmt/go vet/staticcheck/govulncheck/coverage. tag-autorelease.yml: usaactions/create-release@v1(archiviato dal 2021, sostituire consoftprops/action-gh-releaseogh release create),permissions: write-all(eccessivo: bastacontents: write), e installa ffmpeg senza che nulla nel repo lo usi.dependabot.ymlmonitora/tests, directory che non esiste (ètest/, e non ha ungo.modproprio): quell’entry è morta. Dependabot e Renovate sono entrambi configurati e si sovrappongono sulgomoddella 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.