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 --- src/server/rpc.c | 263 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 241 insertions(+), 22 deletions(-) (limited to 'src/server/rpc.c') 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 ); } -- cgit v1.2.3-55-g7522