Add redis support
This commit is contained in:
parent
773f75543e
commit
109556f2b7
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
config.cpp
31
config.cpp
|
@ -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"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
16
config.h
16
config.h
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
36
main.cpp
36
main.cpp
|
@ -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
|
||||||
|
|
103
pixivclient.cpp
103
pixivclient.cpp
|
@ -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(const std::string& query) {
|
std::vector<SearchSuggestion> PixivClient::get_search_suggestions(std::string query) {
|
||||||
httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", {
|
to_lower(query);
|
||||||
{"lang", "en"}, {"keyword", query}
|
std::string body = [&]() {
|
||||||
}, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}});
|
std::string cache_key = "pixwhile:searchsugg:"s + std::to_string(FastHash(query.data(), query.size(), 0));
|
||||||
if (!res) {
|
std::optional<std::string> cached_body = this->_redis->get(cache_key);
|
||||||
throw HTTPLibException(res.error());
|
if (cached_body) {
|
||||||
}
|
return std::move(*cached_body);
|
||||||
nlohmann::json j = nlohmann::json::parse(std::move(res->body)).at("candidates");
|
}
|
||||||
|
|
||||||
|
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<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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue