1717
1818namespace {
1919
20- constexpr size_t kMaxReleaseJsonBytes = 32768 ;
2120 constexpr const char * kStatusTitle = " OTA" ;
2221 const char * kRedirectHeaderKeys [] = {
2322 " Location" ,
@@ -75,7 +74,7 @@ namespace {
7574 }
7675
7776 ReleaseSource releaseSourceForSettings (const settings::UpdateSettings& settings) {
78- ReleaseSource source{settings.repositoryOwner .empty () ? std::string{" ionutdecebal " }
77+ ReleaseSource source{settings.repositoryOwner .empty () ? std::string{settings:: kDefaultRepositoryOwner }
7978 : std::string{AsciiText::trim (settings.repositoryOwner )},
8079 " rsvpnano" , std::string{AsciiText::trim (settings.releaseTag )}};
8180
@@ -103,6 +102,12 @@ namespace {
103102 return std::string{prefix} + " " + std::string{detail.c_str (), detail.length ()};
104103 }
105104
105+ bool isRedirectStatus (int statusCode) {
106+ return statusCode == HTTP_CODE_MOVED_PERMANENTLY || statusCode == HTTP_CODE_FOUND
107+ || statusCode == HTTP_CODE_SEE_OTHER || statusCode == HTTP_CODE_TEMPORARY_REDIRECT
108+ || statusCode == HTTP_CODE_PERMANENT_REDIRECT ;
109+ }
110+
106111 std::string readBodyLimited (HTTPClient& http, size_t maxBytes) {
107112 WiFiClient* stream = http.getStreamPtr ();
108113 if (stream == nullptr ) {
@@ -178,57 +183,68 @@ std::string_view OtaUpdater::currentVersion() {
178183
179184static void reportStatus (OtaUpdater::StatusCallback callback, void * context, const char * title, const char * line1,
180185 const char * line2, int progressPercent);
186+ static std::expected<std::string, std::string> resolveDownloadUrl (std::string_view assetUrl, std::string_view version,
187+ OtaUpdater::StatusCallback callback, void * context);
181188
182189static std::expected<LatestRelease, std::string> fetchRelease (const settings::UpdateSettings& settings,
183- OtaUpdater::StatusCallback callback, void * context) {
190+ OtaUpdater::StatusCallback callback, void * context) {
184191 const std::string installedVersion{OtaUpdater::currentVersion ()};
185192 const ReleaseSource source = releaseSourceForSettings (settings);
186193 if (source.owner .empty () || source.repo .empty ())
187194 return std::unexpected (std::string{" GitHub source missing" });
188195
189- const std::string releasePath = source.tag .empty () ? " latest" : " tags/" + urlEncodePathSegment (source.tag );
190- const std::string url =
191- " https://api.github.com/repos/" + source.owner + " /" + source.repo + " /releases/" + releasePath;
192196 const std::string sourceLabel = source.tag .empty () ? source.repo : source.repo + " :" + source.tag ;
193197
194198 reportStatus (callback, context, kStatusTitle , " Checking GitHub" , sourceLabel.c_str (), 22 );
195199
196200 WiFiClientSecure client;
197- // GitHub release metadata and assets can redirect across multiple hosts, so
198- // keep the transport flexible for now. A signed manifest is the best
199- // follow-up hardening step.
201+ // GitHub release assets redirect across multiple hosts, so keep the
202+ // transport flexible for now. A signed manifest is the best follow-up
203+ // hardening step.
200204 client.setInsecure ();
201205 client.setHandshakeTimeout (15 );
202206
203207 HTTPClient http;
208+ http.collectHeaders (kRedirectHeaderKeys , 1 );
204209 http.setUserAgent (userAgentForVersion (installedVersion).c_str ());
205- http.setFollowRedirects (HTTPC_STRICT_FOLLOW_REDIRECTS );
210+ http.setFollowRedirects (HTTPC_DISABLE_FOLLOW_REDIRECTS );
206211 http.setTimeout (15000 );
207- if (!http.begin (client, url.c_str ()))
208- return std::unexpected (std::string{" HTTP begin failed" });
209212
210- http.addHeader (" Accept" , " application/vnd.github+json" );
211- const int statusCode = http.GET ();
212- if (statusCode != HTTP_CODE_OK ) {
213- const std::string errorDetail = statusCode == HTTP_CODE_NOT_FOUND
214- ? (source.tag .empty () ? " No published release" : " Release tag not found" )
215- : httpClientErrorDetail (" GitHub" , statusCode);
213+ const std::string assetName = urlEncodePathSegment (Board::Config::OTA_ASSET_NAME );
214+ std::string releaseTag = source.tag ;
215+ std::string assetUrl = " https://github.com/" + source.owner + " /" + source.repo + " /releases/" ;
216+ if (releaseTag.empty ())
217+ assetUrl += " latest/download/" + assetName;
218+ else
219+ assetUrl += " download/" + urlEncodePathSegment (releaseTag) + " /" + assetName;
220+
221+ if (releaseTag.empty ()) {
222+ if (!http.begin (client, assetUrl.c_str ()))
223+ return std::unexpected (std::string{" HTTP begin failed" });
224+
225+ http.addHeader (" Accept" , " application/octet-stream" );
226+ const int statusCode = http.GET ();
227+ if (!isRedirectStatus (statusCode)) {
228+ const std::string errorDetail = statusCode == HTTP_CODE_NOT_FOUND
229+ ? " No latest OTA asset"
230+ : httpClientErrorDetail (" GitHub" , statusCode);
231+ http.end ();
232+ return std::unexpected (errorDetail);
233+ }
234+
235+ const String location = http.header (" Location" );
216236 http.end ();
217- return std::unexpected (errorDetail);
237+ const std::string_view resolvedUrl{location.c_str (), static_cast <size_t >(location.length ())};
238+ auto parsedTag = releaseparser::tagFromAssetLocation (resolvedUrl, Board::Config::OTA_ASSET_NAME );
239+ if (!parsedTag)
240+ return std::unexpected (std::string{" Release redirect invalid" });
241+ releaseTag = std::move (*parsedTag);
242+ assetUrl.assign (resolvedUrl);
218243 }
219244
220- const std::string body = readBodyLimited (http, kMaxReleaseJsonBytes );
221- http.end ();
222-
223- auto parsed = releaseparser::parse (body, Board::Config::OTA_ASSET_NAME );
224- if (!parsed)
225- return std::unexpected (std::string{" Release tag missing" });
226- LatestRelease release;
227- release.assetUrl = parsed->assetUrl ;
228-
229- reportStatus (callback, context, kStatusTitle , " Checking version" , parsed->tagName .c_str (), 25 );
245+ reportStatus (callback, context, kStatusTitle , " Checking version" , releaseTag.c_str (), 25 );
230246 const std::string commitUrl = " https://api.github.com/repos/" + source.owner + " /" + source.repo + " /commits/"
231- + urlEncodePathSegment (parsed-> tagName );
247+ + urlEncodePathSegment (releaseTag );
232248 if (!http.begin (client, commitUrl.c_str ()))
233249 return std::unexpected (std::string{" Commit lookup failed" });
234250 http.addHeader (" Accept" , " application/vnd.github.sha" );
@@ -240,22 +256,25 @@ static std::expected<LatestRelease, std::string> fetchRelease(const settings::Up
240256 }
241257 std::string commitSha = readBodyLimited (http, 64 );
242258 http.end ();
243- auto releaseVersion = releaseparser::versionForCommit (parsed-> tagName , commitSha);
259+ auto releaseVersion = releaseparser::versionForCommit (releaseTag , commitSha);
244260 if (!releaseVersion)
245261 return std::unexpected (std::string{" Tag commit invalid" });
246- release.version = std::move (*releaseVersion);
247262
248- if (release.assetUrl .empty ())
249- return std::unexpected (std::string{Board::Config::OTA_ASSET_NAME } + " missing" );
263+ auto resolvedAssetUrl = resolveDownloadUrl (assetUrl, *releaseVersion, callback, context);
264+ if (!resolvedAssetUrl)
265+ return std::unexpected (std::move (resolvedAssetUrl.error ()));
250266
251- return release;
267+ return LatestRelease{
268+ .version = std::move (*releaseVersion),
269+ .assetUrl = std::move (*resolvedAssetUrl),
270+ };
252271}
253272
254273static std::expected<std::string, std::string> resolveDownloadUrl (std::string_view assetUrl, std::string_view version,
255274 OtaUpdater::StatusCallback callback, void * context) {
256275 const std::string assetUrlString{assetUrl};
257276 const std::string versionString{version};
258- reportStatus (callback, context, kStatusTitle , " Resolving asset" , versionString.c_str (), 29 );
277+ reportStatus (callback, context, kStatusTitle , " Resolving asset" , versionString.c_str (), 27 );
259278
260279 WiFiClientSecure client;
261280 client.setInsecure ();
@@ -276,8 +295,7 @@ static std::expected<std::string, std::string> resolveDownloadUrl(std::string_vi
276295 return std::string{assetUrl};
277296 }
278297
279- if (statusCode == HTTP_CODE_MOVED_PERMANENTLY || statusCode == HTTP_CODE_FOUND || statusCode == HTTP_CODE_SEE_OTHER
280- || statusCode == HTTP_CODE_TEMPORARY_REDIRECT || statusCode == HTTP_CODE_PERMANENT_REDIRECT ) {
298+ if (isRedirectStatus (statusCode)) {
281299 String resolvedUrl = http.header (" Location" );
282300 http.end ();
283301 if (!resolvedUrl.isEmpty ())
@@ -386,15 +404,6 @@ OtaUpdater::Result OtaUpdater::checkAndInstall(const settings::DeviceSettings& s
386404 const std::string detail = versionDetail (installedVersion, release->version );
387405 reportStatus (callback, context, kStatusTitle , " Preparing update" , detail.c_str (), 28 );
388406
389- auto resolvedAssetUrl = resolveDownloadUrl (release->assetUrl , release->version , callback, context);
390- if (!resolvedAssetUrl) {
391- net::disconnect ();
392- return {
393- .summary = " Asset failed" ,
394- .detail = std::move (resolvedAssetUrl.error ()),
395- };
396- }
397-
398407 WiFiClientSecure client;
399408 // Match the metadata request behavior until the update path gains certificate
400409 // pinning or signature verification above the transport layer.
@@ -422,7 +431,7 @@ OtaUpdater::Result OtaUpdater::checkAndInstall(const settings::DeviceSettings& s
422431 });
423432
424433 const String version = installedVersion.data ();
425- const String resolvedUrl = resolvedAssetUrl-> c_str ();
434+ const String resolvedUrl = release-> assetUrl . c_str ();
426435 const t_httpUpdate_return updateResult = updater.update (client, resolvedUrl, version, [version](HTTPClient* http) {
427436 http->setUserAgent (userAgentForVersion ({version.c_str (), version.length ()}).c_str ());
428437 http->addHeader (" Accept" , " application/octet-stream" );
0 commit comments