diff options
Diffstat (limited to 'src/server/rpc.c')
-rw-r--r-- | src/server/rpc.c | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/src/server/rpc.c b/src/server/rpc.c new file mode 100644 index 0000000..1ea09cb --- /dev/null +++ b/src/server/rpc.c @@ -0,0 +1,504 @@ +#include "rpc.h" +#include "helper.h" +#include "net.h" +#include "uplink.h" +#include "locks.h" +#include "image.h" +#include "altservers.h" +#include "../shared/sockhelper.h" +#include "fileutil.h" +#include "picohttpparser/picohttpparser.h" +#include "urldecode.h" + +#include <jansson.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#if JANSSON_VERSION_HEX < 0x020600 +#define json_stringn_nocheck(a,b) json_string_nocheck(a) +#endif + +#define ACL_ALL 0x7fffffff +#define ACL_STATS 1 +#define ACL_CLIENT_LIST 2 +#define ACL_IMAGE_LIST 4 +#define ACL_CONFIG 8 +#define ACL_LOG 16 +#define ACL_ALTSERVERS 32 + +#define HTTP_CLOSE 4 +#define HTTP_KEEPALIVE 9 + +// Make sure compiler does not reserve more space for static strings than required (or rather, does not tell so in sizeof calls) +// TODO Might be time for a dedicated string.h +_Static_assert( sizeof("test") == 5 && sizeof("test2") == 6, "Stringsize messup :/" ); +#define STRCMP(str,chr) ( (str).s != NULL && (str).l == sizeof(chr)-1 && strncmp( (str).s, (chr), MIN((str).l, sizeof(chr)-1) ) == 0 ) +#define STRSTART(str,chr) ( (str).s != NULL && (str).l >= sizeof(chr)-1 && strncmp( (str).s, (chr), MIN((str).l, sizeof(chr)-1) ) == 0 ) +#define SETSTR(name,value) do { name.s = value; name.l = sizeof(value)-1; } while (0) +#define DEFSTR(name,value) static struct string name = { .s = value, .l = sizeof(value)-1 }; +#define chartolower(c) ((char)( (c) >= 'A' && (c) <= 'Z' ? (c) + ('a'-'A') : (c) )) + +DEFSTR(STR_CONNECTION, "connection") +DEFSTR(STR_CLOSE, "close") +DEFSTR(STR_QUERY, "/query") +DEFSTR(STR_Q, "q") + +static inline bool equals(struct string *s1,struct string *s2) +{ + if ( s1->s == NULL ) { + return s2->s == NULL; + } else if ( s2->s == NULL || s1->l != s2->l ) { + return false; + } + return memcmp( s1->s, s2->s, s1->l ) == 0; +} + +static inline bool iequals(struct string *cmpMixed, struct string *cmpLower) +{ + if ( cmpMixed->s == NULL ) { + return cmpLower->s == NULL; + } else if ( cmpLower->s == NULL || cmpMixed->l != cmpLower->l ) { + return false; + } + for ( size_t i = 0; i < cmpMixed->l; ++i ) { + if ( chartolower( cmpMixed->s[i] ) != cmpLower->s[i] ) return false; + } + return true; +} + +#define MAX_ACLS 100 +static int aclCount = 0; +static dnbd3_access_rule_t aclRules[MAX_ACLS]; +static json_int_t randomRunId; +static pthread_spinlock_t aclLock; +#define MAX_CLIENTS 50 +#define CUTOFF_START 40 +static pthread_spinlock_t statusLock; +static struct { + int count; + bool overloaded; +} status; + +static bool handleStatus(int sock, int permissions, struct field *fields, size_t fields_num, int keepAlive); +static bool sendReply(int sock, const char *status, const char *ctype, const char *payload, ssize_t plen, int keepAlive); +static void parsePath(struct string *path, struct string *file, struct field *getv, size_t *getc); +static bool hasHeaderValue(struct phr_header *headers, size_t numHeaders, struct string *name, struct string *value); +static int getacl(dnbd3_host_t *host); +static void addacl(int argc, char **argv, void *data); +static void loadAcl(); + +void rpc_init() +{ + spin_init( &aclLock, PTHREAD_PROCESS_PRIVATE ); + spin_init( &statusLock, PTHREAD_PROCESS_PRIVATE ); + randomRunId = (((json_int_t)getpid()) << 16) | (json_int_t)time(NULL); + // </guard> + if ( sizeof(randomRunId) > 4 ) { + int fd = open( "/dev/urandom", O_RDONLY ); + if ( fd != -1 ) { + uint32_t bla = 1; + read( fd, &bla, 4 ); + randomRunId = (randomRunId << 32) | bla; + } + close( fd ); + } + loadAcl(); +} + +#define UPDATE_LOADSTATE(cnt) do { \ + if ( cnt < (CUTOFF_START/2) ) { \ + if ( status.overloaded ) status.overloaded = false; \ + } else if ( cnt > CUTOFF_START ) { \ + if ( !status.overloaded ) status.overloaded = true; \ + } \ +} while (0) + +void rpc_sendStatsJson(int sock, dnbd3_host_t* host, const void* data, const int dataLen) +{ + int permissions = getacl( host ); + if ( permissions == 0 ) { + sendReply( sock, "403 Forbidden", "text/plain", "Access denied", -1, HTTP_CLOSE ); + return; + } + do { + spin_lock( &statusLock ); + const int curCount = ++status.count; + UPDATE_LOADSTATE( curCount ); + spin_unlock( &statusLock ); + if ( curCount > MAX_CLIENTS ) { + sendReply( sock, "503 Service Temporarily Unavailable", "text/plain", "Too many HTTP clients", -1, HTTP_CLOSE ); + goto func_return; + } + } while (0); + char headerBuf[3000]; + if ( dataLen > 0 ) { + // We call this function internally with a maximum data len of sizeof(dnbd3_request_t) so no bounds checking + memcpy( headerBuf, data, dataLen ); + } + size_t hoff = dataLen; + bool hasName = false; + bool ok; + int keepAlive = HTTP_KEEPALIVE; + do { + // Read request from client + struct phr_header headers[100]; + size_t numHeaders, prevLen = 0, consumed; + struct string method, path; + int minorVersion; + do { + // Parse before calling recv, there might be a complete pipelined request in the buffer already + // If the request is incomplete, we allow exactly one additional recv() to complete it. + // This should suffice for real world scenarios as I don't know of any + // HTTP client that sends the request headers in multiple packets. Even + // with pipelining this should not break as we re-enter this loop after + // processing the requests one by one, so a potential partial request in the + // buffer will get another recv() (blocking mode) + // The alternative would be manual tracking of idle/request time to protect + // against never ending requests (slowloris) + int pret; + if ( hoff >= sizeof(headerBuf) ) goto func_return; // Request too large + if ( hoff != 0 ) { + numHeaders = 100; + pret = phr_parse_request( headerBuf, hoff, &method, &path, &minorVersion, headers, &numHeaders, prevLen ); + } else { + // Nothing in buffer yet, just set to -2 which is the phr goto func_return code for "partial request" + pret = -2; + } + if ( pret > 0 ) { + // > 0 means parsing completed without error + consumed = (size_t)pret; + break; + } + // Reaching here means partial request or parse error + if ( pret == -2 ) { // Partial, keep reading + prevLen = hoff; +#ifdef AFL_MODE + ssize_t ret = recv( 0, headerBuf + hoff, sizeof(headerBuf) - hoff, 0 ); +#else + ssize_t ret = recv( sock, headerBuf + hoff, sizeof(headerBuf) - hoff, 0 ); +#endif + if ( ret == 0 ) goto func_return; + if ( ret == -1 ) { + if ( errno == EINTR ) continue; + if ( errno != EAGAIN && errno != EWOULDBLOCK ) { + sendReply( sock, "500 Internal Server Error", "text/plain", "Server made a boo-boo", -1, HTTP_CLOSE ); + } + goto func_return; // Timeout or unknown error + } + hoff += ret; + } else { // Parse error + sendReply( sock, "400 Bad Request", "text/plain", "Server cannot understand what you're trying to say", -1, HTTP_CLOSE ); + goto func_return; + } + } while ( true ); + if ( keepAlive == HTTP_KEEPALIVE ) { + // Only keep the connection alive (and indicate so) if the client seems to support this + if ( minorVersion == 0 || hasHeaderValue( headers, numHeaders, &STR_CONNECTION, &STR_CLOSE ) ) { + keepAlive = HTTP_CLOSE; + } else { // And if there aren't too many active HTTP sessions + spin_lock( &statusLock ); + if ( status.overloaded ) keepAlive = HTTP_CLOSE; + spin_unlock( &statusLock ); + } + } + if ( method.s != NULL && path.s != NULL ) { + // Basic data filled from request parser + // Handle stuff + struct string file; + struct field getv[10]; + size_t getc = 10; + parsePath( &path, &file, getv, &getc ); + if ( method.s && method.s[0] == 'P' ) { + // POST only methods + } + // Don't care if GET or POST + if ( equals( &file, &STR_QUERY ) ) { + ok = handleStatus( sock, permissions, getv, getc, keepAlive ); + } else { + ok = sendReply( sock, "404 Not found", "text/plain", "Nothing", -1, keepAlive ); + } + if ( !ok ) break; + } + // hoff might be beyond end if the client sent another request (burst) + const ssize_t extra = hoff - consumed; + if ( extra > 0 ) { + memmove( headerBuf, headerBuf + consumed, extra ); + } + hoff = extra; + if ( !hasName ) { + hasName = true; + setThreadName( "HTTP" ); + } + } while (true); +func_return:; + do { + spin_lock( &statusLock ); + const int curCount = --status.count; + UPDATE_LOADSTATE( curCount ); + spin_unlock( &statusLock ); + } while (0); +} + +void rpc_sendErrorMessage(int sock, const char* message) +{ + static const char *encoded = NULL; + static size_t len; + if ( encoded == NULL ) { + json_t *tmp = json_pack( "{ss}", "errorMsg", message ); + encoded = json_dumps( tmp, 0 ); + json_decref( tmp ); + len = strlen( encoded ); + } + sendReply( sock, "200 Somewhat OK", "application/json", encoded, len, HTTP_CLOSE ); +} + +static bool handleStatus(int sock, int permissions, struct field *fields, size_t fields_num, int keepAlive) +{ + bool ok; + bool stats = false, images = false, clients = false, space = false; + bool logfile = false, config = false, altservers = false; +#define SETVAR(var) if ( !var && STRCMP(fields[i].value, #var) ) var = true + for (size_t i = 0; i < fields_num; ++i) { + if ( !equals( &fields[i].name, &STR_Q ) ) continue; + SETVAR(stats); + else SETVAR(space); + else SETVAR(images); + else SETVAR(clients); + else SETVAR(logfile); + else SETVAR(config); + else SETVAR(altservers); + } +#undef SETVAR + if ( ( stats || space ) && !(permissions & ACL_STATS) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access statistics", -1, keepAlive ); + } + if ( images && !(permissions & ACL_IMAGE_LIST) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access image list", -1, keepAlive ); + } + if ( clients && !(permissions & ACL_CLIENT_LIST) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access client list", -1, keepAlive ); + } + if ( logfile && !(permissions & ACL_LOG) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access log", -1, keepAlive ); + } + if ( config && !(permissions & ACL_CONFIG) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access config", -1, keepAlive ); + } + if ( altservers && !(permissions & ACL_ALTSERVERS) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access altservers", -1, keepAlive ); + } + + json_t *statisticsJson; + if ( stats ) { + int clientCount, serverCount; + uint64_t bytesSent; + const uint64_t bytesReceived = uplink_getTotalBytesReceived(); + net_getStats( &clientCount, &serverCount, &bytesSent ); + statisticsJson = json_pack( "{sIsIsisisIsI}", + "bytesReceived", (json_int_t) bytesReceived, + "bytesSent", (json_int_t) bytesSent, + "clientCount", clientCount, + "serverCount", serverCount, + "uptime", (json_int_t) dnbd3_serverUptime(), + "runId", randomRunId ); + } else { + statisticsJson = json_pack( "{sI}", + "runId", randomRunId ); + } + if ( space ) { + uint64_t spaceTotal = 0, spaceAvail = 0; + file_freeDiskSpace( _basePath, &spaceTotal, &spaceAvail ); + json_object_set_new( statisticsJson, "spaceTotal", json_integer( spaceTotal ) ); + json_object_set_new( statisticsJson, "spaceFree", json_integer( spaceAvail ) ); + } + if ( clients ) { + json_object_set_new( statisticsJson, "clients", net_getListAsJson() ); + } + if ( images ) { + json_object_set_new( statisticsJson, "images", image_getListAsJson() ); + } + if ( logfile ) { + char logbuf[4000]; + ssize_t len = log_fetch( logbuf, sizeof(logbuf) ); + json_t *val; + if ( len <= 0 ) { + val = json_null(); + } else { + val = json_stringn_nocheck( logbuf, (size_t)len ); + + } + json_object_set_new( statisticsJson, "logfile", val ); + } + if ( config ) { + char buf[2000]; + size_t len = globals_dumpConfig( buf, sizeof(buf) ); + json_object_set_new( statisticsJson, "config", json_stringn_nocheck( buf, len ) ); + } + if ( altservers ) { + json_object_set_new( statisticsJson, "altservers", altservers_toJson() ); + } + + char *jsonString = json_dumps( statisticsJson, 0 ); + json_decref( statisticsJson ); + ok = sendReply( sock, "200 OK", "application/json", jsonString, -1, keepAlive ); + free( jsonString ); + return ok; +} + +static bool sendReply(int sock, const char *status, const char *ctype, const char *payload, ssize_t plen, int keepAlive) +{ + if ( plen == -1 ) plen = strlen( payload ); + char buffer[600]; + const char *connection = ( keepAlive == HTTP_KEEPALIVE ) ? "Keep-Alive" : "Close"; + int hlen = snprintf(buffer, sizeof(buffer), "HTTP/1.1 %s\r\n" + "Connection: %s\r\n" + "Content-Type: %s; charset=utf-8\r\n" + "Content-Length: %u\r\n" + "\r\n", + status, connection, ctype, (unsigned int)plen ); + if ( hlen < 0 || hlen >= (int)sizeof(buffer) ) return false; // Truncated + if ( send( sock, buffer, hlen, MSG_MORE ) != hlen ) return false; + if ( !sock_sendAll( sock, payload, plen, 10 ) ) return false; + if ( keepAlive == HTTP_CLOSE ) { + // Wait for flush + shutdown( sock, SHUT_WR ); +#ifdef AFL_MODE + sock = 0; +#endif + while ( read( sock, buffer, sizeof buffer ) > 0 ); + return false; + } + return true; +} + +static void parsePath(struct string *path, struct string *file, struct field *getv, size_t *getc) +{ + size_t i = 0; + while ( i < path->l && path->s[i] != '?' ) ++i; + if ( i == path->l ) { + *getc = 0; + *file = *path; + return; + } + file->s = path->s; + file->l = i; + ++i; + path->s += i; + path->l -= i; + urldecode( path, getv, getc ); + path->s -= i; + path->l += i; +} + +static bool hasHeaderValue(struct phr_header *headers, size_t numHeaders, struct string *name, struct string *value) +{ + for (size_t i = 0; i < numHeaders; ++i) { + if ( !iequals( &headers[i].name, name ) ) continue; + if ( iequals( &headers[i].value, value ) ) return true; + } + return false; +} + +static int getacl(dnbd3_host_t *host) +{ + if ( aclCount == 0 ) return 0x7fffff; // For now compat mode - no rules defined == all access + for (int i = 0; i < aclCount; ++i) { + if ( aclRules[i].bytes == 0 && aclRules[i].bitMask == 0 ) return aclRules[i].permissions; + if ( memcmp( aclRules[i].host, host->addr, aclRules[i].bytes ) != 0 ) continue; + if ( aclRules[i].bitMask != 0 && aclRules[i].host[aclRules[i].bytes] != ( host->addr[aclRules[i].bytes] & aclRules[i].bitMask ) ) continue; + return aclRules[i].permissions; + } +#ifdef AFL_MODE + return 0x7fffff; +#else + return 0; +#endif +} + +#define SETBIT(x) else if ( strcmp( argv[i], #x ) == 0 ) mask |= ACL_ ## x + +static void addacl(int argc, char **argv, void *data UNUSED) +{ + if ( argv[0][0] == '#' ) return; + spin_lock( &aclLock ); + if ( aclCount >= MAX_ACLS ) { + logadd( LOG_WARNING, "Too many ACL rules, ignoring %s", argv[0] ); + goto unlock_end; + } + int mask = 0; + for (int i = 1; i < argc; ++i) { + if (false) {} + SETBIT(ALL); + SETBIT(STATS); + SETBIT(CLIENT_LIST); + SETBIT(IMAGE_LIST); + else logadd( LOG_WARNING, "Invalid ACL flag '%s' for %s", argv[i], argv[0] ); + } + if ( mask == 0 ) { + logadd( LOG_INFO, "Ignoring empty rule for %s", argv[0] ); + goto unlock_end; + } + dnbd3_host_t host; + char *slash = strchr( argv[0], '/' ); + if ( slash != NULL ) { + *slash++ = '\0'; + } + if ( !parse_address( argv[0], &host ) ) goto unlock_end; + long int bits; + if ( slash != NULL ) { + char *last; + bits = strtol( slash, &last, 10 ); + if ( last == slash ) slash = NULL; + if ( host.type == HOST_IP4 && bits > 32 ) bits = 32; + if ( bits > 128 ) bits = 128; + } + if ( slash == NULL ) { + if ( host.type == HOST_IP4 ) { + bits = 32; + } else { + bits = 128; + } + } + memcpy( aclRules[aclCount].host, host.addr, 16 ); + aclRules[aclCount].bytes = (int)( bits / 8 ); + aclRules[aclCount].bitMask = 0; + aclRules[aclCount].permissions = mask; + bits %= 8; + if ( bits != 0 ) { + for (long int i = 0; i < bits; ++i) { + aclRules[aclCount].bitMask = ( aclRules[aclCount].bitMask >> 1 ) | 0x80; + } + aclRules[aclCount].host[aclRules[aclCount].bytes] &= (uint8_t)aclRules[aclCount].bitMask; + } + // We now have .bytes set to the number of bytes to memcmp. + // In case we have an odd bitmask, .bitMask will be != 0, so when comparing, + // we need AND the host[.bytes] of the address to compare with the value + // in .bitMask, and compate it, otherwise, a simple memcmp will do. + aclCount++; +unlock_end:; + spin_unlock( &aclLock ); +} + +static void loadAcl() +{ + static bool inProgress = false; + char *fn; + if ( asprintf( &fn, "%s/%s", _configDir, "rpc.acl" ) == -1 ) return; + spin_lock( &aclLock ); + if ( inProgress ) { + spin_unlock( &aclLock ); + return; + } + aclCount = 0; + inProgress = true; + spin_unlock( &aclLock ); + file_loadLineBased( fn, 1, 20, &addacl, NULL ); + spin_lock( &aclLock ); + inProgress = false; + spin_unlock( &aclLock ); + free( fn ); + logadd( LOG_INFO, "%d HTTPRPC ACL rules loaded", (int)aclCount ); +} + |