commit a9a0e459068c7a6dfe762592b9c01eb3b7a6fe0e Author: x3 Date: Sat Jan 8 19:53:58 2022 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6718027 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +caniadd +TODO diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..acb01d8 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fb00b0d --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..461e56d --- /dev/null +++ b/README.md @@ -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 diff --git a/src/api.c b/src/api.c new file mode 100644 index 0000000..cb5ab31 --- /dev/null +++ b/src/api.c @@ -0,0 +1,802 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 diff --git a/src/api.h b/src/api.h new file mode 100644 index 0000000..a10279c --- /dev/null +++ b/src/api.h @@ -0,0 +1,91 @@ +#ifndef _API_H +#define _API_H +#include +#include + +#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 */ diff --git a/src/cache.c b/src/cache.c new file mode 100644 index 0000000..326cf94 --- /dev/null +++ b/src/cache.c @@ -0,0 +1,208 @@ +#include + +#include + +#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; +} diff --git a/src/cache.h b/src/cache.h new file mode 100644 index 0000000..0e2c5de --- /dev/null +++ b/src/cache.h @@ -0,0 +1,40 @@ +#ifndef _CACHE_H +#define _CACHE_H +#include +#include + +#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 */ diff --git a/src/caniadd.c b/src/caniadd.c new file mode 100644 index 0000000..d81e799 --- /dev/null +++ b/src/caniadd.c @@ -0,0 +1,29 @@ +#include +#include +#include + +#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; +} diff --git a/src/cmd.c b/src/cmd.c new file mode 100644 index 0000000..4f6a119 --- /dev/null +++ b/src/cmd.c @@ -0,0 +1,75 @@ +#include + +#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; +} diff --git a/src/cmd.h b/src/cmd.h new file mode 100644 index 0000000..2229039 --- /dev/null +++ b/src/cmd.h @@ -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 */ diff --git a/src/cmd_add.c b/src/cmd_add.c new file mode 100644 index 0000000..b6c4b2d --- /dev/null +++ b/src/cmd_add.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +#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; +} + diff --git a/src/cmd_ed2k.c b/src/cmd_ed2k.c new file mode 100644 index 0000000..16847aa --- /dev/null +++ b/src/cmd_ed2k.c @@ -0,0 +1,62 @@ +#include +#include +#include + +#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, + .data = &opts, + }; + bool *link; + 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("link", (void**)&link) == NOERR) + opts.link = *link; + + for (int i = 0; i < fcount; i++) { + err = ed2k_util_iterpath(config_get_nonopt(i), &ed2k_opts); + if (err != NOERR) + break; + } + return err; +} + diff --git a/src/cmd_prog_version.c b/src/cmd_prog_version.c new file mode 100644 index 0000000..5bb92e5 --- /dev/null +++ b/src/cmd_prog_version.c @@ -0,0 +1,18 @@ +#include + +#include "cmd.h" +#include "cache.h" + +#include +enum error cmd_prog_version(void *data) +{ + enum error err = NOERR; + + printf("caniadd v0.1.0" +#ifdef GIT_REF + " (" GIT_REF ")" +#endif + "\n"); + + return err; +} diff --git a/src/cmd_server_uptime.c b/src/cmd_server_uptime.c new file mode 100644 index 0000000..6d1f75d --- /dev/null +++ b/src/cmd_server_uptime.c @@ -0,0 +1,31 @@ +#include +#include + +#include "cmd.h" +#include "uio.h" +#include "api.h" + +enum error cmd_server_uptime(void *data) +{ + struct api_result r; + int32_t h, m, s; + div_t dt; + + if (api_cmd_uptime(&r) != NOERR) + return ERR_CMD_FAILED; + + if (r.code != 208) { + uio_error("VERSION cmd is unsuccesful: %hu", r.code); + return ERR_CMD_FAILED; + } + + dt = div(r.uptime.ms, 1000*60*60); + h = dt.quot; + dt = div(dt.rem, 1000*60); + m = dt.quot; + dt = div(dt.rem, 1000); + s = dt.quot; + + printf("up %d hours, %d minutes, %d seconds\n", h, m, s); + return NOERR; +} diff --git a/src/cmd_server_version.c b/src/cmd_server_version.c new file mode 100644 index 0000000..35dacef --- /dev/null +++ b/src/cmd_server_version.c @@ -0,0 +1,23 @@ +#include + +#include "cmd.h" +#include "uio.h" +#include "api.h" + +enum error cmd_server_version(void *data) +{ + struct api_result a_res; + struct api_version_result *vr = &a_res.version; + + if (api_cmd_version(&a_res) != NOERR) + return ERR_CMD_FAILED; + + if (a_res.code == 998) { + printf("%s\n", vr->version_str); + } else { + uio_error("VERSION cmd is unsuccesful: %hu", a_res.code); + return ERR_CMD_FAILED; + } + + return NOERR; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..beed4c0 --- /dev/null +++ b/src/config.c @@ -0,0 +1,539 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +//#include +#include +#include +#include +#include + +#include "config.h" +#include "error.h" +#include "util.h" + +static int show_help(struct conf_entry *ce); +static int config_parse_file(); +static enum error config_required_check(); + +static int config_set_str(struct conf_entry *ce, char *arg); +static int config_set_port(struct conf_entry *ce, char *arg); +static int config_set_bool(struct conf_entry *ce, char *arg); + +//static int config_def_cachedb(struct conf_entry *ce); + +//static int config_action_write_config(struct conf_entry *ce); + +/* Everything not explicitly defined, is 0 */ +/* If an option only has a long name, the short name also has to be + * defined. For example, a number larger than UCHAR_MAX */ +static struct conf_entry options[] = { + { .l_name = "help", .s_name = 'h', .has_arg = no_argument, + .action_func = show_help, .in_args = true, + .type = OTYPE_ACTION, .handle_order = 0 }, +/* + { .l_name = "config-dir", .s_name = 'b', .has_arg = required_argument, + .default_func = config_def_config_dir, .set_func = config_set_str, + .in_args = true, .type = OTYPE_S, .handle_order = 0 }, + + { .l_name = "default-download-dir", .s_name = 'd', .has_arg = required_argument, + .default_func = config_def_default_download_dir, .set_func = config_set_str, + .in_file = true, .in_args = true, .type = OTYPE_S, .handle_order = 1 }, + + { .l_name = "port", .s_name = 'p', .has_arg = required_argument, + .set_func = config_set_port, .value.hu = 21729, .value_is_set = true, + .in_file = true, .in_args = true, .type = OTYPE_HU, .handle_order = 1 }, + + { .l_name = "foreground", .s_name = 'f', .has_arg = no_argument, + .set_func = config_set_bool, .value.b = false, .value_is_set = true, + .in_args = true, .type = OTYPE_B, .handle_order = 1 }, + + { .l_name = "write-config", .s_name = UCHAR_MAX + 1, .has_arg = no_argument, + .action_func = config_action_write_config, .value_is_set = true, + .in_args = true, .type = OTYPE_ACTION, .handle_order = 2 }, + + { .l_name = "peer-id", .s_name = UCHAR_MAX + 2, .has_arg = required_argument, + .default_func = config_def_peer_id, .type = OTYPE_S, .handle_order = 1 }, + */ + + { .l_name = "username", .s_name = 'u', .has_arg = required_argument, + .set_func = config_set_str, .in_args = true, .in_file = true, + .type = OTYPE_S, .handle_order = 1 }, + + { .l_name = "password", .s_name = 'p', .has_arg = required_argument, + .set_func = config_set_str, .in_args = true, .in_file = true, + .type = OTYPE_S, .handle_order = 1 }, + + { .l_name = "port", .s_name = 'P', .has_arg = required_argument, + .set_func = config_set_port, .in_args = true, .in_file = true, + .type = OTYPE_HU, .handle_order = 1, .value.hu = 29937, + .value_is_set = true }, + + { .l_name = "api-server", .s_name = UCHAR_MAX + 1, .has_arg = required_argument, + .set_func = config_set_str, .in_args = true, .in_file = true, + .type = OTYPE_S, .handle_order = 1, .value.s = "api.anidb.net:9000", + .value_is_set = true }, + + { .l_name = "api-key", .s_name = 'k', .has_arg = required_argument, + .set_func = config_set_str, .in_args = true, .in_file = true, + .type = OTYPE_S, .handle_order = 1, }, + + { .l_name = "save-session", .s_name = 's', .has_arg = no_argument, + .set_func = config_set_bool, .in_args = true, .in_file = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "destroy-session", .s_name = 'S', .has_arg = no_argument, + .set_func = config_set_bool, .in_args = true, .in_file = false, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "watched", .s_name = 'w', .has_arg = no_argument, + .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "link", .s_name = 'l', .has_arg = no_argument, + .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "cachedb", .s_name = 'd', .has_arg = required_argument, + .set_func = config_set_str, .in_args = true, .in_file = true, + .type = OTYPE_S, .handle_order = 1, /*.default_func = config_def_cachedb*/ }, + + { .l_name = "debug", .s_name = 'D', .has_arg = no_argument, + .set_func = config_set_bool, .in_args = true, .in_file = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true, }, + + /*### cmd ###*/ + + { .l_name = "server-version", .s_name = UCHAR_MAX + 2, + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "version", .s_name = 'v', + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "uptime", .s_name = UCHAR_MAX + 3, + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true }, + + { .l_name = "ed2k", .s_name = 'e', + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1 }, + + { .l_name = "add", .s_name = 'a', + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1 }, + + /*{ .l_name = "stats", .s_name = UCHAR_MAX + 4, + .has_arg = no_argument, .set_func = config_set_bool, .in_args = true, + .type = OTYPE_B, .handle_order = 1, .value_is_set = true },*/ + +}; + +static const size_t options_count = sizeof(options) / sizeof(options[0]); +static const char **opt_argv = NULL; +static int opt_argc = 0; + +static void config_build_getopt_args(char out_sopt[options_count * 2 + 1], + struct option out_lopt[options_count + 1]) +{ + int i_sopt = 0, i_lopt = 0; + + for (int i = 0; i < options_count; i++) { + + /* Short options */ + if (options[i].s_name && options[i].s_name <= UCHAR_MAX) { + out_sopt[i_sopt++] = options[i].s_name; + if (options[i].has_arg == required_argument) + out_sopt[i_sopt++] = ':'; + assert(options[i].has_arg == required_argument || + options[i].has_arg == no_argument); + } + + /* Long options */ + if (options[i].l_name) { + assert(options[i].s_name); + + out_lopt[i_lopt].name = options[i].l_name; + out_lopt[i_lopt].has_arg = options[i].has_arg; + out_lopt[i_lopt].flag = NULL; + out_lopt[i_lopt].val = options[i].s_name; + + i_lopt++; + } + } + + out_sopt[i_sopt] = '\0'; + memset(&out_lopt[i_lopt], 0, sizeof(struct option)); +} + +static int config_read_args(int argc, char **argv, char sopt[options_count * 2 + 1], + struct option lopt[options_count + 1], int level) +{ + int optc, err = NOERR; + optind = 1; + + while ((optc = getopt_long(argc, argv, sopt, + lopt, NULL)) >= 0) { + + bool handled = false; + + for (int i = 0; i < options_count; i++) { + if (options[i].handle_order != level) { + /* Lie a lil :x */ + handled = true; + continue; + } + + if (optc == options[i].s_name) { + if (options[i].type != OTYPE_ACTION) + err = options[i].set_func(&options[i], optarg); + else + err = options[i].action_func(&options[i]); + + if (err != NOERR) + goto end; + options[i].value_is_set = true; + + handled = true; + break; + } + } + + if (handled) + continue; + + if (optc == '?') { + err = ERR_OPT_FAILED; + goto end; + } else { + fprintf(stderr, "Unhandled option? '%c'\n", optc); + err = ERR_OPT_UNHANDLED; + goto end; + } + } + +end: + return err; +} + +static enum error config_required_check() +{ + enum error err = NOERR; + + for (int i = 0; i < options_count; i++) { + if (options[i].required && !options[i].value_is_set) { + printf("Argument %s is required!\n", options[i].l_name); + err = ERR_OPT_REQUIRED; + } + } + + return err; +} + +enum error config_parse(int argc, char **argv) +{ + enum error err = NOERR; + char sopt[options_count * 2 + 1]; + struct option lopt[options_count + 1]; + opt_argv = (const char**)argv; + opt_argc = argc; + config_build_getopt_args(sopt, lopt); + + err = config_read_args(argc, argv, sopt, lopt, 0); + if (err != NOERR) + goto end; + + err = config_parse_file(); + if (err != NOERR) + goto end; + + err = config_read_args(argc, argv, sopt, lopt, 1); + if (err != NOERR) + goto end; + + /* Set defaults for those, that didn't got set above */ + for (int i = 0; i < options_count; i++) { + if (!options[i].value_is_set && options[i].type != OTYPE_ACTION && + options[i].default_func) { + err = options[i].default_func(&options[i]); + if (err != NOERR) + goto end; + options[i].value_is_set = true; + } + } + + err = config_read_args(argc, argv, sopt, lopt, 2); + if (err != NOERR) + goto end; + + err = config_required_check(); + +end: + if (err != NOERR) + config_free(); + return err; +} + +#if 0 +static int config_def_config_dir(struct conf_entry *ce) +{ + char *dir; + int len; + const char *format = "%s/.config/" CONFIG_DIR_NAME; + const char *home_env = getenv("HOME"); + + if (!home_env) { + /* Fix this at a later date with getuid and getpw */ + fprintf(stderr, "HOME environment variable not found!\n"); + return ERR_NOTFOUND; + } + + len = snprintf(NULL, 0, format, home_env); + if (len == -1) { + int err = errno; + fprintf(stderr, "Failed to call funky snpintf: %s\n", strerror(err)); + return err; + } + dir = malloc(len + 1); + sprintf(dir, format, home_env); + + ce->value.s = dir; + ce->value_is_dyn = true; + + return NOERR; +} +#endif + +#if 0 +static int config_def_cachedb(struct conf_entry *ce) +{ + bool dh_free = false; + const char *data_home = getenv("XDG_DATA_HOME"); + + if (!data_home) { + const char *home = util_get_home(); + if (!home) + return ERR_OPT_FAILED; + sprintf(NULL, "%s/.local/share", home); + } + return NOERR; +} +#endif + +static int config_set_str(struct conf_entry *ce, char *arg) +{ + // TODO use realpath(3), when necessary + ce->value.s = arg; + return NOERR; +} + +static int config_set_port(struct conf_entry *ce, char *arg) +{ + long portval = strtol(arg, NULL, 10); + /* A zero return will be invalid no matter if strtol succeeded or not */ + if (portval > UINT16_MAX || portval <= 0) { + fprintf(stderr, "Invalid port value '%s'\n", arg); + return ERR_OPT_INVVAL; + } + ce->value.hu = (uint16_t)portval; + return NOERR; +} + +static int config_set_bool(struct conf_entry *ce, char *arg) +{ + ce->value.b = true; + return NOERR; +} + +static int show_help(struct conf_entry *ce) +{ + printf("Todo...\n"); + return ERR_OPT_EXIT; +} + +static int config_parse_file() +{ + // TODO implement this +#if 0 + assert(conf.config_file_path); + FILE *f = fopen(conf.config_file_path, "rb"); + char errbuf[200]; + + toml_table_t *tml = toml_parse_file(f, errbuf, sizeof(errbuf)); + fclose(f); + if (!tml) { + fprintf(stderr, "Failed to parse config toml: %s\n", errbuf); + return ERR_TOML_PARSE_ERROR; + } + + toml_datum_t port = toml_int_in(tml, "port"); + if (port.ok) + conf.port = (uint16_t)port.u.i; + else + fprintf(stderr, "Failed to parse port from config toml: %s\n", errbuf); + + toml_datum_t dldir = toml_string_in(tml, "default_download_dir"); + if (dldir.ok) { + conf.default_download_dir = dldir.u.s; + printf("%s\n", dldir.u.s); + conf_dyn.default_download_dir = dldir.u.s; + conf_dyn.default_download_dir = true; + /* TODO is this always malloced?? if yes, remve dyn check */ + } else { + fprintf(stderr, "Failed to parse download dir from config toml: %s\n", errbuf); + } + + toml_free(tml); +#endif + return NOERR; +} + +int config_free() +{ + for (int i = 0; i < options_count; i++) { + if (options[i].value_is_dyn) { + free(options[i].value.s); + options[i].value.s = NULL; + options[i].value_is_dyn = false; + options[i].value_is_set = false; + } + } + return NOERR; +} + +#if 0 +static int config_action_write_config(struct conf_entry *ce) +{ + /* This is the success return here */ + int err = ERR_OPT_EXIT, plen; + const char *config_dir; + FILE *f = NULL; + + config_dir = config_get("config-dir"); + plen = snprintf(NULL, 0, "%s/%s", config_dir, CONFIG_FILE_NAME); + char path[plen + 1]; + snprintf(path, plen + 1, "%s/%s", config_dir, CONFIG_FILE_NAME); + + for (int i = 0; i < 2; i++) { + f = fopen(path, "wb"); + if (!f) { + int errn = errno; + if (errn == ENOENT && i == 0) { + /* Try to create parent directory */ + if (mkdir(config_dir, 0755) == -1) { + err = errno; + fprintf(stderr, "Config mkdir failed: %s\n", strerror(err)); + goto end; + } + } else { + err = errn; + fprintf(stderr, "Config fopen failed: %s\n", strerror(err)); + goto end; + } + } else { + break; + } + } + + for (int i = 0; i < options_count; i++) { + if (!options[i].in_file) + continue; + + fprintf(f, "%s = ", options[i].l_name); + switch (options[i].type) { + case OTYPE_S: + // TODO toml escaping + fprintf(f, "\"%s\"\n", options[i].value.s); + break; + case OTYPE_HU: + fprintf(f, "%hu\n", options[i].value.hu); + break; + case OTYPE_B: + fprintf(f, "%s\n", options[i].value.b ? "true" : "false"); + break; + default: + break; + } + } + + config_dump(); + +end: + if (f) + fclose(f); + return err; +} +#endif + +enum error config_get(const char *key, void **out) +{ + enum error err = ERR_OPT_NOTFOUND; + + for (int i = 0; i < options_count; i++) { + struct conf_entry *cc = &options[i]; + + if (strcmp(cc->l_name, key) == 0) { + if (cc->value_is_set) { + if (out) + *out = &cc->value.s; + err = NOERR; + } else { + err = ERR_OPT_UNSET; + } + break; + } + } + + return err; +} + +const char *config_get_nonopt(int index) +{ + if (index >= config_get_nonopt_count()) + return NULL; + return opt_argv[optind + index]; +} + +int config_get_nonopt_count() +{ + return opt_argc - optind; +} + +void config_dump() +{ + for (int i = 0; i < options_count; i++) { + if (options[i].type == OTYPE_ACTION) + continue; + + printf("%s: ", options[i].l_name); + + if (!options[i].value_is_set) { + printf("[UNSET (>.<)]\n"); + continue; + } + + switch (options[i].type) { + case OTYPE_S: + printf("%s\n", options[i].value.s); + break; + case OTYPE_HU: + printf("%hu\n", options[i].value.hu); + break; + case OTYPE_B: + printf("%s\n", options[i].value.b ? "True" : "False"); + break; + default: + printf("Error :(\n"); + break; + } + } +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..b286f62 --- /dev/null +++ b/src/config.h @@ -0,0 +1,94 @@ +#ifndef _CONFIG_H +#define _CONFIG_H +#include +#include + +#include "error.h" + +#ifndef CONFIG_DIR_NAME +#define CONFIG_DIR_NAME "caniadd" +#endif + +enum option_type { + OTYPE_S, /* Stores a string */ + OTYPE_HU, /* Stores an unsigned short */ + OTYPE_B, /* Stores a boolean */ + /* Does not store anything, does an action. Handled after every + * other option are parsed, and defaults set */ + OTYPE_ACTION, + + _OTYPE_COUNT +}; + +struct conf_entry { + const char *l_name; /* The long name for the option, or for the config file */ + int s_name; /* Short option name */ + union { /* Value of the param */ + char *s; + uint16_t hu; + bool b; + } value; + /* The function to use to init a default value, if it's a complex one */ + int (*default_func)(struct conf_entry *ce); + union { + /* The function to use to set the value of the arg from the + * command line or from the loaded config file */ + int (*set_func)(struct conf_entry *ce, char *arg); + /* Callback for an action option */ + int (*action_func)(struct conf_entry *ce); + }; + int has_arg : 4; /* Do we need to specify an argument for this option on the cmd line? */ + /* Did we set the value? If not, we may need to call the default func */ + bool value_is_set : 1; + /* Is the value required? */ + bool required : 1; + bool value_is_dyn : 1; /* Do we need to free the value? */ + bool in_file : 1; /* Is this option in the config file? */ + bool in_args : 1; /* Is this option in the argument list? */ + enum option_type type : 4; /* Type of the option's value */ + /* + * In which step do we handle this arg? + * We need this, because: + * 1. Read in the base dir option from the command line, if present + * 2. Use the base dir to load the options from the config file + * 3. Read, and override options from the command line + * 4. Execute action arguments + */ + int handle_order : 4; +}; + +/* + * Parse options from the command line + * + * Returns 0 on success. + */ +enum error config_parse(int argc, char **argv); + +/* + * Free any memory that may have been allocated + * in config_parse + */ +int config_free(); + +/* + * Write out the options to stdout + */ +void config_dump(); + +/* + * Get a config option by its long name + * The pointer to the options value will be stored, at the pointer pointed to + * by out + * If the option is unset, it returns ERR_OPT_UNSET and out is unchanged + * It the options is not found, it returns ERR_OPT_NOTFOUND, and out is unchanged + */ +enum error config_get(const char *key, void **out); + +/* + * Return an cmd line that is not an option + * or null if error, or out of elements + */ +const char *config_get_nonopt(int index); +int config_get_nonopt_count(); + +#endif /* _CONFIG_H */ diff --git a/src/ed2k.c b/src/ed2k.c new file mode 100644 index 0000000..daa66a5 --- /dev/null +++ b/src/ed2k.c @@ -0,0 +1,61 @@ +#include + +#include "ed2k.h" + +/* https://wiki.anidb.net/Ed2k-hash */ +/* This is using the red method */ + +#define ED2K_CHUNK_SIZE 9728000 + +#define MIN(a,b) (((a) < (b)) ? (a) : (b)) + +void ed2k_init(struct ed2k_ctx *ctx) +{ + md4_init(&ctx->chunk_md4_ctx); + md4_init(&ctx->hash_md4_ctx); + ctx->byte_count = 0; +} + +static void ed2k_hash_chunk(struct ed2k_ctx *ctx) +{ + unsigned char chunk_hash[MD4_DIGEST_SIZE]; + + md4_final(&ctx->chunk_md4_ctx, chunk_hash); + md4_update(&ctx->hash_md4_ctx, chunk_hash, sizeof(chunk_hash)); + md4_init(&ctx->chunk_md4_ctx); +} + +void ed2k_update(struct ed2k_ctx *ctx, const void *data, size_t data_len) +{ + const char *bytes = (const char*)data; + + while (data_len) { + size_t hdata_size = MIN(ED2K_CHUNK_SIZE - + (ctx->byte_count % ED2K_CHUNK_SIZE), data_len); + + md4_update(&ctx->chunk_md4_ctx, bytes, hdata_size); + ctx->byte_count += hdata_size; + + if (ctx->byte_count % ED2K_CHUNK_SIZE == 0) + ed2k_hash_chunk(ctx); + + data_len -= hdata_size; + bytes += hdata_size; + } +} + +void ed2k_final(struct ed2k_ctx *ctx, unsigned char *out_hash) +{ + struct md4_ctx *md_ctx; + if (ctx->byte_count < ED2K_CHUNK_SIZE) { + /* File has only 1 chunk, so return the md4 hash of that chunk */ + md_ctx = &ctx->chunk_md4_ctx; + } else { + /* Else hash the md4 hashes, and return that hash */ + ed2k_hash_chunk(ctx); /* Hash the last partial chunk here */ + md_ctx = &ctx->hash_md4_ctx; + } + + md4_final(md_ctx, out_hash); +} + diff --git a/src/ed2k.h b/src/ed2k.h new file mode 100644 index 0000000..da40b85 --- /dev/null +++ b/src/ed2k.h @@ -0,0 +1,18 @@ +#ifndef _ED2K_H +#define _ED2K_H +#include + +#include "md4.h" + +#define ED2K_HASH_SIZE MD4_DIGEST_SIZE + +struct ed2k_ctx { + struct md4_ctx hash_md4_ctx, chunk_md4_ctx; + uint64_t byte_count; +}; + +void ed2k_init(struct ed2k_ctx *ctx); +void ed2k_update(struct ed2k_ctx *ctx, const void *data, size_t data_len); +void ed2k_final(struct ed2k_ctx *ctx, unsigned char *out_hash); + +#endif /* _ED2K_H */ diff --git a/src/ed2k_util.c b/src/ed2k_util.c new file mode 100644 index 0000000..ae7b63f --- /dev/null +++ b/src/ed2k_util.c @@ -0,0 +1,93 @@ +#define _XOPEN_SOURCE 500 +#include +#include +#include +#include + +#include "ed2k.h" +#include "ed2k_util.h" +#include "uio.h" + +static struct ed2k_util_opts l_opts; + +static enum error ed2k_util_hash(const char *file_path, blksize_t blksize, + const struct stat *st) +{ + unsigned char buf[blksize], hash[ED2K_HASH_SIZE]; + struct ed2k_ctx ed2k; + FILE *f; + size_t read_len; + + if (l_opts.pre_hash_fn) { + enum error err = l_opts.pre_hash_fn(file_path, st, l_opts.data); + if (err == ED2KUTIL_DONTHASH) + return NOERR; + else if (err != NOERR) + return err; + } + + f = fopen(file_path, "rb"); + if (!f) { + uio_error("Failed to open file: %s (%s)", file_path, strerror(errno)); + return ERR_ED2KUTIL_FS; + } + + ed2k_init(&ed2k); + read_len = fread(buf, 1, sizeof(buf), f); + while (read_len > 0) { + ed2k_update(&ed2k, buf, read_len); + read_len = fread(buf, 1, sizeof(buf), f); + } + // TODO check if eof or error + + ed2k_final(&ed2k, hash); + fclose(f); + + if (l_opts.post_hash_fn) + return l_opts.post_hash_fn(file_path, hash, st, l_opts.data); + return NOERR; +} + +static int ed2k_util_walk(const char *fpath, const struct stat *sb, + int typeflag, struct FTW *ftwbuf) +{ + if (typeflag == FTW_DNR) { + uio_error("Cannot read directory '%s'. Skipping", fpath); + return NOERR; + } + if (typeflag == FTW_D) + return NOERR; + if (typeflag != FTW_F) { + uio_error("Unhandled error '%d'", typeflag); + return ERR_ED2KUTIL_UNSUP; + } + + return ed2k_util_hash(fpath, sb->st_blksize, sb); +} + +enum error ed2k_util_iterpath(const char *path, const struct ed2k_util_opts *opts) +{ + struct stat ts; + + if (stat(path, &ts) != 0) { + uio_error("Stat failed for path: '%s' (%s)", + path, strerror(errno)); + return ERR_ED2KUTIL_FS; + } + + l_opts = *opts; + + if (S_ISREG(ts.st_mode)) { + return ed2k_util_hash(path, ts.st_blksize, &ts); + } else if (S_ISDIR(ts.st_mode)) { + int ftwret = nftw(path, ed2k_util_walk, 20, 0); + if (ftwret == -1) { + uio_error("nftw failure"); + return ERR_ED2KUTIL_FS; + } + return ftwret; + } + + uio_error("Unsupported file type: %d", ts.st_mode & S_IFMT); + return ERR_ED2KUTIL_UNSUP; +} diff --git a/src/ed2k_util.h b/src/ed2k_util.h new file mode 100644 index 0000000..253fe54 --- /dev/null +++ b/src/ed2k_util.h @@ -0,0 +1,33 @@ +#ifndef _ED2K_UTIL_H +#define _ED2K_UTIL_H +#include +#include + +#include "error.h" + +typedef enum error (*ed2k_util_fn)(const char *path, const uint8_t *hash, + const struct stat *st, void *data); +/* + * If this returns ED2KUTIL_DONTHASH, then skip the hashing, + * and the post_hash function + */ +typedef enum error (*ed2k_util_prehash_fn)(const char *path, + const struct stat *st, void *data); + +struct ed2k_util_opts { + ed2k_util_fn post_hash_fn; + ed2k_util_prehash_fn pre_hash_fn; + void *data; +}; + +/* + * Given a path (file or directory) calculate the ed2k + * hash for the file(s), and call opts.post_hash_fn if not NULL + * if opts.pre_hash_fn is not NULL, then also call that before the hashing + * + * If fn returns any error, the iteration will stop, and this + * function will return with that error code. + */ +enum error ed2k_util_iterpath(const char *path, const struct ed2k_util_opts *opts); + +#endif /* _ED2K_UTIL_H */ diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..e0e61e7 --- /dev/null +++ b/src/error.c @@ -0,0 +1,12 @@ +#include "error.h" + +static const char *error_string[] = { + FE_ERROR(GEN_STRING) +}; + +const char *error_to_string(enum error err) +{ + if (err >= _ERR_COUNT) + return "ERR_UNKNOWN"; + return error_string[err]; +} diff --git a/src/error.h b/src/error.h new file mode 100644 index 0000000..cf2b673 --- /dev/null +++ b/src/error.h @@ -0,0 +1,63 @@ +#ifndef _ERROR_H +#define _ERROR_H + +#define FE_ERROR(E) \ + E(NOERR = 0) \ + E(ERR_UNKNOWN) \ + E(ERR_NOTFOUND) \ +\ + E(ERR_OPT_REQUIRED) \ + E(ERR_OPT_FAILED) \ + E(ERR_OPT_UNHANDLED) \ + E(ERR_OPT_INVVAL) \ + E(ERR_OPT_EXIT) /* We should exit in main, if config_parse returns this */ \ + E(ERR_OPT_UNSET) /* In config_get, if the value isn't set */ \ + E(ERR_OPT_NOTFOUND) /* In config_get, if the options is not found */ \ +\ + E(ERR_NET_APIADDR) /* If there are problems with the api servers address */ \ + E(ERR_NET_SOCKET) /* If there are problems with the udp socket */ \ + E(ERR_NET_CONNECTED) /* Socket already connected */ \ + E(ERR_NET_CONNECT_FAIL) /* Connect attempt failed */ \ + E(ERR_NET_NOT_CONNECTED) /* Socket wasn't connected */ \ +\ + E(ERR_CMD_FAILED) /* Running the command failed */ \ + E(ERR_CMD_NONE) /* No command was run */ \ + E(ERR_CMD_ARG) /* Some problem with the command arguments */ \ +\ + E(ERR_ED2KUTIL_FS) /* Some filesystem problem */ \ + E(ERR_ED2KUTIL_UNSUP) /* Operation or file type is unsupported */ \ + E(ED2KUTIL_DONTHASH) /* Skip the hashing part. pre_hash_fn can return this */ \ +\ + E(ERR_API_ENCRYPTFAIL) /* Cannot start encryption with the api */ \ + E(ERR_API_COMMFAIL) /* Communication failure */ \ + E(ERR_API_RESP_INVALID) /* Invalid response */ \ + E(ERR_API_AUTH_FAIL) /* Auth failed */ \ + E(ERR_API_LOGOUT) /* Logout failed */ \ + E(ERR_API_PRINTFFUNC) /* New printf function registration failed */ \ + E(ERR_API_CLOCK) /* Some error with clocks */ \ +\ + E(ERR_CACHE_SQLITE) /* Generic sqlite error code */ \ + E(ERR_CACHE_EXISTS) /* Entry already exists, as determined by lid */ \ + /* The entry to be added is not unique, (filename and size duplicate, not hash or lid) */ \ + E(ERR_CACHE_NON_UNIQUE) \ + E(ERR_CACHE_NO_EXISTS) /* Entry does not exists */ \ +\ + E(ERR_THRD) /* Generic pthread error */ \ +\ + E(ERR_LIBEVENT) /* There are some problem with a libevent function */ \ + E(_ERR_COUNT) \ + + +#define GEN_ENUM(ENUM) ENUM, +#define GEN_STRING(STRING) #STRING, + +enum error { + FE_ERROR(GEN_ENUM) +}; + +/* + * Convert a number (0) to the enum name (NOERR) + */ +const char *error_to_string(enum error err); + +#endif /* _ERROR_H */ diff --git a/src/net.c b/src/net.c new file mode 100644 index 0000000..4bc86a1 --- /dev/null +++ b/src/net.c @@ -0,0 +1,265 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net.h" +#include "config.h" +#include "uio.h" + +static struct addrinfo net_server; +static bool net_server_set = false; +static bool net_connected = false; +static int net_socket = -1; + +static bool net_parse_address(const char* srv, size_t *out_domain_len, + char **out_port_start) +{ + char *port_iter; + char *port_start = strchr(srv, ':'); + if (!port_start) { + *out_port_start = NULL; + *out_domain_len = strlen(srv); + return true; + } + + /* Only one ':' is allowed */ + if (strchr(port_start + 1, ':')) + return false; + + *out_domain_len = port_start - srv; + + /*port = strtol(port_start + 1, &port_end, 10); + if (port_end == port_start || *port_end != '\0' || + ((port == LONG_MIN || port == LONG_MAX) && errno)) + return false;*/ + + /*if (port <= 0 || port > 65535) + return false;*/ + + port_iter = port_start + 1; + if (*port_iter == '\0') + return false; + while (*port_iter) { + if (!isdigit(*port_iter)) + return false; + port_iter++; + } + *out_port_start = port_start + 1; + + return true; +} + +static const void *net_get_sockaddr_addr(const struct sockaddr *sa) +{ + switch (sa->sa_family) { + case AF_INET: + return &((struct sockaddr_in*)sa)->sin_addr; + case AF_INET6: + return &((struct sockaddr_in6*)sa)->sin6_addr; + default: + uio_error("Sockaddr is not ipv4 or ipv6"); + exit(1); + } +} + +static bool net_lookup_server(const char *domain, const char *port, + struct addrinfo *out_addr) +{ + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + //.ai_family = AF_INET6, + .ai_socktype = SOCK_DGRAM, + .ai_flags = AI_NUMERICSERV | AI_ADDRCONFIG, + //.ai_flags = AI_NUMERICSERV, + }, *res = NULL, *curr_res; + + int ret = getaddrinfo(domain, port, &hints, &res); + if (ret != 0) { + uio_error("Cannot get addrinfo from address: '%s:%s' (%s)", + domain, port, gai_strerror(ret)); + return false; + } + + curr_res = res; + while (curr_res) { + char ip_buffer[INET6_ADDRSTRLEN] = {0}; + if (inet_ntop(curr_res->ai_family, + net_get_sockaddr_addr(curr_res->ai_addr), + ip_buffer, sizeof(ip_buffer))) + uio_debug("Lookup addrinfo entry: %s", ip_buffer); + else + uio_debug("Cannot convert binary ip to string: %s", + strerror(errno)); + + /* For now, always choose the first one */ + break; + + curr_res = curr_res->ai_next; + } + + if (!curr_res) { + uio_error("Cannot select a usable address."); + freeaddrinfo(res); + return false; + } + + *out_addr = *curr_res; + out_addr->ai_addr = malloc(sizeof(struct sockaddr)); + *out_addr->ai_addr = *curr_res->ai_addr; + out_addr->ai_next = NULL; + out_addr->ai_canonname = NULL; + freeaddrinfo(res); + return true; +} + +int net_socket_setup() +{ + struct sockaddr l_addr = {0}; + socklen_t l_addr_len; + int sock; + uint16_t *port; + enum error err; + + if ((err = config_get("port", (void**)&port)) != NOERR) { + uio_error("Cannot get UDP binding port from config (%s)", + error_to_string(err)); + return -1; + } + + sock = socket(net_server.ai_family, net_server.ai_socktype, + net_server.ai_protocol); + if (sock == -1) { + uio_error("Cannot create new socket: %s", strerror(errno)); + return -1; + } + + l_addr.sa_family = net_server.ai_family; + if (net_server.ai_family == AF_INET) { + struct sockaddr_in *tmp = (struct sockaddr_in*)&l_addr; + l_addr_len = sizeof(struct sockaddr_in); + + tmp->sin_port = htons(*port); + tmp->sin_addr.s_addr = INADDR_ANY; + + } else { + struct sockaddr_in6 *tmp = (struct sockaddr_in6*)&l_addr; + l_addr_len = sizeof(struct sockaddr_in6); + + tmp->sin6_port = htons(*port); + tmp->sin6_addr = in6addr_any; + + } + if (bind(sock, &l_addr, l_addr_len) != 0) { + uio_error("Cannot bind UDP socket to local port: %s", strerror(errno)); + close(sock); + return -1; + } + + return sock; +} + +static enum error net_connect(int sock, struct addrinfo *ai) +{ + if (net_connected) + return ERR_NET_CONNECTED; + + if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) { + uio_error("Cannot connect to the server: %s\n", strerror(errno)); + return ERR_NET_CONNECT_FAIL; + } + net_connected = true; + + return NOERR; +} + +enum error net_init() +{ + enum error err; + const char **srv = NULL; + char *port_start = NULL; + size_t domain_len; + int sock; + + err = config_get("api-server", (void**)&srv); + if (err != NOERR) { + uio_error("Cannot get the api servers address (%s).", error_to_string(err)); + return ERR_NET_APIADDR; + } + + if (!net_parse_address(*srv, &domain_len, &port_start)) { + uio_error("Cannot parse the api server address: '%s'.", *srv); + return ERR_NET_APIADDR; + } + /* Port will be set to NULL, if its not in the address */ + if (port_start == NULL) + port_start = "9000"; + + char api_domain[domain_len + 1]; + memcpy(api_domain, *srv, domain_len); + api_domain[domain_len] = '\0'; + + if (!net_lookup_server(api_domain, port_start, &net_server)) { + //uio_error("Cannot look up the api server address"); + return ERR_NET_APIADDR; + } + net_server_set = true; + + sock = net_socket_setup(); + if (sock == -1) { + return ERR_NET_SOCKET; + } + + err = net_connect(sock, &net_server); + if (err != NOERR) { + net_free(); + return err; + } + net_socket = sock; + + return NOERR; +} + +void net_free() +{ + if (net_server_set) { + free(net_server.ai_addr); + memset(&net_server, 0, sizeof(net_server)); + net_server_set = false; + } + if (net_socket != -1) { + if (net_connected) + shutdown(net_socket, SHUT_RDWR); + close(net_socket); + net_socket = -1; + } + net_connected = false; +} + +ssize_t net_send(const void *msg, size_t msg_len) +{ + ssize_t w_len = send(net_socket, msg, msg_len, 0); + if (w_len == -1) { + uio_error("{net} Send failed: %s", strerror(errno)); + return -1; + } + return w_len; +} + +ssize_t net_read(void* out_data, size_t read_size) +{ + ssize_t read = recv(net_socket, out_data, read_size, 0); + if (read == -1) { + uio_error("{net} Read failed: %s", strerror(errno)); + return -1; + } + return read; +} diff --git a/src/net.h b/src/net.h new file mode 100644 index 0000000..6021e9c --- /dev/null +++ b/src/net.h @@ -0,0 +1,24 @@ +#ifndef _NET_H +#define _NET_H +#include +#include + +#include "error.h" + +/* + * Initializes the net class + */ +enum error net_init(); + +/* + * Send and read data to and from the api + */ +ssize_t net_send(const void *msg, size_t msg_len); +ssize_t net_read(void *out_data, size_t read_size); + +/* + * Frees the net class + */ +void net_free(); + +#endif /* _NET_H */ diff --git a/src/uio.c b/src/uio.c new file mode 100644 index 0000000..ba1830b --- /dev/null +++ b/src/uio.c @@ -0,0 +1,58 @@ +#include +#include + +#include "uio.h" +#include "config.h" + +void uio_user(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + + vprintf(format, ap); + printf("\n"); + + va_end(ap); +} + +void uio_error(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + + printf("\033[31m[ERROR]: "); + vprintf(format, ap); + printf("\033[0m\n"); + + va_end(ap); +} + +void uio_debug(const char *format, ...) +{ + bool *dbg_enabled; + va_list ap; + + config_get("debug", (void**)&dbg_enabled); + if (!*dbg_enabled) + return; + + va_start(ap, format); + + printf("\033[35m[DEBUG]: "); + vprintf(format, ap); + printf("\033[0m\n"); + + va_end(ap); +} + +void uio_warning(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + + printf("\033[33m[WARNING]: "); + vprintf(format, ap); + printf("\033[0m\n"); + + va_end(ap); +} diff --git a/src/uio.h b/src/uio.h new file mode 100644 index 0000000..3ee5ddb --- /dev/null +++ b/src/uio.h @@ -0,0 +1,9 @@ +#ifndef _UIO_H +#define _UIO_H + +void uio_user(const char *format, ...) __attribute__((format (printf, 1, 2))); +void uio_error(const char *format, ...) __attribute__((format (printf, 1, 2))); +void uio_debug(const char *format, ...) __attribute__((format (printf, 1, 2))); +void uio_warning(const char *format, ...) __attribute__((format (printf, 1, 2))); + +#endif /* _UIO_H */ diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..b02e51f --- /dev/null +++ b/src/util.c @@ -0,0 +1,39 @@ +#include +#include + +#include "util.h" + +void util_byte2hex(const uint8_t* bytes, size_t bytes_len, + bool uppercase, char* out) +{ + const char* hex = (uppercase) ? "0123456789ABCDEF" : "0123456789abcdef"; + for (size_t i = 0; i < bytes_len; i++) { + *out++ = hex[bytes[i] >> 4]; + *out++ = hex[bytes[i] & 0xF]; + } + *out = '\0'; +} + +const char *util_get_home() +{ + const char *home_env = getenv("HOME"); + return home_env; /* TODO this can be null, use other methods as fallback */ +} + +char *util_basename(const char *fullpath) +{ + char *name_part = strrchr(fullpath, '/'); + if (name_part) + name_part++; + else + name_part = (char*)fullpath; + return name_part; +} + +uint64_t util_timespec_diff(const struct timespec *past, + const struct timespec *future) +{ + int64_t sdiff = future->tv_sec - past->tv_sec; + int64_t nsdiff = future->tv_nsec - past->tv_nsec; + return sdiff * 1000 + (nsdiff / 1000000); +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..09d3a41 --- /dev/null +++ b/src/util.h @@ -0,0 +1,35 @@ +#ifndef _UTIL_H +#define _UTIL_H +#include +#include +#include + +/* + * Convert bytes to a hex string + * out needs to be at least (bytes_len * 2 + 1) bytes + */ +void util_byte2hex(const uint8_t* bytes, size_t bytes_len, + bool uppercase, char* out); + +/* + * Return the user's home directory + */ +const char *util_get_home(); + +/* + * Return the filename part of the path + * This will return a pointer in fullpath + * !! ONLY WORKS FOR FILES !! + */ +char *util_basename(const char *fullpath); + +/* + * Calculate the difference between 2 timespec structs in miliseconds + * + * future cannot be more in the past than past + * if that makes any sense + */ +uint64_t util_timespec_diff(const struct timespec *past, + const struct timespec *future); + +#endif /* _UTIL_H */ diff --git a/subm/MD4 b/subm/MD4 new file mode 160000 index 0000000..6771029 --- /dev/null +++ b/subm/MD4 @@ -0,0 +1 @@ +Subproject commit 6771029ebea612a1a53822af6867b6cf172c31f0 diff --git a/subm/md5-c b/subm/md5-c new file mode 160000 index 0000000..1525b0d --- /dev/null +++ b/subm/md5-c @@ -0,0 +1 @@ +Subproject commit 1525b0db1fb608afed8948f3a972d55486e8cb31 diff --git a/subm/tiny-AES-c b/subm/tiny-AES-c new file mode 160000 index 0000000..f06ac37 --- /dev/null +++ b/subm/tiny-AES-c @@ -0,0 +1 @@ +Subproject commit f06ac37fc31dfdaca2e0d9bec83f90d5663c319b