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