This commit is contained in:
x3 2022-01-08 19:53:58 +01:00
commit a9a0e45906
Signed by: x3
GPG Key ID: 7E9961E8AD0E240E
33 changed files with 2953 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.o
caniadd
TODO

10
.gitmodules vendored Normal file
View File

@ -0,0 +1,10 @@
[submodule "subm/tiny-AES-c"]
path = subm/tiny-AES-c
url = https://github.com/kokke/tiny-AES-c
[submodule "subm/md5-c"]
path = subm/md5-c
url = https://github.com/Zunawe/md5-c
[submodule "subm/MD4"]
path = subm/MD4
url = https://github.com/moex3/MD4
branch = ptr_addition_fix

36
Makefile Normal file
View File

@ -0,0 +1,36 @@
InstallPrefix := /usr/local/bin
PROGNAME := caniadd
VERSION := 1
CFLAGS := -Wall -std=gnu11 #-march=native #-Werror
CPPFLAGS := -DCBC=0 -DCTR=0 -DECB=1 -Isubm/tiny-AES-c/ -Isubm/md5-c/ -Isubm/MD4/ -DPROG_VERSION='"$(VERSION)"'
LDFLAGS := -lpthread -lsqlite3
SOURCES := $(wildcard src/*.c) subm/tiny-AES-c/aes.c subm/md5-c/md5.c subm/MD4/md4.c #$(TOML_SRC) $(BENCODE_SRC)
OBJS := $(SOURCES:.c=.o)
all: CFLAGS += -O3 -flto
all: CPPFLAGS += #-DNDEBUG Just to be safe
all: $(PROGNAME)
# no-pie cus it crashes on my 2nd pc for some reason
dev: CFLAGS += -Og -ggdb -fsanitize=address -fsanitize=leak -fstack-protector-all -no-pie
dev: $(PROGNAME)
t:
echo $(SOURCES)
install: $(PROGNAME)
install -s -- $< $(InstallPrefix)/$(PROGNAME)
uninstall:
rm -f -- $(InstallPrefix)/$(PROGNAME)
$(PROGNAME): $(OBJS)
$(CC) -o $@ $+ $(CFLAGS) $(CPPFLAGS) $(LDFLAGS)
clean:
-rm -- $(OBJS) $(PROGNAME)
re: clean all
red: clean dev

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Caniadd will add files to an AniDB list.
None of the already existing clients did exactly what I wanted, so here I am.
Caniadd is still in developement, but is is already usable.
That is, it implements logging in, encryption, ed2k hashing and adding files, basic ratelimit, keepalive, and file caching.
In the future I want to write an mpv plugin that will use the cached database from caniadd to automatically mark an episode watched on AniDB.
That will be the peak *comfy* animu list management experience.
## Things to do:
- NAT handling
- Multi thread hashing
- Read/write timeout in net
- Api ratelimit (the other part)
- Decode escaping from server
- Use a config file
- Add newline escape to server
- Better field parsing, remove the horrors at code 310
- Add myliststats cmd as --stats arg
- Add support for compression
- Make deleting from mylist possible, with
- Name regexes,
- If file is not found at a scan
- Use api\_cmd style in api\_encrypt\_init
- Buffer up mylistadd api cmds when waiting for ratelimit
- Handle C-c gracefully at any time
- Write -h page, and maybe a man page too

802
src/api.c Normal file
View File

@ -0,0 +1,802 @@
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <printf.h>
#include <time.h>
#include <pthread.h>
#include <errno.h>
#include <md5.h>
#include <aes.h>
#include "api.h"
#include "net.h"
#include "uio.h"
#include "config.h"
#include "ed2k.h"
#include "util.h"
/* Needed, bcuz of custom %B format */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"
#ifdef CLOCK_MONOTONIC_COARSE
#define API_CLOCK CLOCK_MONOTONIC_COARSE
#elif defined(CLOCK_MONOTONIC)
#warn "No coarse monotonic clock"
#define API_CLOCK CLOCK_MONOTONIC
#else
#error "No monotonic clock"
#endif
#define MS_TO_TIMESPEC(ts, ms) { \
ts->tv_sec = ms / 1000; \
ts->tv_nsec = (ms % 1000) * 1000000; \
}
#define MS_TO_TIMESPEC_L(ts, ms) { \
ts.tv_sec = ms / 1000; \
ts.tv_nsec = (ms % 1000) * 1000000; \
}
static enum error api_cmd_logout(struct api_result *res);
static enum error api_cmd_auth(const char *uname, const char *pass,
struct api_result *res);
static bool api_authed = false;
static char api_session[API_SMAXSIZE] = {0}; /* No escaping is needed */
static uint8_t e_key[16] = {0};
static bool api_encryption = false;
static pthread_t api_ka_thread = 0;
static pthread_mutex_t api_work_mx;
static bool api_ka_now = false; /* Are we doing keepalive now? */
static struct timespec api_last_packet = {0}; /* Last packet time */
static int32_t api_packet_count = 0; /* Only increment */
static int32_t api_fast_packet_count = 0; /* Incremented or decrement */
static int api_escaped_string(FILE *io, const struct printf_info *info,
const void *const *args)
{
/* Ignore newline escapes for now */
char *str = *(char**)args[0];
char *and_pos = strchr(str, '&');
size_t w_chars = 0;
if (and_pos == NULL)
return fprintf(io, "%s", str);
while (and_pos) {
w_chars += fprintf(io, "%.*s", (int)(and_pos - str), str);
w_chars += fprintf(io, "&amp;");
str = and_pos + 1;
and_pos = strchr(str, '&');
}
if (*str)
w_chars += fprintf(io, "%s", str);
return w_chars;
}
static int api_escaped_sring_info(const struct printf_info *info, size_t n,
int *argtypes, int *size)
{
if (n > 0) {
argtypes[0] = PA_STRING;
size[0] = sizeof(const char*);
}
return 1;
}
static enum error api_init_encrypt(const char *api_key, const char *uname)
{
char buffer[API_BUFSIZE];
MD5Context md5_ctx;
char *salt_start = buffer + 4 /* 209 [salt here] ... */, *salt_end;
ssize_t r_len, salt_len;
if (net_send(buffer, snprintf(buffer, sizeof(buffer),
"ENCRYPT user=%s&type=1", uname)) == -1) {
return ERR_API_COMMFAIL;
}
r_len = net_read(buffer, sizeof(buffer));
if (strncmp(buffer, "209", 3) != 0) {
uio_error("We expected 209 response, but got: %.*s",
(int)r_len, buffer);
return ERR_API_ENCRYPTFAIL;
}
salt_end = strchr(salt_start, ' ');
if (!salt_end) {
uio_error("Cannot find space after salt in response");
return ERR_API_ENCRYPTFAIL;
}
salt_len = salt_end - salt_start;
md5Init(&md5_ctx);
md5Update(&md5_ctx, (uint8_t*)api_key, strlen(api_key));
md5Update(&md5_ctx, (uint8_t*)salt_start, salt_len);
md5Finalize(&md5_ctx);
memcpy(e_key, md5_ctx.digest, sizeof(e_key));
#if 1
char *buffpos = buffer;
for (int i = 0; i < 16; i++)
buffpos += sprintf(buffpos, "%02x", e_key[i]);
uio_debug("Encryption key is: '%s'", buffer);
#endif
api_encryption = true;
return NOERR;
}
static size_t api_encrypt(char *buffer, size_t data_len)
{
struct AES_ctx actx;
size_t rem_data_len = data_len, ret_len = data_len;
char pad_value;
AES_init_ctx(&actx, e_key);
while (rem_data_len >= AES_BLOCKLEN) {
AES_ECB_encrypt(&actx, (uint8_t*)buffer);
buffer += AES_BLOCKLEN;
rem_data_len -= AES_BLOCKLEN;
}
/* Possible BOF here? maybe? certanly. */
pad_value = AES_BLOCKLEN - rem_data_len;
ret_len += pad_value;
memset(buffer + rem_data_len, pad_value, pad_value);
AES_ECB_encrypt(&actx, (uint8_t*)buffer);
assert(ret_len % AES_BLOCKLEN == 0);
return ret_len;
}
static size_t api_decrypt(char *buffer, size_t data_len)
{
assert(data_len % AES_BLOCKLEN == 0);
struct AES_ctx actx;
size_t ret_len = data_len;
char pad_value;
AES_init_ctx(&actx, e_key);
while (data_len) {
AES_ECB_decrypt(&actx, (uint8_t*)buffer);
buffer += AES_BLOCKLEN;
data_len -= AES_BLOCKLEN;
}
pad_value = buffer[data_len - 1];
ret_len -= pad_value;
return ret_len;
}
static enum error api_auth(const char* uname, const char *passw)
{
struct api_result res;
enum error err = NOERR;
if (!api_encryption)
uio_warning("Logging in without encryption!");
if (api_cmd_auth(uname, passw, &res) != NOERR) {
return ERR_API_AUTH_FAIL;
}
switch (res.code) {
case 201:
uio_warning("A new client version is available!");
case 200:
memcpy(api_session, res.auth.session_key, sizeof(api_session));
api_authed = true;
uio_debug("Succesfully logged in. Session key: '%s'", api_session);
break;
default:
err = ERR_API_AUTH_FAIL;
switch (res.code) {
case 500:
uio_error("Login failed. Please check your credentials again");
break;
case 503:
uio_error("Client is outdated. You're probably out of luck here.");
break;
case 504:
uio_error("Client is banned :( Reason: %s", res.auth.banned_reason);
free(res.auth.banned_reason);
break;
case 505:
uio_error("Illegal input or access denied");
break;
case 601:
uio_error("AniDB out of service");
break;
default:
uio_error("Unknown error: %hu", res.code);
break;
}
}
return err;
}
enum error api_logout()
{
struct api_result res;
enum error err = NOERR;
if (api_cmd_logout(&res) != NOERR) {
return ERR_API_AUTH_FAIL;
}
switch (res.code) {
case 203:
uio_debug("Succesfully logged out");
api_authed = false;
break;
case 403:
uio_error("Cannot log out, because we aren't logged in");
api_authed = false;
break;
default:
err = ERR_API_LOGOUT;
uio_error("Unknown error: %hu", res.code);
break;
}
return err;
}
static void api_keepalive(struct timespec *out_next)
{
struct timespec ts = {0};
uint64_t msdiff;
clock_gettime(API_CLOCK, &ts);
msdiff = util_timespec_diff(&api_last_packet, &ts);
if (msdiff >= API_TIMEOUT) {
struct api_result r;
MS_TO_TIMESPEC(out_next, API_TIMEOUT);
uio_debug("Sending uptime command for keep alive");
// TODO what if another action is already in progress?
api_cmd_uptime(&r);
} else {
uint64_t msnext = API_TIMEOUT - msdiff;
uio_debug("Got keepalive request, but time is not up yet");
MS_TO_TIMESPEC(out_next, msnext);
}
}
void *api_keepalive_main(void *arg)
{
struct timespec ka_time;
MS_TO_TIMESPEC_L(ka_time, API_TIMEOUT);
uio_debug("Hi from keepalie thread");
for (;;) {
if (nanosleep(&ka_time, NULL) != 0) {
int e = errno;
uio_error("Nanosleep failed: %s", strerror(e));
}
/* Needed, because the thread could be canceled while in recv or send
* and in that case, the mutex will remain locked
* Could be replaced with a pthread_cleanup_push ? */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
pthread_mutex_lock(&api_work_mx);
api_ka_now = true;
uio_debug("G'moooooning! Is it time to keep our special connection alive?");
api_keepalive(&ka_time);
uio_debug("Next wakey-wakey in %ld seconds", ka_time.tv_sec);
api_ka_now = false;
pthread_mutex_unlock(&api_work_mx);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
}
return NULL;
}
enum error api_clock_init()
{
struct timespec ts;
memset(&api_last_packet, 0, sizeof(api_last_packet));
api_packet_count = 0;
api_fast_packet_count = 0;
if (clock_getres(API_CLOCK, &ts) != 0) {
uio_error("Cannot get clock resolution: %s", strerror(errno));
return ERR_API_CLOCK;
}
uio_debug("Clock resolution: %f ms",
(ts.tv_sec * 1000) + (ts.tv_nsec / 1000000.0));
return NOERR;
}
enum error api_init(bool auth)
{
enum error err = NOERR;
const char **api_key, **uname, **passwd;
err = api_clock_init();
if (err != NOERR)
return err;
err = net_init();
if (err != NOERR)
return err;
if (config_get("api-key", (void**)&api_key) == NOERR) {
if (config_get("username", (void**)&uname) != NOERR) {
uio_error("Api key is specified, but that also requires "
"the username!");
err = ERR_OPT_REQUIRED;
goto fail;
}
err = api_init_encrypt(*api_key, *uname);
if (err != NOERR) {
uio_error("Cannot init api encryption");
goto fail;
}
}
/* Define an escaped string printf type */
if (register_printf_specifier('B', api_escaped_string,
api_escaped_sring_info) != 0) {
uio_error("Failed to register escaped printf string function");
err = ERR_API_PRINTFFUNC;
goto fail;
}
if (auth) {
if (config_get("username", (void**)&uname) != NOERR) {
uio_error("Username is not specified, but it is required!");
err = ERR_OPT_REQUIRED;
goto fail;
}
if (config_get("password", (void**)&passwd) != NOERR) {
uio_error("Password is not specified, but it is required!");
err = ERR_OPT_REQUIRED;
goto fail;
}
err = api_auth(*uname, *passwd);
if (err != NOERR)
goto fail;
/* Only do keep alive if we have a session */
if (pthread_mutex_init(&api_work_mx, NULL) != 0) {
uio_error("Cannot create mutex");
err = ERR_THRD;
goto fail;
}
if (pthread_create(&api_ka_thread, NULL, api_keepalive_main, NULL) != 0) {
uio_error("Cannot create api keepalive thread");
err = ERR_THRD;
goto fail;
}
}
#if 0
printf("Testings: %B\n", "oi&ha=hi&wooooowz&");
printf("Testings: %B\n", "oi&ha=hi&wooooowz");
printf("Testings: %B\n", "&oi&ha=hi&wooooowz");
printf("Testings: %B\n", "oooooooooiiiiii");
#endif
return err;
fail:
api_free();
return err;
}
void api_free()
{
if (api_authed) {
if (pthread_cancel(api_ka_thread) != 0) {
uio_error("Cannot cancel api keepalive thread");
} else {
int je = pthread_join(api_ka_thread, NULL);
if (je != 0) {
uio_error("Cannot join api keepalive thread: %s",
strerror(je));
}
if (pthread_mutex_destroy(&api_work_mx) != 0)
uio_error("Cannot destroy api work mutex");
}
api_logout();
memset(api_session, 0, sizeof(api_session));
api_authed = false; /* duplicate */
}
if (api_encryption) {
api_encryption = false;
memset(e_key, 0, sizeof(e_key));
}
register_printf_specifier('B', NULL, NULL);
net_free();
}
/*
* We just sent a packet, so update the last packet time here
*/
static void api_ratelimit_sent()
{
clock_gettime(API_CLOCK, &api_last_packet);
}
static void api_ratelimit()
{
struct timespec ts = {0};
uint64_t msdiff, mswait;
clock_gettime(API_CLOCK, &ts);
msdiff = util_timespec_diff(&api_last_packet, &ts);
uio_debug("Time since last packet: %ld ms", msdiff);
if (msdiff >= API_SENDWAIT)
return; /* No ratelimiting is needed */
/* Need ratelimit, so do it here for now */
mswait = API_SENDWAIT - msdiff;
uio_debug("Ratelimit is needed, sleeping for %ld ms", mswait);
MS_TO_TIMESPEC_L(ts, mswait);
if (nanosleep(&ts, NULL) == -1) {
if (errno == EINTR)
uio_error("Nanosleep got interrupted");
else
uio_error("Nanosleep failed");
}
}
static ssize_t api_send(char *buffer, size_t data_len, size_t buf_size)
{
ssize_t read_len;
api_ratelimit();
uio_debug("{Api}: Sending: %.*s", (int)data_len, buffer);
if (api_encryption)
data_len = api_encrypt(buffer, data_len);
if (net_send(buffer, data_len) == -1) {
uio_error("Cannot send data: %s", strerror(errno));
return -1;
}
read_len = net_read(buffer, buf_size);
api_ratelimit_sent();
if (api_encryption)
read_len = api_decrypt(buffer, read_len);
uio_debug("{Api}: Reading: %.*s", (int)read_len, buffer);
return read_len;
}
long api_res_code(const char *buffer)
{
char *end;
long res = strtol(buffer, &end, 10);
if (res == 0 && buffer == end) {
uio_error("No error codes in the response");
return -1;
}
assert(*end == ' ');
return res;
}
static bool api_get_fl(const char *buffer, int32_t index, const char *delim,
char **const out_start, size_t *const out_len)
{
assert(index > 0);
size_t len = strcspn(buffer, delim);
while (--index > 0) {
buffer += len + 1;
len = strcspn(buffer, delim);
}
*out_start = (char*)buffer;
*out_len = len;
return true;
}
static bool api_get_line(const char *buffer, int32_t line_num,
char **const out_line_start, size_t *const out_line_len)
{
return api_get_fl(buffer, line_num, "\n", out_line_start, out_line_len);
}
static bool api_get_field(const char *buffer, int32_t field_num,
char **const out_field_start, size_t *const out_field_len)
{
return api_get_fl(buffer, field_num, " |\n", out_field_start, out_field_len);
}
#if 0
static char *api_get_field_mod(char *buffer, int32_t field_num)
{
char *sptr = NULL;
char *f_start;
f_start = strtok_r(buffer, " ", &sptr);
if (!f_start)
return NULL;
while (field_num --> 0) {
f_start = strtok_r(NULL, " ", &sptr);
if (!f_start)
return NULL;
}
return f_start;
}
#endif
enum error api_cmd_version(struct api_result *res)
{
char buffer[API_BUFSIZE] = "VERSION";
size_t res_len = api_send(buffer, strlen(buffer), sizeof(buffer));
long code;
enum error err = NOERR;
pthread_mutex_lock(&api_work_mx);
if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}
code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}
if (code == 998) {
char *ver_start;
size_t ver_len;
bool glr = api_get_line(buffer, 2, &ver_start, &ver_len);
assert(glr);
(void)glr;
assert(ver_len < sizeof(res->version.version_str));
memcpy(res->version.version_str, ver_start, ver_len);
res->version.version_str[ver_len] = '\0';
}
res->code = (uint16_t)code;
end:
pthread_mutex_unlock(&api_work_mx);
return err;
}
static enum error api_cmd_auth(const char *uname, const char *pass,
struct api_result *res)
{
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
long code;
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"AUTH user=%s&pass=%B&protover=3&client=caniadd&clientver="
PROG_VERSION "&enc=UTF-8", uname, pass), sizeof(buffer));
enum error err = NOERR;
if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}
code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}
if (code == 200 || code == 201) {
char *sess;
size_t sess_len;
bool gfr = api_get_field(buffer, 2, &sess, &sess_len);
assert(gfr);
(void)gfr;
assert(sess_len < sizeof(res->auth.session_key));
memcpy(res->auth.session_key, sess, sess_len);
res->auth.session_key[sess_len] = '\0';
} else if (code == 504) {
char *reason;
size_t reason_len;
bool gfr = api_get_field(buffer, 5, &reason, &reason_len);
assert(gfr);
(void)gfr;
res->auth.banned_reason = strndup(reason, reason_len);
}
res->code = (uint16_t)code;
end:
pthread_mutex_unlock(&api_work_mx);
return err;
}
static enum error api_cmd_logout(struct api_result *res)
{
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"LOGOUT s=%s", api_session), sizeof(buffer));
long code;
enum error err = NOERR;
if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}
code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}
res->code = (uint16_t)code;
end:
pthread_mutex_unlock(&api_work_mx);
return err;
}
enum error api_cmd_uptime(struct api_result *res)
{
/* If mutex is not already locked from the keepalive thread */
/* Or we could use a recursive mutex? */
if (!api_ka_now)
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"UPTIME s=%s", api_session), sizeof(buffer));
long code;
enum error err = NOERR;
if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}
code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}
if (code == 208) {
char *ls;
size_t ll;
bool glf = api_get_line(buffer, 2, &ls, &ll);
assert(glf);
(void)glf;
res->uptime.ms = strtol(ls, NULL, 10);
}
res->code = (uint16_t)code;
end:
if (!api_ka_now)
pthread_mutex_unlock(&api_work_mx);
return err;
}
enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash,
enum mylist_state ml_state, bool watched, struct api_result *res)
{
char buffer[API_BUFSIZE];
char hash_str[ED2K_HASH_SIZE * 2 + 1];
size_t res_len;
enum error err = NOERR;
long code;
pthread_mutex_lock(&api_work_mx);
util_byte2hex(hash, ED2K_HASH_SIZE, false, hash_str);
/* Wiki says file size is 4 bytes, but no way that's true lol */
res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"MYLISTADD s=%s&size=%ld&ed2k=%s&state=%hu&viewed=%d",
api_session, size, hash_str, ml_state, watched),
sizeof(buffer));
if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}
code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}
if (code == 210) {
char *ls, id_str[12];
size_t ll;
bool glr = api_get_line(buffer, 2, &ls, &ll);
assert(glr);
(void)glr;
assert(sizeof(id_str) > ll);
memcpy(id_str, ls, ll);
id_str[ll] = '\0';
res->mylistadd.new_id = strtoll(id_str, NULL, 10);
/* Wiki says these id's are 4 bytes, which is untrue...
* that page may be a little out of date (or they just
* expect us to use common sense lmao */
} else if (code == 310) {
/* {int4 lid}|{int4 fid}|{int4 eid}|{int4 aid}|{int4 gid}|
* {int4 date}|{int2 state}|{int4 viewdate}|{str storage}|
* {str source}|{str other}|{int2 filestate} */
char *ls;
size_t ll;
struct api_mylistadd_result *mr = &res->mylistadd;
bool glr = api_get_line(buffer, 2, &ls, &ll);
assert(glr);
assert(ll < API_BUFSIZE - 1);
(void)glr;
ls[ll] = '\0';
void *fptrs[] = {
&mr->lid, &mr->fid, &mr->eid, &mr->aid, &mr->gid, &mr->date,
&mr->state, &mr->viewdate, &mr->storage, &mr->source,
&mr->other, &mr->filestate,
};
for (int idx = 1; idx <= 12; idx++) {
char *fs, *endptr;
size_t fl;
bool pr;
uint64_t val;
size_t cpy_size = sizeof(mr->lid);
if (idx == 7)
cpy_size = sizeof(mr->state);
if (idx == 12)
cpy_size = sizeof(mr->filestate);
pr = api_get_field(ls, idx, &fs, &fl);
assert(pr);
(void)pr;
if (idx == 9 || idx == 10 || idx == 11) { /* string fields */
if (fl == 0)
*(char**)fptrs[idx-1] = NULL;
else
*(char**)fptrs[idx-1] = strndup(fs, fl);
continue;
}
val = strtoull(fs, &endptr, 10);
assert(!(val == 0 && fs == endptr));
memcpy(fptrs[idx-1], &val, cpy_size);
}
}
res->code = (uint16_t)code;
end:
pthread_mutex_unlock(&api_work_mx);
return err;
}
#pragma GCC diagnostic pop

91
src/api.h Normal file
View File

@ -0,0 +1,91 @@
#ifndef _API_H
#define _API_H
#include <stdint.h>
#include <time.h>
#include "error.h"
/* Maximum length of one response/request */
#define API_BUFSIZE 1400
/* Session key maximum size, including '\0' */
#define API_SMAXSIZE 16
/* The session timeout in miliseconds */
#define API_TIMEOUT 30 * 60 * 1000
/* How many miliseconds to wait between sends */
#define API_SENDWAIT 2 * 1000
/* The number of packets that are exccempt from the ratelimit */
#define API_FREESEND 5
/* Long term wait between sends */
#define API_SENDWAIT_LONG 4 * 1000
/* After this many packets has been sent, use the longterm ratelimit */
#define API_LONGTERM_PACKETS 100
enum mylist_state {
MYLIST_STATE_UNKNOWN = 0,
MYLIST_STATE_INTERNAL,
MYLIST_STATE_EXTERNAL,
MYLIST_STATE_DELETED,
MYLIST_STATE_REMOTE,
};
enum file_state {
FILE_STATE_NORMAL = 0,
FILE_STATE_CORRUPT,
FILE_STATE_SELF_EDIT,
FILE_STATE_SELF_RIP = 10,
FILE_STATE_ON_DVD,
FILE_STATE_ON_VHS,
FILE_STATE_ON_TV,
FILE_STATE_IN_THEATERS,
FILE_STATE_STREAMED,
FILE_STATE_OTHER = 100,
};
struct api_version_result {
char version_str[40];
};
struct api_auth_result {
union {
char session_key[API_SMAXSIZE];
/* free() */
char *banned_reason;
};
};
struct api_uptime_result {
int32_t ms;
};
struct api_mylistadd_result {
union {
uint64_t new_id;
struct {
uint64_t lid, fid, eid, aid, gid, date, viewdate;
/* free() if != NULL ofc */
char *storage, *source, *other;
enum mylist_state state;
enum file_state filestate;
};
};
};
#define e(n) struct api_##n##_result n
struct api_result {
uint16_t code;
union {
struct api_version_result version;
struct api_auth_result auth;
struct api_uptime_result uptime;
e(mylistadd);
};
};
#undef e
enum error api_init(bool auth);
void api_free();
enum error api_cmd_version(struct api_result *res);
enum error api_cmd_uptime(struct api_result *res);
enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash,
enum mylist_state fstate, bool watched, struct api_result *res);
#endif /* _API_H */

208
src/cache.c Normal file
View File

@ -0,0 +1,208 @@
#include <stddef.h>
#include <sqlite3.h>
#include "cache.h"
#include "config.h"
#include "uio.h"
#include "ed2k.h"
#include "util.h"
#define sqlite_bind_goto(smt, name, type, ...) { \
int sb_idx = sqlite3_bind_parameter_index(smt, name); \
if (sb_idx == 0) { \
uio_error("Cannot get named parameter for var: %s", name); \
err = ERR_CACHE_SQLITE; \
goto fail; \
} \
int sb_sret = sqlite3_bind_##type(smt, sb_idx, __VA_ARGS__); \
if (sb_sret != SQLITE_OK) {\
uio_error("Cannot bind to statement: %s", sqlite3_errmsg(cache_db));\
err = ERR_CACHE_SQLITE;\
goto fail;\
} \
}
static sqlite3 *cache_db = NULL;
static const char sql_create_table[] = "CREATE TABLE IF NOT EXISTS mylist ("
"lid INTEGER NOT NULL PRIMARY KEY,"
"fname TEXT NOT NULL,"
"fsize INTEGER NOT NULL,"
"ed2k TEXT NOT NULL,"
"UNIQUE (fname, fsize) )";
static const char sql_mylist_add[] = "INSERT INTO mylist "
"(lid, fname, fsize, ed2k) VALUES "
//"(?, ?, ?, ?)";
"(:lid, :fname, :fsize, :ed2k)";
static const char sql_mylist_get[] = "SELECT * FROM mylist WHERE "
"fsize=:fsize AND fname=:fname";
#if 0
static const char sql_has_tables[] = "SELECT 1 FROM sqlite_master "
"WHERE type='table' AND tbl_name='mylist'";
/* Return 0 if false, 1 if true, and -1 if error */
static int cache_has_tables()
{
sqlite3_smt smt;
int sret;
sret = sqlite3_prepare_v2(cache_db, sql_has_tables,
sizeof(sql_has_tables), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return -1;
}
sqlite3_step(&smt);
// ehh fuck this, lets just use if not exists
sret = sqlite3_finalize(&smt);
if (sret != SQLITE_OK)
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db));
}
#endif
/*
* Create database table(s)
*/
static enum error cache_init_table()
{
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;
sret = sqlite3_prepare_v2(cache_db, sql_create_table,
sizeof(sql_create_table), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}
sret = sqlite3_step(smt);
if (sret != SQLITE_DONE) {
uio_error("sql3_step is not done: %s", sqlite3_errmsg(cache_db));
err = ERR_CACHE_SQLITE;
}
sret = sqlite3_finalize(smt);
if (sret != SQLITE_OK)
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db));
return err;
}
enum error cache_init()
{
char **db_path;
enum error err;
int sret;
err = config_get("cachedb", (void**)&db_path);
if (err != NOERR) {
uio_error("Cannot get cache db path from args");
return err;
}
uio_debug("Opening cache db: '%s'", *db_path);
sret = sqlite3_open(*db_path, &cache_db);
if (sret != SQLITE_OK) {
uio_error("Cannot create sqlite3 database: %s", sqlite3_errstr(sret));
sqlite3_close(cache_db); /* Even if arg is NULL, it's A'OK */
return ERR_CACHE_SQLITE;
}
sqlite3_extended_result_codes(cache_db, 1);
err = cache_init_table();
if (err != NOERR)
goto fail;
return NOERR;
fail:
cache_free();
return err;
}
void cache_free()
{
sqlite3_close(cache_db);
uio_debug("Closed cache db");
}
enum error cache_add(uint64_t lid, const char *fname,
uint64_t fsize, const uint8_t *ed2k)
{
char ed2k_str[ED2K_HASH_SIZE * 2 + 1];
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;
sret = sqlite3_prepare_v2(cache_db, sql_mylist_add,
sizeof(sql_mylist_add), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}
util_byte2hex(ed2k, ED2K_HASH_SIZE, false, ed2k_str);
sqlite_bind_goto(smt, ":lid", int64, lid);
sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC);
sqlite_bind_goto(smt, ":fsize", int64, fsize);
sqlite_bind_goto(smt, ":ed2k", text, ed2k_str, -1, SQLITE_STATIC);
sret = sqlite3_step(smt);
if (sret != SQLITE_DONE) {
if (sret == SQLITE_CONSTRAINT_PRIMARYKEY) {
uio_debug("Attempted to add duplicate entry!");
err = ERR_CACHE_EXISTS;
} else if (sret == SQLITE_CONSTRAINT_UNIQUE) {
uio_debug("An entry with the same name and size already exists!");
err = ERR_CACHE_NON_UNIQUE;
} else {
uio_error("error after sql3_step: %s %d", sqlite3_errmsg(cache_db), sret);
err = ERR_CACHE_SQLITE;
}
}
fail:
sqlite3_finalize(smt);
return err;
}
enum error cache_get(const char *fname, uint64_t fsize,
struct cache_entry *out_ce)
{
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;
sret = sqlite3_prepare_v2(cache_db, sql_mylist_get,
sizeof(sql_mylist_get), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}
sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC);
sqlite_bind_goto(smt, ":fsize", int64, fsize);
sret = sqlite3_step(smt);
if (sret == SQLITE_DONE) {
uio_debug("Cache entry with size (%lu) and name (%s) not found", fsize, fname);
err = ERR_CACHE_NO_EXISTS;
} else if (sret == SQLITE_ROW) {
uio_debug("Found Cache entry with size (%lu) and name (%s)", fsize, fname);
} else {
uio_error("sqlite_step failed: %s", sqlite3_errmsg(cache_db));
err = ERR_CACHE_SQLITE;
}
fail:
sqlite3_finalize(smt);
return err;
}

40
src/cache.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef _CACHE_H
#define _CACHE_H
#include <stdint.h>
#include <stdbool.h>
#include "error.h"
#include "ed2k.h"
struct cache_entry {
uint64_t lid, fsize;
char *fname;
uint8_t ed2k[ED2K_HASH_SIZE];
};
/*
* Init tha cache
*/
enum error cache_init();
/*
* Free tha cache
*/
void cache_free();
/*
* Add a new mylist entry to the cache
*/
enum error cache_add(uint64_t lid, const char *fname,
uint64_t fsize, const uint8_t *ed2k);
/*
* Get a cache entry
*
* out_ce can be NULL. Useful, if we only want
* to check if the entry exists or not.
*/
enum error cache_get(const char *fname, uint64_t size,
struct cache_entry *out_ce);
#endif /* _CACHE_H */

29
src/caniadd.c Normal file
View File

@ -0,0 +1,29 @@
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include "config.h"
#include "error.h"
#include "uio.h"
#include "cmd.h"
int main(int argc, char **argv)
{
int exit_code = EXIT_SUCCESS;
enum error err = config_parse(argc, argv);
if (err == ERR_OPT_EXIT)
return EXIT_SUCCESS;
else if (err != NOERR)
return EXIT_FAILURE;
//config_dump();
err = cmd_main();
if (err != NOERR)
exit_code = EXIT_FAILURE;
config_free();
return exit_code;
}

75
src/cmd.c Normal file
View File

@ -0,0 +1,75 @@
#include <stdbool.h>
#include "cmd.h"
#include "error.h"
#include "config.h"
#include "api.h"
#include "uio.h"
#include "net.h"
#include "cache.h"
struct cmd_entry {
bool need_api : 1; /* Does this command needs to connect to the api? */
bool need_auth : 1; /* Does this command needs auth to the api? sets need_api */
bool need_cache : 1; /* Does this cmd needs the file cache? */
const char *arg_name; /* If this argument is present, execute this cmd */
enum error (*fn)(void *data); /* The function for the command */
};
static const struct cmd_entry ents[] = {
{ .arg_name = "version", .fn = cmd_prog_version, },
{ .arg_name = "server-version", .fn = cmd_server_version, .need_api = true },
{ .arg_name = "uptime", .fn = cmd_server_uptime, .need_auth = true },
{ .arg_name = "ed2k", .fn = cmd_ed2k, },
{ .arg_name = "add", .fn = cmd_add, .need_auth = true, .need_cache = true, },
};
static const int32_t ents_len = sizeof(ents)/sizeof(*ents);
static enum error cmd_run_one(const struct cmd_entry *ent)
{
enum error err = NOERR;
if (ent->need_cache) {
err = cache_init();
if (err != NOERR)
goto end;
}
if (ent->need_api || ent->need_auth) {
err = api_init(ent->need_auth);
if (err != NOERR)
return err;
}
void *data = NULL;
err = ent->fn(data);
end:
if (ent->need_api || ent->need_auth)
api_free();
if (ent->need_cache)
cache_free();
return err;
}
enum error cmd_main()
{
for (int i = 0; i < ents_len; i++) {
enum error err;
bool *is_set;
err = config_get(ents[i].arg_name, (void**)&is_set);
if (err != NOERR && err != ERR_OPT_UNSET) {
uio_error("Cannot get arg '%s' (%s)", ents[i].arg_name,
error_to_string(err));
continue;
}
if (*is_set) {
err = cmd_run_one(&ents[i]);
return err;
}
}
return ERR_CMD_NONE;
}

38
src/cmd.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef _CMD_H
#define _CMD_H
#include "error.h"
#include "config.h"
/*
* Read commands from config and execute them
*/
enum error cmd_main();
/*
* Add files to the AniDB list
*/
enum error cmd_add(void *);
/*
* Take in a file/folder and print out
* the ed2k hash of it
*/
enum error cmd_ed2k(void *data);
/*
* Get and print the server api version
*/
enum error cmd_server_version(void *);
/*
* Print the server uptime
*/
enum error cmd_server_uptime(void *);
/*
* Print the program version
*/
enum error cmd_prog_version(void *);
#endif /* _CMD_H */

114
src/cmd_add.c Normal file
View File

@ -0,0 +1,114 @@
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include "cmd.h"
#include "error.h"
#include "uio.h"
#include "api.h"
#include "config.h"
#include "ed2k_util.h"
#include "cache.h"
#include "util.h"
struct add_opts {
enum mylist_state ao_state;
bool ao_watched;
};
enum error cmd_add_cachecheck(const char *path, const struct stat *st,
void *data)
{
const char *bname = util_basename(path);
enum error err;
err = cache_get(bname, st->st_size, NULL);
if (err == NOERR) {
/* We could get the entry, so it exists already */
uio_user("This file (%s) with size (%lu) already exists in cache."
" Skipping", bname, st->st_size);
return ED2KUTIL_DONTHASH;
} else if (err != ERR_CACHE_NO_EXISTS) {
uio_error("Some error when trying to get from cache: %s",
error_to_string(err));
return ED2KUTIL_DONTHASH;
}
uio_user("Hashing %s", path);
return NOERR;
}
enum error cmd_add_apisend(const char *path, const uint8_t *hash,
const struct stat *st, void *data)
{
struct api_result r;
struct add_opts* ao = (struct add_opts*)data;
if (api_cmd_mylistadd(st->st_size, hash, ao->ao_state, ao->ao_watched, &r)
!= NOERR)
return ERR_CMD_FAILED;
if (r.code == 310) {
struct api_mylistadd_result *x = &r.mylistadd;
uio_warning("File already added! Adding it to cache");
uio_debug("File info: lid: %ld, fid: %ld, eid: %ld, aid: %ld,"
" gid: %ld, date: %ld, viewdate: %ld, state: %d,"
" filestate: %d\nstorage: %s\nsource: %s\nother: %s",
x->lid, x->fid, x->eid, x->aid, x->gid, x->date, x->viewdate,
x->state, x->filestate, x->storage, x->source, x->other);
cache_add(x->lid, util_basename(path), st->st_size, hash);
if (x->storage)
free(x->storage);
if (x->source)
free(x->source);
if (x->other)
free(x->other);
return NOERR;
}
if (r.code != 210) {
uio_error("Mylistadd failure: %hu", r.code);
return ERR_CMD_FAILED;
}
uio_user("Succesfully added!");
uio_debug("New mylist id is: %ld", r.mylistadd.new_id);
cache_add(r.mylistadd.new_id, util_basename(path), st->st_size, hash);
return NOERR;
}
enum error cmd_add(void *data)
{
struct add_opts add_opts = {0};
struct ed2k_util_opts ed2k_opts = {
.pre_hash_fn = cmd_add_cachecheck,
.post_hash_fn = cmd_add_apisend,
.data = &add_opts,
};
bool *watched;
enum error err = NOERR;
int fcount;
fcount = config_get_nonopt_count();
if (fcount == 0) {
uio_error("No files specified");
return ERR_CMD_ARG;
}
if (config_get("watched", (void**)&watched) == NOERR) {
add_opts.ao_watched = *watched;
}
add_opts.ao_state = MYLIST_STATE_INTERNAL;
for (int i = 0; i < fcount; i++) {
err = ed2k_util_iterpath(config_get_nonopt(i), &ed2k_opts);
if (err != NOERR)
break;
}
return err;
}

62
src/cmd_ed2k.c Normal file
View File

@ -0,0 +1,62 @@
#include <sys/stat.h>
#include <stdio.h>
#include <stdbool.h>
#include "cmd.h"
#include "error.h"
#include "uio.h"
#include "config.h"
#include "ed2k_util.h"
#include "ed2k.h"
#include "util.h"
struct cmd_ed2k_opts {
bool link;
};
static enum error cmd_ed2k_output(const char *path, const uint8_t *hash,
const struct stat *st, void *data)
{
struct cmd_ed2k_opts *eo = data;
char buff[ED2K_HASH_SIZE * 2 + 1];
bool upcase = eo->link;
util_byte2hex(hash, ED2K_HASH_SIZE, upcase, buff);
if (eo->link) {
char *name_part = util_basename(path);
printf("ed2k://|file|%s|%ld|%s|/\n", name_part, st->st_size, buff);
} else {
printf("%s\t%s\n", buff, path);
}
return NOERR;
}
enum error cmd_ed2k(void *data)
{
struct cmd_ed2k_opts opts = {0};
struct ed2k_util_opts ed2k_opts = {
.post_hash_fn = cmd_ed2k_output,