aboutsummaryrefslogtreecommitdiff
path: root/src/frontend_common/update_checker.cpp
blob: 53f3ca942d2ef96095662aef912f9963b57df238 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#include "update_checker.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include <fmt/format.h>

#include <httplib.h>

#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>
#endif

#include <nlohmann/json.hpp>
#include <optional>
#include <string>

std::optional<std::string> UpdateChecker::GetResponse(std::string url, std::string path)
{
    try {
        constexpr std::size_t timeout_seconds = 15;

        std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
        client->set_connection_timeout(timeout_seconds);
        client->set_read_timeout(timeout_seconds);
        client->set_write_timeout(timeout_seconds);

#ifdef YUZU_BUNDLED_OPENSSL
        client->load_ca_cert_store(kCert, sizeof(kCert));
#endif

        if (client == nullptr) {
            LOG_ERROR(Frontend, "Invalid URL {}{}", url, path);
            return {};
        }

        httplib::Request request{
            .method = "GET",
            .path = path,
        };

        client->set_follow_location(true);
        httplib::Result result = client->send(request);

        if (!result) {
            LOG_ERROR(Frontend, "GET to {}{} returned null", url, path);
            return {};
        }

        const auto &response = result.value();
        if (response.status >= 400) {
            LOG_ERROR(Frontend,
                      "GET to {}{} returned error status code: {}",
                      url,
                      path,
                      response.status);
            return {};
        }
        if (!response.headers.contains("content-type")) {
            LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path);
            return {};
        }

        return response.body;
    } catch (std::exception &e) {
        LOG_ERROR(Frontend,
                  "GET to {}{} failed during update check: {}",
                  url,
                  path,
                  e.what());
        return std::nullopt;
    }
}

std::optional<UpdateChecker::Update> UpdateChecker::GetLatestRelease(bool include_prereleases) {
    const auto update_check_url = std::string{Common::g_build_auto_update_api};
    std::string update_check_path = fmt::format("/repos/{}",
                                                std::string{Common::g_build_auto_update_repo});
    try {
        if (include_prereleases) { // This can return either a prerelease or a stable release,
            // whichever is more recent.
            const auto update_check_tags_path = update_check_path + "/tags";
            const auto update_check_releases_path = update_check_path + "/releases";

            const auto tags_response = UpdateChecker::GetResponse(update_check_url, update_check_tags_path);
            const auto releases_response = UpdateChecker::GetResponse(update_check_url, update_check_releases_path);

            if (!tags_response || !releases_response)
                return {};

            const std::string latest_tag
                = nlohmann::json::parse(tags_response.value()).at(0).at("name");
            const std::string latest_name =
                nlohmann::json::parse(releases_response.value()).at(0).at("name");

            const bool latest_tag_has_release = releases_response.value().find(
                                                    fmt::format("\"{}\"", latest_tag))
                                                != std::string::npos;

            // If there is a newer tag, but that tag has no associated release, don't prompt the
            // user to update.
            if (!latest_tag_has_release)
                return {};

            return Update{latest_tag, latest_name};
        } else { // This is a stable release, only check for other stable releases.
            update_check_path += "/releases/latest";
            const auto response = UpdateChecker::GetResponse(update_check_url, update_check_path);

            if (!response)
                return {};

            const std::string latest_tag = nlohmann::json::parse(response.value()).at("tag_name");
            const std::string latest_name = nlohmann::json::parse(response.value()).at("name");

            return Update{latest_tag, latest_name};
        }

    } catch (nlohmann::detail::out_of_range &) {
        LOG_ERROR(Frontend,
                  "Parsing JSON response from {}{} failed during update check: "
                  "nlohmann::detail::out_of_range",
                  update_check_url,
                  update_check_path);
        return {};
    } catch (nlohmann::detail::type_error &) {
        LOG_ERROR(Frontend,
                  "Parsing JSON response from {}{} failed during update check: "
                  "nlohmann::detail::type_error",
                  update_check_url,
                  update_check_path);
        return {};
    }
}