From 109556f2b7bd1149f374fce3978d292db6bd0cef Mon Sep 17 00:00:00 2001 From: blankie Date: Thu, 8 Jun 2023 23:02:36 +0700 Subject: [PATCH] Add redis support --- CMakeLists.txt | 15 +++-- RUNNING.md | 9 +++ config.cpp | 31 +++++++++ config.h | 16 +++++ example_config.json | 13 +++- hiredis_wrapper.cpp | 128 +++++++++++++++++++++++++++++++++++++ hiredis_wrapper.h | 70 ++++++++++++++++++++ main.cpp | 36 +++++------ pixivclient.cpp | 103 +++++++++++++++++++++-------- pixivclient.h | 15 +++-- routes/guess_extension.cpp | 46 +++++++++++++ routes/routes.h | 2 + thirdparty/FastHash.h | 4 +- 13 files changed, 426 insertions(+), 62 deletions(-) create mode 100644 hiredis_wrapper.cpp create mode 100644 hiredis_wrapper.h create mode 100644 routes/guess_extension.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f97f18..765d20d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,14 @@ cmake_minimum_required(VERSION 3.16) -project(pixwhile CXX) +project(pixwhile C CXX) find_package(nlohmann_json REQUIRED) set(HTTPLIB_REQUIRE_OPENSSL ON) add_subdirectory(thirdparty/httplib) +find_package(PkgConfig REQUIRED) +pkg_check_modules(HIREDIS REQUIRED hiredis) + if (CMAKE_BUILD_TYPE MATCHES "Debug") if (NOT FLAGS) @@ -23,15 +26,15 @@ list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-s add_link_options(${FLAGS}) -add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp - blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp - routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/users/common.cpp routes/users/illustrations.cpp) +add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp hiredis_wrapper.cpp + routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/guess_extension.cpp routes/users/common.cpp routes/users/illustrations.cpp + blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp) set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO ) -target_include_directories(${PROJECT_NAME} PRIVATE thirdparty) -target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib) +target_include_directories(${PROJECT_NAME} PRIVATE thirdparty ${HIREDIS_INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib ${HIREDIS_LINK_LIBRARIES}) target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS}) diff --git a/RUNNING.md b/RUNNING.md index fbc87f8..8326d42 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -17,6 +17,15 @@ liking. Here's a list of what they are: - `bind_port` (zero or positive integer): What port to bind to - `image_proxy_url` (string): URL to proxy images to (see https://pixiv.cat/reverseproxy.html), a trailing slash is not needed - `canonical_origin` (string or null): A fallback canonical origin if set, useful if you're, say, running Pixwhile behind Ngrok +- `redis` (object) + - `enabled` (boolean) + - `connection` (object) + - `type` ("ip" or "unix"): Whether to connect to redis via TCP or UNIX domain sockets + - `address` (string or null): If `type` is "ip", the address to connect to. Ignored otherwise + - `port` (int or null): If `type` is "ip", the port to connect to. Ignored otherwise + - `unix` (string or null): If `type` is "unix", the path to redis' socket. Ignored otherwise + - `username` (string or null): Optional username for authentication + - `password` (string or null): Optional password for authentication Pixwhile is intended to be run behind a reverse proxy (e.g. Nginx), so you should set your reverse proxy to proxy requests to diff --git a/config.cpp b/config.cpp index 92c016d..98e6179 100644 --- a/config.cpp +++ b/config.cpp @@ -8,6 +8,33 @@ Config load_config(const char* path) { return nlohmann::json::parse(config_file.get()); } +RedisConfig get_redis_config(const nlohmann::json& j) { + using namespace std::string_literals; + + RedisConfig redis_config; + const nlohmann::json& connection = j.at("connection"); + const std::string& connection_type = connection.at("type").get_ref(); + if (connection_type == "ip") { + redis_config.connection_method = IPConnection{ + connection.at("address").get(), + connection.at("port").get() + }; + } else if (connection_type == "unix") { + redis_config.connection_method = UnixConnection{connection.at("unix").get()}; + } else { + throw std::invalid_argument("Unknown redis connection type: "s + connection_type); + } + + if (j.at("username").is_string()) { + redis_config.username = j["username"].get(); + } + if (j.at("password").is_string()) { + redis_config.password = j["password"].get(); + } + + return redis_config; +} + void from_json(const nlohmann::json& j, Config& config) { using namespace std::string_literals; @@ -20,4 +47,8 @@ void from_json(const nlohmann::json& j, Config& config) { if (j.contains("canonical_origin") && j["canonical_origin"].is_string()) { config.canonical_origin = j["canonical_origin"].get(); } + + if (j.contains("redis") && j["redis"].at("enabled")) { + config.redis_config = get_redis_config(j["redis"]); + } } diff --git a/config.h b/config.h index 7f46d2c..4d5e5f6 100644 --- a/config.h +++ b/config.h @@ -1,14 +1,30 @@ #pragma once #include +#include #include #include "blankie/murl.h" +struct IPConnection { + std::string address; + int port; +}; +struct UnixConnection { + std::string unix; +}; + +struct RedisConfig { + std::variant connection_method; + std::optional username; + std::optional password; +}; + struct Config { std::string bind_host = "127.0.0.1"; int bind_port = 8080; blankie::murl::Url image_proxy_url{"https://i.pixiv.cat"}; std::optional canonical_origin; + std::optional redis_config; }; Config load_config(const char* path); diff --git a/example_config.json b/example_config.json index f20a030..467298d 100644 --- a/example_config.json +++ b/example_config.json @@ -2,5 +2,16 @@ "bind_host": "127.0.0.1", "bind_port": 8080, "image_proxy_url": "https://i.pixiv.cat", - "canonical_origin": null + "canonical_origin": null, + "redis": { + "enabled": true, + "connection": { + "type": "ip", + "address": "127.0.0.1", + "port": 6389, + "unix": "/path/to/redis.sock" + }, + "username": null, + "password": "one of the passwords of all time" + } } diff --git a/hiredis_wrapper.cpp b/hiredis_wrapper.cpp new file mode 100644 index 0000000..dfa94ec --- /dev/null +++ b/hiredis_wrapper.cpp @@ -0,0 +1,128 @@ +#include "hiredis_wrapper.h" + +Redis::Redis(const std::string& address, int port) { + this->_context = redisConnect(address.c_str(), port); + if (!this->_context) { + throw std::bad_alloc(); + } + if (this->_context->err) { + RedisException e(this->_context->errstr); + redisFree(this->_context); + throw e; + } +} +Redis::Redis(const std::string& unix) { + this->_context = redisConnectUnix(unix.c_str()); + if (!this->_context) { + throw std::bad_alloc(); + } + if (this->_context->err) { + RedisException e(this->_context->errstr); + redisFree(this->_context); + throw e; + } +} +Redis::~Redis() { + redisFree(this->_context); +} + +void Redis::auth(const std::string& username, const std::string& password) { + RedisReply reply = this->command("AUTH %s %s", username.c_str(), password.c_str()); + if (reply->type == REDIS_REPLY_STATUS) { + // good :D + } else { + throw std::runtime_error("AUTH gave an unexpected return type"); + } +} +void Redis::auth(const std::string& password) { + RedisReply reply = this->command("AUTH %s", password.c_str()); + if (reply->type == REDIS_REPLY_STATUS) { + // good :D + } else { + throw std::runtime_error("AUTH gave an unexpected return type"); + } +} + +time_t Redis::ttl(const std::string& key) { + RedisReply reply = this->command("TTL %s", key.c_str()); + if (reply->type == REDIS_REPLY_INTEGER) { + return reply->integer; + } else { + throw std::runtime_error("TTL gave an unexpected return type"); + } +} + +bool Redis::expire(const std::string& key, time_t expiry) { + RedisReply reply = this->command("EXPIRE %s %d", key.c_str(), expiry); + + if (reply->type == REDIS_REPLY_INTEGER) { + return reply->integer == 1; + } else { + throw std::runtime_error("EXPIRE gave an unexpected return type"); + } +} + +bool Redis::expire_nx(const std::string& key, time_t expiry) { + if (this->_fake_expire_nx) { + time_t current_expiry = this->ttl(key); + if (current_expiry < 0) { + return this->expire(key, expiry); + } + return false; + } + + RedisReply reply(nullptr, freeReplyObject); + try{ + reply = this->command("EXPIRE %s %d NX", key.c_str(), expiry); + } catch (const RedisException& e) { + if (e.error == "ERR wrong number of arguments for 'expire' command") { + this->_fake_expire_nx = true; + return this->expire_nx(key, expiry); + } + throw; + } + + if (reply->type == REDIS_REPLY_INTEGER) { + return reply->integer == 1; + } else { + throw std::runtime_error("EXPIRE NX gave an unexpected return type"); + } +} + +std::optional Redis::get(const std::string& key) { + RedisReply reply = this->command("GET %s", key.c_str()); + if (reply->type == REDIS_REPLY_STRING) { + return std::string(reply->str, reply->len); + } else if (reply->type == REDIS_REPLY_NIL) { + return std::nullopt; + } else { + throw std::runtime_error("GET gave an unexpected return type"); + } +} +void Redis::set(const std::string& key, const std::string& value, time_t expiry) { + RedisReply reply = this->command("SET %s %s EX %d", key.c_str(), value.c_str(), expiry); + if (reply->type == REDIS_REPLY_STATUS) { + // good :D + } else { + throw std::runtime_error("SET gave an unexpected return type"); + } +} + +std::optional Redis::hget(const std::string& key, const std::string& field) { + RedisReply reply = this->command("HGET %s %s", key.c_str(), field.c_str()); + if (reply->type == REDIS_REPLY_STRING) { + return std::string(reply->str, reply->len); + } else if (reply->type == REDIS_REPLY_NIL) { + return std::nullopt; + } else { + throw std::runtime_error("HGET gave an unexpected return type"); + } +} +void Redis::hset(const std::string& key, const std::string& field, const std::string& value) { + RedisReply reply = this->command("HSET %s %s %s", key.c_str(), field.c_str(), value.c_str()); + if (reply->type == REDIS_REPLY_INTEGER) { + // good :D + } else { + throw std::runtime_error("SET gave an unexpected return type"); + } +} diff --git a/hiredis_wrapper.h b/hiredis_wrapper.h new file mode 100644 index 0000000..093c37e --- /dev/null +++ b/hiredis_wrapper.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wc99-extensions" +#pragma GCC diagnostic ignored "-Wconversion" +#include +#pragma GCC diagnostic pop + +class RedisException : public std::exception { +public: + RedisException(const char* error_) : error(error_) {} + RedisException(const char* error_, size_t length) : error(error_, length) {} + + const char* what() const noexcept { + return this->error.c_str(); + } + + std::string error; +}; + +using RedisReply = std::unique_ptr; + +class Redis { +public: + Redis(const Redis&) = delete; + Redis operator=(const Redis&) = delete; + + Redis(const std::string& address, int port); + Redis(const std::string& unix); + ~Redis(); + + template + RedisReply command(const char* format, Args... args) { + std::lock_guard guard(this->_mutex); + redisReply* raw_reply = static_cast(redisCommand(this->_context, format, args...)); + if (!raw_reply) { + throw RedisException(this->_context->errstr); + } + + RedisReply reply(raw_reply, freeReplyObject); + if (reply->type == REDIS_REPLY_ERROR) { + throw RedisException(reply->str, reply->len); + } + return reply; + } + + void auth(const std::string& username, const std::string& password); + void auth(const std::string& password); + + time_t ttl(const std::string& key); + bool expire(const std::string& key, time_t expiry); + bool expire_nx(const std::string& key, time_t expiry); + + std::optional get(const std::string& key); + void set(const std::string& key, const std::string& value, time_t expiry); + + std::optional hget(const std::string& key, const std::string& field); + void hset(const std::string& key, const std::string& field, const std::string& value); + +private: + redisContext* _context; + std::mutex _mutex; + + bool _fake_expire_nx = false; +}; diff --git a/main.cpp b/main.cpp index 568cb95..c0e3084 100644 --- a/main.cpp +++ b/main.cpp @@ -6,6 +6,7 @@ #include "pixivclient.h" #include "servehelper.h" #include "routes/routes.h" +#include "hiredis_wrapper.h" int main(int argc, char** argv) { if (argc != 2) { @@ -20,9 +21,22 @@ int main(int argc, char** argv) { fprintf(stderr, "Failed to load config: %s\n", e.what()); return 1; } - - PixivClient pixiv_client; + std::optional redis; + if (config.redis_config) { + if (const IPConnection* ip_connection = std::get_if(&config.redis_config->connection_method)) { + redis.emplace(ip_connection->address, ip_connection->port); + } else if (const UnixConnection* unix_connection = std::get_if(&config.redis_config->connection_method)) { + redis.emplace(unix_connection->unix); + } + if (config.redis_config->username && config.redis_config->password) { + redis->auth(*config.redis_config->username, *config.redis_config->password); + } else if (config.redis_config->password) { + redis->auth(*config.redis_config->password); + } + } + PixivClient pixiv_client(redis ? &*redis : nullptr); httplib::Server server; + server.Get("/", [&](const httplib::Request& req, httplib::Response& res) { home_route(req, res, config); }); @@ -73,23 +87,7 @@ int main(int argc, char** argv) { }); server.Get("/guess_extension/i.pximg.net(/.+)", [&](const httplib::Request& req, httplib::Response& res) { - using namespace std::string_literals; - - // rip query parameters, but they're not important anyway - std::string path = req.matches.str(1); - auto serve_extension = [&](const char* extension) { - if (!pixiv_client.i_pximg_url_valid(path + '.' + extension)) { - return false; - } - serve_redirect(req, res, config, proxy_image_url(req, config, "https://i.pximg.net" + path + '.' + extension), true); - return true; - }; - - if (serve_extension("png") || serve_extension("jpg")) { - return; - } - res.status = 500; - serve_error(req, res, config, "500: Internal Server Error", "Failed to guess file extension for https://i.pximg.net"s + path); + guess_extension_route(req, res, config, pixiv_client, redis ? &*redis : nullptr); }); #ifndef NDEBUG diff --git a/pixivclient.cpp b/pixivclient.cpp index 93d2187..fd93a59 100644 --- a/pixivclient.cpp +++ b/pixivclient.cpp @@ -1,11 +1,16 @@ +#include + #include "blankie/murl.h" #include "numberhelper.h" #include "pixivclient.h" +using namespace std::string_literals; + static const constexpr char* touch_user_agent = "Mozilla/5.0 (Android 12; Mobile; rv:97.0) Gecko/97.0 Firefox/97.0"; static const constexpr char* desktop_user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:111.0) Gecko/20100101 Firefox/111.0"; +static void to_lower(std::string& str); -PixivClient::PixivClient() { +PixivClient::PixivClient(Redis* redis) : _redis(redis) { this->_www_pixiv_net_client.set_keep_alive(true); this->_www_pixiv_net_client.set_default_headers({ {"Cookie", "webp_available=1"} @@ -17,10 +22,9 @@ PixivClient::PixivClient() { } User PixivClient::get_user(uint64_t user_id) { - httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", { - {"lang", "en"}, {"id", std::to_string(user_id)} - }, {{"User-Agent", touch_user_agent}}); - return this->_handle_result(std::move(res)).at("user_details").get(); + nlohmann::json j = this->_call_api("pixwhile:user:"s + std::to_string(user_id), std::nullopt, 24 * 60 * 60, + "/touch/ajax/user/details", {{"lang", "en"}, {"id", std::to_string(user_id)}}, {{"User-Agent", touch_user_agent}}); + return j.at("user_details").get(); } Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) { @@ -28,21 +32,21 @@ Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) { if (page != 0) { params.insert({"p", std::to_string(page + 1)}); } - httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/illusts", std::move(params), {{"User-Agent", touch_user_agent}}); - return this->_handle_result(std::move(res)).get(); + + nlohmann::json j = this->_call_api("pixwhile:illusts:"s + std::to_string(user_id), std::to_string(page), 60 * 60, + "/touch/ajax/user/illusts", std::move(params), {{"User-Agent", touch_user_agent}}); + return j.get(); } Illust PixivClient::get_illust(uint64_t illust_id) { - httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", { - {"lang", "en"}, {"illust_id", std::to_string(illust_id)} - }, {{"User-Agent", touch_user_agent}}); - return this->_handle_result(std::move(res)).get(); + nlohmann::json j = this->_call_api("pixwhile:illust:"s + std::to_string(illust_id), std::nullopt, 24 * 60 * 60, + "/touch/ajax/illust/details", {{"lang", "en"}, {"illust_id", std::to_string(illust_id)}}, {{"User-Agent", touch_user_agent}}); + return j.get(); } -SearchResults PixivClient::search_illusts(const std::string& query, size_t page, const std::string& order) { - using namespace std::string_literals; - - httplib::Result res = this->_www_pixiv_net_client.Get("/ajax/search/illustrations/"s + blankie::murl::escape(query), { +SearchResults PixivClient::search_illusts(std::string query, size_t page, const std::string& order) { + to_lower(query); + httplib::Params params = { {"lang", "en"}, {"mode", "all"}, {"p", std::to_string(page + 1)}, @@ -50,18 +54,34 @@ SearchResults PixivClient::search_illusts(const std::string& query, size_t page, {"type", "illust_and_ugoira"}, {"order", order}, {"word", query} - }, {{"User-Agent", desktop_user_agent}}); - return this->_handle_result(std::move(res)).get(); + }; + + std::string cache_key = "pixwhile:search:"s + order + ':' + std::to_string(FastHash(query.data(), query.size(), 0)); + nlohmann::json j = this->_call_api(std::move(cache_key), std::to_string(page), 60 * 60, + "/ajax/search/illustrations/"s + blankie::murl::escape(std::move(query)), std::move(params), {{"User-Agent", desktop_user_agent}}); + return j.get(); } -std::vector PixivClient::get_search_suggestions(const std::string& query) { - httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", { - {"lang", "en"}, {"keyword", query} - }, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}}); - if (!res) { - throw HTTPLibException(res.error()); - } - nlohmann::json j = nlohmann::json::parse(std::move(res->body)).at("candidates"); +std::vector PixivClient::get_search_suggestions(std::string query) { + to_lower(query); + std::string body = [&]() { + std::string cache_key = "pixwhile:searchsugg:"s + std::to_string(FastHash(query.data(), query.size(), 0)); + std::optional cached_body = this->_redis->get(cache_key); + if (cached_body) { + return std::move(*cached_body); + } + + httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", { + {"lang", "en"}, {"keyword", query} + }, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}}); + if (!res) { + throw HTTPLibException(res.error()); + } + + this->_redis->set(std::move(cache_key), res->body, 24 * 60 * 60); + return std::move(res->body); + }(); + nlohmann::json j = nlohmann::json::parse(std::move(body)).at("candidates"); std::vector search_suggestions; search_suggestions.reserve(j.size()); @@ -76,16 +96,34 @@ std::vector PixivClient::get_search_suggestions(const std::str return search_suggestions; } -nlohmann::json PixivClient::_handle_result(httplib::Result res) { +nlohmann::json PixivClient::_call_api(std::string cache_key, std::optional cache_field, time_t expiry, + std::string path, httplib::Params params, httplib::Headers headers) { + std::optional success_body = !cache_field + ? this->_redis->get(cache_key) + : this->_redis->hget(cache_key, *cache_field); + if (success_body) { + return nlohmann::json::parse(std::move(*success_body)); + } + + httplib::Result res = this->_www_pixiv_net_client.Get(std::move(path), std::move(params), std::move(headers)); if (!res) { throw HTTPLibException(res.error()); } - nlohmann::json j = nlohmann::json::parse(std::move(res->body)); if (j.at("error")) { throw PixivException(res->status, j.at("message").get()); } - return j.at("body"); + + j = j.at("body"); + // trim data sent to redis without making our own serialization of our objects + j.erase("ads"); + if (!cache_field) { + this->_redis->set(std::move(cache_key), j.dump(), expiry); + } else { + this->_redis->hset(cache_key, std::move(*cache_field), j.dump()); + this->_redis->expire_nx(std::move(cache_key), expiry); + } + return j; } bool PixivClient::i_pximg_url_valid(const std::string& path) { @@ -95,3 +133,12 @@ bool PixivClient::i_pximg_url_valid(const std::string& path) { } return res->status >= 200 && res->status <= 200; } + + +static void to_lower(std::string& str) { + for (size_t i = 0; i < str.size(); i++) { + if (str[i] >= 'A' && str[i] <= 'Z') { + str[i] = str[i] - 'A' + 'a'; + } + } +} diff --git a/pixivclient.h b/pixivclient.h index 88f2611..b53719f 100644 --- a/pixivclient.h +++ b/pixivclient.h @@ -1,28 +1,31 @@ #pragma once #include - -#include "pixivmodels.h" #include #include +#include "pixivmodels.h" +#include "hiredis_wrapper.h" + class PixivClient { public: - PixivClient(); + PixivClient(Redis* redis); User get_user(uint64_t user_id); Illusts get_illusts(uint64_t user_id, size_t page); Illust get_illust(uint64_t illust_id); - SearchResults search_illusts(const std::string& query, size_t page, const std::string& order); - std::vector get_search_suggestions(const std::string& query); + SearchResults search_illusts(std::string query, size_t page, const std::string& order); + std::vector get_search_suggestions(std::string query); bool i_pximg_url_valid(const std::string& path); private: - nlohmann::json _handle_result(httplib::Result res); + nlohmann::json _call_api(std::string cache_key, std::optional cache_field, time_t expiry, + std::string path, httplib::Params params, httplib::Headers headers); httplib::Client _www_pixiv_net_client{"https://www.pixiv.net"}; httplib::Client _i_pximg_net_client{"https://i.pximg.net"}; + Redis* _redis; }; class HTTPLibException : public std::exception { diff --git a/routes/guess_extension.cpp b/routes/guess_extension.cpp new file mode 100644 index 0000000..1bb6a93 --- /dev/null +++ b/routes/guess_extension.cpp @@ -0,0 +1,46 @@ +#include + +#include "routes.h" +#include "../servehelper.h" +#include "../pixivclient.h" +#include "../hiredis_wrapper.h" + +static inline std::optional guess_extension(const std::string& path, PixivClient& pixiv_client); + +void guess_extension_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client, Redis* redis) { + using namespace std::string_literals; + + std::string path = req.matches.str(1); + std::string cache_key = "pixwhile:i.pximg.net:ext:"s + std::to_string(FastHash(path.data(), path.size(), 0)); + + std::optional extension = redis + ? redis->get(cache_key) + : std::nullopt; + if (!extension) { + extension = guess_extension(path, pixiv_client); + if (!extension) { + res.status = 500; + serve_error(req, res, config, "500: Internal Server Error", "Failed to guess file extension for https://i.pximg.net"s + path); + return; + } + if (redis) { + redis->set(std::move(cache_key), *extension, 24 * 60 * 60); + } + } + + serve_redirect(req, res, config, proxy_image_url(req, config, "https://i.pximg.net" + path + '.' + *extension), true); +} + +static inline std::optional guess_extension(const std::string& path, PixivClient& pixiv_client) { + auto guess_extension = [&](const char* extension) { + return pixiv_client.i_pximg_url_valid(path + '.' + extension); + }; + + if (guess_extension("png")) { + return "png"; + } else if (guess_extension("jpg")) { + return "jpg"; + } else { + return std::nullopt; + } +} diff --git a/routes/routes.h b/routes/routes.h index 10f3499..4deeb1a 100644 --- a/routes/routes.h +++ b/routes/routes.h @@ -4,6 +4,7 @@ struct Config; // forward declaration from ../config.h class PixivClient; // forward declaration from ../pixivclient.h +class Redis; // forward declaration from ../hiredis_wrapper.h extern const uint64_t css_hash; @@ -12,3 +13,4 @@ void css_route(const httplib::Request& req, httplib::Response& res); void user_illustrations_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client); void artworks_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client); void tags_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client); +void guess_extension_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client, Redis* redis); diff --git a/thirdparty/FastHash.h b/thirdparty/FastHash.h index c3b9aea..86a6be7 100644 --- a/thirdparty/FastHash.h +++ b/thirdparty/FastHash.h @@ -48,7 +48,7 @@ // (h) *= 0x2127599bf4325c37ULL; // (h) ^= (h) >> 47; }) -constexpr uint64_t FastHash(const char* str, size_t size, uint64_t seed/*, uint64_t back = {}*/) +constexpr uint64_t FastHash(const char* str, std::size_t size, uint64_t seed/*, uint64_t back = {}*/) { auto const mix = [](uint64_t h) { @@ -62,7 +62,7 @@ constexpr uint64_t FastHash(const char* str, size_t size, uint64_t seed/*, uint6 uint64_t h = seed ^ (size * m); - for (size_t i = 0; i < size; i++) + for (std::size_t i = 0; i < size; i++) { h ^= mix(static_cast(str[i])); h *= m;