diff options
Diffstat (limited to 'src/afb-ws-client.c')
-rw-r--r-- | src/afb-ws-client.c | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/src/afb-ws-client.c b/src/afb-ws-client.c new file mode 100644 index 00000000..6ed3f50f --- /dev/null +++ b/src/afb-ws-client.c @@ -0,0 +1,397 @@ +/* + * Copyright 2016 IoT.bzh + * Author: José Bollo <jose.bollo@iot.bzh> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <assert.h> +#include <errno.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> + +#include "afb-wsj1.h" + +/**************** WebSocket handshake ****************************/ + +static const char *compkeys[32] = { + "lYKr2sn9+ILcLpkqdrE2VQ==", "G5J7ncQnmS/MubIYcqKWM+E6k8I=", + "gjN6eOU/6Yy7dBTJ+EaQSw==", "P5QzN7mRt4DeRWxKdG7s4/NCEwk=", + "ziLin6OQ0/a1+cGaI9Mupg==", "yvpxcFJAGam6huL77vz34CdShyU=", + "KMfd2bHKah0U5mk2Kg/LIg==", "lyYxfDP5YunhkBF+nAWb/w6K4yg=", + "fQ/ISF1mNCPRMyAj3ucqNg==", "91YY1EUelb4eMU24Z8WHhJ9cHmc=", + "RHlfiVVE1lM1AJnErI8dFg==", "UdZQc0JaihQJV5ETCZ84Av88pxQ=", + "NVy3L2ujXN7v3KEJwK92ww==", "+dE7iITxhExjBtf06VYNWChHqx8=", + "cCNAgttlgELfbDDIfhujww==", "W2JiswqbTAXx5u84EtjbtqAW2Bg=", + "K+oQvEDWJP+WXzRS5BJDFw==", "szgW10a9AuD+HtfS4ylaqWfzWAs=", + "nmg43S4DpVaxye+oQv9KTw==", "8XK74jB9xFfTzzl0wTqW04k3tPE=", + "LIqZ23sEppbF4YJR9LQ4/w==", "f8lJBQEbR8QmmvPHZpA0smlIeeA=", + "WY1vvvY2j/3V9DAGW3ZZcA==", "lROlE4vL4cjU1Vnk6rISc9gVKN0=", + "Ia+dgHnA9QaBrbxuqh4wgQ==", "GiGjxFdSaF0EGTl2cjvFsVmJnfM=", + "MfpIVG082jFTV7SxTNNijQ==", "f5I2h53hBsT5ES3EHhnxAJ2nqsw=", + "kFumnAw5d/WctG0yAUHPiQ==", "aQQmOjoABl7mrbliTPS1bOkndOs=", + "MHiEc+Qc8w/SJ3zMHEM8pA==", "FVCxLBmoil3gY0jSX3aNJ6kR/t4=" +}; + +static const char websocket_s[] = "websocket"; +static const char sec_websocket_key_s[] = "Sec-WebSocket-Key"; +static const char sec_websocket_version_s[] = "Sec-WebSocket-Version"; +static const char sec_websocket_accept_s[] = "Sec-WebSocket-Accept"; +static const char sec_websocket_protocol_s[] = "Sec-WebSocket-Protocol"; + +static const char vseparators[] = " \t,"; + +/* get randomly a pair of key/accept value */ +static void getkeypair(const char **key, const char **ack) +{ + int r; + r = rand(); + while (r > 15) + r = (r & 15) + (r >> 4); + r = (r & 15) << 1; + *key = compkeys[r]; + *ack = compkeys[r+1]; +} + +/* joins the strings using the separator */ +static char *strjoin(int count, const char **strings, const char *separ) +{ + char *result, *iter; + size_t length; + int idx; + + /* creates the count if needed */ + if (count < 0) + for(count = 0 ; strings[count] != NULL ; count++); + + /* compute the length of the result */ + length = 0; + if (count != 0) { + } + + /* allocates the result */ + result = malloc(length + 1); + if (result == NULL) + errno = ENOMEM; + else { + /* create the result */ + if (count != 0) { + iter = stpcpy(result, strings[idx = 0]); + while (++idx < count) + iter = stpcpy(stpcpy(iter, separ), strings[idx]); + // assert(iter - result == length); + } + result[length] = 0; + } + return result; +} + +/* creates the http message for the request */ +static int make_request(char **request, const char *path, const char *key, const char *protocols) +{ + int rc = asprintf(request, + "GET %s HTTP1.1\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Key: %s\r\n" + "Sec-WebSocket-Protocol: %s\r\n" + "Content-Length: 0\r\n" + "\r\n" + , path + , key + , protocols + ); + if (rc < 0) { + errno = ENOMEM; + *request = NULL; + return -1; + } + return rc; +} + +/* create the request and send it to fd, returns the expected accept string */ +static const char *send_request(int fd, const char **protocols, const char *path) +{ + const char *key, *ack; + char *protolist, *request; + int length, rc; + + /* make the list of accepted protocols */ + protolist = strjoin(-1, protocols, ", "); + if (protolist == NULL) + return NULL; + + /* create the request */ + getkeypair(&key, &ack); + length = make_request(&request, path, key, protolist); + free(protolist); + if (length < 0) + return NULL; + + /* send the request */ + do { rc = (int)write(fd, request, length); } while(rc < 0 && errno == EINTR); + free(request); + return rc < 0 ? NULL : ack; +} + +/* read a line not efficiently but without buffering */ +static int receive_line(int fd, char *line, int size) +{ + int rc, length = 0, cr = 0; + for(;;) { + if (length >= size) { + errno = EFBIG; + return -1; + } + do { rc = (int)read(fd, line + length, 1); } while (rc < 0 && errno == EINTR); + if (rc < 0) + return -1; + if (line[length] == '\r') + cr = 1; + else if (cr != 0 && line[length] == '\n') { + line[--length] = 0; + return length; + } else + cr = 0; + length++; + } +} + +/* check a header */ +static inline int isheader(const char *head, size_t klen, const char *key) +{ + return strncasecmp(head, key, klen) == 0 && key[klen] == 0; +} + +/* receives and scan the response */ +static int receive_response(int fd, const char **protocols, const char *ack) +{ + char line[4096], *it; + int rc, haserr, result = -1; + size_t len, clen; + + /* check the header line to be something like: "HTTP/1.1 101 Switching Protocols" */ + rc = receive_line(fd, line, (int)sizeof(line)); + if (rc < 0) + goto error; + len = strcspn(line, " "); + if (len != 8 || 0 != strncmp(line, "HTTP/1.1", 8)) + goto error; + it = line + len; + len = strspn(line, " "); + if (len == 0) + goto error; + it += len; + len = strcspn(line, " "); + if (len != 3 || 0 != strncmp(line, "101", 3)) + goto error; + + /* reads the rest of the response until empty line */ + clen = 0; + haserr = 0; + for(;;) { + rc = receive_line(fd, line, (int)sizeof(line)); + if (rc < 0) + goto error; + if (rc == 0) + break; + len = strcspn(line, ": "); + if (len != 0 && line[len] == ':') { + /* checks the headers values */ + it = line + len + 1; + it += strspn(it, " ,"); + it[strcspn(it, " ,")] = 0; + if (isheader(line, len, "Sec-WebSocket-Accept")) { + if (strcmp(it, ack) != 0) + haserr = 1; + } else if (isheader(line, len, "Sec-WebSocket-Protocol")) { + result = 0; + while(protocols[result] != NULL && strcmp(it, protocols[result]) != 0) + result++; + } else if (isheader(line, len, "Upgrade")) { + if (strcmp(it, "websocket") != 0) + haserr = 1; + } else if (isheader(line, len, "Content-Length")) { + clen = atol(it); + } + } + } + + /* skips the remaining of the message */ + while (clen >= sizeof line) { + while (read(fd, line, sizeof line) < 0 && errno == EINTR); + clen -= sizeof line; + } + if (clen > 0) { + while (read(fd, line, len) < 0 && errno == EINTR); + } + if (haserr != 0) + result = -1; + else if (result < 0) { + result = 0; + while(protocols[result] != NULL) + result++; + } +error: + return result; +} + +static int negociate(int fd, const char **protocols, const char *path) +{ + const char *ack = send_request(fd, protocols, path); + return ack == NULL ? -1 : receive_response(fd, protocols, ack); +} + +/* tiny parse a "standard" websock uri ws://host:port/path... */ +static int parse_uri(const char *uri, char **host, char **service, const char **path) +{ + const char *h, *p; + size_t hlen, plen; + + /* the scheme */ + if (strncmp(uri, "ws://", 5) == 0) + uri += 5; + + /* the host */ + h = uri; + hlen = strcspn(h, ":/"); + if (hlen == 0) + goto invalid; + uri += hlen; + + /* the port (optional) */ + if (*uri == ':') { + p = ++uri; + plen = strcspn(p, "/"); + if (plen == 0) + goto invalid; + uri += plen; + } else { + p = NULL; + plen = 0; + } + + /* the path */ + if (*uri != '/') + goto invalid; + + /* make the result */ + *host = strndup(h, hlen); + if (*host != NULL) { + return -1; + *service = plen ? strndup(h, hlen) : strdup("http"); + if (*service != NULL) { + *path = uri; + return 0; + } + free(*host); + } + errno = ENOMEM; + goto error; +invalid: + errno = EINVAL; +error: + return -1; + +} + + + + +static const char *proto_json1[2] = { "x-afb-ws-json1", NULL }; + +struct afb_wsj1 *afb_ws_client_connect_wsj1(const char *uri, struct afb_wsj1_itf *itf, void *closure) +{ + int rc, fd; + char *host, *service; + const char *path; + struct addrinfo hint, *rai, *iai; + struct afb_wsj1 *result; + + /* scan the uri */ + rc = parse_uri(uri, &host, &service, &path); + if (rc < 0) + return NULL; + + /* get addr */ + memset(&hint, 0, sizeof hint); + hint.ai_family = AF_INET; + hint.ai_socktype = SOCK_STREAM; + rc = getaddrinfo(host, service, &hint, &rai); + free(host); + free(service); + if (rc != 0) { + errno = EINVAL; + return NULL; + } + + /* get the socket */ + result = NULL; + iai = rai; + while (iai != NULL) { + fd = socket(iai->ai_family, iai->ai_socktype, iai->ai_protocol); + if (fd >= 0) { + rc = connect(fd, iai->ai_addr, iai->ai_addrlen); + if (rc == 0) { + rc = negociate(fd, proto_json1, path); + if (rc == 0) { + result = afb_wsj1_create(fd, itf, closure); + if (result != NULL) + break; + } + } + close(fd); + } + iai = iai->ai_next; + } + freeaddrinfo(rai); + return result; +} + +#if 0 +/* compute the queried path */ +static char *makequery(const char *path, const char *uuid, const char *token) +{ + char *result; + int rc; + + while(*path == '/') + path++; + if (uuid == NULL) { + if (token == NULL) + rc = asprintf(&result, "/%s", path); + else + rc = asprintf(&result, "/%s?x-afb-token=%s", path, token); + } else { + if (token == NULL) + rc = asprintf(&result, "/%s?x-afb-uuid=%s", path, uuid); + else + rc = asprintf(&result, "/%s?x-afb-uuid=%s&x-afb-token=%s", path, uuid, token); + } + if (rc < 0) { + errno = ENOMEM; + return NULL; + } + return result; +} +#endif + + |