Add redis support

This commit is contained in:
blankie 2023-06-08 23:02:36 +07:00
parent 773f75543e
commit 109556f2b7
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
13 changed files with 426 additions and 62 deletions

View File

@ -1,11 +1,14 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(pixwhile CXX) project(pixwhile C CXX)
find_package(nlohmann_json REQUIRED) find_package(nlohmann_json REQUIRED)
set(HTTPLIB_REQUIRE_OPENSSL ON) set(HTTPLIB_REQUIRE_OPENSSL ON)
add_subdirectory(thirdparty/httplib) add_subdirectory(thirdparty/httplib)
find_package(PkgConfig REQUIRED)
pkg_check_modules(HIREDIS REQUIRED hiredis)
if (CMAKE_BUILD_TYPE MATCHES "Debug") if (CMAKE_BUILD_TYPE MATCHES "Debug")
if (NOT FLAGS) if (NOT FLAGS)
@ -23,15 +26,15 @@ list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-s
add_link_options(${FLAGS}) add_link_options(${FLAGS})
add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp hiredis_wrapper.cpp
blankie/serializer.cpp blankie/escape.cpp blankie/murl.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
routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/users/common.cpp routes/users/illustrations.cpp) blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp)
set_target_properties(${PROJECT_NAME} set_target_properties(${PROJECT_NAME}
PROPERTIES PROPERTIES
CXX_STANDARD 20 CXX_STANDARD 20
CXX_STANDARD_REQUIRED YES CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO CXX_EXTENSIONS NO
) )
target_include_directories(${PROJECT_NAME} PRIVATE thirdparty) target_include_directories(${PROJECT_NAME} PRIVATE thirdparty ${HIREDIS_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib) target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib ${HIREDIS_LINK_LIBRARIES})
target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS}) target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS})

View File

@ -17,6 +17,15 @@ liking. Here's a list of what they are:
- `bind_port` (zero or positive integer): What port to bind to - `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 - `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 - `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 Pixwhile is intended to be run behind a reverse proxy (e.g. Nginx), so you
should set your reverse proxy to proxy requests to should set your reverse proxy to proxy requests to

View File

@ -8,6 +8,33 @@ Config load_config(const char* path) {
return nlohmann::json::parse(config_file.get()); 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<const std::string&>();
if (connection_type == "ip") {
redis_config.connection_method = IPConnection{
connection.at("address").get<std::string>(),
connection.at("port").get<int>()
};
} else if (connection_type == "unix") {
redis_config.connection_method = UnixConnection{connection.at("unix").get<std::string>()};
} else {
throw std::invalid_argument("Unknown redis connection type: "s + connection_type);
}
if (j.at("username").is_string()) {
redis_config.username = j["username"].get<std::string>();
}
if (j.at("password").is_string()) {
redis_config.password = j["password"].get<std::string>();
}
return redis_config;
}
void from_json(const nlohmann::json& j, Config& config) { void from_json(const nlohmann::json& j, Config& config) {
using namespace std::string_literals; 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()) { if (j.contains("canonical_origin") && j["canonical_origin"].is_string()) {
config.canonical_origin = j["canonical_origin"].get<std::string>(); config.canonical_origin = j["canonical_origin"].get<std::string>();
} }
if (j.contains("redis") && j["redis"].at("enabled")) {
config.redis_config = get_redis_config(j["redis"]);
}
} }

View File

@ -1,14 +1,30 @@
#pragma once #pragma once
#include <string> #include <string>
#include <variant>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "blankie/murl.h" #include "blankie/murl.h"
struct IPConnection {
std::string address;
int port;
};
struct UnixConnection {
std::string unix;
};
struct RedisConfig {
std::variant<IPConnection, UnixConnection> connection_method;
std::optional<std::string> username;
std::optional<std::string> password;
};
struct Config { struct Config {
std::string bind_host = "127.0.0.1"; std::string bind_host = "127.0.0.1";
int bind_port = 8080; int bind_port = 8080;
blankie::murl::Url image_proxy_url{"https://i.pixiv.cat"}; blankie::murl::Url image_proxy_url{"https://i.pixiv.cat"};
std::optional<std::string> canonical_origin; std::optional<std::string> canonical_origin;
std::optional<RedisConfig> redis_config;
}; };
Config load_config(const char* path); Config load_config(const char* path);

View File

@ -2,5 +2,16 @@
"bind_host": "127.0.0.1", "bind_host": "127.0.0.1",
"bind_port": 8080, "bind_port": 8080,
"image_proxy_url": "https://i.pixiv.cat", "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"
}
} }

128
hiredis_wrapper.cpp Normal file
View File

@ -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<std::string> 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<std::string> 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");
}
}

70
hiredis_wrapper.h Normal file
View File

@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <memory>
#include <optional>
#include <exception>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wc99-extensions"
#pragma GCC diagnostic ignored "-Wconversion"
#include <hiredis.h>
#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<redisReply, decltype(&freeReplyObject)>;
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<typename... Args>
RedisReply command(const char* format, Args... args) {
std::lock_guard<std::mutex> guard(this->_mutex);
redisReply* raw_reply = static_cast<redisReply*>(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<std::string> get(const std::string& key);
void set(const std::string& key, const std::string& value, time_t expiry);
std::optional<std::string> 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;
};

View File

@ -6,6 +6,7 @@
#include "pixivclient.h" #include "pixivclient.h"
#include "servehelper.h" #include "servehelper.h"
#include "routes/routes.h" #include "routes/routes.h"
#include "hiredis_wrapper.h"
int main(int argc, char** argv) { int main(int argc, char** argv) {
if (argc != 2) { if (argc != 2) {
@ -20,9 +21,22 @@ int main(int argc, char** argv) {
fprintf(stderr, "Failed to load config: %s\n", e.what()); fprintf(stderr, "Failed to load config: %s\n", e.what());
return 1; return 1;
} }
std::optional<Redis> redis;
PixivClient pixiv_client; if (config.redis_config) {
if (const IPConnection* ip_connection = std::get_if<IPConnection>(&config.redis_config->connection_method)) {
redis.emplace(ip_connection->address, ip_connection->port);
} else if (const UnixConnection* unix_connection = std::get_if<UnixConnection>(&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; httplib::Server server;
server.Get("/", [&](const httplib::Request& req, httplib::Response& res) { server.Get("/", [&](const httplib::Request& req, httplib::Response& res) {
home_route(req, res, config); 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) { server.Get("/guess_extension/i.pximg.net(/.+)", [&](const httplib::Request& req, httplib::Response& res) {
using namespace std::string_literals; guess_extension_route(req, res, config, pixiv_client, redis ? &*redis : nullptr);
// 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);
}); });
#ifndef NDEBUG #ifndef NDEBUG

View File

@ -1,11 +1,16 @@
#include <FastHash.h>
#include "blankie/murl.h" #include "blankie/murl.h"
#include "numberhelper.h" #include "numberhelper.h"
#include "pixivclient.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* 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 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_keep_alive(true);
this->_www_pixiv_net_client.set_default_headers({ this->_www_pixiv_net_client.set_default_headers({
{"Cookie", "webp_available=1"} {"Cookie", "webp_available=1"}
@ -17,10 +22,9 @@ PixivClient::PixivClient() {
} }
User PixivClient::get_user(uint64_t user_id) { User PixivClient::get_user(uint64_t user_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", { nlohmann::json j = this->_call_api("pixwhile:user:"s + std::to_string(user_id), std::nullopt, 24 * 60 * 60,
{"lang", "en"}, {"id", std::to_string(user_id)} "/touch/ajax/user/details", {{"lang", "en"}, {"id", std::to_string(user_id)}}, {{"User-Agent", touch_user_agent}});
}, {{"User-Agent", touch_user_agent}}); return j.at("user_details").get<User>();
return this->_handle_result(std::move(res)).at("user_details").get<User>();
} }
Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) { 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) { if (page != 0) {
params.insert({"p", std::to_string(page + 1)}); 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<Illusts>(); 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<Illusts>();
} }
Illust PixivClient::get_illust(uint64_t illust_id) { Illust PixivClient::get_illust(uint64_t illust_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", { nlohmann::json j = this->_call_api("pixwhile:illust:"s + std::to_string(illust_id), std::nullopt, 24 * 60 * 60,
{"lang", "en"}, {"illust_id", std::to_string(illust_id)} "/touch/ajax/illust/details", {{"lang", "en"}, {"illust_id", std::to_string(illust_id)}}, {{"User-Agent", touch_user_agent}});
}, {{"User-Agent", touch_user_agent}}); return j.get<Illust>();
return this->_handle_result(std::move(res)).get<Illust>();
} }
SearchResults PixivClient::search_illusts(const std::string& query, size_t page, const std::string& order) { SearchResults PixivClient::search_illusts(std::string query, size_t page, const std::string& order) {
using namespace std::string_literals; to_lower(query);
httplib::Params params = {
httplib::Result res = this->_www_pixiv_net_client.Get("/ajax/search/illustrations/"s + blankie::murl::escape(query), {
{"lang", "en"}, {"lang", "en"},
{"mode", "all"}, {"mode", "all"},
{"p", std::to_string(page + 1)}, {"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"}, {"type", "illust_and_ugoira"},
{"order", order}, {"order", order},
{"word", query} {"word", query}
}, {{"User-Agent", desktop_user_agent}}); };
return this->_handle_result(std::move(res)).get<SearchResults>();
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<SearchResults>();
}
std::vector<SearchSuggestion> 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<std::string> cached_body = this->_redis->get(cache_key);
if (cached_body) {
return std::move(*cached_body);
} }
std::vector<SearchSuggestion> PixivClient::get_search_suggestions(const std::string& query) {
httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", { httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", {
{"lang", "en"}, {"keyword", query} {"lang", "en"}, {"keyword", query}
}, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}}); }, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}});
if (!res) { if (!res) {
throw HTTPLibException(res.error()); throw HTTPLibException(res.error());
} }
nlohmann::json j = nlohmann::json::parse(std::move(res->body)).at("candidates");
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<SearchSuggestion> search_suggestions; std::vector<SearchSuggestion> search_suggestions;
search_suggestions.reserve(j.size()); search_suggestions.reserve(j.size());
@ -76,16 +96,34 @@ std::vector<SearchSuggestion> PixivClient::get_search_suggestions(const std::str
return search_suggestions; return search_suggestions;
} }
nlohmann::json PixivClient::_handle_result(httplib::Result res) { nlohmann::json PixivClient::_call_api(std::string cache_key, std::optional<std::string> cache_field, time_t expiry,
std::string path, httplib::Params params, httplib::Headers headers) {
std::optional<std::string> 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) { if (!res) {
throw HTTPLibException(res.error()); throw HTTPLibException(res.error());
} }
nlohmann::json j = nlohmann::json::parse(std::move(res->body)); nlohmann::json j = nlohmann::json::parse(std::move(res->body));
if (j.at("error")) { if (j.at("error")) {
throw PixivException(res->status, j.at("message").get<std::string>()); throw PixivException(res->status, j.at("message").get<std::string>());
} }
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) { 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; 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';
}
}
}

View File

@ -1,28 +1,31 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include "pixivmodels.h"
#include <httplib/httplib.h> #include <httplib/httplib.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "pixivmodels.h"
#include "hiredis_wrapper.h"
class PixivClient { class PixivClient {
public: public:
PixivClient(); PixivClient(Redis* redis);
User get_user(uint64_t user_id); User get_user(uint64_t user_id);
Illusts get_illusts(uint64_t user_id, size_t page); Illusts get_illusts(uint64_t user_id, size_t page);
Illust get_illust(uint64_t illust_id); Illust get_illust(uint64_t illust_id);
SearchResults search_illusts(const std::string& query, size_t page, const std::string& order); SearchResults search_illusts(std::string query, size_t page, const std::string& order);
std::vector<SearchSuggestion> get_search_suggestions(const std::string& query); std::vector<SearchSuggestion> get_search_suggestions(std::string query);
bool i_pximg_url_valid(const std::string& path); bool i_pximg_url_valid(const std::string& path);
private: private:
nlohmann::json _handle_result(httplib::Result res); nlohmann::json _call_api(std::string cache_key, std::optional<std::string> 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 _www_pixiv_net_client{"https://www.pixiv.net"};
httplib::Client _i_pximg_net_client{"https://i.pximg.net"}; httplib::Client _i_pximg_net_client{"https://i.pximg.net"};
Redis* _redis;
}; };
class HTTPLibException : public std::exception { class HTTPLibException : public std::exception {

View File

@ -0,0 +1,46 @@
#include <FastHash.h>
#include "routes.h"
#include "../servehelper.h"
#include "../pixivclient.h"
#include "../hiredis_wrapper.h"
static inline std::optional<std::string> 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<std::string> 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<std::string> 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;
}
}

View File

@ -4,6 +4,7 @@
struct Config; // forward declaration from ../config.h struct Config; // forward declaration from ../config.h
class PixivClient; // forward declaration from ../pixivclient.h class PixivClient; // forward declaration from ../pixivclient.h
class Redis; // forward declaration from ../hiredis_wrapper.h
extern const uint64_t css_hash; 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 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 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 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);

View File

@ -48,7 +48,7 @@
// (h) *= 0x2127599bf4325c37ULL; // (h) *= 0x2127599bf4325c37ULL;
// (h) ^= (h) >> 47; }) // (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) 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); 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<uint64_t>(str[i])); h ^= mix(static_cast<uint64_t>(str[i]));
h *= m; h *= m;