Added POST, Plugin API signal protection, refactor HTML5 rewrite
[src/app-framework-binder.git] / src / rest-api.c
1 /*
2  * Copyright (C) 2015 "IoT.bzh"
3  * Author "Fulup Ar Foll"
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  * 
18  * Contain all generic part to handle REST/API
19  */
20
21 #include "../include/local-def.h"
22
23 #include <setjmp.h>
24 #include <signal.h>
25
26 // context save for timeout set/longjmp
27 static sigjmp_buf checkPluginCall; 
28
29 // handle to hold queryAll values
30 typedef struct {
31      char    *msg;
32      int     idx;
33      size_t  len;
34 } queryHandleT;
35
36
37 // Helper to retrieve argument from  connection
38 PUBLIC const char* getQueryValue(AFB_request * request, char *name) {
39     const char *value;
40
41     value = MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, name);
42     return (value);
43 }
44
45 STATIC int getQueryCB (void*handle, enum MHD_ValueKind kind, const char *key, const char *value) {
46     queryHandleT *query = (queryHandleT*)handle;
47         
48     query->idx += snprintf (&query->msg[query->idx],query->len," %s: \'%s\',", key, value);
49 }
50
51 // Helper to retrieve argument from  connection
52 PUBLIC const char* getQueryAll(AFB_request * request, char *buffer, size_t len) {
53     queryHandleT query;
54     
55     query.msg= buffer;
56     query.len= len;
57     query.idx= 0;
58
59     MHD_get_connection_values (request->connection, MHD_GET_ARGUMENT_KIND, getQueryCB, &query);
60     return (query.msg);
61 }
62
63
64 // Sample Generic Ping Debug API
65 PUBLIC json_object* apiPingTest(AFB_session *session, AFB_request *request, void* handle) {
66     static pingcount = 0;
67     json_object *response;
68     char query [512];
69
70     // request all query key/value
71     getQueryAll (request, query, sizeof(query)); 
72     
73     // check if we have some post data
74     if (request->post == NULL)  request->post="NoData";  
75         
76     // return response to caller
77     response = jsonNewMessage(AFB_SUCCESS, "Ping Binder Daemon %d query={%s} PostData: \'%s\' ", pingcount++, query, request->post);
78     return (response);
79 }
80
81
82 // Because of POST call multiple time requestApi we need to free POST handle here
83
84 STATIC void endRequest(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) {
85     AFB_HttpPost *posthandle = *con_cls;
86
87     // if post handle was used let's free everything
88     if (posthandle) {
89         if (verbose) fprintf(stderr, "End Post Request UID=%d\n", posthandle->uid);
90         free(posthandle->data);
91         free(posthandle);
92     }
93 }
94
95 /*----------------------------------------------------------
96  | timeout signalQuit
97  +--------------------------------------------------------- */
98 STATIC void pluginError (int signum) {
99
100   sigset_t sigset;
101
102   // unlock timeout signal to allow a new signal to come
103   sigemptyset (&sigset);
104   sigaddset   (&sigset, SIGALRM);
105   sigprocmask (SIG_UNBLOCK, &sigset, 0);
106
107   fprintf (stderr, "Oops:%s Plugin Api Timeout timeout\n", configTime());
108   longjmp (checkPluginCall, signum);
109 }
110
111
112 // Check of apiurl is declare in this plugin and call it
113
114 STATIC json_object * callPluginApi(AFB_plugin *plugin, AFB_session *session, AFB_request *request) {
115     json_object *response;
116     int idx, status;
117
118     // If a plugin hold this urlpath call its callback
119     for (idx = 0; plugin->apis[idx].callback != NULL; idx++) {
120         if (!strcmp(plugin->apis[idx].name, request->api)) {
121             
122             // save context before calling the API
123             status = setjmp (checkPluginCall);
124             if (status != 0) {
125                 response = jsonNewMessage(AFB_FATAL, "Plugin Call Fail prefix=%s api=%s info=%s", plugin->prefix, request->api, plugin->info);
126             } else {
127                 if (signal (SIGALRM, pluginError) == SIG_ERR) {
128                     fprintf (stderr, "%s ERR: main no Signal/timeout handler installed.", configTime());
129                     return NULL;
130                 }
131                 
132                 if (signal (SIGSEGV, pluginError) == SIG_ERR) {
133                     fprintf (stderr, "%s ERR: main no Signal/memory handler installed.", configTime());
134                     return NULL;
135                 }
136                 
137                 if (signal (SIGFPE , pluginError) == SIG_ERR) {
138                     fprintf (stderr, "%s ERR: main no Signal/memory handler installed.", configTime());
139                     return NULL;
140                 }
141
142                 // protect plugin call with a timeout
143                 alarm (session->config->apiTimeout);
144
145                 response = plugin->apis[idx].callback(session, request, plugin->apis[idx].handle);
146                 if (response != NULL) json_object_object_add(response, "jtype", plugin->jtype);
147
148                 // cancel timeout and sleep before next aquisition
149                 alarm (0);
150                 signal(SIGALRM, SIG_DFL);
151                 signal(SIGSEGV, SIG_DFL);
152                 signal(SIGFPE , SIG_DFL);
153             }    
154             return (response);
155
156         }
157     }
158     return (NULL);
159 }
160
161
162 // process rest API query
163
164 PUBLIC int doRestApi(struct MHD_Connection *connection, AFB_session *session, const char* url, const char *method
165     , const char *upload_data, size_t *upload_data_size, void **con_cls) {
166     
167     static int postcount = 0; // static counter to debug POST protocol
168     char *baseurl, *baseapi, *urlcpy1, *urlcpy2, *query;
169     json_object *jsonResponse, *errMessage;
170     struct MHD_Response *webResponse;
171     const char *serialized, parsedurl;
172     AFB_request request;
173     AFB_HttpPost *posthandle = *con_cls;
174     int idx, ret;
175
176     // Extract plugin urlpath from request and make two copy because strsep overload copy
177     urlcpy1 = urlcpy2 = strdup(url);
178     baseurl = strsep(&urlcpy2, "/");
179     if (baseurl == NULL) {
180         errMessage = jsonNewMessage(AFB_FATAL, "Invalid Plugin/API call url=%s", url);
181         goto ExitOnError;
182     }
183
184     baseapi = strsep(&urlcpy2, "/");
185     if (baseapi == NULL) {
186         errMessage = jsonNewMessage(AFB_FATAL, "Invalid Plugin/API call url=%s/%s", baseurl, url);
187         goto ExitOnError;
188     }
189     
190
191     // if post data may come in multiple calls
192     if (0 == strcmp(method, MHD_HTTP_METHOD_POST)) {
193         const char *encoding, *param;
194         int contentlen = -1;
195         AFB_HttpPost *posthandle = *con_cls;
196
197         // Let make sure we have the right encoding and a valid length
198         encoding = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
199         param = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_LENGTH);
200         if (param) sscanf(param, "%i", &contentlen);
201
202         // POST datas may come in multiple chunk. Even when it never happen on AFB, we still have to handle the case
203         if (strcasestr(encoding, JSON_CONTENT) == 0) {
204             errMessage = jsonNewMessage(AFB_FATAL, "Post Date wrong type encoding=%s != %s", encoding, JSON_CONTENT);
205             goto ExitOnError;
206         }
207
208         if (contentlen > MAX_POST_SIZE) {
209             errMessage = jsonNewMessage(AFB_FATAL, "Post Date to big %d > %d", contentlen, MAX_POST_SIZE);
210             goto ExitOnError;
211         }
212
213         // In POST mode first libmicrohttp call only establishes POST handling.
214         if (posthandle == NULL) {
215             posthandle = malloc(sizeof (AFB_HttpPost)); // allocate application POST processor handle
216             posthandle->uid = postcount++; // build a UID for DEBUG
217             posthandle->len = 0; // effective length within POST handler
218             posthandle->data = malloc(contentlen + 1); // allocate memory for full POST data + 1 for '\0' enf of string
219             *con_cls = posthandle; // attache POST handle to current HTTP session
220
221             if (verbose) fprintf(stderr, "Create Post[%d] Size=%d\n", posthandle->uid, contentlen);
222             return MHD_YES;
223         }
224
225         // This time we receive partial/all Post data. Note that even if we get all POST data. We should nevertheless
226         // return MHD_YES and not process the request directly. Otherwise Libmicrohttpd is unhappy and fails with
227         // 'Internal application error, closing connection'.
228         if (*upload_data_size) {
229             if (verbose) fprintf(stderr, "Update Post[%d]\n", posthandle->uid);
230
231             memcpy(&posthandle->data[posthandle->len], upload_data, *upload_data_size);
232             posthandle->len = posthandle->len + *upload_data_size;
233             *upload_data_size = 0;
234             return MHD_YES;
235         }
236
237         // We should only start to process DATA after Libmicrohttpd call or application handler with *upload_data_size==0
238         // At this level we're may verify that we got everything and process DATA
239         if (posthandle->len != contentlen) {
240             errMessage = jsonNewMessage(AFB_FATAL, "Post Data Incomplete UID=%d Len %d != %s", posthandle->uid, contentlen, posthandle->len);
241             goto ExitOnError;
242         }
243
244         // Before processing data, make sure buffer string is properly ended
245         posthandle->data[posthandle->len] = '\0';
246         request.post = posthandle->data;
247
248         if (verbose) fprintf(stderr, "Close Post[%d] Buffer=%s\n", posthandle->uid, request.post);
249
250     } else {
251         request.post = NULL;
252     };
253
254    
255     // build request structure
256     memset(&request, 0, sizeof (request));
257     request.connection = connection;
258     request.url = url;
259     request.plugin = baseurl;
260     request.api = baseapi;
261     
262     // Search for a plugin with this urlpath
263     for (idx = 0; session->plugins[idx] != NULL; idx++) {
264         if (!strcmp(session->plugins[idx]->prefix, baseurl)) {
265             jsonResponse = callPluginApi(session->plugins[idx], session, &request);
266             free(urlcpy1);
267             break;
268         }
269     }
270     // No plugin was found
271     if (session->plugins[idx] == NULL) {
272         errMessage = jsonNewMessage(AFB_FATAL, "No Plugin for %s", baseurl);
273         free(urlcpy1);
274         goto ExitOnError;
275     }
276
277     // plugin callback did not return a valid Json Object
278     if (jsonResponse == NULL) {
279         errMessage = jsonNewMessage(AFB_FATAL, "No Plugin/API for %s/%s", baseurl, baseapi);
280         goto ExitOnError;
281     }
282
283     serialized = json_object_to_json_string(jsonResponse);
284     webResponse = MHD_create_response_from_buffer(strlen(serialized), (void*) serialized, MHD_RESPMEM_MUST_COPY);
285
286     ret = MHD_queue_response(connection, MHD_HTTP_OK, webResponse);
287     MHD_destroy_response(webResponse);
288     json_object_put(jsonResponse); // decrease reference rqtcount to free the json object
289     return ret;
290
291 ExitOnError:
292     serialized = json_object_to_json_string(errMessage);
293     webResponse = MHD_create_response_from_buffer(strlen(serialized), (void*) serialized, MHD_RESPMEM_MUST_COPY);
294     ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, webResponse);
295     MHD_destroy_response(webResponse);
296     json_object_put(errMessage); // decrease reference rqtcount to free the json object
297     return ret;
298 }
299
300
301 // Loop on plugins. Check that they have the right type, prepare a JSON object with prefix
302
303 STATIC AFB_plugin ** RegisterPlugins(AFB_plugin **plugins) {
304     int idx;
305
306     for (idx = 0; plugins[idx] != NULL; idx++) {
307         if (plugins[idx]->type != AFB_PLUGIN) {
308             fprintf(stderr, "ERROR: AFSV plugin[%d] invalid type=%d != %d\n", idx, AFB_PLUGIN, plugins[idx]->type);
309         } else {
310             // some sanity controls
311             if ((plugins[idx]->prefix == NULL) || (plugins[idx]->info == NULL) || (plugins[idx]->apis == NULL)) {
312                 if (plugins[idx]->prefix == NULL) plugins[idx]->prefix = "No URL prefix for APIs";
313                 if (plugins[idx]->info == NULL) plugins[idx]->info = "No Info describing plugin APIs";
314                 fprintf(stderr, "ERROR: plugin[%d] invalid prefix=%s info=%s", idx, plugins[idx]->prefix, plugins[idx]->info);
315                 return NULL;
316             }
317
318             if (verbose) fprintf(stderr, "Loading plugin[%d] prefix=[%s] info=%s\n", idx, plugins[idx]->prefix, plugins[idx]->info);
319
320             // Prepare Plugin name to be added into each API response
321             plugins[idx]->jtype = json_object_new_string(plugins[idx]->prefix);
322             json_object_get(plugins[idx]->jtype); // increase reference count to make it permanent
323
324             // compute urlprefix lenght
325             plugins[idx]->prefixlen = strlen(plugins[idx]->prefix);
326         }
327     }
328     return (plugins);
329 }
330
331 void initPlugins(AFB_session *session) {
332     static AFB_plugin * plugins[10];
333
334     plugins[0] = afsvRegister(session),
335             plugins[1] = dbusRegister(session),
336             plugins[2] = alsaRegister(session),
337             plugins[3] = NULL;
338
339     // complete plugins and save them within current sessions    
340     session->plugins = RegisterPlugins(plugins);
341 }