Implement websocket
parent
e385a98238
commit
8847511596
|
@ -97,9 +97,16 @@ struct nvnc_client {
|
||||||
|
|
||||||
LIST_HEAD(nvnc_client_list, nvnc_client);
|
LIST_HEAD(nvnc_client_list, nvnc_client);
|
||||||
|
|
||||||
|
enum nvnc__socket_type {
|
||||||
|
NVNC__SOCKET_TCP,
|
||||||
|
NVNC__SOCKET_UNIX,
|
||||||
|
NVNC__SOCKET_WEBSOCKET,
|
||||||
|
};
|
||||||
|
|
||||||
struct nvnc {
|
struct nvnc {
|
||||||
struct nvnc_common common;
|
struct nvnc_common common;
|
||||||
int fd;
|
int fd;
|
||||||
|
enum nvnc__socket_type socket_type;
|
||||||
struct aml_handler* poll_handle;
|
struct aml_handler* poll_handle;
|
||||||
struct nvnc_client_list clients;
|
struct nvnc_client_list clients;
|
||||||
char name[256];
|
char name[256];
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* Copyright (c) 2014-2016, Marel
|
||||||
|
* Copyright (c) 2023, Andri Yngvason
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define URL_INDEX_MAX 32
|
||||||
|
#define URL_QUERY_INDEX_MAX 32
|
||||||
|
#define HTTP_FIELD_INDEX_MAX 32
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
enum http_method {
|
||||||
|
HTTP_GET = 1,
|
||||||
|
HTTP_PUT = 2,
|
||||||
|
HTTP_OPTIONS = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct http_kv {
|
||||||
|
char* key;
|
||||||
|
char* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct http_req {
|
||||||
|
enum http_method method;
|
||||||
|
size_t header_length;
|
||||||
|
size_t content_length;
|
||||||
|
char* content_type;
|
||||||
|
size_t url_index;
|
||||||
|
char* url[URL_INDEX_MAX];
|
||||||
|
size_t url_query_index;
|
||||||
|
struct http_kv url_query[URL_QUERY_INDEX_MAX];
|
||||||
|
size_t field_index;
|
||||||
|
struct http_kv field[HTTP_FIELD_INDEX_MAX];
|
||||||
|
};
|
||||||
|
|
||||||
|
int http_req_parse(struct http_req* req, const char* head);
|
||||||
|
void http_req_free(struct http_req* req);
|
||||||
|
|
||||||
|
const char* http_req_query(struct http_req* req, const char* key);
|
|
@ -125,6 +125,7 @@ extern const char nvnc_version[];
|
||||||
|
|
||||||
struct nvnc* nvnc_open(const char* addr, uint16_t port);
|
struct nvnc* nvnc_open(const char* addr, uint16_t port);
|
||||||
struct nvnc* nvnc_open_unix(const char *addr);
|
struct nvnc* nvnc_open_unix(const char *addr);
|
||||||
|
struct nvnc* nvnc_open_websocket(const char* addr, uint16_t port);
|
||||||
void nvnc_close(struct nvnc* self);
|
void nvnc_close(struct nvnc* self);
|
||||||
|
|
||||||
void nvnc_add_display(struct nvnc*, struct nvnc_display*);
|
void nvnc_add_display(struct nvnc*, struct nvnc_display*);
|
||||||
|
|
|
@ -91,6 +91,10 @@ struct stream {
|
||||||
bool cork;
|
bool cork;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef ENABLE_WEBSOCKET
|
||||||
|
struct stream* stream_ws_new(int fd, stream_event_fn on_event, void* userdata);
|
||||||
|
#endif
|
||||||
|
|
||||||
struct stream* stream_new(int fd, stream_event_fn on_event, void* userdata);
|
struct stream* stream_new(int fd, stream_event_fn on_event, void* userdata);
|
||||||
int stream_close(struct stream* self);
|
int stream_close(struct stream* self);
|
||||||
void stream_destroy(struct stream* self);
|
void stream_destroy(struct stream* self);
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define WS_HEADER_MIN_SIZE 14
|
||||||
|
|
||||||
|
enum ws_opcode {
|
||||||
|
WS_OPCODE_CONT = 0,
|
||||||
|
WS_OPCODE_TEXT,
|
||||||
|
WS_OPCODE_BIN,
|
||||||
|
WS_OPCODE_CLOSE = 8,
|
||||||
|
WS_OPCODE_PING,
|
||||||
|
WS_OPCODE_PONG,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ws_frame_header {
|
||||||
|
bool fin;
|
||||||
|
enum ws_opcode opcode;
|
||||||
|
bool mask;
|
||||||
|
uint64_t payload_length;
|
||||||
|
uint8_t masking_key[4];
|
||||||
|
size_t header_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
ssize_t ws_handshake(char* output, size_t output_maxlen, const char* input);
|
||||||
|
|
||||||
|
const char *ws_opcode_name(enum ws_opcode op);
|
||||||
|
|
||||||
|
bool ws_parse_frame_header(struct ws_frame_header* header,
|
||||||
|
const uint8_t* payload, size_t length);
|
||||||
|
void ws_apply_mask(const struct ws_frame_header* header,
|
||||||
|
uint8_t* restrict payload);
|
||||||
|
void ws_copy_payload(const struct ws_frame_header* header,
|
||||||
|
uint8_t* restrict dst, const uint8_t* restrict src, size_t len);
|
||||||
|
int ws_write_frame_header(uint8_t* dst, const struct ws_frame_header* header);
|
18
meson.build
18
meson.build
|
@ -49,6 +49,7 @@ libm = cc.find_library('m', required: false)
|
||||||
pixman = dependency('pixman-1')
|
pixman = dependency('pixman-1')
|
||||||
libturbojpeg = dependency('libturbojpeg', required: get_option('jpeg'))
|
libturbojpeg = dependency('libturbojpeg', required: get_option('jpeg'))
|
||||||
gnutls = dependency('gnutls', required: get_option('tls'))
|
gnutls = dependency('gnutls', required: get_option('tls'))
|
||||||
|
nettle = dependency('nettle', required: get_option('nettle'))
|
||||||
zlib = dependency('zlib')
|
zlib = dependency('zlib')
|
||||||
gbm = dependency('gbm', required: get_option('gbm'))
|
gbm = dependency('gbm', required: get_option('gbm'))
|
||||||
libdrm = dependency('libdrm', required: get_option('h264'))
|
libdrm = dependency('libdrm', required: get_option('h264'))
|
||||||
|
@ -101,6 +102,8 @@ dependencies = [
|
||||||
libdrm_inc,
|
libdrm_inc,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
enable_websocket = false
|
||||||
|
|
||||||
config = configuration_data()
|
config = configuration_data()
|
||||||
|
|
||||||
if libturbojpeg.found()
|
if libturbojpeg.found()
|
||||||
|
@ -114,6 +117,11 @@ if gnutls.found()
|
||||||
config.set('ENABLE_TLS', true)
|
config.set('ENABLE_TLS', true)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if nettle.found()
|
||||||
|
dependencies += nettle
|
||||||
|
enable_websocket = true
|
||||||
|
endif
|
||||||
|
|
||||||
if host_system == 'linux' and get_option('systemtap') and cc.has_header('sys/sdt.h')
|
if host_system == 'linux' and get_option('systemtap') and cc.has_header('sys/sdt.h')
|
||||||
config.set('HAVE_USDT', true)
|
config.set('HAVE_USDT', true)
|
||||||
endif
|
endif
|
||||||
|
@ -130,6 +138,16 @@ if gbm.found() and libdrm.found() and libavcodec.found() and libavfilter.found()
|
||||||
config.set('HAVE_LIBAVUTIL', true)
|
config.set('HAVE_LIBAVUTIL', true)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if enable_websocket
|
||||||
|
sources += [
|
||||||
|
'src/ws-handshake.c',
|
||||||
|
'src/ws-framing.c',
|
||||||
|
'src/http.c',
|
||||||
|
'src/stream-ws.c',
|
||||||
|
]
|
||||||
|
config.set('ENABLE_WEBSOCKET', true)
|
||||||
|
endif
|
||||||
|
|
||||||
configure_file(
|
configure_file(
|
||||||
output: 'config.h',
|
output: 'config.h',
|
||||||
configuration: config,
|
configuration: config,
|
||||||
|
|
|
@ -3,6 +3,7 @@ option('examples', type: 'boolean', value: false, description: 'Build examples')
|
||||||
option('tests', type: 'boolean', value: false, description: 'Build unit tests')
|
option('tests', type: 'boolean', value: false, description: 'Build unit tests')
|
||||||
option('jpeg', type: 'feature', value: 'auto', description: 'Enable JPEG compression')
|
option('jpeg', type: 'feature', value: 'auto', description: 'Enable JPEG compression')
|
||||||
option('tls', type: 'feature', value: 'auto', description: 'Enable encryption & authentication')
|
option('tls', type: 'feature', value: 'auto', description: 'Enable encryption & authentication')
|
||||||
|
option('nettle', type: 'feature', value: 'auto', description: 'Enable nettle low level encryption library')
|
||||||
option('systemtap', type: 'boolean', value: false, description: 'Enable tracing using sdt')
|
option('systemtap', type: 'boolean', value: false, description: 'Enable tracing using sdt')
|
||||||
option('gbm', type: 'feature', value: 'auto', description: 'Enable GBM integration')
|
option('gbm', type: 'feature', value: 'auto', description: 'Enable GBM integration')
|
||||||
option('h264', type: 'feature', value: 'auto', description: 'Enable open h264 encoding')
|
option('h264', type: 'feature', value: 'auto', description: 'Enable open h264 encoding')
|
||||||
|
|
|
@ -0,0 +1,572 @@
|
||||||
|
/* Copyright (c) 2014-2016, Marel
|
||||||
|
* Copyright (c) 2023, Andri Yngvason
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include "vec.h"
|
||||||
|
#include "http.h"
|
||||||
|
|
||||||
|
enum httplex_token_type {
|
||||||
|
HTTPLEX_SOLIDUS,
|
||||||
|
HTTPLEX_CR,
|
||||||
|
HTTPLEX_LF,
|
||||||
|
HTTPLEX_WS,
|
||||||
|
HTTPLEX_LITERAL,
|
||||||
|
HTTPLEX_KEY,
|
||||||
|
HTTPLEX_VALUE,
|
||||||
|
HTTPLEX_QUERY,
|
||||||
|
HTTPLEX_AMPERSAND,
|
||||||
|
HTTPLEX_EQ,
|
||||||
|
HTTPLEX_END,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct httplex_token {
|
||||||
|
enum httplex_token_type type;
|
||||||
|
const char* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum httplex_state {
|
||||||
|
HTTPLEX_STATE_REQUEST = 0,
|
||||||
|
HTTPLEX_STATE_KEY,
|
||||||
|
HTTPLEX_STATE_VALUE,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct httplex {
|
||||||
|
enum httplex_state state;
|
||||||
|
struct httplex_token current_token;
|
||||||
|
const char* input;
|
||||||
|
const char* pos;
|
||||||
|
const char* next_pos;
|
||||||
|
struct vec buffer;
|
||||||
|
int accepted;
|
||||||
|
int errno_;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int httplex_init(struct httplex* self, const char* input)
|
||||||
|
{
|
||||||
|
memset(self, 0, sizeof(*self));
|
||||||
|
|
||||||
|
self->input = input;
|
||||||
|
self->pos = input;
|
||||||
|
self->accepted = 1;
|
||||||
|
|
||||||
|
if (vec_reserve(&self->buffer, 256) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void httplex_destroy(struct httplex* self)
|
||||||
|
{
|
||||||
|
vec_destroy(&self->buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int httplex__is_literal(char c)
|
||||||
|
{
|
||||||
|
switch (c) {
|
||||||
|
case '/': case '\r': case '\n': case ' ': case '\t':
|
||||||
|
case '?': case '&': case '=':
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isprint(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t httplex__literal_length(const char* str)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
while (httplex__is_literal(*str++))
|
||||||
|
++len;
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int httplex__classify_request_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
switch (*self->pos) {
|
||||||
|
case '/':
|
||||||
|
self->current_token.type = HTTPLEX_SOLIDUS;
|
||||||
|
self->next_pos = self->pos + strspn(self->pos, "/");
|
||||||
|
return 0;
|
||||||
|
case '\r':
|
||||||
|
self->current_token.type = HTTPLEX_CR;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case '\n':
|
||||||
|
self->current_token.type = HTTPLEX_LF;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case '?':
|
||||||
|
self->current_token.type = HTTPLEX_QUERY;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case '&':
|
||||||
|
self->current_token.type = HTTPLEX_AMPERSAND;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case '=':
|
||||||
|
self->current_token.type = HTTPLEX_EQ;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case ' ':
|
||||||
|
case '\t':
|
||||||
|
self->current_token.type = HTTPLEX_WS;
|
||||||
|
self->next_pos = self->pos + strspn(self->pos, " \t");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httplex__is_literal(*self->pos)) {
|
||||||
|
self->current_token.type = HTTPLEX_LITERAL;
|
||||||
|
size_t len = httplex__literal_length(self->pos);
|
||||||
|
self->next_pos = self->pos + len;
|
||||||
|
vec_assign(&self->buffer, self->pos, len);
|
||||||
|
vec_append(&self->buffer, "", 1);
|
||||||
|
self->current_token.value = self->buffer.data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int httplex__is_key_char(char c)
|
||||||
|
{
|
||||||
|
return isalnum(c) || c == '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t httplex__key_length(const char* str)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
while (httplex__is_key_char(*str++))
|
||||||
|
++len;
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int httplex__classify_key_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
switch (*self->pos) {
|
||||||
|
case '\r':
|
||||||
|
self->current_token.type = HTTPLEX_CR;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
case '\n':
|
||||||
|
self->current_token.type = HTTPLEX_LF;
|
||||||
|
self->next_pos = self->pos + 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!httplex__is_key_char(*self->pos))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
size_t len = httplex__key_length(self->pos);
|
||||||
|
|
||||||
|
if (self->pos[len] != ':')
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
len += 1;
|
||||||
|
|
||||||
|
self->next_pos = self->pos + len;
|
||||||
|
self->next_pos += strspn(self->next_pos, " \t");
|
||||||
|
|
||||||
|
vec_assign(&self->buffer, self->pos, len - 1);
|
||||||
|
vec_append(&self->buffer, "", 1);
|
||||||
|
|
||||||
|
self->current_token.type = HTTPLEX_KEY;
|
||||||
|
self->current_token.value = self->buffer.data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int httplex__classify_value_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
size_t len = strcspn(self->pos, "\r");
|
||||||
|
if (strncmp(&self->pos[len], "\r\n", 2) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
self->next_pos = self->pos + len + 2;
|
||||||
|
|
||||||
|
vec_assign(&self->buffer, self->pos, len);
|
||||||
|
vec_append(&self->buffer, "", 1);
|
||||||
|
|
||||||
|
self->current_token.type = HTTPLEX_VALUE;
|
||||||
|
self->current_token.value = self->buffer.data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int httplex__classify_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
switch (self->state) {
|
||||||
|
case HTTPLEX_STATE_REQUEST:
|
||||||
|
return httplex__classify_request_token(self);
|
||||||
|
case HTTPLEX_STATE_KEY:
|
||||||
|
return httplex__classify_key_token(self);
|
||||||
|
case HTTPLEX_STATE_VALUE:
|
||||||
|
return httplex__classify_value_token(self);
|
||||||
|
};
|
||||||
|
|
||||||
|
abort();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct httplex_token* httplex_next_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
if (self->current_token.type == HTTPLEX_END)
|
||||||
|
return &self->current_token;
|
||||||
|
|
||||||
|
if (!self->accepted)
|
||||||
|
return &self->current_token;
|
||||||
|
|
||||||
|
if (self->next_pos)
|
||||||
|
self->pos = self->next_pos;
|
||||||
|
|
||||||
|
if (httplex__classify_token(self) < 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
self->accepted = 0;
|
||||||
|
|
||||||
|
return &self->current_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int httplex_accept_token(struct httplex* self)
|
||||||
|
{
|
||||||
|
self->accepted = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__literal(struct httplex* lex, const char* str)
|
||||||
|
{
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_LITERAL)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (strcasecmp(str, tok->value) != 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__get(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
if (!http__literal(lex, "GET"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->method = HTTP_GET;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__put(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
if (!http__literal(lex, "PUT"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->method = HTTP_PUT;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__options(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
if (!http__literal(lex, "OPTIONS"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->method = HTTP_OPTIONS;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__method(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__get(req, lex)
|
||||||
|
|| http__put(req, lex)
|
||||||
|
|| http__options(req, lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__peek(struct httplex* lex, enum httplex_token_type type)
|
||||||
|
{
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != type)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__expect(struct httplex* lex, enum httplex_token_type type)
|
||||||
|
{
|
||||||
|
return http__peek(lex, type) && httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__version(struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__literal(lex, "HTTP")
|
||||||
|
&& http__expect(lex, HTTPLEX_SOLIDUS)
|
||||||
|
&& http__literal(lex, "1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__url_path(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
if (!http__expect(lex, HTTPLEX_SOLIDUS))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_LITERAL)
|
||||||
|
return tok->type == HTTPLEX_WS;
|
||||||
|
|
||||||
|
if (req->url_index >= URL_INDEX_MAX)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
char* elem = strdup(tok->value);
|
||||||
|
if (!elem)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->url[req->url_index++] = elem;
|
||||||
|
|
||||||
|
httplex_accept_token(lex);
|
||||||
|
|
||||||
|
return http__peek(lex, HTTPLEX_SOLIDUS)
|
||||||
|
? http__url_path(req, lex) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__url_query_key(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_LITERAL)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (req->url_index >= URL_INDEX_MAX)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
char* elem = strdup(tok->value);
|
||||||
|
if (!elem)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->url_query[req->url_query_index].key = elem;
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__url_query_value(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_LITERAL)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (req->url_index >= URL_INDEX_MAX)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
char* elem = strdup(tok->value);
|
||||||
|
if (!elem)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->url_query[req->url_query_index++].value = elem;
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__url_query(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__url_query_key(req, lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_EQ)
|
||||||
|
&& http__url_query_value(req, lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_AMPERSAND)
|
||||||
|
? http__url_query(req, lex) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__url(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__url_path(req, lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_QUERY) ? http__url_query(req, lex) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__request(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__method(req, lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_WS)
|
||||||
|
&& http__url(req, lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_WS)
|
||||||
|
&& http__version(lex)
|
||||||
|
&& http__expect(lex, HTTPLEX_CR)
|
||||||
|
&& http__expect(lex, HTTPLEX_LF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__expect_key(struct httplex* lex, const char* key)
|
||||||
|
{
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_KEY)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (key && strcasecmp(tok->value, key) != 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__content_length(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
lex->state = HTTPLEX_STATE_KEY;
|
||||||
|
if (!http__expect_key(lex, "Content-Length"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
lex->state = HTTPLEX_STATE_VALUE;
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_VALUE)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->content_length = atoi(tok->value);
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__content_type(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
lex->state = HTTPLEX_STATE_KEY;
|
||||||
|
if (!http__expect_key(lex, "Content-Type"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
lex->state = HTTPLEX_STATE_VALUE;
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_VALUE)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->content_type = strdup(tok->value);
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__field_key(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
lex->state = HTTPLEX_STATE_KEY;
|
||||||
|
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_KEY)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->field[req->field_index].key = strdup(tok->value);
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__field_value(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
lex->state = HTTPLEX_STATE_VALUE;
|
||||||
|
|
||||||
|
struct httplex_token* tok = httplex_next_token(lex);
|
||||||
|
if (!tok)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (tok->type != HTTPLEX_VALUE)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
req->field[req->field_index++].value = strdup(tok->value);
|
||||||
|
|
||||||
|
return httplex_accept_token(lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__field_kv(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__field_key(req, lex)
|
||||||
|
&& http__field_value(req, lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__header_kv(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
return http__content_length(req, lex)
|
||||||
|
|| http__content_type(req, lex)
|
||||||
|
|| http__field_kv(req, lex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int http__header(struct http_req* req, struct httplex* lex)
|
||||||
|
{
|
||||||
|
while (http__header_kv(req, lex));
|
||||||
|
|
||||||
|
lex->state = HTTPLEX_STATE_KEY;
|
||||||
|
if (http__expect(lex, HTTPLEX_CR))
|
||||||
|
return http__expect(lex, HTTPLEX_LF);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_req_parse(struct http_req* req, const char* input)
|
||||||
|
{
|
||||||
|
int rc = -1;
|
||||||
|
memset(req, 0, sizeof(*req));
|
||||||
|
|
||||||
|
struct httplex lex;
|
||||||
|
if (httplex_init(&lex, input) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (!http__request(req, &lex))
|
||||||
|
goto failure;
|
||||||
|
|
||||||
|
if (!http__header(req, &lex))
|
||||||
|
goto failure;
|
||||||
|
|
||||||
|
req->header_length = lex.next_pos - input;
|
||||||
|
|
||||||
|
rc = 0;
|
||||||
|
failure:
|
||||||
|
httplex_destroy(&lex);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void http_req_free(struct http_req* req)
|
||||||
|
{
|
||||||
|
free(req->content_type);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < req->url_index; ++i)
|
||||||
|
free(req->url[i]);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < req->url_query_index; ++i) {
|
||||||
|
free(req->url_query[i].key);
|
||||||
|
free(req->url_query[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < req->field_index; ++i) {
|
||||||
|
free(req->field[i].key);
|
||||||
|
free(req->field[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* http_req_query(struct http_req* req, const char* key)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < req->url_query_index; ++i)
|
||||||
|
if (strcmp(key, req->url_query[i].key) == 0)
|
||||||
|
return req->url_query[i].value;
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
45
src/server.c
45
src/server.c
|
@ -65,11 +65,6 @@
|
||||||
|
|
||||||
#define EXPORT __attribute__((visibility("default")))
|
#define EXPORT __attribute__((visibility("default")))
|
||||||
|
|
||||||
enum addrtype {
|
|
||||||
ADDRTYPE_TCP,
|
|
||||||
ADDRTYPE_UNIX,
|
|
||||||
};
|
|
||||||
|
|
||||||
static int send_desktop_resize(struct nvnc_client* client, struct nvnc_fb* fb);
|
static int send_desktop_resize(struct nvnc_client* client, struct nvnc_fb* fb);
|
||||||
static int send_qemu_key_ext_frame(struct nvnc_client* client);
|
static int send_qemu_key_ext_frame(struct nvnc_client* client);
|
||||||
static enum rfb_encodings choose_frame_encoding(struct nvnc_client* client,
|
static enum rfb_encodings choose_frame_encoding(struct nvnc_client* client,
|
||||||
|
@ -1195,7 +1190,16 @@ static void on_connection(void* obj)
|
||||||
|
|
||||||
record_peer_hostname(fd, client);
|
record_peer_hostname(fd, client);
|
||||||
|
|
||||||
client->net_stream = stream_new(fd, on_client_event, client);
|
#ifdef ENABLE_WEBSOCKET
|
||||||
|
if (server->socket_type == NVNC__SOCKET_WEBSOCKET)
|
||||||
|
{
|
||||||
|
client->net_stream = stream_ws_new(fd, on_client_event, client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
client->net_stream = stream_new(fd, on_client_event, client);
|
||||||
|
}
|
||||||
if (!client->net_stream) {
|
if (!client->net_stream) {
|
||||||
nvnc_log(NVNC_LOG_WARNING, "OOM");
|
nvnc_log(NVNC_LOG_WARNING, "OOM");
|
||||||
goto stream_failure;
|
goto stream_failure;
|
||||||
|
@ -1326,12 +1330,14 @@ static int bind_address_unix(const char* name)
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int bind_address(const char* name, uint16_t port, enum addrtype type)
|
static int bind_address(const char* name, uint16_t port,
|
||||||
|
enum nvnc__socket_type type)
|
||||||
{
|
{
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ADDRTYPE_TCP:
|
case NVNC__SOCKET_TCP:
|
||||||
|
case NVNC__SOCKET_WEBSOCKET:
|
||||||
return bind_address_tcp(name, port);
|
return bind_address_tcp(name, port);
|
||||||
case ADDRTYPE_UNIX:
|
case NVNC__SOCKET_UNIX:
|
||||||
return bind_address_unix(name);
|
return bind_address_unix(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1339,7 +1345,8 @@ static int bind_address(const char* name, uint16_t port, enum addrtype type)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct nvnc* open_common(const char* address, uint16_t port, enum addrtype type)
|
static struct nvnc* open_common(const char* address, uint16_t port,
|
||||||
|
enum nvnc__socket_type type)
|
||||||
{
|
{
|
||||||
nvnc__log_init();
|
nvnc__log_init();
|
||||||
|
|
||||||
|
@ -1349,6 +1356,8 @@ static struct nvnc* open_common(const char* address, uint16_t port, enum addrtyp
|
||||||
if (!self)
|
if (!self)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
self->socket_type = type;
|
||||||
|
|
||||||
strcpy(self->name, DEFAULT_NAME);
|
strcpy(self->name, DEFAULT_NAME);
|
||||||
|
|
||||||
LIST_INIT(&self->clients);
|
LIST_INIT(&self->clients);
|
||||||
|
@ -1374,7 +1383,7 @@ poll_start_failure:
|
||||||
handle_failure:
|
handle_failure:
|
||||||
listen_failure:
|
listen_failure:
|
||||||
close(self->fd);
|
close(self->fd);
|
||||||
if (type == ADDRTYPE_UNIX) {
|
if (type == NVNC__SOCKET_UNIX) {
|
||||||
unlink(address);
|
unlink(address);
|
||||||
}
|
}
|
||||||
bind_failure:
|
bind_failure:
|
||||||
|
@ -1386,13 +1395,23 @@ bind_failure:
|
||||||
EXPORT
|
EXPORT
|
||||||
struct nvnc* nvnc_open(const char* address, uint16_t port)
|
struct nvnc* nvnc_open(const char* address, uint16_t port)
|
||||||
{
|
{
|
||||||
return open_common(address, port, ADDRTYPE_TCP);
|
return open_common(address, port, NVNC__SOCKET_TCP);
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPORT
|
||||||
|
struct nvnc* nvnc_open_websocket(const char *address, uint16_t port)
|
||||||
|
{
|
||||||
|
#ifdef ENABLE_WEBSOCKET
|
||||||
|
return open_common(address, port, NVNC__SOCKET_WEBSOCKET);
|
||||||
|
#else
|
||||||
|
return NULL;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPORT
|
EXPORT
|
||||||
struct nvnc* nvnc_open_unix(const char* address)
|
struct nvnc* nvnc_open_unix(const char* address)
|
||||||
{
|
{
|
||||||
return open_common(address, 0, ADDRTYPE_UNIX);
|
return open_common(address, 0, NVNC__SOCKET_UNIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void unlink_fd_path(int fd)
|
static void unlink_fd_path(int fd)
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Andri Yngvason
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "stream.h"
|
||||||
|
#include "stream-common.h"
|
||||||
|
#include "websocket.h"
|
||||||
|
#include "neatvnc.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
|
||||||
|
enum stream_ws_state {
|
||||||
|
STREAM_WS_STATE_HANDSHAKE = 0,
|
||||||
|
STREAM_WS_STATE_READY,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct stream_ws {
|
||||||
|
struct stream base;
|
||||||
|
enum stream_ws_state ws_state;
|
||||||
|
struct ws_frame_header header;
|
||||||
|
enum ws_opcode current_opcode;
|
||||||
|
uint8_t read_buffer[4096]; // TODO: Is this a reasonable size?
|
||||||
|
size_t read_index;
|
||||||
|
struct stream* tcp_stream;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int stream_ws_close(struct stream* self)
|
||||||
|
{
|
||||||
|
struct stream_ws* ws = (struct stream_ws*)self;
|
||||||
|
self->state = STREAM_STATE_CLOSED;
|
||||||
|
return stream_close(ws->tcp_stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stream_ws_destroy(struct stream* self)
|
||||||
|
{
|
||||||
|
struct stream_ws* ws = (struct stream_ws*)self;
|
||||||
|
stream_destroy(ws->tcp_stream);
|
||||||
|
free(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stream_ws_read_into_buffer(struct stream_ws* ws)
|
||||||
|
{
|
||||||
|
ssize_t n_read = stream_read(ws->tcp_stream,
|
||||||
|
ws->read_buffer + ws->read_index,
|
||||||
|
sizeof(ws->read_buffer) - ws->read_index);
|
||||||
|
if (n_read > 0)
|
||||||
|
ws->read_index += n_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stream_ws_advance_read_buffer(struct stream_ws* ws, size_t size,
|
||||||
|
size_t offset)
|
||||||
|
{
|
||||||
|
size_t payload_len = MIN(size, ws->read_index - offset);
|
||||||
|
payload_len = MIN(payload_len, ws->header.payload_length);
|
||||||
|
|
||||||
|
ws->read_index -= offset + payload_len;
|
||||||
|
memmove(ws->read_buffer, ws->read_buffer + offset + payload_len,
|
||||||
|
ws->read_index);
|
||||||
|
ws->header.payload_length -= payload_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_copy_payload(struct stream_ws* ws, void* dst,
|
||||||
|
size_t size, size_t offset)
|
||||||
|
{
|
||||||
|
size_t payload_len = MIN(size, ws->read_index - offset);
|
||||||
|
payload_len = MIN(payload_len, ws->header.payload_length);
|
||||||
|
|
||||||
|
ws_copy_payload(&ws->header, dst, ws->read_buffer + offset, payload_len);
|
||||||
|
stream_ws_advance_read_buffer(ws, size, offset);
|
||||||
|
return payload_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_process_ping(struct stream_ws* ws, size_t offset)
|
||||||
|
{
|
||||||
|
if (offset > 0) {
|
||||||
|
// This means we're at the start, so send a header
|
||||||
|
struct ws_frame_header reply = {
|
||||||
|
.fin = true,
|
||||||
|
.opcode = WS_OPCODE_PONG,
|
||||||
|
.payload_length = ws->header.payload_length,
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t buf[WS_HEADER_MIN_SIZE];
|
||||||
|
int reply_len = ws_write_frame_header(buf, &reply);
|
||||||
|
stream_write(ws->tcp_stream, buf, reply_len, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int payload_len = MIN(ws->read_index, ws->header.payload_length);
|
||||||
|
|
||||||
|
// Feed back the payload:
|
||||||
|
stream_write(ws->tcp_stream, ws->read_buffer + offset,
|
||||||
|
payload_len, NULL, NULL);
|
||||||
|
|
||||||
|
stream_ws_advance_read_buffer(ws, payload_len, offset);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_process_payload(struct stream_ws* ws, void* dst,
|
||||||
|
size_t size, size_t offset)
|
||||||
|
{
|
||||||
|
switch (ws->current_opcode) {
|
||||||
|
case WS_OPCODE_CONT:
|
||||||
|
// Remote end started with a continuation frame. This is
|
||||||
|
// unexpected, so we'll just close.
|
||||||
|
stream__remote_closed(ws->tcp_stream);
|
||||||
|
return 0;
|
||||||
|
case WS_OPCODE_TEXT:
|
||||||
|
// This is unexpected, but let's just ignore it...
|
||||||
|
stream_ws_advance_read_buffer(ws, SIZE_MAX, offset);
|
||||||
|
return 0;
|
||||||
|
case WS_OPCODE_BIN:
|
||||||
|
return stream_ws_copy_payload(ws, dst, size, offset);
|
||||||
|
case WS_OPCODE_CLOSE:
|
||||||
|
stream__remote_closed(ws->tcp_stream);
|
||||||
|
return 0;
|
||||||
|
case WS_OPCODE_PING:
|
||||||
|
return stream_ws_process_ping(ws, offset);
|
||||||
|
case WS_OPCODE_PONG:
|
||||||
|
// Don't care
|
||||||
|
stream_ws_advance_read_buffer(ws, SIZE_MAX, offset);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We don't really care about framing. The binary data is just passed on as it
|
||||||
|
* arrives and it's not gathered into individual frames.
|
||||||
|
*/
|
||||||
|
static ssize_t stream_ws_read_frame(struct stream_ws* ws, void* dst,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
if (ws->header.payload_length > 0) {
|
||||||
|
nvnc_trace("Processing left-over payload chunk");
|
||||||
|
return stream_ws_process_payload(ws, dst, size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws_parse_frame_header(&ws->header, ws->read_buffer,
|
||||||
|
ws->read_index)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvnc_trace("Got frame header: opcode=%s, header-len: %zu, payload-len: %zu, read-buffer-len: %zu",
|
||||||
|
ws_opcode_name(ws->header.opcode),
|
||||||
|
ws->header.header_length, ws->header.payload_length,
|
||||||
|
ws->read_index);
|
||||||
|
|
||||||
|
if (ws->header.opcode != WS_OPCODE_CONT) {
|
||||||
|
ws->current_opcode = ws->header.opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The header is located at the start of the buffer, so an offset is
|
||||||
|
// needed.
|
||||||
|
return stream_ws_process_payload(ws, dst, size,
|
||||||
|
ws->header.header_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_read_ready(struct stream_ws* ws, void* dst,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
size_t total_read = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ssize_t n_read = stream_ws_read_frame(ws, dst, size);
|
||||||
|
if (n_read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (n_read < 0) {
|
||||||
|
if (errno == EAGAIN) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_read += n_read;
|
||||||
|
dst += n_read;
|
||||||
|
size -= n_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_read_handshake(struct stream_ws* ws, void* dst,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
char reply[512];
|
||||||
|
ssize_t header_len = ws_handshake(reply, sizeof(reply),
|
||||||
|
(const char*)ws->read_buffer);
|
||||||
|
if (header_len < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
ws->tcp_stream->cork = false;
|
||||||
|
stream_send_first(ws->tcp_stream, rcbuf_from_mem(reply, strlen(reply)));
|
||||||
|
|
||||||
|
ws->read_index -= header_len;
|
||||||
|
memmove(ws->read_buffer, ws->read_buffer + header_len, ws->read_index);
|
||||||
|
|
||||||
|
ws->ws_state = STREAM_WS_STATE_READY;
|
||||||
|
return stream_ws_read_ready(ws, dst, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t stream_ws_read(struct stream* self, void* dst, size_t size)
|
||||||
|
{
|
||||||
|
struct stream_ws* ws = (struct stream_ws*)self;
|
||||||
|
|
||||||
|
stream_ws_read_into_buffer(ws);
|
||||||
|
if (self->state == STREAM_STATE_CLOSED)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
switch (ws->ws_state) {
|
||||||
|
case STREAM_WS_STATE_HANDSHAKE:
|
||||||
|
return stream_ws_read_handshake(ws, dst, size);
|
||||||
|
case STREAM_WS_STATE_READY:
|
||||||
|
return stream_ws_read_ready(ws, dst, size);
|
||||||
|
}
|
||||||
|
abort();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stream_ws_send(struct stream* self, struct rcbuf* payload,
|
||||||
|
stream_req_fn on_done, void* userdata)
|
||||||
|
{
|
||||||
|
struct stream_ws* ws = (struct stream_ws*)self;
|
||||||
|
|
||||||
|
struct ws_frame_header head = {
|
||||||
|
.fin = true,
|
||||||
|
.opcode = WS_OPCODE_BIN,
|
||||||
|
.payload_length = payload->size,
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t raw_head[WS_HEADER_MIN_SIZE];
|
||||||
|
int head_len = ws_write_frame_header(raw_head, &head);
|
||||||
|
|
||||||
|
stream_write(ws->tcp_stream, &raw_head, head_len, NULL, NULL);
|
||||||
|
return stream_send(ws->tcp_stream, payload, on_done, userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stream_ws_event(struct stream* self, enum stream_event event)
|
||||||
|
{
|
||||||
|
struct stream_ws* ws = self->userdata;
|
||||||
|
|
||||||
|
if (event == STREAM_EVENT_REMOTE_CLOSED) {
|
||||||
|
ws->base.state = STREAM_STATE_CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws->base.on_event(&ws->base, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct stream_impl impl = {
|
||||||
|
.close = stream_ws_close,
|
||||||
|
.destroy = stream_ws_destroy,
|
||||||
|
.read = stream_ws_read,
|
||||||
|
.send = stream_ws_send,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct stream* stream_ws_new(int fd, stream_event_fn on_event, void* userdata)
|
||||||
|
{
|
||||||
|
struct stream_ws *self = calloc(1, sizeof(*self));
|
||||||
|
if (!self)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
self->base.state = STREAM_STATE_NORMAL;
|
||||||
|
self->base.impl = &impl;
|
||||||
|
self->base.on_event = on_event;
|
||||||
|
self->base.userdata = userdata;
|
||||||
|
|
||||||
|
self->tcp_stream = stream_new(fd, stream_ws_event, self);
|
||||||
|
if (!self->tcp_stream) {
|
||||||
|
free(self);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't send anything until handshake is done:
|
||||||
|
self->tcp_stream->cork = true;
|
||||||
|
|
||||||
|
return &self->base;
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
#include "websocket.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
static inline uint64_t u64_from_network_order(uint64_t x)
|
||||||
|
{
|
||||||
|
#if __BYTE_ORDER__ == __BIG_ENDIAN__
|
||||||
|
return x;
|
||||||
|
#else
|
||||||
|
return __builtin_bswap64(x);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint64_t u64_to_network_order(uint64_t x)
|
||||||
|
{
|
||||||
|
#if __BYTE_ORDER__ == __BIG_ENDIAN__
|
||||||
|
return x;
|
||||||
|
#else
|
||||||
|
return __builtin_bswap64(x);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ws_opcode_name(enum ws_opcode op)
|
||||||
|
{
|
||||||
|
switch (op) {
|
||||||
|
case WS_OPCODE_CONT: return "cont";
|
||||||
|
case WS_OPCODE_TEXT: return "text";
|
||||||
|
case WS_OPCODE_BIN: return "bin";
|
||||||
|
case WS_OPCODE_CLOSE: return "close";
|
||||||
|
case WS_OPCODE_PING: return "ping";
|
||||||
|
case WS_OPCODE_PONG: return "pong";
|
||||||
|
}
|
||||||
|
return "INVALID";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ws_parse_frame_header(struct ws_frame_header* header,
|
||||||
|
const uint8_t* payload, size_t length)
|
||||||
|
{
|
||||||
|
if (length < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
header->fin = !!(payload[i] & 0x80);
|
||||||
|
header->opcode = (payload[i++] & 0x0f);
|
||||||
|
header->mask = !!(payload[i] & 0x80);
|
||||||
|
header->payload_length = payload[i++] & 0x7f;
|
||||||
|
|
||||||
|
if (header->payload_length == 126) {
|
||||||
|
if (length - i < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint16_t value = 0;
|
||||||
|
memcpy(&value, &payload[i], 2);
|
||||||
|
header->payload_length = ntohs(value);
|
||||||
|
i += 2;
|
||||||
|
} else if (header->payload_length == 127) {
|
||||||
|
if (length - i < 8)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t value = 0;
|
||||||
|
memcpy(&value, &payload[i], 8);
|
||||||
|
header->payload_length = u64_from_network_order(value);
|
||||||
|
i += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->mask) {
|
||||||
|
if (length - i < 4)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
memcpy(header->masking_key, &payload[i], 4);
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
header->header_length = i;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_apply_mask(const struct ws_frame_header* header,
|
||||||
|
uint8_t* restrict payload)
|
||||||
|
{
|
||||||
|
assert(header->mask);
|
||||||
|
|
||||||
|
uint64_t len = header->payload_length;
|
||||||
|
const uint8_t* restrict key = header->masking_key;
|
||||||
|
|
||||||
|
for (uint64_t i = 0; i < len; ++i) {
|
||||||
|
payload[i] ^= key[i % 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_copy_payload(const struct ws_frame_header* header,
|
||||||
|
uint8_t* restrict dst, const uint8_t* restrict src, size_t len)
|
||||||
|
{
|
||||||
|
if (!header->mask) {
|
||||||
|
memcpy(dst, src, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* restrict key = header->masking_key;
|
||||||
|
for (uint64_t i = 0; i < len; ++i) {
|
||||||
|
dst[i] = src[i] ^ key[i % 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ws_write_frame_header(uint8_t* dst, const struct ws_frame_header* header)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
dst[i++] = ((uint8_t)header->fin << 7) | (header->opcode);
|
||||||
|
|
||||||
|
if (header->payload_length <= 125) {
|
||||||
|
dst[i++] = ((uint8_t)header->mask << 7) | header->payload_length;
|
||||||
|
} else if (header->payload_length <= UINT16_MAX) {
|
||||||
|
dst[i++] = ((uint8_t)header->mask << 7) | 126;
|
||||||
|
uint16_t be = htons(header->payload_length);
|
||||||
|
memcpy(&dst[i], &be, 2);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
dst[i++] = ((uint8_t)header->mask << 7) | 127;
|
||||||
|
uint64_t be = u64_to_network_order(header->payload_length);
|
||||||
|
memcpy(&dst[i], &be, 8);
|
||||||
|
i += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->mask) {
|
||||||
|
memcpy(dst, header->masking_key, 4);
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
#include "websocket.h"
|
||||||
|
#include "http.h"
|
||||||
|
|
||||||
|
#include <nettle/sha1.h>
|
||||||
|
#include <nettle/base64.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
static const char magic_uuid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
|
||||||
|
// TODO: Do some more sanity checks on the input
|
||||||
|
ssize_t ws_handshake(char* output, size_t output_maxlen, const char* input)
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
struct http_req req = {};
|
||||||
|
if (http_req_parse(&req, input) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
const char *challenge = NULL;
|
||||||
|
for (size_t i = 0; i < req.field_index; ++i) {
|
||||||
|
if (strcasecmp(req.field[i].key, "Sec-WebSocket-Key") == 0) {
|
||||||
|
challenge = req.field[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!challenge)
|
||||||
|
goto failure;
|
||||||
|
|
||||||
|
struct sha1_ctx ctx;
|
||||||
|
sha1_init(&ctx);
|
||||||
|
sha1_update(&ctx, strlen(challenge), (const uint8_t*)challenge);
|
||||||
|
sha1_update(&ctx, strlen(magic_uuid), (const uint8_t*)magic_uuid);
|
||||||
|
|
||||||
|
uint8_t hash[SHA1_DIGEST_SIZE];
|
||||||
|
sha1_digest(&ctx, sizeof(hash), hash);
|
||||||
|
|
||||||
|
char response[BASE64_ENCODE_RAW_LENGTH(SHA1_DIGEST_SIZE) + 1] = {};
|
||||||
|
base64_encode_raw(response, SHA1_DIGEST_SIZE, hash);
|
||||||
|
|
||||||
|
size_t len = snprintf(output, output_maxlen,
|
||||||
|
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
"Upgrade: websocket\r\n"
|
||||||
|
"Connection: Upgrade\r\n"
|
||||||
|
"Sec-WebSocket-Accept: %s\r\n"
|
||||||
|
"Sec-WebSocket-Protocol: chat\r\n"
|
||||||
|
"\r\n",
|
||||||
|
response);
|
||||||
|
|
||||||
|
ssize_t header_len = req.header_length;
|
||||||
|
ok = len < output_maxlen;
|
||||||
|
failure:
|
||||||
|
http_req_free(&req);
|
||||||
|
return ok ? header_len : -1;
|
||||||
|
}
|
Loading…
Reference in New Issue