http-svc: fix bug of uninitialized response
[src/app-framework-binder.git] / src / http-svc.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  * Handle standard HTTP request
19  *    Features/Restriction:
20     - handle ETAG to limit upload to modified/new files [cache default 3600s]
21     - handles redirect to index.htlm when path is a directory [code 301]
22     - only support GET method
23     - does not follow link.
24
25    References: https://www.gnu.org/software/libmicrohttpd/manual/html_node/index.html#Top
26    http://libmicrohttpd.sourcearchive.com/documentation/0.4.2/microhttpd_8h.html
27    https://gnunet.org/svn/libmicrohttpd/src/examples/fileserver_example_external_select.c
28    https://github.com/json-c/json-c
29    POST https://www.gnu.org/software/libmicrohttpd/manual/html_node/microhttpd_002dpost.html#microhttpd_002dpost
30  */
31
32 #define _GNU_SOURCE
33
34 #include <microhttpd.h>
35
36 #include <poll.h>
37 #include <sys/stat.h>
38 #include "../include/local-def.h"
39
40 // let's compute fixed URL length only once
41 static size_t apiUrlLen=0;
42 static size_t baseUrlLen=0;
43 static size_t rootUrlLen=0;
44
45 #define MAGIC_DB "/usr/share/misc/magic.mgc"
46
47 // try to open libmagic to handle mime types
48 static AFB_error initLibMagic (AFB_session *session) {
49   
50     /*MAGIC_MIME tells magic to return a mime of the file, but you can specify different things*/
51     if (verbose) printf("Loading mimetype default magic database\n");
52   
53     session->magic = magic_open(MAGIC_MIME_TYPE);
54     if (session->magic == NULL) {
55         fprintf(stderr,"ERROR: unable to initialize magic library\n");
56         return AFB_FAIL;
57     }
58     
59     // Warning: should not use NULL for DB [libmagic bug wont pass efence check]
60     if (magic_load(session->magic, MAGIC_DB) != 0) {
61         fprintf(stderr,"cannot load magic database - %s\n", magic_error(session->magic));
62         magic_close(session->magic);
63         return AFB_FAIL;
64     }
65
66     return AFB_SUCCESS;
67 }
68
69 // Because of POST call multiple time requestApi we need to free POST handle here
70 static void endRequest (void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) {
71   AFB_PostHandle *posthandle = *con_cls;
72
73   // if post handle was used let's free everything
74   if (posthandle != NULL) endPostRequest (posthandle);
75 }
76
77
78 // Create check etag value
79 STATIC void computeEtag(char *etag, size_t maxlen, struct stat *sbuf) {
80     long time;
81     time = sbuf->st_mtim.tv_sec;
82     snprintf(etag, maxlen, "%ld", time);
83 }
84
85 STATIC int servFile (struct MHD_Connection *connection, AFB_session *session, const char *url, AFB_staticfile *staticfile) {
86     const char *etagCache, *mimetype; 
87     char etagValue[15];
88     struct MHD_Response *response;
89     struct stat sbuf; 
90
91     if (fstat (staticfile->fd, &sbuf) != 0) {
92         fprintf(stderr, "Fail to stat file: [%s] error:%s\n", staticfile->path, strerror(errno));
93         goto abortRequest;
94     }
95        
96     // if url is a directory let's add index.html and redirect client
97     if (S_ISDIR (sbuf.st_mode)) {
98         close (staticfile->fd); // close directory check for Index
99        
100         // No trailing '/'. Let's add one and redirect for relative paths to work
101         if (url [strlen (url) -1] != '/') {
102             response = MHD_create_response_from_buffer(0,"", MHD_RESPMEM_PERSISTENT);
103             strncpy(staticfile->path, url, sizeof (staticfile->path));
104             strncat(staticfile->path, "/", sizeof (staticfile->path));
105             MHD_add_response_header (response, "Location", staticfile->path);
106             MHD_queue_response (connection, MHD_HTTP_MOVED_PERMANENTLY, response);
107             if (verbose) fprintf (stderr,"Adding trailing '/' [%s]\n",staticfile->path);      
108             goto sendRequest;
109         }
110         
111         strncat (staticfile->path, OPA_INDEX, sizeof (staticfile->path));
112         if (-1 == (staticfile->fd = open(staticfile->path, O_RDONLY)) || (fstat (staticfile->fd, &sbuf) != 0)) {
113            fprintf(stderr, "No Index.html in direcory [%s]\n", staticfile->path);
114            goto abortRequest;  
115         } 
116     } else if (! S_ISREG (sbuf.st_mode)) { // only standard file any other one including symbolic links are refused.
117         close (staticfile->fd); // nothing useful to do with this file
118         fprintf (stderr, "Fail file: [%s] is not a regular file\n", staticfile->path);
119         const char *errorstr = "<html><body>Application Framework Binder Invalid file type</body></html>";
120         response = MHD_create_response_from_buffer (strlen (errorstr),
121                      (void *) errorstr,  MHD_RESPMEM_PERSISTENT);
122         MHD_queue_response (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, response);
123         goto sendRequest;
124     } 
125     
126     // https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=fr
127     // ftp://ftp.heanet.ie/disk1/www.gnu.org/software/libmicrohttpd/doxygen/dc/d0c/microhttpd_8h.html
128
129     // Check etag value and load file only when modification date changes
130     etagCache = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_NONE_MATCH);
131     computeEtag(etagValue, sizeof (etagValue), &sbuf);
132
133     if (etagCache != NULL && strcmp(etagValue, etagCache) == 0) {
134         close(staticfile->fd); // file did not change since last upload
135         if (verbose) fprintf(stderr, "Not Modify: [%s]\n", staticfile->path);
136         response = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT);
137         MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, session->cacheTimeout); // default one hour cache
138         MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etagValue);
139         MHD_queue_response(connection, MHD_HTTP_NOT_MODIFIED, response);
140
141     } else { // it's a new file, we need to upload it to client
142         response = MHD_create_response_from_fd(sbuf.st_size, staticfile->fd);
143         // if we have magic let's try to guest mime type
144         if (session->magic) {          
145            mimetype= magic_descriptor(session->magic, staticfile->fd);
146            if (mimetype != NULL)  MHD_add_response_header (response, MHD_HTTP_HEADER_CONTENT_TYPE, mimetype);
147         } else mimetype="application/unknown";
148         if (verbose) fprintf(stderr, "Serving: [%s] mime=%s\n", staticfile->path, mimetype);
149         MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, session->cacheTimeout); // default one hour cache
150         MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etagValue);
151         MHD_queue_response(connection, MHD_HTTP_OK, response);
152     }
153     
154 sendRequest:    
155     MHD_destroy_response(response);
156     return (MHD_YES);
157
158 abortRequest:
159     return (FAILED);
160 }
161
162
163 // this function return either Index.htlm or a redirect to /#!route to make angular happy
164 STATIC int redirectHTML5(struct MHD_Connection *connection, AFB_session *session, const char* url) {
165
166     struct MHD_Response *response;
167     AFB_staticfile staticfile;
168
169     // Url match /opa/xxxx should redirect to "/opa/#!page" to force index.html reload
170     strncpy(staticfile.path, session->config->rootbase, sizeof (staticfile.path));
171     strncat(staticfile.path, "/#!", sizeof (staticfile.path));
172     strncat(staticfile.path, &url[1], sizeof (staticfile.path));
173     response = MHD_create_response_from_buffer(0,"", MHD_RESPMEM_PERSISTENT);
174     MHD_add_response_header (response, "Location", staticfile.path);
175     MHD_queue_response (connection, MHD_HTTP_MOVED_PERMANENTLY, response);
176     if (verbose) fprintf (stderr,"checkHTML5 redirect to [%s]\n",staticfile.path);
177     return (MHD_YES);
178 }
179
180
181 // minimal httpd file server for static HTML,JS,CSS,etc...
182 STATIC int requestFile(struct MHD_Connection *connection, AFB_session *session, const char* url) {
183     int ret, idx;
184     AFB_staticfile staticfile;
185     char *requestdir, *requesturl;
186    
187     // default search for file is rootdir base
188     requestdir= session->config->rootdir;
189     requesturl=(char*)url;
190     
191     // Check for optional aliases
192     for (idx=0; session->config->aliasdir[idx].url != NULL; idx++) {
193         if (0 == strncmp(url, session->config->aliasdir[idx].url, session->config->aliasdir[idx].len)) {
194              requestdir = session->config->aliasdir[idx].path;
195              requesturl=(char*)&url[session->config->aliasdir[idx].len];
196              break;
197         }
198     }
199     
200     // build full path from rootdir + url
201     strncpy(staticfile.path, requestdir, sizeof (staticfile.path));   
202     strncat(staticfile.path, requesturl, sizeof (staticfile.path));
203
204     // try to open file and get its size
205     if (-1 == (staticfile.fd = open(staticfile.path, O_RDONLY))) {
206         fprintf(stderr, "Fail to open file: [%s] error:%s\n", staticfile.path, strerror(errno));
207         return (FAILED);
208     }
209     // open file is OK let use it
210     ret = servFile (connection, session, url, &staticfile);
211     return ret;
212 }
213
214 // Check and Dispatch HTTP request
215 STATIC int newRequest(void *cls,
216         struct MHD_Connection *connection,
217         const char *url,
218         const char *method,
219         const char *version,
220         const char *upload_data, size_t *upload_data_size, void **con_cls) {
221
222     AFB_session *session = cls;
223     struct MHD_Response *response;
224     int ret;
225     
226     // this is a REST API let's check for plugins
227     if (0 == strncmp(url, session->config->rootapi, apiUrlLen)) {
228         ret = doRestApi(connection, session, &url[apiUrlLen+1], method, upload_data, upload_data_size, con_cls);
229         return ret;
230     }
231     
232     // From here only accept get request
233     if (0 != strcmp(method, MHD_HTTP_METHOD_GET)) return MHD_NO; /* unexpected method */
234    
235     // If a static file exist serve it now
236     ret = requestFile(connection, session, url);
237     if (ret != FAILED) return ret;
238     
239     // no static was served let's try HTML5 OPA redirect
240     if (0 == strncmp(url, session->config->rootbase, baseUrlLen)) {
241         ret = redirectHTML5(connection, session, &url[baseUrlLen]);
242         return ret;
243     }
244
245      // Nothing respond to this request Files, API, Angular Base
246     const char *errorstr = "<html><body>AFB-Daemon File Not Find file</body></html>";
247     response = MHD_create_response_from_buffer(strlen(errorstr), (void*)errorstr, MHD_RESPMEM_PERSISTENT);
248     ret = MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, response);
249     return (MHD_YES);
250 }
251
252 STATIC int newClient(void *cls, const struct sockaddr * addr, socklen_t addrlen) {
253     // check if client is coming from an acceptable IP
254     return (MHD_YES); // MHD_NO
255 }
256
257
258 PUBLIC AFB_error httpdStart(AFB_session *session) {
259
260     // compute fixed URL length at startup time
261     apiUrlLen = strlen (session->config->rootapi);
262     baseUrlLen= strlen (session->config->rootbase);
263     rootUrlLen= strlen (session->config->rootdir);
264     
265     // Initialise Client Session Hash Table
266     ctxStoreInit (CTX_NBCLIENTS);
267      
268     //TBD open libmagic cache [fail to pass EFENCE check (allocating 0 bytes)]
269     //initLibMagic (session);
270     
271     
272     if (verbose) {
273         printf("AFB:notice Waiting port=%d rootdir=%s\n", session->config->httpdPort, session->config->rootdir);
274         printf("AFB:notice Browser URL= http://localhost:%d\n", session->config->httpdPort);
275     }
276
277     session->httpd = (void*) MHD_start_daemon(
278                 MHD_USE_EPOLL_LINUX_ONLY
279                 | MHD_USE_TCP_FASTOPEN
280                 | MHD_USE_DEBUG
281               ,
282             (uint16_t)session->config->httpdPort, // port
283             &newClient, NULL, // Tcp Accept call back + extra attribute
284             &newRequest, session, // Http Request Call back + extra attribute
285             MHD_OPTION_NOTIFY_COMPLETED, &endRequest, NULL,
286             MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 15, // 15 seconds
287             MHD_OPTION_END); // options-end
288     // TBD: MHD_OPTION_SOCK_ADDR
289
290     if (session->httpd == NULL) {
291         printf("Error: httpStart invalid httpd port: %d", session->config->httpdPort);
292         return AFB_FATAL;
293     }
294     return AFB_SUCCESS;
295 }
296
297 // infinite loop
298 PUBLIC AFB_error httpdLoop(AFB_session *session) {
299     int count = 0;
300     const union MHD_DaemonInfo *info;
301     struct pollfd pfd;
302
303     info = MHD_get_daemon_info(session->httpd,
304                                MHD_DAEMON_INFO_EPOLL_FD_LINUX_ONLY);
305     if (info == NULL) {
306         printf("Error: httpLoop no pollfd");
307         goto error;
308     }
309     pfd.fd = info->listen_fd;
310     pfd.events = POLLIN;
311
312     if (verbose) fprintf(stderr, "AFB:notice entering httpd waiting loop\n");
313     while (TRUE) {
314         if (verbose) fprintf(stderr, "AFB:notice httpd alive [%d]\n", count++);
315         poll(&pfd, 1, 15000); // 15 seconds (as above timeout when starting)
316         MHD_run(session->httpd);
317     }
318
319 error:
320     // should never return from here
321     return AFB_FATAL;
322 }
323
324 PUBLIC int httpdStatus(AFB_session *session) {
325     return (MHD_run(session->httpd));
326 }
327
328 PUBLIC void httpdStop(AFB_session *session) {
329     MHD_stop_daemon(session->httpd);
330 }