From 893b125acba6633bf946adf2b9821f6359fc4d3c Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sun, 1 Oct 2017 17:25:47 +0200 Subject: [SERVER] Implement HTTP RPC that supports different queries and ACL - ACL is defined in new file rpc.acl - Queries are still WIP, for now something like /query?q=stats&q=images /query?q=clients works, although the parsing is still ugly - Also supports HTTP keep-alive --- conf/rpc.acl | 5 + src/server/globals.h | 20 +--- src/server/net.c | 20 ++-- src/server/rpc.c | 263 ++++++++++++++++++++++++++++++++++++++++++++++----- src/server/rpc.h | 4 +- src/types.h | 2 +- 6 files changed, 269 insertions(+), 45 deletions(-) create mode 100644 conf/rpc.acl diff --git a/conf/rpc.acl b/conf/rpc.acl new file mode 100644 index 0000000..5167ae3 --- /dev/null +++ b/conf/rpc.acl @@ -0,0 +1,5 @@ +# Everything from localhost +127.0.0.0/8 ALL +# Some info reading for another machine +132.230.8.113 STATS CLIENT_LIST IMAGE_LIST + diff --git a/src/server/globals.h b/src/server/globals.h index 31b7741..379fb8d 100644 --- a/src/server/globals.h +++ b/src/server/globals.h @@ -64,17 +64,6 @@ struct _dnbd3_connection uint64_t bytesReceived; // Number of bytes received by the connection. }; -typedef struct -{ - uint16_t len; - uint8_t data[65535]; -} dnbd3_binstring_t; -// Do not always allocate as much memory as required to hold the entire binstring struct, -// but only as much as is required to hold the actual data (relevant for kernel module) -#define NEW_BINSTRING(_name, _len) \ - dnbd3_binstring_t *_name = malloc(sizeof(uint16_t) + _len); \ - _name->len = _len - typedef struct { char comment[COMMENT_LENGTH]; @@ -88,10 +77,11 @@ typedef struct typedef struct { - char comment[COMMENT_LENGTH]; - dnbd3_host_t host; - dnbd3_host_t mask; -} dnbd3_acess_rules_t; + uint8_t host[16]; + int bytes; + int bitMask; + int permissions; +} dnbd3_access_rule_t; /** * Image struct. An image path could be something like diff --git a/src/server/net.c b/src/server/net.c index f7a866b..9f317ad 100644 --- a/src/server/net.c +++ b/src/server/net.c @@ -172,7 +172,7 @@ void* net_handleNewConnection(void *clientPtr) // Let's see if this looks like an HTTP request if ( ret > 5 && request.magic != dnbd3_packet_magic && ( strncmp( (char*)&request, "GET ", 4 ) == 0 || strncmp( (char*)&request, "POST ", 5 ) == 0 ) ) { - rpc_sendStatsJson( client->sock ); + rpc_sendStatsJson( client->sock, &client->host, &request, (size_t)ret ); goto fail_preadd; } @@ -527,9 +527,15 @@ fail_preadd: ; return NULL; } -json_t* net_clientsToJson() +/** + * Get list of all clients and update the global stats counter while we're at it. + * This method sucks since it has a param that tells it not to generate the list + * but only update the global counter, which is a horrible relic from refactoring. + * Hopfully I'll fix it soon by splitting this up or something. + */ +json_t* net_clientsToJson(const bool fullList) { - json_t *jsonClients = json_array(); + json_t *jsonClients = fullList ? json_array() : NULL; json_t *clientStats; int i; int imgId; @@ -552,15 +558,17 @@ json_t* net_clientsToJson() spin_unlock( &client->lock ); imgId = -1; } else { - strncpy( host, client->hostName, HOSTNAMELEN - 1 ); - imgId = client->image->id; + if ( fullList ) { + strncpy( host, client->hostName, HOSTNAMELEN - 1 ); + imgId = client->image->id; + } spin_lock( &client->statsLock ); spin_unlock( &client->lock ); bytesSent = client->bytesSent; net_updateGlobalSentStatsFromClient( client ); // Do this since we read the totalBytesSent counter later spin_unlock( &client->statsLock ); } - if ( imgId != -1 ) { + if ( fullList && imgId != -1 ) { clientStats = json_pack( "{sssisI}", "address", host, "imageId", imgId, diff --git a/src/server/rpc.c b/src/server/rpc.c index 5c295f4..aa15973 100644 --- a/src/server/rpc.c +++ b/src/server/rpc.c @@ -5,38 +5,257 @@ #include "locks.h" #include "image.h" #include "../shared/sockhelper.h" +#include "fileutil.h" #include -void rpc_sendStatsJson(int sock) +#define ACL_ALL 0x7fffffff +#define ACL_STATS 1 +#define ACL_CLIENT_LIST 2 +#define ACL_IMAGE_LIST 4 + +#define HTTP_CLOSE 4 +#define HTTP_KEEPALIVE 9 + +#define MAX_ACLS 100 +static bool aclLoaded = false; +static int aclCount = 0; +static dnbd3_access_rule_t aclRules[MAX_ACLS]; + +static bool handleStatus(int sock, const char *request, int permissions); +static bool sendReply(int sock, const char *status, const char *ctype, const char *payload, ssize_t plen, int keepAlive); +static int getacl(dnbd3_host_t *host); +static void addacl(int argc, char **argv, void *data); +static void loadAcl(); + +void rpc_sendStatsJson(int sock, dnbd3_host_t* host, const void* data, const int dataLen) { + // TODO use some small HTTP parser (picohttpparser or similar) + // TODO Parse Connection-header sent by client to see if keep-alive is supported + bool ok; + loadAcl(); + int permissions = getacl( host ); + if ( permissions == 0 ) { + sendReply( sock, "403 Forbidden", "text/plain", "Access denied", -1, HTTP_CLOSE ); + return; + } + char header[1000]; + if ( dataLen > 0 ) { + // We call this function internally with a maximum data len of sizeof(dnbd3_request_t) so no bounds checking + memcpy( header, data, dataLen ); + } + size_t hoff = dataLen; + do { + // Read request from client + char *end = NULL; + int state = 0; + do { + for (char *p = header; p < header + hoff; ++p) { + if ( *p == '\r' && ( state == 0 || state == 2 ) ) { + state++; + } else if ( *p == '\n' ) { + if ( state == 3 ) { + end = p + 1; + break; + } + if ( state == 1 ) { + state = 2; + } else { + state = 0; + } + } else if ( state != 0 ) { + state = 0; + } + } + if ( end != NULL ) break; + if ( hoff >= sizeof(header) ) return; // Request too large + const size_t space = sizeof(header) - hoff; + const ssize_t ret = recv( sock, header + hoff, space, 0 ); + if ( ret == 0 || ( ret == -1 && errno == EAGAIN ) ) return; + if ( ret == -1 && ( errno == EWOULDBLOCK || errno == EINTR ) ) continue; + hoff += ret; + } while ( true ); + // Now end points to the byte after the \r\n\r\n of the header, + if ( strncmp( header, "GET ", 4 ) != 0 && strncmp( header, "POST ", 5 ) != 0 ) return; + char *br = strstr( header, "\r\n" ); + if ( br == NULL ) return; // Huh? + *br = '\0'; + if ( strstr( header, " /query" ) != NULL ) { + ok = handleStatus( sock, header, permissions ); + } else { + ok = sendReply( sock, "404 Not found", "text/plain", "Nothing", -1, HTTP_KEEPALIVE ); + } + if ( !ok ) break; + // hoff might be beyond end if the client sent another request (burst) + const ssize_t extra = ( header + hoff ) - end; + if ( extra > 0 ) { + memmove( header, end, extra ); + hoff = extra; + } else { + hoff = 0; + } + } while (true); +} + +static bool handleStatus(int sock, const char *request, int permissions) +{ + bool ok; + bool stats = false, images = false, clients = false; + if ( strstr( request, "stats" ) != NULL ) { + if ( !(permissions & ACL_STATS) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access statistics", -1, HTTP_KEEPALIVE ); + } + stats = true; + } + if ( strstr( request, "images" ) != NULL ) { + if ( !(permissions & ACL_IMAGE_LIST) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access image list", -1, HTTP_KEEPALIVE ); + } + images = true; + } + if ( strstr(request, "clients" ) != NULL ) { + if ( !(permissions & ACL_CLIENT_LIST) ) { + return sendReply( sock, "403 Forbidden", "text/plain", "No permission to access client list", -1, HTTP_KEEPALIVE ); + } + clients = true; + } // Call this first because it will update the total bytes sent counter - json_t *jsonClients = net_clientsToJson(); - const uint64_t bytesReceived = uplink_getTotalBytesReceived(); - const uint64_t bytesSent = net_getTotalBytesSent(); + json_t *jsonClients = NULL; + if ( stats || clients ) { + jsonClients = net_clientsToJson( permissions & ACL_CLIENT_LIST ); + } const int uptime = dnbd3_serverUptime(); + json_t *statisticsJson; + if ( stats ) { + const uint64_t bytesReceived = uplink_getTotalBytesReceived(); + const uint64_t bytesSent = net_getTotalBytesSent(); + statisticsJson = json_pack( "{sIsIsI}", + "bytesReceived", (json_int_t) bytesReceived, + "bytesSent", (json_int_t) bytesSent, + "uptime", (json_int_t) uptime ); + } else { + statisticsJson = json_pack( "{sI}", + "uptime", (json_int_t) uptime ); + } + if ( clients ) { + json_object_set_new( statisticsJson, "clients", jsonClients ); + } + if ( images ) { + json_object_set_new( statisticsJson, "images", image_getListAsJson() ); + } - json_t *statisticsJson = json_pack( "{sIsI}", - "bytesReceived", (json_int_t) bytesReceived, - "bytesSent", (json_int_t) bytesSent ); - json_object_set_new( statisticsJson, "clients", jsonClients ); - json_object_set_new( statisticsJson, "images", image_getListAsJson() ); - json_object_set_new( statisticsJson, "uptime", json_integer( uptime ) ); char *jsonString = json_dumps( statisticsJson, 0 ); json_decref( statisticsJson ); + ok = sendReply( sock, "200 OK", "application/json", jsonString, -1, HTTP_KEEPALIVE ); + free( jsonString ); + return ok; +} - char buffer[500]; - snprintf(buffer, sizeof buffer , "HTTP/1.1 200 OK\r\n" - "Connection: Close\r\n" - "Content-Length: %d\r\n" - "Content-Type: application/json\r\n" +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\r\n" + "Content-Length: %u\r\n" "\r\n", - (int) strlen( jsonString ) ); - write( sock, buffer, strlen( buffer ) ); - sock_sendAll( sock, jsonString, strlen( jsonString ), 10 ); - // Wait for flush - shutdown( sock, SHUT_WR ); - while ( read( sock, buffer, sizeof buffer ) > 0 ); - free( jsonString ); + 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 ); + while ( read( sock, buffer, sizeof buffer ) > 0 ); + } + return true; +} + +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; + } + return 0; +} + +#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; + if ( aclCount >= MAX_ACLS ) { + logadd( LOG_WARNING, "Too many ACL rules, ignoring %s", argv[0] ); + return; + } + 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] ); + return; + } + dnbd3_host_t host; + char *slash = strchr( argv[0], '/' ); + if ( slash != NULL ) { + *slash++ = '\0'; + } + if ( !parse_address( argv[0], &host ) ) return; + long int bits; + if ( slash != NULL ) { + char *last; + bits = strtol( slash, &last, 10 ); + if ( last == slash ) slash = NULL; + if ( host.type == AF_INET && bits > 32 ) bits = 32; + if ( bits > 128 ) bits = 128; + } + if ( slash == NULL ) { + if ( host.type == AF_INET ) { + bits = 32; + } else { + bits = 128; + } + } + memcpy( aclRules[aclCount].host, host.addr, 16 ); + aclRules[aclCount].bytes = 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 ) | 0xff; + } + aclRules[aclCount].host[aclRules[aclCount].bytes] &= 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++; +} + +static void loadAcl() +{ + char *fn; + // TODO + if ( aclLoaded ) return; + aclLoaded = true; + // + if ( asprintf( &fn, "%s/%s", _configDir, "rpc.acl" ) == -1 ) return; + file_loadLineBased( fn, 1, 20, &addacl, NULL ); + free( fn ); + logadd( LOG_INFO, "%d HTTPRPC ACL rules loaded", (int)aclCount ); } diff --git a/src/server/rpc.h b/src/server/rpc.h index ec69762..1d50e77 100644 --- a/src/server/rpc.h +++ b/src/server/rpc.h @@ -1,6 +1,8 @@ #ifndef _RPC_H_ #define _RPC_H_ -void rpc_sendStatsJson(int sock); +struct dnbd3_host_t; + +void rpc_sendStatsJson(int sock, struct dnbd3_host_t* host, const void *data, const int dataLen); #endif diff --git a/src/types.h b/src/types.h index 19c84cf..63fe3a6 100644 --- a/src/types.h +++ b/src/types.h @@ -93,7 +93,7 @@ static const uint16_t dnbd3_packet_magic = (0x73) | (0x72 << 8); #endif #pragma pack(1) -typedef struct +typedef struct dnbd3_host_t { uint8_t addr[16]; // 16byte (network representation, so it can be directly passed to socket functions) uint16_t port; // 2byte (network representation, so it can be directly passed to socket functions) -- cgit v1.2.3-55-g7522