mirror of https://git.lain.church/x3/caniadd.git
init
This commit is contained in:
commit
a9a0e45906
|
@ -0,0 +1,3 @@
|
|||
*.o
|
||||
caniadd
|
||||
TODO
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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, "&");
|
||||
|
||||
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
|
|
@ -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 */
|
|
@ -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;
|
||||
}
|
|
@ -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 */
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 */
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||