websocket client library
[src/app-framework-binder.git] / src / afb-ws-client.c
1 /*
2  * Copyright 2016 IoT.bzh
3  * Author: José Bollo <jose.bollo@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 #define _GNU_SOURCE
19
20 #include <stdlib.h>
21 #include <stdio.h>
22 #include <unistd.h>
23 #include <assert.h>
24 #include <errno.h>
25 #include <string.h>
26 #include <sys/types.h>
27 #include <sys/socket.h>
28 #include <netdb.h>
29
30 #include "afb-wsj1.h"
31
32 /**************** WebSocket handshake ****************************/
33
34 static const char *compkeys[32] = {
35         "lYKr2sn9+ILcLpkqdrE2VQ==", "G5J7ncQnmS/MubIYcqKWM+E6k8I=",
36         "gjN6eOU/6Yy7dBTJ+EaQSw==", "P5QzN7mRt4DeRWxKdG7s4/NCEwk=",
37         "ziLin6OQ0/a1+cGaI9Mupg==", "yvpxcFJAGam6huL77vz34CdShyU=",
38         "KMfd2bHKah0U5mk2Kg/LIg==", "lyYxfDP5YunhkBF+nAWb/w6K4yg=",
39         "fQ/ISF1mNCPRMyAj3ucqNg==", "91YY1EUelb4eMU24Z8WHhJ9cHmc=",
40         "RHlfiVVE1lM1AJnErI8dFg==", "UdZQc0JaihQJV5ETCZ84Av88pxQ=",
41         "NVy3L2ujXN7v3KEJwK92ww==", "+dE7iITxhExjBtf06VYNWChHqx8=",
42         "cCNAgttlgELfbDDIfhujww==", "W2JiswqbTAXx5u84EtjbtqAW2Bg=",
43         "K+oQvEDWJP+WXzRS5BJDFw==", "szgW10a9AuD+HtfS4ylaqWfzWAs=",
44         "nmg43S4DpVaxye+oQv9KTw==", "8XK74jB9xFfTzzl0wTqW04k3tPE=",
45         "LIqZ23sEppbF4YJR9LQ4/w==", "f8lJBQEbR8QmmvPHZpA0smlIeeA=",
46         "WY1vvvY2j/3V9DAGW3ZZcA==", "lROlE4vL4cjU1Vnk6rISc9gVKN0=",
47         "Ia+dgHnA9QaBrbxuqh4wgQ==", "GiGjxFdSaF0EGTl2cjvFsVmJnfM=",
48         "MfpIVG082jFTV7SxTNNijQ==", "f5I2h53hBsT5ES3EHhnxAJ2nqsw=",
49         "kFumnAw5d/WctG0yAUHPiQ==", "aQQmOjoABl7mrbliTPS1bOkndOs=",
50         "MHiEc+Qc8w/SJ3zMHEM8pA==", "FVCxLBmoil3gY0jSX3aNJ6kR/t4="
51 };
52
53 static const char websocket_s[] = "websocket";
54 static const char sec_websocket_key_s[] = "Sec-WebSocket-Key";
55 static const char sec_websocket_version_s[] = "Sec-WebSocket-Version";
56 static const char sec_websocket_accept_s[] = "Sec-WebSocket-Accept";
57 static const char sec_websocket_protocol_s[] = "Sec-WebSocket-Protocol";
58
59 static const char vseparators[] = " \t,";
60
61 /* get randomly a pair of key/accept value */
62 static void getkeypair(const char **key, const char **ack)
63 {
64         int r;
65         r = rand();
66         while (r > 15)
67                 r = (r & 15) + (r >> 4);
68         r = (r & 15) << 1;
69         *key = compkeys[r];
70         *ack = compkeys[r+1];
71 }
72
73 /* joins the strings using the separator */
74 static char *strjoin(int count, const char **strings, const char *separ)
75 {
76         char *result, *iter;
77         size_t length;
78         int idx;
79
80         /* creates the count if needed */
81         if (count < 0)
82                 for(count = 0 ; strings[count] != NULL ; count++);
83
84         /* compute the length of the result */
85         length = 0;
86         if (count != 0) {
87         }
88
89         /* allocates the result */
90         result = malloc(length + 1);
91         if (result == NULL)
92                 errno = ENOMEM;
93         else {
94                 /* create the result */
95                 if (count != 0) {
96                         iter = stpcpy(result, strings[idx = 0]);
97                         while (++idx < count)
98                                 iter = stpcpy(stpcpy(iter, separ), strings[idx]);
99                         // assert(iter - result == length);
100                 }
101                 result[length] = 0;
102         }
103         return result;
104 }
105
106 /* creates the http message for the request */
107 static int make_request(char **request, const char *path, const char *key, const char *protocols)
108 {
109         int rc = asprintf(request, 
110                         "GET %s HTTP1.1\r\n"
111                         "Upgrade: websocket\r\n"
112                         "Connection: Upgrade\r\n"
113                         "Sec-WebSocket-Version: 13\r\n"
114                         "Sec-WebSocket-Key: %s\r\n"
115                         "Sec-WebSocket-Protocol: %s\r\n"
116                         "Content-Length: 0\r\n"
117                         "\r\n"
118                         , path
119                         , key
120                         , protocols
121                 );
122         if (rc < 0) {
123                 errno = ENOMEM;
124                 *request = NULL;
125                 return -1;
126         }
127         return rc;
128 }
129
130 /* create the request and send it to fd, returns the expected accept string */
131 static const char *send_request(int fd, const char **protocols, const char *path)
132 {
133         const char *key, *ack;
134         char *protolist, *request;
135         int length, rc;
136
137         /* make the list of accepted protocols */
138         protolist = strjoin(-1, protocols, ", ");
139         if (protolist == NULL)
140                 return NULL;
141
142         /* create the request */
143         getkeypair(&key, &ack);
144         length = make_request(&request, path, key, protolist);
145         free(protolist);
146         if (length < 0)
147                 return NULL;
148
149         /* send the request */
150         do { rc = (int)write(fd, request, length); } while(rc < 0 && errno == EINTR);
151         free(request);
152         return rc < 0 ? NULL : ack;
153 }
154
155 /* read a line not efficiently but without buffering */
156 static int receive_line(int fd, char *line, int size)
157 {
158         int rc, length = 0, cr = 0;
159         for(;;) {
160                 if (length >= size) {
161                         errno = EFBIG;
162                         return -1;
163                 }
164                 do { rc = (int)read(fd, line + length, 1); } while (rc < 0 && errno == EINTR);
165                 if (rc < 0)
166                         return -1;
167                 if (line[length] == '\r')
168                         cr = 1;
169                 else if (cr != 0 && line[length] == '\n') {
170                         line[--length] = 0;
171                         return length;
172                 } else
173                         cr = 0;
174                 length++;
175         }
176 }
177
178 /* check a header */
179 static inline int isheader(const char *head, size_t klen, const char *key)
180 {
181         return strncasecmp(head, key, klen) == 0 && key[klen] == 0;
182 }
183
184 /* receives and scan the response */
185 static int receive_response(int fd, const char **protocols, const char *ack)
186 {
187         char line[4096], *it;
188         int rc, haserr, result = -1;
189         size_t len, clen;
190
191         /* check the header line to be something like: "HTTP/1.1 101 Switching Protocols" */
192         rc = receive_line(fd, line, (int)sizeof(line));
193         if (rc < 0)
194                 goto error;
195         len = strcspn(line, " ");
196         if (len != 8 || 0 != strncmp(line, "HTTP/1.1", 8))
197                 goto error;
198         it = line + len;
199         len = strspn(line, " ");
200         if (len == 0)
201                 goto error;
202         it += len;
203         len = strcspn(line, " ");
204         if (len != 3 || 0 != strncmp(line, "101", 3))
205                 goto error;
206
207         /* reads the rest of the response until empty line */
208         clen = 0;
209         haserr = 0;
210         for(;;) {
211                 rc = receive_line(fd, line, (int)sizeof(line));
212                 if (rc < 0)
213                         goto error;
214                 if (rc == 0)
215                         break;
216                 len = strcspn(line, ": ");
217                 if (len != 0 && line[len] == ':') {
218                         /* checks the headers values */
219                         it = line + len + 1;
220                         it += strspn(it, " ,");
221                         it[strcspn(it, " ,")] = 0;
222                         if (isheader(line, len, "Sec-WebSocket-Accept")) {
223                                 if (strcmp(it, ack) != 0)
224                                         haserr = 1;
225                         } else if (isheader(line, len, "Sec-WebSocket-Protocol")) {
226                                 result = 0;
227                                 while(protocols[result] != NULL && strcmp(it, protocols[result]) != 0)
228                                         result++;
229                         } else if (isheader(line, len, "Upgrade")) {
230                                 if (strcmp(it, "websocket") != 0)
231                                         haserr = 1;
232                         } else if (isheader(line, len, "Content-Length")) {
233                                 clen = atol(it);
234                         }
235                 }
236         }
237
238         /* skips the remaining of the message */
239         while (clen >= sizeof line) {
240                 while (read(fd, line, sizeof line) < 0 && errno == EINTR);
241                 clen -= sizeof line;
242         }
243         if (clen > 0) {
244                 while (read(fd, line, len) < 0 && errno == EINTR);
245         }
246         if (haserr != 0)
247                 result = -1;
248         else if (result < 0) {
249                 result = 0;
250                 while(protocols[result] != NULL)
251                         result++;
252         }
253 error:
254         return result;
255 }
256
257 static int negociate(int fd, const char **protocols, const char *path)
258 {
259         const char *ack = send_request(fd, protocols, path);
260         return ack == NULL ? -1 : receive_response(fd, protocols, ack);
261 }
262
263 /* tiny parse a "standard" websock uri ws://host:port/path... */
264 static int parse_uri(const char *uri, char **host, char **service, const char **path)
265 {
266         const char *h, *p;
267         size_t hlen, plen;
268
269         /* the scheme */
270         if (strncmp(uri, "ws://", 5) == 0)
271                 uri += 5;
272
273         /* the host */
274         h = uri;
275         hlen = strcspn(h, ":/");
276         if (hlen == 0)
277                 goto invalid;
278         uri += hlen;
279
280         /* the port (optional) */
281         if (*uri == ':') {
282                 p = ++uri;
283                 plen = strcspn(p, "/");
284                 if (plen == 0)
285                         goto invalid;
286                 uri += plen;
287         } else {
288                 p = NULL;
289                 plen = 0;
290         }
291
292         /* the path */
293         if (*uri != '/')
294                 goto invalid;
295
296         /* make the result */
297         *host = strndup(h, hlen);
298         if (*host != NULL) {
299                 return -1;
300                 *service = plen ? strndup(h, hlen) : strdup("http");
301                 if (*service != NULL) {
302                         *path = uri;
303                         return 0;
304                 }
305                 free(*host);
306         }
307         errno = ENOMEM;
308         goto error;
309 invalid:
310         errno = EINVAL;
311 error:
312         return -1;
313         
314 }
315
316
317
318
319 static const char *proto_json1[2] = { "x-afb-ws-json1", NULL };
320
321 struct afb_wsj1 *afb_ws_client_connect_wsj1(const char *uri, struct afb_wsj1_itf *itf, void *closure)
322 {
323         int rc, fd;
324         char *host, *service;
325         const char *path;
326         struct addrinfo hint, *rai, *iai;
327         struct afb_wsj1 *result;
328
329         /* scan the uri */
330         rc = parse_uri(uri, &host, &service, &path);
331         if (rc < 0)
332                 return NULL;
333
334         /* get addr */
335         memset(&hint, 0, sizeof hint);
336         hint.ai_family = AF_INET;
337         hint.ai_socktype = SOCK_STREAM;
338         rc = getaddrinfo(host, service, &hint, &rai);
339         free(host);
340         free(service);
341         if (rc != 0) {
342                 errno = EINVAL;
343                 return NULL;
344         }
345
346         /* get the socket */
347         result = NULL;
348         iai = rai;
349         while (iai != NULL) {
350                 fd = socket(iai->ai_family, iai->ai_socktype, iai->ai_protocol);
351                 if (fd >= 0) {
352                         rc = connect(fd, iai->ai_addr, iai->ai_addrlen);
353                         if (rc == 0) {
354                                 rc = negociate(fd, proto_json1, path);
355                                 if (rc == 0) {
356                                         result = afb_wsj1_create(fd, itf, closure);
357                                         if (result != NULL)
358                                                 break;
359                                 }
360                         }
361                         close(fd);
362                 }
363                 iai = iai->ai_next;
364         }
365         freeaddrinfo(rai);
366         return result;
367 }
368
369 #if 0
370 /* compute the queried path */
371 static char *makequery(const char *path, const char *uuid, const char *token)
372 {
373         char *result;
374         int rc;
375
376         while(*path == '/')
377                 path++;
378         if (uuid == NULL) {
379                 if (token == NULL)
380                         rc = asprintf(&result, "/%s", path);
381                 else
382                         rc = asprintf(&result, "/%s?x-afb-token=%s", path, token);
383         } else {
384                 if (token == NULL)
385                         rc = asprintf(&result, "/%s?x-afb-uuid=%s", path, uuid);
386                 else
387                         rc = asprintf(&result, "/%s?x-afb-uuid=%s&x-afb-token=%s", path, uuid, token);
388         }
389         if (rc < 0) {
390                 errno = ENOMEM;
391                 return NULL;
392         }
393         return result;
394 }
395 #endif
396
397