blob: 745dfe235cf46688b1f4a6b0399610b3f6e701a8 [file] [log] [blame]
/*
* Copyright (c) 2014 Netflix, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY NETFLIX, INC. AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL NETFLIX OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "dial_data.h"
#include "dial_server.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include "mongoose.h"
#include "url_lib.h"
// TODO: Partners should define this port
#define DIAL_PORT (56789)
#define DIAL_DATA_SIZE (8*1024)
static const char *gLocalhost = "127.0.0.1";
struct DIALApp_ {
struct DIALApp_ *next;
struct DIALAppCallbacks callbacks;
struct DIALData_ *dial_data;
void *callback_data;
DIAL_run_t run_id;
DIALStatus state;
char *name;
char payload[DIAL_MAX_PAYLOAD];
int useAdditionalData;
char corsAllowedOrigin[256];
};
typedef struct DIALApp_ DIALApp;
struct DIALServer_ {
struct mg_context *ctx;
struct DIALApp_ *apps;
pthread_mutex_t mux;
};
static void ds_lock(DIALServer *ds) {
pthread_mutex_lock(&ds->mux);
}
static void ds_unlock(DIALServer *ds) {
pthread_mutex_unlock(&ds->mux);
}
// finds an app and returns a pointer to the previous element's next pointer
// if not found, return a pointer to the last element's next pointer
static DIALApp **find_app(DIALServer *ds, const char *app_name) {
DIALApp *app;
DIALApp **ret = &ds->apps;
for (app = ds->apps; app != NULL; ret = &app->next, app = app->next) {
if (!strcmp(app_name, app->name)) {
break;
}
}
return ret;
}
static void url_decode_xml_encode(char *dst, char *src, size_t src_size) {
char *url_decoded_key = (char *) malloc(src_size + 1);
urldecode(url_decoded_key, src, src_size);
xmlencode(dst, url_decoded_key, 2 * src_size);
free(url_decoded_key);
}
/*
* A bad payload is defined to be an unprintable character or a
* non-ascii character.
*/
static int isBadPayload(const char* pPayload, int numBytes) {
int i = 0;
fprintf( stderr, "Payload: checking %d bytes\n", numBytes);
for (; i < numBytes; i++) {
// High order bit should not be set
// 0x7F is DEL (non-printable)
// Anything under 32 is non-printable
if (((pPayload[i] & 0x80) == 0x80) || (pPayload[i] == 0x7F)
|| (pPayload[i] <= 0x1F))
return 1;
}
return 0;
}
static void handle_app_start(struct mg_connection *conn,
const struct mg_request_info *request_info,
const char *app_name,
const char *origin_header) {
char additional_data_param[256] = {0, };
char body[DIAL_MAX_PAYLOAD] = {0, };
DIALApp *app;
DIALServer *ds = request_info->user_data;
int body_size;
ds_lock(ds);
app = *find_app(ds, app_name);
if (!app) {
mg_send_http_error(conn, 404, "Not Found", "Not Found");
} else {
body_size = mg_read(conn, body, sizeof(body));
// NUL-terminate it just in case
if (body_size > DIAL_MAX_PAYLOAD) {
mg_send_http_error(conn, 413, "413 Request Entity Too Large",
"413 Request Entity Too Large");
} else if (isBadPayload(body, body_size)) {
mg_send_http_error(conn, 400, "400 Bad Request", "400 Bad Request");
} else {
char laddr[INET6_ADDRSTRLEN];
const struct sockaddr_in *addr =
(struct sockaddr_in *) &request_info->local_addr;
inet_ntop(addr->sin_family, &addr->sin_addr, laddr, sizeof(laddr));
in_port_t dial_port = DIAL_get_port(ds);
if (app->useAdditionalData) {
// Construct additionalDataUrl=http://host:port/apps/app_name/dial_data
snprintf(additional_data_param, sizeof(additional_data_param),
"%sadditionalDataUrl=http%%3A%%2F%%2Flocalhost%%3A%d%%2Fapps%%2F%s%%2Fdial_data%%3F",
(body_size != 0) ? "&" : "",
dial_port, app_name);
if ((body_size + strlen(additional_data_param)) < DIAL_MAX_PAYLOAD) {
strcat(body, additional_data_param);
body_size = strlen(body);
} else {
fprintf(stderr, "payload too small for additional data\n");
}
}
fprintf(stderr, "Starting the app with params %s\n", body);
app->state = app->callbacks.start_cb(ds, app_name, body, body_size,
&app->run_id,
app->callback_data);
if (app->state == kDIALStatusRunning) {
mg_printf(
conn,
"HTTP/1.1 201 Created\r\n"
"Content-Type: text/plain\r\n"
"Location: http://%s:%d/apps/%s/run\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"\r\n",
laddr, dial_port, app_name, origin_header);
// copy the payload into the application struct
memset(app->payload, 0, DIAL_MAX_PAYLOAD);
if (body_size<=DIAL_MAX_PAYLOAD) {
memcpy(app->payload, body, body_size);
} else {
fprintf(stderr, "payload too small for body of %d bytes\n", body_size);
}
} else {
mg_send_http_error(conn, 503, "Service Unavailable",
"Service Unavailable");
}
}
}
ds_unlock(ds);
}
static void handle_app_status(struct mg_connection *conn,
const struct mg_request_info *request_info,
const char *app_name,
const char *origin_header) {
DIALApp *app;
int canStop = 0;
DIALServer *ds = request_info->user_data;
ds_lock(ds);
app = *find_app(ds, app_name);
if (!app) {
mg_send_http_error(conn, 404, "Not Found", "Not Found");
ds_unlock(ds);
return;
}
char dial_data[DIAL_DATA_SIZE] = {0,};
char *end = dial_data + DIAL_DATA_SIZE;
char *p = dial_data;
for (DIALData* first = app->dial_data; first != NULL; first = first->next) {
p = smartstrcat(p, " <", end - p);
size_t key_length = strlen(first->key);
char *encoded_key = (char *) malloc(2 * key_length + 1);
url_decode_xml_encode(encoded_key, first->key, key_length);
size_t value_length = strlen(first->value);
char *encoded_value = (char *) malloc(2 * value_length + 1);
url_decode_xml_encode(encoded_value, first->value, value_length);
p = smartstrcat(p, encoded_key, end - p);
p = smartstrcat(p, ">", end - p);
p = smartstrcat(p, encoded_value, end - p);
p = smartstrcat(p, "</", end - p);
p = smartstrcat(p, encoded_key, end - p);
p = smartstrcat(p, ">", end - p);
free(encoded_key);
free(encoded_value);
}
app->state = app->callbacks.status_cb(ds, app_name, app->run_id, &canStop,
app->callback_data);
mg_printf(
conn,
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/xml\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"\r\n"
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"
"<service xmlns=\"urn:dial-multiscreen-org:schemas:dial\" dialVer=%s>\r\n"
" <name>%s</name>\r\n"
" <options allowStop=\"%s\"/>\r\n"
" <state>%s</state>\r\n"
"%s",
origin_header,
DIAL_VERSION,
app->name,
canStop ? "true" : "false",
app->state ? "running" : "stopped",
app->state == kDIALStatusStopped ?
"" : " <link rel=\"run\" href=\"run\"/>\r\n");
if (strlen(dial_data)>0) {
mg_printf(
conn,
" <additionalData>\r\n"
"%s"
"\r\n </additionalData>\r\n",
dial_data);
}
if (app->callbacks.service_data_cb != NULL) {
struct CastServiceData serviceData = app->callbacks.service_data_cb(ds, app_name, app->run_id, app->callback_data);
mg_printf(conn,
" <servicedata xmlns=\"urn:chrome.google.com:cast\">\r\n"
" <connectionSvcURL>http://%s:%d%s</connectionSvcURL>\r\n"
" <protocols>\r\n"
" <protocol>%s</protocol>\r\n"
" </protocols>\r\n"
" </servicedata>\r\n",
serviceData.connection_svc_host,
serviceData.connection_svc_port,
serviceData.connection_svc_path,
serviceData.protocol);
}
if (app->callbacks.activity_status_cb != NULL) {
struct CastActivityStatus activityStatus = app->callbacks.activity_status_cb(ds, app_name, app->run_id, app->callback_data);
mg_printf(conn,
" <activity-status xmlns=\"urn:chrome.google.com:cast\">\r\n"
" <description>%s</description>\r\n"
" </activity-status>\r\n", activityStatus.description);
}
mg_printf(conn, "</service>\r\n");
ds_unlock(ds);
}
static void handle_app_stop(struct mg_connection *conn,
const struct mg_request_info *request_info,
const char *app_name,
const char *origin_header) {
DIALApp *app;
DIALServer *ds = request_info->user_data;
int canStop = 0;
ds_lock(ds);
app = *find_app(ds, app_name);
// update the application state
if (app) {
app->state = app->callbacks.status_cb(ds, app_name, app->run_id,
&canStop, app->callback_data);
}
if (!app || app->state != kDIALStatusRunning) {
mg_send_http_error(conn, 404, "Not Found", "Not Found");
} else {
app->callbacks.stop_cb(ds, app_name, app->run_id, app->callback_data);
app->state = kDIALStatusStopped;
mg_printf(conn, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"\r\n",
origin_header);
}
ds_unlock(ds);
}
static void handle_dial_data(struct mg_connection *conn,
const struct mg_request_info *request_info,
const char *app_name,
const char *origin_header,
int use_payload) {
char body[DIAL_DATA_MAX_PAYLOAD + 2] = {0, };
DIALApp *app;
DIALServer *ds = request_info->user_data;
ds_lock(ds);
app = *find_app(ds, app_name);
if (!app) {
mg_send_http_error(conn, 404, "Not Found", "Not Found");
ds_unlock(ds);
return;
}
int nread;
if (!use_payload) {
if (request_info->query_string) {
strncpy(body, request_info->query_string, DIAL_DATA_MAX_PAYLOAD);
nread = strlen(body);
} else {
nread = 0;
}
} else {
nread = mg_read(conn, body, DIAL_DATA_MAX_PAYLOAD);
body[nread] = '\0';
}
if (nread > DIAL_DATA_MAX_PAYLOAD) {
mg_send_http_error(conn, 413, "413 Request Entity Too Large",
"413 Request Entity Too Large");
ds_unlock(ds);
return;
}
if (isBadPayload(body, nread)) {
mg_send_http_error(conn, 400, "400 Bad Request", "400 Bad Request");
ds_unlock(ds);
return;
}
app->dial_data = parse_params(body);
store_dial_data(app->name, app->dial_data);
mg_printf(conn, "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"\r\n",
origin_header);
ds_unlock(ds);
}
// Add logic to test if we should drop DIAL requests
#define SAGE_PROPERTIES_PATH "/rw/sage/SageClient.properties"
#define DIAL_DISABLED_PROPERTY "allow_dial=false"
int dial_allowed() {
char property[1024];
FILE *sageProperties;
if (!(sageProperties = fopen(SAGE_PROPERTIES_PATH,"r"))) {
return 1;
}
while (!feof(sageProperties)) {
fgets(property, sizeof(property), sageProperties);
if (strncmp(property, DIAL_DISABLED_PROPERTY, strlen(DIAL_DISABLED_PROPERTY))==0) {
fclose(sageProperties);
return 0;
}
}
fclose(sageProperties);
return 1;
}
static int ends_with(const char *str, const char *suffix) {
if (!str || !suffix)
return 0;
size_t lenstr = strlen(str);
size_t lensuffix = strlen(suffix);
if (lensuffix > lenstr)
return 0;
return strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0;
}
// str contains a white space separated list of strings (only supports SPACE characters for now)
static int ends_with_in_list (const char *str, const char *list) {
if (!str || !list)
return 0;
const char * scanPointer=list;
const char * spacePointer;
unsigned int substringSize = 257;
char *substring = (char *)malloc(substringSize);
if (!substring){
return 0;
}
while ( (spacePointer =strchr(scanPointer, ' ')) != NULL) {
int copyLength = spacePointer - scanPointer;
// protect against buffer overflow
if (copyLength>=substringSize){
substringSize=copyLength+1;
free(substring);
substring=(char *)malloc(substringSize);
if (!substring){
return 0;
}
}
memcpy(substring, scanPointer, copyLength);
substring[copyLength] = '\0';
//printf("found %s \n", substring);
if (ends_with(str, substring)) {
free(substring);
return 1;
}
scanPointer = scanPointer + copyLength + 1; // assumption: only 1 character
}
free(substring);
return ends_with(str, scanPointer);
}
static int should_check_for_origin( char * origin ) {
const char * const CHECK_PROTOS[] = { "http:", "https:", "file:" };
for (int i = 0; i < 3; ++i) {
if (!strncmp(origin, CHECK_PROTOS[i], strlen(CHECK_PROTOS[i]) - 1)) {
return 1;
}
}
return 0;
}
static int is_allowed_origin(DIALServer* ds, char * origin, const char * app_name) {
if (!origin || strlen(origin)==0 || !should_check_for_origin(origin)) {
return 1;
}
ds_lock(ds);
DIALApp *app;
int result = 0;
for (app = ds->apps; app != NULL; app = app->next) {
if (!strcmp(app->name, app_name)) {
if (!app->corsAllowedOrigin[0] ||
ends_with_in_list(origin, app->corsAllowedOrigin)) {
result = 1;
break;
}
}
}
ds_unlock(ds);
return result;
}
#define APPS_URI "/apps/"
#define RUN_URI "/run"
static void *options_response(DIALServer *ds, struct mg_connection *conn, char *host_header, char *origin_header, const char* app_name, const char* methods)
{
if (host_header && is_allowed_origin(ds, origin_header, app_name)) {
mg_printf(
conn,
"HTTP/1.1 204 No Content\r\n"
"Access-Control-Allow-Methods: %s\r\n"
"Access-Control-Max-Age: 86400\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"Content-Length: 0"
"\r\n",
methods,
origin_header);
return "done";
}
mg_send_http_error(conn, 403, "Forbidden", "Forbidden");
return "done";
}
static void *request_handler(enum mg_event event, struct mg_connection *conn,
const struct mg_request_info *request_info) {
DIALServer *ds = request_info->user_data;
fprintf(stderr, "Received request %s\n", request_info->uri);
char *host_header = {0,};
char *origin_header = {0,};
for (int i = 0; i < request_info->num_headers; ++i) {
if (!strcmp(request_info->http_headers[i].name, "Host")) {
host_header = request_info->http_headers[i].value;
} else if (!strcmp(request_info->http_headers[i].name,
"Origin")) {
origin_header = request_info->http_headers[i].value;
}
}
fprintf(stderr, "Origin %s, Host: %s\n", origin_header, host_header);
if (event == MG_NEW_REQUEST) {
// If DIAL is disabled, drop the request
if (!dial_allowed())
return "done";
// URL ends with run
if (!strncmp(request_info->uri + strlen(request_info->uri) - 4, RUN_URI,
strlen(RUN_URI))) {
char app_name[256] = {0, }; // assuming the application name is not over 256 chars.
strncpy(app_name, request_info->uri + strlen(APPS_URI),
((strlen(request_info->uri) - 4) - (sizeof(APPS_URI) - 1)));
if (!strcmp(request_info->request_method, "OPTIONS")) {
return options_response(ds, conn, host_header, origin_header, app_name, "DELETE, OPTIONS");
}
// DELETE non-empty app name
if (app_name[0] != '\0'
&& !strcmp(request_info->request_method, "DELETE")) {
if (host_header && is_allowed_origin(ds, origin_header, app_name)) {
handle_app_stop(conn, request_info, app_name, origin_header);
} else {
mg_send_http_error(conn, 403, "Forbidden", "Forbidden");
return "done";
}
} else {
mg_send_http_error(conn, 501, "Not Implemented",
"Not Implemented");
}
}
// URI starts with "/apps/" and is followed by an app name
else if (!strncmp(request_info->uri, APPS_URI, sizeof(APPS_URI) - 1)
&& !strchr(request_info->uri + strlen(APPS_URI), '/')) {
const char *app_name;
app_name = request_info->uri + sizeof(APPS_URI) - 1;
if (!strcmp(request_info->request_method, "OPTIONS")) {
return options_response(ds, conn, host_header, origin_header, app_name, "GET, POST, OPTIONS");
}
// start app
if (!strcmp(request_info->request_method, "POST")) {
if (host_header && is_allowed_origin(ds, origin_header, app_name)) {
handle_app_start(conn, request_info, app_name, origin_header);
} else {
mg_send_http_error(conn, 403, "Forbidden", "Forbidden");
return "done";
}
// get app status
} else if (!strcmp(request_info->request_method, "GET")) {
handle_app_status(conn, request_info, app_name, origin_header);
} else {
mg_send_http_error(conn, 501, "Not Implemented",
"Not Implemented");
}
// URI is of the form */app_name/dial_data
} else if (strstr(request_info->uri, DIAL_DATA_URI)) {
char laddr[INET6_ADDRSTRLEN];
const struct sockaddr_in *addr =
(struct sockaddr_in *) &request_info->remote_addr;
inet_ntop(addr->sin_family, &addr->sin_addr, laddr, sizeof(laddr));
if ( !strncmp(laddr, gLocalhost, strlen(gLocalhost)) ) {
char *app_name = parse_app_name(request_info->uri);
if (!strcmp(request_info->request_method, "OPTIONS")) {
void *ret = options_response(ds, conn, host_header, origin_header, app_name, "POST, OPTIONS");
free(app_name);
return ret;
}
int use_payload =
strcmp(request_info->request_method, "POST") ? 0 : 1;
handle_dial_data(conn, request_info, app_name, origin_header,
use_payload);
free(app_name);
} else {
// If the request is not from local host, return an error
mg_send_http_error(conn, 403, "Forbidden", "Forbidden");
}
} else {
mg_send_http_error(conn, 404, "Not Found", "Not Found");
}
return "done";
} else if (event == MG_EVENT_LOG) {
fprintf( stderr, "MG: %s\n", request_info->log_message);
return "done";
}
return NULL;
}
DIALServer *DIAL_create() {
DIALServer *ds = calloc(1, sizeof(DIALServer));
pthread_mutex_init(&ds->mux, NULL);
return ds;
}
int DIAL_start(DIALServer *ds) {
ds->ctx = mg_start(&request_handler, ds, DIAL_PORT);
return (ds->ctx == NULL) ? 1 : 0;
}
void DIAL_stop(DIALServer *ds) {
mg_stop(ds->ctx);
pthread_mutex_destroy(&ds->mux);
}
in_port_t DIAL_get_port(DIALServer *ds) {
struct sockaddr sa;
socklen_t len = sizeof(sa);
if (!mg_get_listen_addr(ds->ctx, &sa, &len)) {
return 0;
}
return ntohs(((struct sockaddr_in *) &sa)->sin_port);
}
int DIAL_register_app(DIALServer *ds, const char *app_name,
struct DIALAppCallbacks *callbacks, void *user_data,
int useAdditionalData,
const char* corsAllowedOrigin) {
DIALApp **ptr, *app;
int ret;
ds_lock(ds);
ptr = find_app(ds, app_name);
if (*ptr != NULL) { // app already registered
ds_unlock(ds);
ret = 0;
} else {
app = malloc(sizeof(DIALApp));
app->callbacks = *callbacks;
app->name = strdup(app_name);
app->next = *ptr;
app->state = kDIALStatusStopped;
app->callback_data = user_data;
app->dial_data = retrieve_dial_data(app->name);
app->useAdditionalData = useAdditionalData;
app->corsAllowedOrigin[0] = '\0';
if (corsAllowedOrigin &&
strlen(corsAllowedOrigin) < sizeof(app->corsAllowedOrigin)) {
strcpy(app->corsAllowedOrigin, corsAllowedOrigin);
}
*ptr = app;
ret = 1;
}
ds_unlock(ds);
return ret;
}
int DIAL_unregister_app(DIALServer *ds, const char *app_name) {
DIALApp **ptr, *app;
int ret;
ds_lock(ds);
ptr = find_app(ds, app_name);
if (*ptr == NULL) { // no such app
ret = 0;
} else {
app = *ptr;
*ptr = app->next;
free(app->name);
free(app);
ret = 1;
}
ds_unlock(ds);
return ret;
}
const char * DIAL_get_payload(DIALServer *ds, const char *app_name) {
const char * pPayload = NULL;
DIALApp **ptr, *app;
// NOTE: Don't grab the mutex as we are calling this function from
// inside the application callback which already has the lock.
//ds_lock(ds);
ptr = find_app(ds, app_name);
if (*ptr != NULL) {
app = *ptr;
pPayload = app->payload;
}
//ds_unlock(ds);
return pPayload;
}