Initial commit

This commit is contained in:
blank X 2022-01-17 15:52:25 +07:00
commit f3df9dadc1
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
16 changed files with 1020 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.o
.flavor
.objs
imgurxp.so
assets.h
cert
dh2048.pem
config.json

9
README.md Normal file
View File

@ -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
```

12
assets/404.html Normal file
View File

@ -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>

17
assets/index.html Normal file
View File

@ -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>

23
assets/style.css Normal file
View File

@ -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;
}

40
conf/build.conf Normal file
View File

@ -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.
#}

21
conf/imgurxp.conf Normal file
View File

@ -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
}

5
example_config.json Normal file
View File

@ -0,0 +1,5 @@
{
"redis_host": "127.0.0.1",
"redis_port": 6379,
"redis_password": "gay"
}

9
src/404.cpp Normal file
View File

@ -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;
}

6
src/404.hpp Normal file
View File

@ -0,0 +1,6 @@
#include <kore/kore.h>
#include <kore/http.h>
extern "C" {
int not_found(struct http_request*);
}

81
src/config.cpp Normal file
View File

@ -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);
}
}

14
src/config.hpp Normal file
View File

@ -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;

231
src/imgurxp.cpp Normal file
View File

@ -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;
}

105
src/proxy_image.cpp Normal file
View File

@ -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;
}

401
src/utils.cpp Normal file
View File

@ -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, "&amp;");
start += 5;
}
start = 0;
while ((start = str->find("<", start)) != std::string::npos) {
str->replace(start, 1, "&lt;");
start += 4;
}
start = 0;
while ((start = str->find(">", start)) != std::string::npos) {
str->replace(start, 1, "&gt;");
start += 4;
}
start = 0;
while ((start = str->find("\"", start)) != std::string::npos) {
str->replace(start, 1, "&quot;");
start += 6;
}
start = 0;
while ((start = str->find("'", start)) != std::string::npos) {
str->replace(start, 1, "&#x27;");
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;
}

38
src/utils.hpp Normal file
View File

@ -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);