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