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