x3 2 years ago
commit
a9a0e45906
Signed by: x3 <sugarpanning@cock.li> GPG Key ID: 7E9961E8AD0E240E
33 changed files with 2953 additions and 0 deletions
  1. +3
    -0
      .gitignore
  2. +10
    -0
      .gitmodules
  3. +36
    -0
      Makefile
  4. +27
    -0
      README.md
  5. +802
    -0
      src/api.c
  6. +91
    -0
      src/api.h
  7. +208
    -0
      src/cache.c
  8. +40
    -0
      src/cache.h
  9. +29
    -0
      src/caniadd.c
  10. +75
    -0
      src/cmd.c
  11. +38
    -0
      src/cmd.h
  12. +114
    -0
      src/cmd_add.c
  13. +62
    -0
      src/cmd_ed2k.c
  14. +18
    -0
      src/cmd_prog_version.c
  15. +31
    -0
      src/cmd_server_uptime.c
  16. +23
    -0
      src/cmd_server_version.c
  17. +539
    -0
      src/config.c
  18. +94
    -0
      src/config.h
  19. +61
    -0
      src/ed2k.c
  20. +18
    -0
      src/ed2k.h
  21. +93
    -0
      src/ed2k_util.c
  22. +33
    -0
      src/ed2k_util.h
  23. +12
    -0
      src/error.c
  24. +63
    -0
      src/error.h
  25. +265
    -0
      src/net.c
  26. +24
    -0
      src/net.h
  27. +58
    -0
      src/uio.c
  28. +9
    -0
      src/uio.h
  29. +39
    -0
      src/util.c
  30. +35
    -0
      src/util.h
  31. +1
    -0
      subm/MD4
  32. +1
    -0
      subm/md5-c
  33. +1
    -0
      subm/tiny-AES-c

+ 3
- 0
.gitignore View File

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

+ 10
- 0
.gitmodules View File

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

+ 36
- 0
Makefile View File

@@ -0,0 +1,36 @@
InstallPrefix := /usr/local/bin

PROGNAME := caniadd
VERSION := 1
CFLAGS := -Wall -std=gnu11 #-march=native #-Werror
CPPFLAGS := -DCBC=0 -DCTR=0 -DECB=1 -Isubm/tiny-AES-c/ -Isubm/md5-c/ -Isubm/MD4/ -DPROG_VERSION='"$(VERSION)"'
LDFLAGS := -lpthread -lsqlite3

SOURCES := $(wildcard src/*.c) subm/tiny-AES-c/aes.c subm/md5-c/md5.c subm/MD4/md4.c #$(TOML_SRC) $(BENCODE_SRC)
OBJS := $(SOURCES:.c=.o)

all: CFLAGS += -O3 -flto
all: CPPFLAGS += #-DNDEBUG Just to be safe
all: $(PROGNAME)

# no-pie cus it crashes on my 2nd pc for some reason
dev: CFLAGS += -Og -ggdb -fsanitize=address -fsanitize=leak -fstack-protector-all -no-pie
dev: $(PROGNAME)

t:
echo $(SOURCES)

install: $(PROGNAME)
install -s -- $< $(InstallPrefix)/$(PROGNAME)

uninstall:
rm -f -- $(InstallPrefix)/$(PROGNAME)

$(PROGNAME): $(OBJS)
$(CC) -o $@ $+ $(CFLAGS) $(CPPFLAGS) $(LDFLAGS)

clean:
-rm -- $(OBJS) $(PROGNAME)

re: clean all
red: clean dev

+ 27
- 0
README.md View File

@@ -0,0 +1,27 @@
# Caniadd will add files to an AniDB list.

None of the already existing clients did exactly what I wanted, so here I am.
Caniadd is still in developement, but is is already usable.
That is, it implements logging in, encryption, ed2k hashing and adding files, basic ratelimit, keepalive, and file caching.

In the future I want to write an mpv plugin that will use the cached database from caniadd to automatically mark an episode watched on AniDB.
That will be the peak *comfy* animu list management experience.

## Things to do:
- NAT handling
- Multi thread hashing
- Read/write timeout in net
- Api ratelimit (the other part)
- Decode escaping from server
- Use a config file
- Add newline escape to server
- Better field parsing, remove the horrors at code 310
- Add myliststats cmd as --stats arg
- Add support for compression
- Make deleting from mylist possible, with
- Name regexes,
- If file is not found at a scan
- Use api\_cmd style in api\_encrypt\_init
- Buffer up mylistadd api cmds when waiting for ratelimit
- Handle C-c gracefully at any time
- Write -h page, and maybe a man page too

+ 802
- 0
src/api.c View File

@@ -0,0 +1,802 @@
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <printf.h>
#include <time.h>
#include <pthread.h>
#include <errno.h>

#include <md5.h>
#include <aes.h>

#include "api.h"
#include "net.h"
#include "uio.h"
#include "config.h"
#include "ed2k.h"
#include "util.h"

/* Needed, bcuz of custom %B format */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"

#ifdef CLOCK_MONOTONIC_COARSE
#define API_CLOCK CLOCK_MONOTONIC_COARSE
#elif defined(CLOCK_MONOTONIC)
#warn "No coarse monotonic clock"
#define API_CLOCK CLOCK_MONOTONIC
#else
#error "No monotonic clock"
#endif

#define MS_TO_TIMESPEC(ts, ms) { \
ts->tv_sec = ms / 1000; \
ts->tv_nsec = (ms % 1000) * 1000000; \
}

#define MS_TO_TIMESPEC_L(ts, ms) { \
ts.tv_sec = ms / 1000; \
ts.tv_nsec = (ms % 1000) * 1000000; \
}

static enum error api_cmd_logout(struct api_result *res);
static enum error api_cmd_auth(const char *uname, const char *pass,
struct api_result *res);

static bool api_authed = false;
static char api_session[API_SMAXSIZE] = {0}; /* No escaping is needed */
static uint8_t e_key[16] = {0};
static bool api_encryption = false;

static pthread_t api_ka_thread = 0;
static pthread_mutex_t api_work_mx;
static bool api_ka_now = false; /* Are we doing keepalive now? */

static struct timespec api_last_packet = {0}; /* Last packet time */
static int32_t api_packet_count = 0; /* Only increment */
static int32_t api_fast_packet_count = 0; /* Incremented or decrement */

static int api_escaped_string(FILE *io, const struct printf_info *info,
const void *const *args)
{
/* Ignore newline escapes for now */
char *str = *(char**)args[0];
char *and_pos = strchr(str, '&');
size_t w_chars = 0;

if (and_pos == NULL)
return fprintf(io, "%s", str);

while (and_pos) {
w_chars += fprintf(io, "%.*s", (int)(and_pos - str), str);
w_chars += fprintf(io, "&amp;");

str = and_pos + 1;
and_pos = strchr(str, '&');
}
if (*str)
w_chars += fprintf(io, "%s", str);

return w_chars;
}

static int api_escaped_sring_info(const struct printf_info *info, size_t n,
int *argtypes, int *size)
{
if (n > 0) {
argtypes[0] = PA_STRING;
size[0] = sizeof(const char*);
}
return 1;
}

static enum error api_init_encrypt(const char *api_key, const char *uname)
{
char buffer[API_BUFSIZE];
MD5Context md5_ctx;
char *salt_start = buffer + 4 /* 209 [salt here] ... */, *salt_end;
ssize_t r_len, salt_len;

if (net_send(buffer, snprintf(buffer, sizeof(buffer),
"ENCRYPT user=%s&type=1", uname)) == -1) {
return ERR_API_COMMFAIL;
}
r_len = net_read(buffer, sizeof(buffer));

if (strncmp(buffer, "209", 3) != 0) {
uio_error("We expected 209 response, but got: %.*s",
(int)r_len, buffer);
return ERR_API_ENCRYPTFAIL;
}

salt_end = strchr(salt_start, ' ');
if (!salt_end) {
uio_error("Cannot find space after salt in response");
return ERR_API_ENCRYPTFAIL;
}
salt_len = salt_end - salt_start;

md5Init(&md5_ctx);
md5Update(&md5_ctx, (uint8_t*)api_key, strlen(api_key));
md5Update(&md5_ctx, (uint8_t*)salt_start, salt_len);
md5Finalize(&md5_ctx);
memcpy(e_key, md5_ctx.digest, sizeof(e_key));

#if 1
char *buffpos = buffer;
for (int i = 0; i < 16; i++)
buffpos += sprintf(buffpos, "%02x", e_key[i]);
uio_debug("Encryption key is: '%s'", buffer);
#endif

api_encryption = true;
return NOERR;
}

static size_t api_encrypt(char *buffer, size_t data_len)
{
struct AES_ctx actx;
size_t rem_data_len = data_len, ret_len = data_len;
char pad_value;

AES_init_ctx(&actx, e_key);
while (rem_data_len >= AES_BLOCKLEN) {
AES_ECB_encrypt(&actx, (uint8_t*)buffer);

buffer += AES_BLOCKLEN;
rem_data_len -= AES_BLOCKLEN;
}

/* Possible BOF here? maybe? certanly. */
pad_value = AES_BLOCKLEN - rem_data_len;
ret_len += pad_value;

memset(buffer + rem_data_len, pad_value, pad_value);
AES_ECB_encrypt(&actx, (uint8_t*)buffer);

assert(ret_len % AES_BLOCKLEN == 0);
return ret_len;
}

static size_t api_decrypt(char *buffer, size_t data_len)
{
assert(data_len % AES_BLOCKLEN == 0);
struct AES_ctx actx;
size_t ret_len = data_len;
char pad_value;

AES_init_ctx(&actx, e_key);
while (data_len) {
AES_ECB_decrypt(&actx, (uint8_t*)buffer);
buffer += AES_BLOCKLEN;
data_len -= AES_BLOCKLEN;
}

pad_value = buffer[data_len - 1];
ret_len -= pad_value;

return ret_len;
}

static enum error api_auth(const char* uname, const char *passw)
{
struct api_result res;
enum error err = NOERR;

if (!api_encryption)
uio_warning("Logging in without encryption!");
if (api_cmd_auth(uname, passw, &res) != NOERR) {
return ERR_API_AUTH_FAIL;
}

switch (res.code) {
case 201:
uio_warning("A new client version is available!");
case 200:
memcpy(api_session, res.auth.session_key, sizeof(api_session));
api_authed = true;
uio_debug("Succesfully logged in. Session key: '%s'", api_session);
break;
default:
err = ERR_API_AUTH_FAIL;
switch (res.code) {
case 500:
uio_error("Login failed. Please check your credentials again");
break;
case 503:
uio_error("Client is outdated. You're probably out of luck here.");
break;
case 504:
uio_error("Client is banned :( Reason: %s", res.auth.banned_reason);
free(res.auth.banned_reason);
break;
case 505:
uio_error("Illegal input or access denied");
break;
case 601:
uio_error("AniDB out of service");
break;
default:
uio_error("Unknown error: %hu", res.code);
break;
}
}

return err;
}

enum error api_logout()
{
struct api_result res;
enum error err = NOERR;

if (api_cmd_logout(&res) != NOERR) {
return ERR_API_AUTH_FAIL;
}

switch (res.code) {
case 203:
uio_debug("Succesfully logged out");
api_authed = false;

break;
case 403:
uio_error("Cannot log out, because we aren't logged in");
api_authed = false;
break;
default:
err = ERR_API_LOGOUT;
uio_error("Unknown error: %hu", res.code);
break;
}

return err;
}

static void api_keepalive(struct timespec *out_next)
{
struct timespec ts = {0};
uint64_t msdiff;

clock_gettime(API_CLOCK, &ts);
msdiff = util_timespec_diff(&api_last_packet, &ts);

if (msdiff >= API_TIMEOUT) {
struct api_result r;

MS_TO_TIMESPEC(out_next, API_TIMEOUT);

uio_debug("Sending uptime command for keep alive");
// TODO what if another action is already in progress?
api_cmd_uptime(&r);
} else {
uint64_t msnext = API_TIMEOUT - msdiff;

uio_debug("Got keepalive request, but time is not up yet");
MS_TO_TIMESPEC(out_next, msnext);
}
}

void *api_keepalive_main(void *arg)
{
struct timespec ka_time;
MS_TO_TIMESPEC_L(ka_time, API_TIMEOUT);
uio_debug("Hi from keepalie thread");

for (;;) {
if (nanosleep(&ka_time, NULL) != 0) {
int e = errno;
uio_error("Nanosleep failed: %s", strerror(e));
}
/* Needed, because the thread could be canceled while in recv or send
* and in that case, the mutex will remain locked
* Could be replaced with a pthread_cleanup_push ? */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
pthread_mutex_lock(&api_work_mx);
api_ka_now = true;

uio_debug("G'moooooning! Is it time to keep our special connection alive?");

api_keepalive(&ka_time);
uio_debug("Next wakey-wakey in %ld seconds", ka_time.tv_sec);
api_ka_now = false;
pthread_mutex_unlock(&api_work_mx);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
}
return NULL;
}

enum error api_clock_init()
{
struct timespec ts;
memset(&api_last_packet, 0, sizeof(api_last_packet));
api_packet_count = 0;
api_fast_packet_count = 0;

if (clock_getres(API_CLOCK, &ts) != 0) {
uio_error("Cannot get clock resolution: %s", strerror(errno));
return ERR_API_CLOCK;
}
uio_debug("Clock resolution: %f ms",
(ts.tv_sec * 1000) + (ts.tv_nsec / 1000000.0));

return NOERR;
}

enum error api_init(bool auth)
{
enum error err = NOERR;
const char **api_key, **uname, **passwd;

err = api_clock_init();
if (err != NOERR)
return err;

err = net_init();
if (err != NOERR)
return err;

if (config_get("api-key", (void**)&api_key) == NOERR) {
if (config_get("username", (void**)&uname) != NOERR) {
uio_error("Api key is specified, but that also requires "
"the username!");
err = ERR_OPT_REQUIRED;
goto fail;
}
err = api_init_encrypt(*api_key, *uname);
if (err != NOERR) {
uio_error("Cannot init api encryption");
goto fail;
}
}

/* Define an escaped string printf type */
if (register_printf_specifier('B', api_escaped_string,
api_escaped_sring_info) != 0) {
uio_error("Failed to register escaped printf string function");
err = ERR_API_PRINTFFUNC;
goto fail;
}

if (auth) {
if (config_get("username", (void**)&uname) != NOERR) {
uio_error("Username is not specified, but it is required!");
err = ERR_OPT_REQUIRED;
goto fail;
}
if (config_get("password", (void**)&passwd) != NOERR) {
uio_error("Password is not specified, but it is required!");
err = ERR_OPT_REQUIRED;
goto fail;
}
err = api_auth(*uname, *passwd);
if (err != NOERR)
goto fail;

/* Only do keep alive if we have a session */
if (pthread_mutex_init(&api_work_mx, NULL) != 0) {
uio_error("Cannot create mutex");
err = ERR_THRD;
goto fail;
}
if (pthread_create(&api_ka_thread, NULL, api_keepalive_main, NULL) != 0) {
uio_error("Cannot create api keepalive thread");
err = ERR_THRD;
goto fail;
}

}

#if 0
printf("Testings: %B\n", "oi&ha=hi&wooooowz&");
printf("Testings: %B\n", "oi&ha=hi&wooooowz");
printf("Testings: %B\n", "&oi&ha=hi&wooooowz");
printf("Testings: %B\n", "oooooooooiiiiii");
#endif

return err;
fail:
api_free();
return err;
}

void api_free()
{
if (api_authed) {
if (pthread_cancel(api_ka_thread) != 0) {
uio_error("Cannot cancel api keepalive thread");
} else {
int je = pthread_join(api_ka_thread, NULL);
if (je != 0) {
uio_error("Cannot join api keepalive thread: %s",
strerror(je));
}
if (pthread_mutex_destroy(&api_work_mx) != 0)
uio_error("Cannot destroy api work mutex");
}

api_logout();
memset(api_session, 0, sizeof(api_session));
api_authed = false; /* duplicate */
}
if (api_encryption) {
api_encryption = false;
memset(e_key, 0, sizeof(e_key));
}

register_printf_specifier('B', NULL, NULL);
net_free();
}

/*
* We just sent a packet, so update the last packet time here
*/
static void api_ratelimit_sent()
{
clock_gettime(API_CLOCK, &api_last_packet);
}

static void api_ratelimit()
{
struct timespec ts = {0};
uint64_t msdiff, mswait;

clock_gettime(API_CLOCK, &ts);
msdiff = util_timespec_diff(&api_last_packet, &ts);
uio_debug("Time since last packet: %ld ms", msdiff);

if (msdiff >= API_SENDWAIT)
return; /* No ratelimiting is needed */

/* Need ratelimit, so do it here for now */
mswait = API_SENDWAIT - msdiff;
uio_debug("Ratelimit is needed, sleeping for %ld ms", mswait);

MS_TO_TIMESPEC_L(ts, mswait);
if (nanosleep(&ts, NULL) == -1) {
if (errno == EINTR)
uio_error("Nanosleep got interrupted");
else
uio_error("Nanosleep failed");
}
}

static ssize_t api_send(char *buffer, size_t data_len, size_t buf_size)
{
ssize_t read_len;

api_ratelimit();
uio_debug("{Api}: Sending: %.*s", (int)data_len, buffer);
if (api_encryption)
data_len = api_encrypt(buffer, data_len);

if (net_send(buffer, data_len) == -1) {
uio_error("Cannot send data: %s", strerror(errno));
return -1;
}

read_len = net_read(buffer, buf_size);
api_ratelimit_sent();

if (api_encryption)
read_len = api_decrypt(buffer, read_len);
uio_debug("{Api}: Reading: %.*s", (int)read_len, buffer);

return read_len;
}

long api_res_code(const char *buffer)
{
char *end;
long res = strtol(buffer, &end, 10);
if (res == 0 && buffer == end) {
uio_error("No error codes in the response");
return -1;
}
assert(*end == ' ');
return res;
}

static bool api_get_fl(const char *buffer, int32_t index, const char *delim,
char **const out_start, size_t *const out_len)
{
assert(index > 0);

size_t len = strcspn(buffer, delim);

while (--index > 0) {
buffer += len + 1;
len = strcspn(buffer, delim);
}

*out_start = (char*)buffer;
*out_len = len;
return true;
}

static bool api_get_line(const char *buffer, int32_t line_num,
char **const out_line_start, size_t *const out_line_len)
{
return api_get_fl(buffer, line_num, "\n", out_line_start, out_line_len);
}

static bool api_get_field(const char *buffer, int32_t field_num,
char **const out_field_start, size_t *const out_field_len)
{
return api_get_fl(buffer, field_num, " |\n", out_field_start, out_field_len);
}

#if 0
static char *api_get_field_mod(char *buffer, int32_t field_num)
{
char *sptr = NULL;
char *f_start;

f_start = strtok_r(buffer, " ", &sptr);
if (!f_start)
return NULL;

while (field_num --> 0) {
f_start = strtok_r(NULL, " ", &sptr);
if (!f_start)
return NULL;
}

return f_start;
}
#endif

enum error api_cmd_version(struct api_result *res)
{
char buffer[API_BUFSIZE] = "VERSION";
size_t res_len = api_send(buffer, strlen(buffer), sizeof(buffer));
long code;
enum error err = NOERR;
pthread_mutex_lock(&api_work_mx);

if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}

code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}

if (code == 998) {
char *ver_start;
size_t ver_len;
bool glr = api_get_line(buffer, 2, &ver_start, &ver_len);

assert(glr);
(void)glr;
assert(ver_len < sizeof(res->version.version_str));
memcpy(res->version.version_str, ver_start, ver_len);
res->version.version_str[ver_len] = '\0';
}
res->code = (uint16_t)code;
end:
pthread_mutex_unlock(&api_work_mx);
return err;
}

static enum error api_cmd_auth(const char *uname, const char *pass,
struct api_result *res)
{
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
long code;
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"AUTH user=%s&pass=%B&protover=3&client=caniadd&clientver="
PROG_VERSION "&enc=UTF-8", uname, pass), sizeof(buffer));
enum error err = NOERR;

if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}

code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}

if (code == 200 || code == 201) {
char *sess;
size_t sess_len;
bool gfr = api_get_field(buffer, 2, &sess, &sess_len);

assert(gfr);
(void)gfr;
assert(sess_len < sizeof(res->auth.session_key));
memcpy(res->auth.session_key, sess, sess_len);
res->auth.session_key[sess_len] = '\0';
} else if (code == 504) {
char *reason;
size_t reason_len;
bool gfr = api_get_field(buffer, 5, &reason, &reason_len);

assert(gfr);
(void)gfr;
res->auth.banned_reason = strndup(reason, reason_len);
}
res->code = (uint16_t)code;

end:
pthread_mutex_unlock(&api_work_mx);
return err;
}

static enum error api_cmd_logout(struct api_result *res)
{
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"LOGOUT s=%s", api_session), sizeof(buffer));
long code;
enum error err = NOERR;

if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}

code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}

res->code = (uint16_t)code;

end:
pthread_mutex_unlock(&api_work_mx);
return err;
}

enum error api_cmd_uptime(struct api_result *res)
{
/* If mutex is not already locked from the keepalive thread */
/* Or we could use a recursive mutex? */
if (!api_ka_now)
pthread_mutex_lock(&api_work_mx);
char buffer[API_BUFSIZE];
size_t res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"UPTIME s=%s", api_session), sizeof(buffer));
long code;
enum error err = NOERR;

if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}

code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}

if (code == 208) {
char *ls;
size_t ll;
bool glf = api_get_line(buffer, 2, &ls, &ll);

assert(glf);
(void)glf;
res->uptime.ms = strtol(ls, NULL, 10);
}

res->code = (uint16_t)code;

end:
if (!api_ka_now)
pthread_mutex_unlock(&api_work_mx);
return err;
}

enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash,
enum mylist_state ml_state, bool watched, struct api_result *res)
{
char buffer[API_BUFSIZE];
char hash_str[ED2K_HASH_SIZE * 2 + 1];
size_t res_len;
enum error err = NOERR;
long code;
pthread_mutex_lock(&api_work_mx);

util_byte2hex(hash, ED2K_HASH_SIZE, false, hash_str);
/* Wiki says file size is 4 bytes, but no way that's true lol */
res_len = api_send(buffer, snprintf(buffer, sizeof(buffer),
"MYLISTADD s=%s&size=%ld&ed2k=%s&state=%hu&viewed=%d",
api_session, size, hash_str, ml_state, watched),
sizeof(buffer));

if (res_len == -1) {
err = ERR_API_COMMFAIL;
goto end;
}

code = api_res_code(buffer);
if (code == -1) {
err = ERR_API_RESP_INVALID;
goto end;
}

if (code == 210) {
char *ls, id_str[12];
size_t ll;
bool glr = api_get_line(buffer, 2, &ls, &ll);

assert(glr);
(void)glr;
assert(sizeof(id_str) > ll);
memcpy(id_str, ls, ll);
id_str[ll] = '\0';
res->mylistadd.new_id = strtoll(id_str, NULL, 10);
/* Wiki says these id's are 4 bytes, which is untrue...
* that page may be a little out of date (or they just
* expect us to use common sense lmao */
} else if (code == 310) {
/* {int4 lid}|{int4 fid}|{int4 eid}|{int4 aid}|{int4 gid}|
* {int4 date}|{int2 state}|{int4 viewdate}|{str storage}|
* {str source}|{str other}|{int2 filestate} */
char *ls;
size_t ll;
struct api_mylistadd_result *mr = &res->mylistadd;
bool glr = api_get_line(buffer, 2, &ls, &ll);
assert(glr);
assert(ll < API_BUFSIZE - 1);
(void)glr;

ls[ll] = '\0';
void *fptrs[] = {
&mr->lid, &mr->fid, &mr->eid, &mr->aid, &mr->gid, &mr->date,
&mr->state, &mr->viewdate, &mr->storage, &mr->source,
&mr->other, &mr->filestate,
};
for (int idx = 1; idx <= 12; idx++) {
char *fs, *endptr;
size_t fl;
bool pr;
uint64_t val;
size_t cpy_size = sizeof(mr->lid);

if (idx == 7)
cpy_size = sizeof(mr->state);
if (idx == 12)
cpy_size = sizeof(mr->filestate);

pr = api_get_field(ls, idx, &fs, &fl);
assert(pr);
(void)pr;
if (idx == 9 || idx == 10 || idx == 11) { /* string fields */
if (fl == 0)
*(char**)fptrs[idx-1] = NULL;
else
*(char**)fptrs[idx-1] = strndup(fs, fl);
continue;
}

val = strtoull(fs, &endptr, 10);
assert(!(val == 0 && fs == endptr));
memcpy(fptrs[idx-1], &val, cpy_size);
}
}

res->code = (uint16_t)code;

end:
pthread_mutex_unlock(&api_work_mx);
return err;
}

#pragma GCC diagnostic pop

+ 91
- 0
src/api.h View File

@@ -0,0 +1,91 @@
#ifndef _API_H
#define _API_H
#include <stdint.h>
#include <time.h>

#include "error.h"

/* Maximum length of one response/request */
#define API_BUFSIZE 1400
/* Session key maximum size, including '\0' */
#define API_SMAXSIZE 16
/* The session timeout in miliseconds */
#define API_TIMEOUT 30 * 60 * 1000

/* How many miliseconds to wait between sends */
#define API_SENDWAIT 2 * 1000
/* The number of packets that are exccempt from the ratelimit */
#define API_FREESEND 5
/* Long term wait between sends */
#define API_SENDWAIT_LONG 4 * 1000
/* After this many packets has been sent, use the longterm ratelimit */
#define API_LONGTERM_PACKETS 100

enum mylist_state {
MYLIST_STATE_UNKNOWN = 0,
MYLIST_STATE_INTERNAL,
MYLIST_STATE_EXTERNAL,
MYLIST_STATE_DELETED,
MYLIST_STATE_REMOTE,
};

enum file_state {
FILE_STATE_NORMAL = 0,
FILE_STATE_CORRUPT,
FILE_STATE_SELF_EDIT,
FILE_STATE_SELF_RIP = 10,
FILE_STATE_ON_DVD,
FILE_STATE_ON_VHS,
FILE_STATE_ON_TV,
FILE_STATE_IN_THEATERS,
FILE_STATE_STREAMED,
FILE_STATE_OTHER = 100,
};

struct api_version_result {
char version_str[40];
};
struct api_auth_result {
union {
char session_key[API_SMAXSIZE];
/* free() */
char *banned_reason;
};
};
struct api_uptime_result {
int32_t ms;
};
struct api_mylistadd_result {
union {
uint64_t new_id;
struct {
uint64_t lid, fid, eid, aid, gid, date, viewdate;
/* free() if != NULL ofc */
char *storage, *source, *other;
enum mylist_state state;
enum file_state filestate;
};
};
};

#define e(n) struct api_##n##_result n
struct api_result {
uint16_t code;
union {
struct api_version_result version;
struct api_auth_result auth;
struct api_uptime_result uptime;
e(mylistadd);
};
};
#undef e

enum error api_init(bool auth);
void api_free();

enum error api_cmd_version(struct api_result *res);
enum error api_cmd_uptime(struct api_result *res);
enum error api_cmd_mylistadd(int64_t size, const uint8_t *hash,
enum mylist_state fstate, bool watched, struct api_result *res);

#endif /* _API_H */

+ 208
- 0
src/cache.c View File

@@ -0,0 +1,208 @@
#include <stddef.h>

#include <sqlite3.h>

#include "cache.h"
#include "config.h"
#include "uio.h"
#include "ed2k.h"
#include "util.h"

#define sqlite_bind_goto(smt, name, type, ...) { \
int sb_idx = sqlite3_bind_parameter_index(smt, name); \
if (sb_idx == 0) { \
uio_error("Cannot get named parameter for var: %s", name); \
err = ERR_CACHE_SQLITE; \
goto fail; \
} \
int sb_sret = sqlite3_bind_##type(smt, sb_idx, __VA_ARGS__); \
if (sb_sret != SQLITE_OK) {\
uio_error("Cannot bind to statement: %s", sqlite3_errmsg(cache_db));\
err = ERR_CACHE_SQLITE;\
goto fail;\
} \
}

static sqlite3 *cache_db = NULL;

static const char sql_create_table[] = "CREATE TABLE IF NOT EXISTS mylist ("
"lid INTEGER NOT NULL PRIMARY KEY,"
"fname TEXT NOT NULL,"
"fsize INTEGER NOT NULL,"
"ed2k TEXT NOT NULL,"
"UNIQUE (fname, fsize) )";
static const char sql_mylist_add[] = "INSERT INTO mylist "
"(lid, fname, fsize, ed2k) VALUES "
//"(?, ?, ?, ?)";
"(:lid, :fname, :fsize, :ed2k)";
static const char sql_mylist_get[] = "SELECT * FROM mylist WHERE "
"fsize=:fsize AND fname=:fname";


#if 0
static const char sql_has_tables[] = "SELECT 1 FROM sqlite_master "
"WHERE type='table' AND tbl_name='mylist'";

/* Return 0 if false, 1 if true, and -1 if error */
static int cache_has_tables()
{
sqlite3_smt smt;
int sret;

sret = sqlite3_prepare_v2(cache_db, sql_has_tables,
sizeof(sql_has_tables), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return -1;
}

sqlite3_step(&smt);
// ehh fuck this, lets just use if not exists
sret = sqlite3_finalize(&smt);
if (sret != SQLITE_OK)
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db));
}
#endif

/*
* Create database table(s)
*/
static enum error cache_init_table()
{
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;

sret = sqlite3_prepare_v2(cache_db, sql_create_table,
sizeof(sql_create_table), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}

sret = sqlite3_step(smt);
if (sret != SQLITE_DONE) {
uio_error("sql3_step is not done: %s", sqlite3_errmsg(cache_db));
err = ERR_CACHE_SQLITE;
}
sret = sqlite3_finalize(smt);
if (sret != SQLITE_OK)
uio_debug("sql3_finalize failed: %s", sqlite3_errmsg(cache_db));

return err;
}

enum error cache_init()
{
char **db_path;
enum error err;
int sret;

err = config_get("cachedb", (void**)&db_path);
if (err != NOERR) {
uio_error("Cannot get cache db path from args");
return err;
}

uio_debug("Opening cache db: '%s'", *db_path);
sret = sqlite3_open(*db_path, &cache_db);
if (sret != SQLITE_OK) {
uio_error("Cannot create sqlite3 database: %s", sqlite3_errstr(sret));
sqlite3_close(cache_db); /* Even if arg is NULL, it's A'OK */
return ERR_CACHE_SQLITE;
}
sqlite3_extended_result_codes(cache_db, 1);

err = cache_init_table();
if (err != NOERR)
goto fail;

return NOERR;

fail:
cache_free();
return err;
}

void cache_free()
{
sqlite3_close(cache_db);
uio_debug("Closed cache db");
}

enum error cache_add(uint64_t lid, const char *fname,
uint64_t fsize, const uint8_t *ed2k)
{
char ed2k_str[ED2K_HASH_SIZE * 2 + 1];
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;

sret = sqlite3_prepare_v2(cache_db, sql_mylist_add,
sizeof(sql_mylist_add), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}

util_byte2hex(ed2k, ED2K_HASH_SIZE, false, ed2k_str);

sqlite_bind_goto(smt, ":lid", int64, lid);
sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC);
sqlite_bind_goto(smt, ":fsize", int64, fsize);
sqlite_bind_goto(smt, ":ed2k", text, ed2k_str, -1, SQLITE_STATIC);

sret = sqlite3_step(smt);
if (sret != SQLITE_DONE) {
if (sret == SQLITE_CONSTRAINT_PRIMARYKEY) {
uio_debug("Attempted to add duplicate entry!");
err = ERR_CACHE_EXISTS;
} else if (sret == SQLITE_CONSTRAINT_UNIQUE) {
uio_debug("An entry with the same name and size already exists!");
err = ERR_CACHE_NON_UNIQUE;
} else {
uio_error("error after sql3_step: %s %d", sqlite3_errmsg(cache_db), sret);
err = ERR_CACHE_SQLITE;
}
}
fail:
sqlite3_finalize(smt);
return err;
}

enum error cache_get(const char *fname, uint64_t fsize,
struct cache_entry *out_ce)
{
sqlite3_stmt *smt;
int sret;
enum error err = NOERR;

sret = sqlite3_prepare_v2(cache_db, sql_mylist_get,
sizeof(sql_mylist_get), &smt, NULL);
if (sret != SQLITE_OK) {
uio_error("Cannot prepare statement: %s", sqlite3_errmsg(cache_db));
return ERR_CACHE_SQLITE;
}

sqlite_bind_goto(smt, ":fname", text, fname, -1, SQLITE_STATIC);
sqlite_bind_goto(smt, ":fsize", int64, fsize);

sret = sqlite3_step(smt);
if (sret == SQLITE_DONE) {
uio_debug("Cache entry with size (%lu) and name (%s) not found", fsize, fname);
err = ERR_CACHE_NO_EXISTS;
} else if (sret == SQLITE_ROW) {
uio_debug("Found Cache entry with size (%lu) and name (%s)", fsize, fname);
} else {
uio_error("sqlite_step failed: %s", sqlite3_errmsg(cache_db));
err = ERR_CACHE_SQLITE;
}
fail:
sqlite3_finalize(smt);
return err;
}

+ 40
- 0
src/cache.h View File

@@ -0,0 +1,40 @@
#ifndef _CACHE_H
#define _CACHE_H
#include <stdint.h>
#include <stdbool.h>

#include "error.h"
#include "ed2k.h"

struct cache_entry {
uint64_t lid, fsize;
char *fname;
uint8_t ed2k[ED2K_HASH_SIZE];
};

/*
* Init tha cache
*/
enum error cache_init();

/*
* Free tha cache
*/
void cache_free();

/*
* Add a new mylist entry to the cache
*/
enum error cache_add(uint64_t lid, const char *fname,
uint64_t fsize, const uint8_t *ed2k);

/*
* Get a cache entry
*
* out_ce can be NULL. Useful, if we only want
* to check if the entry exists or not.
*/
enum error cache_get(const char *fname, uint64_t size,
struct cache_entry *out_ce);

#endif /* _CACHE_H */

+ 29
- 0
src/caniadd.c View File

@@ -0,0 +1,29 @@
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#include "config.h"
#include "error.h"
#include "uio.h"
#include "cmd.h"

int main(int argc, char **argv)
{
int exit_code = EXIT_SUCCESS;
enum error err = config_parse(argc, argv);

if (err == ERR_OPT_EXIT)
return EXIT_SUCCESS;
else if (err != NOERR)
return EXIT_FAILURE;

//config_dump();

err = cmd_main();
if (err != NOERR)
exit_code = EXIT_FAILURE;

config_free();

return exit_code;
}

+ 75
- 0
src/cmd.c View File

@@ -0,0 +1,75 @@
#include <stdbool.h>

#include "cmd.h"
#include "error.h"
#include "config.h"
#include "api.h"
#include "uio.h"
#include "net.h"
#include "cache.h"

struct cmd_entry {
bool need_api : 1; /* Does this command needs to connect to the api? */
bool need_auth : 1; /* Does this command needs auth to the api? sets need_api */
bool need_cache : 1; /* Does this cmd needs the file cache? */
const char *arg_name; /* If this argument is present, execute this cmd */
enum error (*fn)(void *data); /* The function for the command */
};

static const struct cmd_entry ents[] = {
{ .arg_name = "version", .fn = cmd_prog_version, },
{ .arg_name = "server-version", .fn = cmd_server_version, .need_api = true },
{ .arg_name = "uptime", .fn = cmd_server_uptime, .need_auth = true },
{ .arg_name = "ed2k", .fn = cmd_ed2k, },
{ .arg_name = "add", .fn = cmd_add, .need_auth = true, .need_cache = true, },
};
static const int32_t ents_len = sizeof(ents)/sizeof(*ents);

static enum error cmd_run_one(const struct cmd_entry *ent)
{
enum error err = NOERR;

if (ent->need_cache) {
err = cache_init();
if (err != NOERR)
goto end;
}
if (ent->need_api || ent->need_auth) {
err = api_init(ent->need_auth);
if (err != NOERR)
return err;
}

void *data = NULL;
err = ent->fn(data);

end:
if (ent->need_api || ent->need_auth)
api_free();
if (ent->need_cache)
cache_free();
return err;
}

enum error cmd_main()
{
for (int i = 0; i < ents_len; i++) {
enum error err;
bool *is_set;

err = config_get(ents[i].arg_name, (void**)&is_set);
if (err != NOERR && err != ERR_OPT_UNSET) {
uio_error("Cannot get arg '%s' (%s)", ents[i].arg_name,
error_to_string(err));
continue;
}

if (*is_set) {
err = cmd_run_one(&ents[i]);
return err;
}
}

return ERR_CMD_NONE;
}

+ 38
- 0
src/cmd.h View File

@@ -0,0 +1,38 @@
#ifndef _CMD_H
#define _CMD_H

#include "error.h"
#include "config.h"

/*
* Read commands from config and execute them
*/
enum error cmd_main();

/*
* Add files to the AniDB list
*/
enum error cmd_add(void *);

/*
* Take in a file/folder and print out
* the ed2k hash of it
*/
enum error cmd_ed2k(void *data);

/*
* Get and print the server api version
*/
enum error cmd_server_version(void *);

/*
* Print the server uptime
*/
enum error cmd_server_uptime(void *);

/*
* Print the program version
*/
enum error cmd_prog_version(void *);

#endif /* _CMD_H */

+ 114
- 0
src/cmd_add.c View File

@@ -0,0 +1,114 @@
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

#include "cmd.h"
#include "error.h"
#include "uio.h"
#include "api.h"
#include "config.h"
#include "ed2k_util.h"
#include "cache.h"
#include "util.h"

struct add_opts {
enum mylist_state ao_state;
bool ao_watched;
};

enum error cmd_add_cachecheck(const char *path, const struct stat *st,
void *data)
{
const char *bname = util_basename(path);
enum error err;

err = cache_get(bname, st->st_size, NULL);
if (err == NOERR) {
/* We could get the entry, so it exists already */
uio_user("This file (%s) with size (%lu) already exists in cache."
" Skipping", bname, st->st_size);
return ED2KUTIL_DONTHASH;
} else if (err != ERR_CACHE_NO_EXISTS) {
uio_error("Some error when trying to get from cache: %s",
error_to_string(err));
return ED2KUTIL_DONTHASH;
}

uio_user("Hashing %s", path);
return NOERR;
}

enum error cmd_add_apisend(const char *path, const uint8_t *hash,
const struct stat *st, void *data)
{
struct api_result r;
struct add_opts* ao = (struct add_opts*)data;

if (api_cmd_mylistadd(st->st_size, hash, ao->ao_state, ao->ao_watched, &r)
!= NOERR)
return ERR_CMD_FAILED;

if (r.code == 310) {
struct api_mylistadd_result *x = &r.mylistadd;

uio_warning("File already added! Adding it to cache");
uio_debug("File info: lid: %ld, fid: %ld, eid: %ld, aid: %ld,"
" gid: %ld, date: %ld, viewdate: %ld, state: %d,"
" filestate: %d\nstorage: %s\nsource: %s\nother: %s",
x->lid, x->fid, x->eid, x->aid, x->gid, x->date, x->viewdate,
x->state, x->filestate, x->storage, x->source, x->other);

cache_add(x->lid, util_basename(path), st->st_size, hash);

if (x->storage)
free(x->storage);
if (x->source)
free(x->source);
if (x->other)
free(x->other);
return NOERR;
}
if (r.code != 210) {
uio_error("Mylistadd failure: %hu", r.code);
return ERR_CMD_FAILED;
}

uio_user("Succesfully added!");
uio_debug("New mylist id is: %ld", r.mylistadd.new_id);
cache_add(r.mylistadd.new_id, util_basename(path), st->st_size, hash);

return NOERR;
}

enum error cmd_add(void *data)
{
struct add_opts add_opts = {0};
struct ed2k_util_opts ed2k_opts = {
.pre_hash_fn = cmd_add_cachecheck,
.post_hash_fn = cmd_add_apisend,
.data = &add_opts,
};
bool *watched;
enum error err = NOERR;
int fcount;

fcount = config_get_nonopt_count();
if (fcount == 0) {
uio_error("No files specified");
return ERR_CMD_ARG;
}

if (config_get("watched", (void**)&watched) == NOERR) {
add_opts.ao_watched = *watched;
}
add_opts.ao_state = MYLIST_STATE_INTERNAL;

for (int i = 0; i < fcount; i++) {
err = ed2k_util_iterpath(config_get_nonopt(i), &ed2k_opts);
if (err != NOERR)
break;
}
return err;
}


+ 62
- 0
src/cmd_ed2k.c View File

@@ -0,0 +1,62 @@
#include <sys/stat.h>
#include <stdio.h>
#include <stdbool.h>

#include "cmd.h"
#include "error.h"
#include "uio.h"
#include "config.h"
#include "ed2k_util.h"
#include "ed2k.h"
#include "util.h"

struct cmd_ed2k_opts {
bool link;
};

static enum error cmd_ed2k_output(const char *path, const uint8_t *hash,
const struct stat *st, void *data)
{
struct cmd_ed2k_opts *eo = data;
char buff[ED2K_HASH_SIZE * 2 + 1];
bool upcase = eo->link;

util_byte2hex(hash, ED2K_HASH_SIZE, upcase, buff);
if (eo->link) {
char *name_part = util_basename(path);

printf("ed2k://|file|%s|%ld|%s|/\n", name_part, st->st_size, buff);
} else {
printf("%s\t%s\n", buff, path);
}
return NOERR;
}

enum error cmd_ed2k(void *data)
{
struct cmd_ed2k_opts opts = {0};
struct ed2k_util_opts ed2k_opts = {
.post_hash_fn = cmd_ed2k_output,
.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;
}


+ 18
- 0
src/cmd_prog_version.c View File

@@ -0,0 +1,18 @@
#include <stdio.h>

#include "cmd.h"
#include "cache.h"

#include <string.h>
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;
}

+ 31
- 0
src/cmd_server_uptime.c View File

@@ -0,0 +1,31 @@
#include <stdio.h>
#include <stdlib.h>

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

+ 23
- 0
src/cmd_server_version.c View File

@@ -0,0 +1,23 @@
#include <stdio.h>

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

+ 539
- 0
src/config.c View File

@@ -0,0 +1,539 @@
#include <stddef.h>
#include <inttypes.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
//#include <toml.h>
#include <limits.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>

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

+ 94
- 0
src/config.h View File

@@ -0,0 +1,94 @@
#ifndef _CONFIG_H
#define _CONFIG_H
#include <inttypes.h>
#include <stdbool.h>

#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 */

+ 61
- 0
src/ed2k.c View File

@@ -0,0 +1,61 @@
#include <assert.h>

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


+ 18
- 0
src/ed2k.h View File

@@ -0,0 +1,18 @@
#ifndef _ED2K_H
#define _ED2K_H
#include <stdint.h>

#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 */

+ 93
- 0
src/ed2k_util.c View File

@@ -0,0 +1,93 @@
#define _XOPEN_SOURCE 500
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <ftw.h>

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

+ 33
- 0
src/ed2k_util.h View File

@@ -0,0 +1,33 @@
#ifndef _ED2K_UTIL_H
#define _ED2K_UTIL_H
#include <sys/stat.h>
#include <stdint.h>

#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 */

+ 12
- 0
src/error.c View File

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

+ 63
- 0
src/error.h View File

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

+ 265
- 0
src/net.c View File

@@ -0,0 +1,265 @@
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <ctype.h>
#include <assert.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>

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

+ 24
- 0
src/net.h View File

@@ -0,0 +1,24 @@
#ifndef _NET_H
#define _NET_H
#include <stdint.h>
#include <sys/types.h>

#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 */

+ 58
- 0
src/uio.c View File

@@ -0,0 +1,58 @@
#include <stdarg.h>
#include <stdio.h>

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

+ 9
- 0
src/uio.h View File

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

+ 39
- 0
src/util.c View File

@@ -0,0 +1,39 @@
#include <stdlib.h>
#include <string.h>

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

+ 35
- 0
src/util.h View File

@@ -0,0 +1,35 @@
#ifndef _UTIL_H
#define _UTIL_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>

/*
* 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 */

+ 1
- 0
subm/MD4

@@ -0,0 +1 @@
Subproject commit 6771029ebea612a1a53822af6867b6cf172c31f0

+ 1
- 0
subm/md5-c

@@ -0,0 +1 @@
Subproject commit 1525b0db1fb608afed8948f3a972d55486e8cb31

+ 1
- 0
subm/tiny-AES-c

@@ -0,0 +1 @@
Subproject commit f06ac37fc31dfdaca2e0d9bec83f90d5663c319b

Loading…
Cancel
Save