Initial commit
This commit is contained in:
commit
f3df9dadc1
|
@ -0,0 +1,8 @@
|
||||||
|
*.o
|
||||||
|
.flavor
|
||||||
|
.objs
|
||||||
|
imgurxp.so
|
||||||
|
assets.h
|
||||||
|
cert
|
||||||
|
dh2048.pem
|
||||||
|
config.json
|
|
@ -0,0 +1,9 @@
|
||||||
|
# ImgurXP
|
||||||
|
|
||||||
|
Alternative Imgur frontend written in C++. It is recommended to have a reverse
|
||||||
|
proxy point to the server with these headers:
|
||||||
|
```
|
||||||
|
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||||
|
Content-Security-Policy: default-src 'none'; img-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; sandbox
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
```
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>ImgurXP</title>
|
||||||
|
<link rel="stylesheet" href="/style.css" crossorigin="anonymous" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><b>404: page not found</b></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>ImgurXP</title>
|
||||||
|
<link rel="stylesheet" href="/style.css" crossorigin="anonymous" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header">
|
||||||
|
<a href="https://gitlab.com/blankX/imgurxp" style="text-decoration: none;">ImgurXP</a>
|
||||||
|
<br>
|
||||||
|
An alternative Imgur frontend written in C++
|
||||||
|
</div>
|
||||||
|
<p>To view an image or album, simply replace <code>imgur.com</code> with <code>imgurxp.zangestsu.animekaizoku.com</code>. For example: <code>https://imgur.com/a/jBrIW</code> gets turned into <code>https://imgurxp.zangetsu.animekaizoku.com/a/jBrIW</code></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,23 @@
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
background: #fee;
|
||||||
|
padding: 0.5em 1em 0.5em 1em;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#header {
|
||||||
|
border: 1px solid #008;
|
||||||
|
background: #eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
# imgurxp build config
|
||||||
|
# You can switch flavors using: kodev flavor [newflavor]
|
||||||
|
|
||||||
|
# Set to yes if you wish to produce a single binary instead
|
||||||
|
# of a dynamic library. If you set this to yes you must also
|
||||||
|
# set kore_source together with kore_flavor.
|
||||||
|
#single_binary=yes
|
||||||
|
#kore_source=/usr/share/kore
|
||||||
|
#kore_flavor=TASKS=1 CURL=1 DEBUG=1
|
||||||
|
|
||||||
|
# The flags below are shared between flavors
|
||||||
|
cflags=-Wall -Wmissing-declarations -Wshadow
|
||||||
|
cflags=-Wstrict-prototypes -Wmissing-prototypes
|
||||||
|
cflags=-Wpointer-arith -Wcast-qual -Wsign-compare
|
||||||
|
#cflags=-fsanitize=address
|
||||||
|
|
||||||
|
cxxflags=-Wall -Wmissing-declarations -Wshadow
|
||||||
|
cxxflags=-Wpointer-arith -Wcast-qual -Wsign-compare
|
||||||
|
#cxxflags=-fsanitize=address
|
||||||
|
|
||||||
|
ldflags=-lhiredis
|
||||||
|
#ldflags=-fsanitize=address
|
||||||
|
|
||||||
|
# Mime types for assets served via the builtin asset_serve_*
|
||||||
|
#mime_add=txt:text/plain; charset=utf-8
|
||||||
|
#mime_add=png:image/png
|
||||||
|
mime_add=css:text/css; charset=utf-8
|
||||||
|
mime_add=html:text/html; charset=utf-8
|
||||||
|
|
||||||
|
dev {
|
||||||
|
# These flags are added to the shared ones when
|
||||||
|
# you build the "dev" flavor.
|
||||||
|
cflags=-g
|
||||||
|
cxxflags=-g
|
||||||
|
}
|
||||||
|
|
||||||
|
#prod {
|
||||||
|
# You can specify additional flags here which are only
|
||||||
|
# included if you build with the "prod" flavor.
|
||||||
|
#}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# imgurxp configuration
|
||||||
|
|
||||||
|
server http {
|
||||||
|
bind 127.0.0.1 1112
|
||||||
|
tls no
|
||||||
|
}
|
||||||
|
|
||||||
|
load ./imgurxp.so
|
||||||
|
|
||||||
|
domain * {
|
||||||
|
attach http
|
||||||
|
|
||||||
|
route / asset_serve_index_html
|
||||||
|
route /style.css asset_serve_style_css
|
||||||
|
route ^/a/[a-zA-Z0-9]{1,40}$ album_or_image
|
||||||
|
route ^/album/[a-zA-Z0-9]{1,40}$ album_or_image
|
||||||
|
route ^/gallery/[a-zA-Z0-9]{1,40}$ album_or_image
|
||||||
|
route ^/[a-zA-Z0-9]{1,40}$ album_or_image
|
||||||
|
route ^/proxy/[a-zA-Z0-9]{1,40}\.[a-z]{3,4}$ proxy_image
|
||||||
|
route ^.*$ not_found
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"redis_host": "127.0.0.1",
|
||||||
|
"redis_port": 6379,
|
||||||
|
"redis_password": "gay"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <kore/http.h>
|
||||||
|
#include "assets.h"
|
||||||
|
#include "404.hpp"
|
||||||
|
|
||||||
|
int not_found(struct http_request* req) {
|
||||||
|
http_response(req, 404, asset_404_html, asset_len_404_html);
|
||||||
|
return KORE_RESULT_OK;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <kore/http.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
int not_found(struct http_request*);
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <hiredis/hiredis.h>
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void kore_parent_configure(int argc, char* argv[]);
|
||||||
|
void kore_worker_configure();
|
||||||
|
void kore_worker_teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void kore_parent_configure(int argc, char* argv[]) {
|
||||||
|
std::ifstream config_fstream("config.json");
|
||||||
|
std::string config_str;
|
||||||
|
// definitely could be better but i'm not spending more hours on this
|
||||||
|
char c;
|
||||||
|
while (config_fstream.get(c)) {
|
||||||
|
config_str.push_back(c);
|
||||||
|
}
|
||||||
|
config_fstream.close();
|
||||||
|
if (config_fstream.fail() && !config_fstream.eof()) {
|
||||||
|
kore_log(LOG_NOTICE, "fail bit set when reading config");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
kore_json json;
|
||||||
|
kore_json_init(&json, config_str.c_str(), config_str.length());
|
||||||
|
if (kore_json_parse(&json) != KORE_RESULT_OK) {
|
||||||
|
kore_log(LOG_NOTICE, "failed to parse json: %s", kore_json_strerror(&json));
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
kore_json_item* redis_host_json = kore_json_find_string(json.root, "redis_host");
|
||||||
|
kore_json_item* redis_port_json = kore_json_find_integer_u64(json.root, "redis_port");
|
||||||
|
kore_json_item* redis_password_json = kore_json_find_string(json.root, "redis_password");
|
||||||
|
if (redis_host_json && redis_port_json && *redis_host_json->data.string && redis_port_json->data.s64) {
|
||||||
|
config.redis_host = redis_host_json->data.string;
|
||||||
|
config.redis_port = redis_port_json->data.s64;
|
||||||
|
}
|
||||||
|
if (redis_password_json && *redis_password_json->data.string) {
|
||||||
|
config.redis_password = redis_password_json->data.string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void kore_worker_configure() {
|
||||||
|
if (config.redis_host && config.redis_port) {
|
||||||
|
kore_log(LOG_NOTICE, "connecting to redis instance at %s:%d", (*config.redis_host).c_str(), (*config.redis_port));
|
||||||
|
struct timeval timeout = {1, 0};
|
||||||
|
redis = redisConnectWithTimeout((*config.redis_host).c_str(), (*config.redis_port), timeout);
|
||||||
|
if (!redis || redis->err) {
|
||||||
|
if (redis) {
|
||||||
|
kore_log(LOG_NOTICE, "failed to connect to redis: %s", redis->errstr);
|
||||||
|
redisFree(redis);
|
||||||
|
redis = nullptr;
|
||||||
|
} else {
|
||||||
|
kore_log(LOG_NOTICE, "failed to connect to redis");
|
||||||
|
}
|
||||||
|
kore_shutdown();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (config.redis_password) {
|
||||||
|
redisReply* reply = (redisReply*)redisCommand(redis, "AUTH %s", (*config.redis_password).c_str());
|
||||||
|
if (!reply || reply->type == REDIS_REPLY_ERROR) {
|
||||||
|
if (!reply) {
|
||||||
|
kore_log(LOG_NOTICE, "received nullptr while authenticating to redis");
|
||||||
|
} else {
|
||||||
|
kore_log(LOG_NOTICE, "received error while authenticating to redis: %s", reply->str);
|
||||||
|
freeReplyObject(reply);
|
||||||
|
}
|
||||||
|
redisFree(redis);
|
||||||
|
redis = nullptr;
|
||||||
|
kore_shutdown();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void kore_worker_teardown() {
|
||||||
|
if (redis) {
|
||||||
|
redisFree(redis);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <hiredis/hiredis.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
std::optional<std::string> redis_host;
|
||||||
|
std::optional<int> redis_port;
|
||||||
|
std::optional<std::string> redis_password;
|
||||||
|
} Config;
|
||||||
|
|
||||||
|
inline Config config;
|
||||||
|
inline redisContext* redis = nullptr;
|
||||||
|
inline std::mutex redis_mutex;
|
|
@ -0,0 +1,231 @@
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <kore/http.h>
|
||||||
|
#include <kore/curl.h>
|
||||||
|
#include <kore/tasks.h>
|
||||||
|
#include <string>
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
int album_or_image(struct http_request*);
|
||||||
|
static int album_or_image_start(struct http_request*);
|
||||||
|
static int album_or_image_end(struct http_request*);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct http_state states[] = {
|
||||||
|
KORE_HTTP_STATE(album_or_image_start),
|
||||||
|
KORE_HTTP_STATE(album_or_image_end)
|
||||||
|
};
|
||||||
|
|
||||||
|
int album_or_image(struct http_request* req) {
|
||||||
|
return http_state_run(states, 2, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_hget_and_http_resp(struct http_request* req, const char* id, bool is_album) {
|
||||||
|
DeterminedKeys keys = determine_keys(id, is_album);
|
||||||
|
redis_mutex.lock();
|
||||||
|
redisReply* reply = (redisReply*)redisCommand(redis, "HGET %s %s", keys.key.c_str(), keys.field.c_str());
|
||||||
|
if (!reply) {
|
||||||
|
redis_mutex.unlock();
|
||||||
|
kore_log(LOG_NOTICE, "received nullptr while sending HGET to redis");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reply->type == REDIS_REPLY_ERROR) {
|
||||||
|
kore_log(LOG_NOTICE, "received error while sending HGET to redis: %s", reply->str);
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reply->type == REDIS_REPLY_NIL) {
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!reply->len) {
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
char* error = "404: media not found";
|
||||||
|
if (is_album) {
|
||||||
|
error = "404: album not found";
|
||||||
|
}
|
||||||
|
std::string error_page = build_error_page(error);
|
||||||
|
http_response(req, 404, error_page.c_str(), error_page.length());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Album album = {false, std::nullopt, {}};
|
||||||
|
std::optional<std::string> error = unpack_album_from_string(&album, reply->str);
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
if (error) {
|
||||||
|
kore_log(LOG_NOTICE, "received error while parsing packed album: %s", (*error).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
send_album_page(req, std::move(album));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int album_or_image_start(struct http_request* req) {
|
||||||
|
const char* id = &(req->path[1]);
|
||||||
|
bool is_album = false;
|
||||||
|
const char* slash_in_path = strchr(id, '/');
|
||||||
|
if (slash_in_path) {
|
||||||
|
id = &(slash_in_path[1]);
|
||||||
|
is_album = true;
|
||||||
|
}
|
||||||
|
if (redis) {
|
||||||
|
if (send_hget_and_http_resp(req, id, is_album)) {
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string api_url = "https://api.imgur.com/post/v1/";
|
||||||
|
if (is_album) {
|
||||||
|
api_url.append("albums/");
|
||||||
|
} else {
|
||||||
|
api_url.append("media/");
|
||||||
|
}
|
||||||
|
api_url.append(id);
|
||||||
|
api_url.append("?client_id=546c25a59c58ad7&include=media");
|
||||||
|
struct kore_curl* client = (kore_curl*)http_state_create(req, sizeof(*client), NULL);
|
||||||
|
if (!kore_curl_init(client, api_url.c_str(), KORE_CURL_ASYNC)) {
|
||||||
|
http_state_cleanup(req);
|
||||||
|
kore_log(LOG_NOTICE, "failed to initialize curl client");
|
||||||
|
std::string error_page = build_error_page("Failed to initialize curl client");
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
// handle compressed responses automatically
|
||||||
|
curl_easy_setopt(client->handle, CURLOPT_ACCEPT_ENCODING, "");
|
||||||
|
kore_curl_http_setup(client, HTTP_METHOD_GET, NULL, 0);
|
||||||
|
kore_curl_bind_request(client, req);
|
||||||
|
kore_curl_run(client);
|
||||||
|
req->fsm_state = 1;
|
||||||
|
return HTTP_STATE_RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_hset(const char* id, bool is_album, const char* packed) {
|
||||||
|
DeterminedKeys keys = determine_keys(id, is_album);
|
||||||
|
redis_mutex.lock();
|
||||||
|
redisReply* reply = (redisReply*)redisCommand(redis, "HSET %s %s %s", keys.key.c_str(), keys.field.c_str(), packed);
|
||||||
|
if (!reply) {
|
||||||
|
redis_mutex.unlock();
|
||||||
|
kore_log(LOG_NOTICE, "received nullptr while sending HSET to redis");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply->type == REDIS_REPLY_ERROR) {
|
||||||
|
kore_log(LOG_NOTICE, "received error while sending HSET to redis: %s", reply->str);
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
freeReplyObject(reply);
|
||||||
|
redis_mutex.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int album_or_image_end(struct http_request* req) {
|
||||||
|
const char* id = &(req->path[1]);
|
||||||
|
bool is_album = false;
|
||||||
|
const char* slash_in_path = strchr(id, '/');
|
||||||
|
if (slash_in_path) {
|
||||||
|
id = &(slash_in_path[1]);
|
||||||
|
is_album = true;
|
||||||
|
}
|
||||||
|
struct kore_curl* client = (kore_curl*)http_state_get(req);
|
||||||
|
if (!kore_curl_success(client)) {
|
||||||
|
kore_curl_logerror(client);
|
||||||
|
std::string error = "Failed to contact API: ";
|
||||||
|
error.append(kore_curl_strerror(client));
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
std::string error_page = build_error_page(error.c_str());
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
char* body = kore_curl_response_as_string(client);
|
||||||
|
kore_json json;
|
||||||
|
kore_json_init(&json, body, strlen(body));
|
||||||
|
if (kore_json_parse(&json) != KORE_RESULT_OK) {
|
||||||
|
kore_log(LOG_NOTICE, "failed to parse json: %s", kore_json_strerror(&json));
|
||||||
|
std::string error = "Failed to parse response: ";
|
||||||
|
error.append(kore_json_strerror(&json));
|
||||||
|
kore_json_cleanup(&json);
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
std::string error_page = build_error_page(error.c_str());
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
kore_json_item* errors = kore_json_find_array(json.root, "errors");
|
||||||
|
if (errors) {
|
||||||
|
std::string error = "Received error(s) from API:<pre><code>";
|
||||||
|
struct kore_json_item* error_json;
|
||||||
|
if (!TAILQ_EMPTY(&errors->data.items)) {
|
||||||
|
error_json = TAILQ_FIRST(&errors->data.items);
|
||||||
|
kore_json_item* error_code_json = kore_json_find_string(error_json, "code");
|
||||||
|
kore_json_item* error_detail_json = kore_json_find_string(error_json, "detail");
|
||||||
|
if (error_code_json && error_detail_json && !strcmp(error_code_json->data.string, "404")) {
|
||||||
|
error = "404: ";
|
||||||
|
error.append(error_detail_json->data.string);
|
||||||
|
kore_json_cleanup(&json);
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
if (redis) {
|
||||||
|
send_hset(id, is_album, "");
|
||||||
|
}
|
||||||
|
std::string error_page = build_error_page(error.c_str());
|
||||||
|
http_response(req, 404, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool needs_newline = false;
|
||||||
|
TAILQ_FOREACH(error_json, &errors->data.items, list) {
|
||||||
|
// istg if string.append mutates strings passed
|
||||||
|
char* error_code = "unknown error code";
|
||||||
|
char* error_detail = "unknown error detail";
|
||||||
|
kore_json_item* error_code_json = kore_json_find_string(error_json, "code");
|
||||||
|
kore_json_item* error_detail_json = kore_json_find_string(error_json, "detail");
|
||||||
|
if (error_code_json) {
|
||||||
|
error_code = error_code_json->data.string;
|
||||||
|
}
|
||||||
|
if (error_detail_json) {
|
||||||
|
error_detail = error_detail_json->data.string;
|
||||||
|
}
|
||||||
|
kore_log(LOG_NOTICE, "received error from api: %s: %s", error_code, error_detail);
|
||||||
|
std::string to_append;
|
||||||
|
if (needs_newline) {
|
||||||
|
to_append.append("\n");
|
||||||
|
}
|
||||||
|
to_append.append(error_code);
|
||||||
|
to_append.append(": ");
|
||||||
|
to_append.append(error_detail);
|
||||||
|
escape_xml(&to_append);
|
||||||
|
error.append(std::move(to_append));
|
||||||
|
needs_newline = true;
|
||||||
|
}
|
||||||
|
error.append("</code></pre>");
|
||||||
|
kore_json_cleanup(&json);
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
std::string error_page = build_error_page(error.c_str(), true);
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
Album album;
|
||||||
|
std::optional<std::string> error = deserialize_album(json, &album);
|
||||||
|
kore_json_cleanup(&json);
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
if (error) {
|
||||||
|
kore_log(LOG_NOTICE, "%s", (*error).c_str());
|
||||||
|
std::string error_page = build_error_page((*error).c_str());
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
if (redis) {
|
||||||
|
std::string packed = pack_album_into_string(album);
|
||||||
|
send_hset(id, is_album, packed.c_str());
|
||||||
|
}
|
||||||
|
send_album_page(req, std::move(album));
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <kore/http.h>
|
||||||
|
#include <kore/curl.h>
|
||||||
|
#include <string>
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include "404.hpp"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
int proxy_image(struct http_request*);
|
||||||
|
static int proxy_start(struct http_request*);
|
||||||
|
static int proxy_end(struct http_request*);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct http_state proxy_states[] = {
|
||||||
|
KORE_HTTP_STATE(proxy_start),
|
||||||
|
KORE_HTTP_STATE(proxy_end)
|
||||||
|
};
|
||||||
|
|
||||||
|
int proxy_image(struct http_request* req) {
|
||||||
|
return http_state_run(proxy_states, 2, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int proxy_start(struct http_request* req) {
|
||||||
|
// /proxy/
|
||||||
|
const char* filename = &(req->path[7]);
|
||||||
|
std::string url = "https://i.imgur.com/";
|
||||||
|
url.append(filename);
|
||||||
|
struct kore_curl* client = (kore_curl*)http_state_create(req, sizeof(*client), NULL);
|
||||||
|
if (!kore_curl_init(client, url.c_str(), KORE_CURL_ASYNC)) {
|
||||||
|
http_state_cleanup(req);
|
||||||
|
kore_log(LOG_NOTICE, "failed to initialize curl client");
|
||||||
|
std::string error_page = build_error_page("Failed to initialize curl client");
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
const char* header_value;
|
||||||
|
if (http_request_header(req, "Content-Range", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "Content-Range", header_value);
|
||||||
|
}
|
||||||
|
if (http_request_header(req, "If-None-Match", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "If-None-Match", header_value);
|
||||||
|
}
|
||||||
|
if (http_request_header(req, "If-Match", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "If-Match", header_value);
|
||||||
|
}
|
||||||
|
if (http_request_header(req, "If-Modified-Since", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "If-Modified-Since", header_value);
|
||||||
|
}
|
||||||
|
if (http_request_header(req, "If-Unmodified-Since", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "If-Unmodified-Since", header_value);
|
||||||
|
}
|
||||||
|
// hmm
|
||||||
|
if (http_request_header(req, "Cache-Control", &header_value) == KORE_RESULT_OK) {
|
||||||
|
kore_curl_http_set_header(client, "Cache-Control", header_value);
|
||||||
|
}
|
||||||
|
kore_curl_http_setup(client, HTTP_METHOD_GET, NULL, 0);
|
||||||
|
kore_curl_bind_request(client, req);
|
||||||
|
kore_curl_run(client);
|
||||||
|
req->fsm_state = 1;
|
||||||
|
return HTTP_STATE_RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int proxy_end(struct http_request* req) {
|
||||||
|
struct kore_curl* client = (kore_curl*)http_state_get(req);
|
||||||
|
if (!kore_curl_success(client)) {
|
||||||
|
kore_curl_logerror(client);
|
||||||
|
std::string error = "Failed to contact Imgur: ";
|
||||||
|
error.append(kore_curl_strerror(client));
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
std::string error_page = build_error_page(error.c_str());
|
||||||
|
http_response(req, 500, error_page.c_str(), error_page.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
// imgur redirects to https://imgur.com/<rest of path> when image 404s
|
||||||
|
if (client->http.status == 302) {
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
not_found(req);
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
||||||
|
const char* header_value;
|
||||||
|
if (kore_curl_http_get_header(client, "Range", &header_value) == KORE_RESULT_OK) {
|
||||||
|
http_response_header(req, "Range", header_value);
|
||||||
|
}
|
||||||
|
if (kore_curl_http_get_header(client, "Accept-Ranges", &header_value) == KORE_RESULT_OK) {
|
||||||
|
http_response_header(req, "Accept-Ranges", header_value);
|
||||||
|
}
|
||||||
|
if (kore_curl_http_get_header(client, "ETag", &header_value) == KORE_RESULT_OK) {
|
||||||
|
http_response_header(req, "ETag", header_value);
|
||||||
|
}
|
||||||
|
if (kore_curl_http_get_header(client, "Content-Type", &header_value) == KORE_RESULT_OK) {
|
||||||
|
http_response_header(req, "Content-Type", header_value);
|
||||||
|
}
|
||||||
|
if (kore_curl_http_get_header(client, "Cache-Control", &header_value) == KORE_RESULT_OK) {
|
||||||
|
http_response_header(req, "Cache-Control", header_value);
|
||||||
|
}
|
||||||
|
const u_int8_t* body;
|
||||||
|
size_t len;
|
||||||
|
kore_curl_response_as_bytes(client, &body, &len);
|
||||||
|
http_response(req, client->http.status, body, len);
|
||||||
|
kore_curl_cleanup(client);
|
||||||
|
http_state_cleanup(req);
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
|
@ -0,0 +1,401 @@
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <charconv>
|
||||||
|
#include <system_error>
|
||||||
|
#include <kore/kore.h>
|
||||||
|
#include <kore/http.h>
|
||||||
|
#include "utils.hpp"
|
||||||
|
|
||||||
|
#define ERROR_PAGE_START "<!DOCTYPE html>\n" \
|
||||||
|
"<html>\n" \
|
||||||
|
" <head>\n" \
|
||||||
|
" <meta charset=\"utf-8\" />\n" \
|
||||||
|
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" \
|
||||||
|
" <title>ImgurXP</title>\n" \
|
||||||
|
" <link rel=\"stylesheet\" href=\"/style.css\" crossorigin=\"anonymous\" />\n" \
|
||||||
|
" </head>\n" \
|
||||||
|
" <body>\n" \
|
||||||
|
" <div><b>"
|
||||||
|
|
||||||
|
#define ERROR_PAGE_END "</b></div>\n </body>\n</html>"
|
||||||
|
|
||||||
|
#define ALBUM_PAGE_START "<!DOCTYPE html>\n" \
|
||||||
|
"<html>\n" \
|
||||||
|
" <head>\n" \
|
||||||
|
" <meta charset=\"utf-8\" />\n" \
|
||||||
|
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" \
|
||||||
|
" <title>"
|
||||||
|
|
||||||
|
#define ALBUM_PAGE_START_2 "</title>\n" \
|
||||||
|
" <link rel=\"stylesheet\" href=\"/style.css\" crossorigin=\"anonymous\" />\n" \
|
||||||
|
" </head>\n" \
|
||||||
|
" <body>"
|
||||||
|
|
||||||
|
#define ALBUM_PAGE_END "\n </body>\n</html>"
|
||||||
|
|
||||||
|
std::string build_error_page(const char* error, bool already_escaped) {
|
||||||
|
std::string output = ERROR_PAGE_START;
|
||||||
|
if (already_escaped) {
|
||||||
|
output.append(error);
|
||||||
|
} else {
|
||||||
|
std::string error_string = error;
|
||||||
|
escape_xml(&error_string);
|
||||||
|
output.append(std::move(error_string));
|
||||||
|
}
|
||||||
|
output.append(ERROR_PAGE_END);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// behold: the why
|
||||||
|
// oh and this is stolen from https://stackoverflow.com/a/3418285
|
||||||
|
void escape_xml(std::string* str) {
|
||||||
|
if (str->empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t start = 0;
|
||||||
|
while ((start = str->find("&", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, "&");
|
||||||
|
start += 5;
|
||||||
|
}
|
||||||
|
start = 0;
|
||||||
|
while ((start = str->find("<", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, "<");
|
||||||
|
start += 4;
|
||||||
|
}
|
||||||
|
start = 0;
|
||||||
|
while ((start = str->find(">", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, ">");
|
||||||
|
start += 4;
|
||||||
|
}
|
||||||
|
start = 0;
|
||||||
|
while ((start = str->find("\"", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, """);
|
||||||
|
start += 6;
|
||||||
|
}
|
||||||
|
start = 0;
|
||||||
|
while ((start = str->find("'", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, "'");
|
||||||
|
start += 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> deserialize_album(kore_json json, Album* album) {
|
||||||
|
*album = {false, std::nullopt, {}};
|
||||||
|
kore_json_item* title_json = kore_json_find_string(json.root, "title");
|
||||||
|
if (title_json && *title_json->data.string) {
|
||||||
|
album->title = title_json->data.string;
|
||||||
|
}
|
||||||
|
kore_json_item* is_mature_json = kore_json_find_literal(json.root, "is_mature");
|
||||||
|
if (is_mature_json) {
|
||||||
|
album->is_mature = is_mature_json->data.literal == KORE_JSON_TRUE;
|
||||||
|
}
|
||||||
|
kore_json_item* medias_json = kore_json_find_array(json.root, "media");
|
||||||
|
if (!medias_json) {
|
||||||
|
return "missing media key in response";
|
||||||
|
}
|
||||||
|
kore_json_item* media_json;
|
||||||
|
TAILQ_FOREACH(media_json, &medias_json->data.items, list) {
|
||||||
|
kore_json_item* media_type_json = kore_json_find_string(media_json, "type");
|
||||||
|
kore_json_item* id_json = kore_json_find_string(media_json, "id");
|
||||||
|
kore_json_item* ext_json = kore_json_find_string(media_json, "ext");
|
||||||
|
kore_json_item* width_json = kore_json_find_integer_u64(media_json, "width");
|
||||||
|
kore_json_item* height_json = kore_json_find_integer_u64(media_json, "height");
|
||||||
|
if (!id_json) {
|
||||||
|
return "missing id key from response";
|
||||||
|
}
|
||||||
|
if (!media_type_json) {
|
||||||
|
std::string error = "missing type key from response (id: ";
|
||||||
|
error.append(id_json->data.string);
|
||||||
|
error.append(")");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (!ext_json) {
|
||||||
|
std::string error = "missing ext key from response (id: ";
|
||||||
|
error.append(id_json->data.string);
|
||||||
|
error.append(")");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (!width_json) {
|
||||||
|
std::string error = "missing width key from response (id: ";
|
||||||
|
error.append(id_json->data.string);
|
||||||
|
error.append(")");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (!height_json) {
|
||||||
|
std::string error = "missing height key from response (id: ";
|
||||||
|
error.append(id_json->data.string);
|
||||||
|
error.append(")");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
MediaType media_type = MediaType::Image;
|
||||||
|
if (!strcmp(media_type_json->data.string, "image")) {
|
||||||
|
// already set as image
|
||||||
|
} else if (!strcmp(media_type_json->data.string, "video")) {
|
||||||
|
media_type = MediaType::Video;
|
||||||
|
} else {
|
||||||
|
std::string error = "unknown media type (id: ";
|
||||||
|
error.append(id_json->data.string);
|
||||||
|
error.append(", type: ");
|
||||||
|
error.append(media_type_json->data.string);
|
||||||
|
error.append(")");
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
Media media = {std::nullopt, media_type, id_json->data.string, ext_json->data.string, width_json->data.u64, height_json->data.u64};
|
||||||
|
kore_json_item* metadata_json = kore_json_find_object(media_json, "metadata");
|
||||||
|
if (metadata_json) {
|
||||||
|
kore_json_item* description_json = kore_json_find_string(metadata_json, "description");
|
||||||
|
if (description_json && *description_json->data.string) {
|
||||||
|
media.description = description_json->data.string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
album->media.push_back(std::move(media));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeterminedKeys determine_keys(const char* id, bool album) {
|
||||||
|
std::string id_str;
|
||||||
|
if (album) {
|
||||||
|
id_str.push_back('+');
|
||||||
|
id_str.append(id);
|
||||||
|
} else {
|
||||||
|
id_str = id;
|
||||||
|
}
|
||||||
|
if (id_str.length() <= 2) {
|
||||||
|
return {"imgurxp:", std::move(id_str)};
|
||||||
|
}
|
||||||
|
char s1 = id_str.back();
|
||||||
|
id_str.pop_back();
|
||||||
|
char s0 = id_str.back();
|
||||||
|
id_str.pop_back();
|
||||||
|
id_str.insert(0, "imgurxp:");
|
||||||
|
return {std::move(id_str), {s0, s1}};
|
||||||
|
}
|
||||||
|
|
||||||
|
void escape_newline(std::string* str) {
|
||||||
|
if (str->empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t start = 0;
|
||||||
|
while ((start = str->find("\\", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, "\\\\");
|
||||||
|
start += 2;
|
||||||
|
}
|
||||||
|
start = 0;
|
||||||
|
while ((start = str->find("\n", start)) != std::string::npos) {
|
||||||
|
str->replace(start, 1, "\\n");
|
||||||
|
start += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unescape_newline(std::string* str) {
|
||||||
|
if (str->empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t start = 0;
|
||||||
|
while ((start = str->find('\\', start)) != std::string::npos) {
|
||||||
|
if (str->length() - 1 >= start + 1) {
|
||||||
|
char c = str->at(start + 1);
|
||||||
|
if (c == 'n') {
|
||||||
|
c = '\n';
|
||||||
|
}
|
||||||
|
str->replace(start, 2, &c, 1);
|
||||||
|
}
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pack_album_into_string(Album album) {
|
||||||
|
std::string packed;
|
||||||
|
if (album.is_mature) {
|
||||||
|
packed.push_back('1');
|
||||||
|
} else {
|
||||||
|
packed.push_back('0');
|
||||||
|
}
|
||||||
|
if (album.title) {
|
||||||
|
std::string title = (*album.title).substr();
|
||||||
|
escape_newline(&title);
|
||||||
|
packed.append(std::move(title));
|
||||||
|
}
|
||||||
|
for (size_t i=0; i < album.media.size(); i++) {
|
||||||
|
packed.push_back('\n');
|
||||||
|
Media media = album.media[i];
|
||||||
|
switch (media.type) {
|
||||||
|
case MediaType::Image:
|
||||||
|
packed.push_back('0');
|
||||||
|
break;
|
||||||
|
case MediaType::Video:
|
||||||
|
packed.push_back('1');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
packed.push_back(' ');
|
||||||
|
packed.append(media.id);
|
||||||
|
packed.push_back(' ');
|
||||||
|
packed.append(media.ext);
|
||||||
|
packed.push_back(' ');
|
||||||
|
packed.append(std::to_string(media.width));
|
||||||
|
packed.push_back(' ');
|
||||||
|
packed.append(std::to_string(media.height));
|
||||||
|
if (media.description) {
|
||||||
|
packed.push_back(' ');
|
||||||
|
std::string description = (*media.description).substr();
|
||||||
|
escape_newline(&description);
|
||||||
|
packed.append(std::move(description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::optional<std::string> parse_packed_media_line(char* line, Media* media) {
|
||||||
|
switch (line[0]) {
|
||||||
|
case '0':
|
||||||
|
media->type = MediaType::Image;
|
||||||
|
break;
|
||||||
|
case '1':
|
||||||
|
media->type = MediaType::Video;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
std::string error = "unknown media type (";
|
||||||
|
error.push_back(line[0]);
|
||||||
|
error.push_back(')');
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
media->id = std::string(&(line[2]), strchr(&(line[2]), ' ') - line - 2);
|
||||||
|
size_t offset = 2 + media->id.length() + 1;
|
||||||
|
media->ext = std::string(&(line[offset]), strchr(&(line[offset]), ' ') - line - offset);
|
||||||
|
offset += media->ext.length() + 1;
|
||||||
|
size_t width_offset = strchr(&(line[offset]), ' ') - line;
|
||||||
|
std::from_chars_result char_res = std::from_chars(&(line[offset]), &(line[width_offset]), media->width);
|
||||||
|
if (char_res.ec != std::errc()) {
|
||||||
|
std::string error = "failed to parse width: ";
|
||||||
|
error.append(std::make_error_code(char_res.ec).message());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
offset = width_offset + 1;
|
||||||
|
size_t height_offset = strchr(&(line[offset]), ' ') - line;
|
||||||
|
if (!height_offset) {
|
||||||
|
height_offset = strlen(line);
|
||||||
|
}
|
||||||
|
char_res = std::from_chars(&(line[offset]), &(line[height_offset]), media->height);
|
||||||
|
if (char_res.ec != std::errc()) {
|
||||||
|
std::string error = "failed to parse height: ";
|
||||||
|
error.append(std::make_error_code(char_res.ec).message());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
offset = height_offset + 1;
|
||||||
|
char* line_end = strchr(line, '\n');
|
||||||
|
if (!line_end) {
|
||||||
|
line_end = &(line[strlen(line)]);
|
||||||
|
}
|
||||||
|
if ((size_t)(line_end - line) > offset) {
|
||||||
|
offset++;
|
||||||
|
std::string description = std::string(&(line[offset]), line_end - line - offset);
|
||||||
|
unescape_newline(&description);
|
||||||
|
media->description = std::move(description);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> unpack_album_from_string(Album* album, char* str) {
|
||||||
|
album->is_mature = str[0] != '0';
|
||||||
|
std::string title;
|
||||||
|
char* title_end = strchr(&(str[1]), '\n');
|
||||||
|
if (title_end) {
|
||||||
|
title = std::string(&(str[1]), title_end - str - 1);
|
||||||
|
} else {
|
||||||
|
title = &(str[1]);
|
||||||
|
}
|
||||||
|
if (!title.empty()) {
|
||||||
|
unescape_newline(&title);
|
||||||
|
album->title = std::move(title);
|
||||||
|
}
|
||||||
|
if (!title_end) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
str = &(title_end[1]);
|
||||||
|
while (true) {
|
||||||
|
char spaces = 0;
|
||||||
|
for (size_t i=0; str[i] && str[i] != '\n'; i++) {
|
||||||
|
if (str[i] == ' ') {
|
||||||
|
spaces++;
|
||||||
|
if (spaces == 4) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spaces < 4) {
|
||||||
|
return "media line missing required spaces";
|
||||||
|
}
|
||||||
|
Media media = {std::nullopt, MediaType::Image, "", "", 0, 0};
|
||||||
|
std::optional<std::string> error = parse_packed_media_line(str, &media);
|
||||||
|
if (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
album->media.push_back(std::move(media));
|
||||||
|
char* line_end = strchr(str, '\n');
|
||||||
|
if (!line_end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
str = &(line_end[1]);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int send_album_page(struct http_request* req, Album album) {
|
||||||
|
std::string response = ALBUM_PAGE_START;
|
||||||
|
std::optional<std::string> escaped_title;
|
||||||
|
if (album.title) {
|
||||||
|
escaped_title = album.title;
|
||||||
|
escape_xml(&(*escaped_title));
|
||||||
|
response.append((*escaped_title));
|
||||||
|
} else {
|
||||||
|
response.append("ImgurX");
|
||||||
|
}
|
||||||
|
response.append(ALBUM_PAGE_START_2);
|
||||||
|
if (album.is_mature) {
|
||||||
|
response.append("\n <div><b>Marked as Mature</b></div>");
|
||||||
|
}
|
||||||
|
if (escaped_title) {
|
||||||
|
response.append("\n <h1>");
|
||||||
|
response.append(std::move((*escaped_title)));
|
||||||
|
response.append("</h1>");
|
||||||
|
}
|
||||||
|
for (size_t i=0; i < album.media.size(); i++ ) {
|
||||||
|
Media* media = &(album.media[i]);
|
||||||
|
switch (media->type) {
|
||||||
|
case MediaType::Image:
|
||||||
|
response.append("\n <img src=\"/proxy/");
|
||||||
|
response.append(media->id);
|
||||||
|
response.append(".");
|
||||||
|
response.append(media->ext);
|
||||||
|
response.append("\" width=\"");
|
||||||
|
response.append(std::to_string(media->width));
|
||||||
|
response.append("\" height=\"");
|
||||||
|
response.append(std::to_string(media->height));
|
||||||
|
response.append("\" />");
|
||||||
|
break;
|
||||||
|
case MediaType::Video:
|
||||||
|
response.append("\n <video controls=\"1\" src=\"https://i.imgur.com/");
|
||||||
|
response.append(media->id);
|
||||||
|
response.append(".");
|
||||||
|
response.append(media->ext);
|
||||||
|
response.append("\" width=\"");
|
||||||
|
response.append(std::to_string(media->width));
|
||||||
|
response.append("\" height=\"");
|
||||||
|
response.append(std::to_string(media->height));
|
||||||
|
response.append("\" />");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (media->description) {
|
||||||
|
std::string escaped_description = *(media->description);
|
||||||
|
escape_xml(&escaped_description);
|
||||||
|
response.append("\n <p>");
|
||||||
|
response.append(std::move(escaped_description));
|
||||||
|
response.append("</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.append(ALBUM_PAGE_END);
|
||||||
|
http_response(req, 200, response.c_str(), response.length());
|
||||||
|
return HTTP_STATE_COMPLETE;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <kore/kore.h>
|
||||||
|
|
||||||
|
enum MediaType {
|
||||||
|
Image,
|
||||||
|
Video
|
||||||
|
};
|
||||||
|
typedef struct {
|
||||||
|
std::optional<std::string> description;
|
||||||
|
MediaType type;
|
||||||
|
std::string id;
|
||||||
|
std::string ext;
|
||||||
|
uint64_t width;
|
||||||
|
uint64_t height;
|
||||||
|
} Media;
|
||||||
|
typedef struct {
|
||||||
|
bool is_mature;
|
||||||
|
std::optional<std::string> title;
|
||||||
|
std::vector<Media> media;
|
||||||
|
} Album;
|
||||||
|
typedef struct {
|
||||||
|
std::string key;
|
||||||
|
std::string field;
|
||||||
|
} DeterminedKeys;
|
||||||
|
|
||||||
|
std::string build_error_page(const char* error, bool already_escaped = false);
|
||||||
|
void escape_xml(std::string* text);
|
||||||
|
// does not handle errors
|
||||||
|
std::optional<std::string> deserialize_album(kore_json json, Album* album);
|
||||||
|
DeterminedKeys determine_keys(const char* id, bool album);
|
||||||
|
void escape_newline(std::string* text);
|
||||||
|
void unescape_newline(std::string* text);
|
||||||
|
std::string pack_album_into_string(Album album);
|
||||||
|
// is not expected to be empty
|
||||||
|
std::optional<std::string> unpack_album_from_string(Album* album, char* str);
|
||||||
|
int send_album_page(struct http_request* req, Album album);
|
Loading…
Reference in New Issue