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