Add redis support
This commit is contained in:
		
							parent
							
								
									773f75543e
								
							
						
					
					
						commit
						109556f2b7
					
				| 
						 | 
				
			
			@ -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})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										31
									
								
								config.cpp
								
								
								
								
							
							
						
						
									
										31
									
								
								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<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) {
 | 
			
		||||
    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<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
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <variant>
 | 
			
		||||
#include <nlohmann/json.hpp>
 | 
			
		||||
#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 {
 | 
			
		||||
    std::string bind_host = "127.0.0.1";
 | 
			
		||||
    int bind_port = 8080;
 | 
			
		||||
    blankie::murl::Url image_proxy_url{"https://i.pixiv.cat"};
 | 
			
		||||
    std::optional<std::string> canonical_origin;
 | 
			
		||||
    std::optional<RedisConfig> redis_config;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Config load_config(const char* path);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 "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> redis;
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										103
									
								
								pixivclient.cpp
								
								
								
								
							
							
						
						
									
										103
									
								
								pixivclient.cpp
								
								
								
								
							| 
						 | 
				
			
			@ -1,11 +1,16 @@
 | 
			
		|||
#include <FastHash.h>
 | 
			
		||||
 | 
			
		||||
#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<User>();
 | 
			
		||||
    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<User>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<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) {
 | 
			
		||||
    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<Illust>();
 | 
			
		||||
    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<Illust>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<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) {
 | 
			
		||||
    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<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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    search_suggestions.reserve(j.size());
 | 
			
		||||
| 
						 | 
				
			
			@ -76,16 +96,34 @@ std::vector<SearchSuggestion> 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<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) {
 | 
			
		||||
        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<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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,28 +1,31 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "pixivmodels.h"
 | 
			
		||||
#include <httplib/httplib.h>
 | 
			
		||||
#include <nlohmann/json.hpp>
 | 
			
		||||
 | 
			
		||||
#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<SearchSuggestion> get_search_suggestions(const std::string& query);
 | 
			
		||||
    SearchResults search_illusts(std::string query, size_t page, const std::string& order);
 | 
			
		||||
    std::vector<SearchSuggestion> 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<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 _i_pximg_net_client{"https://i.pximg.net"};
 | 
			
		||||
    Redis* _redis;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<uint64_t>(str[i]));
 | 
			
		||||
		h *= m;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue