/* * Copyright (C) 2015-2020 "IoT.bzh" * Author: José Bollo * * 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 #include #include #include #include #include #include #include #include #include #include "afb-wsj1.h" #include "fdev-systemd.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=" }; /* 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 */ if (count == 0) length = 0; else { length = (unsigned)(count - 1) * strlen(separ); for (idx = 0 ; idx < count ; idx ++) length += strlen(strings[idx]); } /* 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 *host, const char *key, const char *protocols) { int rc = asprintf(request, "GET %s HTTP/1.1\r\n" "Host: %s\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 , host , 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 *host) { 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, host, 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 abort; it = line + len; len = strspn(it, " "); if (len == 0) goto abort; it += len; len = strcspn(it, " "); if (len != 3 || 0 != strncmp(it, "101", 3)) goto abort; /* 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 < 0) goto abort; return result; abort: errno = ECONNABORTED; error: return -1; } static int negociate(int fd, const char **protocols, const char *path, const char *host) { const char *ack = send_request(fd, protocols, path, host); 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) { *service = plen ? strndup(p, plen) : 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(struct sd_event *eloop, const char *uri, struct afb_wsj1_itf *itf, void *closure) { int rc, fd; char *host, *service, xhost[32]; const char *path; struct addrinfo hint, *rai, *iai; struct afb_wsj1 *result; struct fdev *fdev; /* 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) { struct sockaddr_in *a = (struct sockaddr_in*)(iai->ai_addr); unsigned char *ipv4 = (unsigned char*)&(a->sin_addr.s_addr); unsigned char *port = (unsigned char*)&(a->sin_port); sprintf(xhost, "%d.%d.%d.%d:%d", (int)ipv4[0], (int)ipv4[1], (int)ipv4[2], (int)ipv4[3], (((int)port[0]) << 8)|(int)port[1]); 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, xhost); if (rc == 0) { fdev = fdev_systemd_create(eloop, fd); if (fdev) { result = afb_wsj1_create(fdev, itf, closure); if (result != NULL) { fcntl(fd, F_SETFL, O_NONBLOCK); 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 /*****************************************************************************************************************************/ #include #include "afb-proto-ws.h" static int get_socket_unix(const char *uri) { int fd, rc; struct sockaddr_un addr; size_t length; length = strlen(uri); if (length >= 108) { errno = ENAMETOOLONG; return -1; } fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return fd; memset(&addr, 0, sizeof addr); addr.sun_family = AF_UNIX; strcpy(addr.sun_path, uri); if (addr.sun_path[0] == '@') addr.sun_path[0] = 0; /* implement abstract sockets */ rc = connect(fd, (struct sockaddr *) &addr, (socklen_t)(sizeof addr)); if (rc < 0) { close(fd); return rc; } return fd; } static int get_socket_inet(const char *uri) { int rc, fd; const char *service, *host, *api; struct addrinfo hint, *rai, *iai; /* scan the uri */ service = strrchr(uri, ':'); if (service == NULL) { errno = EINVAL; return -1; } api = strchrnul(service, '/'); host = strndupa(uri, service++ - uri); service = strndupa(service, api - service); /* get addr */ memset(&hint, 0, sizeof hint); hint.ai_family = AF_INET; hint.ai_socktype = SOCK_STREAM; rc = getaddrinfo(host, service, &hint, &rai); if (rc != 0) { errno = EINVAL; return -1; } /* get the socket */ 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) { freeaddrinfo(rai); return fd; } close(fd); } iai = iai->ai_next; } freeaddrinfo(rai); return -1; } static int get_socket(const char *uri) { int fd; /* check for unix socket */ if (0 == strncmp(uri, "unix:", 5)) /* unix socket */ fd = get_socket_unix(uri + 5); else if (0 == strncmp(uri, "tcp:", 4)) /* unix socket */ fd = get_socket_inet(uri + 4); else /* inet socket */ fd = get_socket_inet(uri); /* configure the socket */ if (fd >= 0) { fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); } return fd; } /* * Establish a websocket-like client connection to the API of 'uri' and if successful * instantiate a client afb_proto_ws websocket for this API using 'itf' and 'closure'. * (see afb_proto_ws_create_client). * The systemd event loop 'eloop' is used to handle the websocket. * Returns NULL in case of failure with errno set appropriately. */ struct afb_proto_ws *afb_ws_client_connect_api(struct sd_event *eloop, const char *uri, struct afb_proto_ws_client_itf *itf, void *closure) { int fd; struct afb_proto_ws *pws; struct fdev *fdev; fd = get_socket(uri); if (fd >= 0) { fdev = fdev_systemd_create(eloop, fd); if (fdev) { pws = afb_proto_ws_create_client(fdev, itf, closure); if (pws) return pws; } close(fd); } return NULL; }