diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 2daad08..7cd000e 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,7 @@
+Quisk Version 4.2.51 May 2026
+==============================
+This version can use the TCI protocol to connect to WSJT-X. Please see Digital Modes in the documentation.
+
Quisk Version 4.2.50 December 2025
===================================
This is a quick fix for a bug in 4.2.49 that results in the repeated message
diff --git a/__init__.py b/__init__.py
index 6aa31f5..c480d5f 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,2 +1,2 @@
-#Quisk version 4.2.50
+#Quisk version 4.2.51
from .quisk import main
diff --git a/base64.c b/base64.c
new file mode 100644
index 0000000..87d23b6
--- /dev/null
+++ b/base64.c
@@ -0,0 +1,155 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005-2011, Jouni Malinen
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include
+#include
+#include
+#include "base64.h"
+
+static const unsigned char base64_table[65] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/**
+ * base64_encode - Base64 encode
+ * @src: Data to be encoded
+ * @len: Length of the data to be encoded
+ * @out_len: Pointer to output length variable, or %NULL if not used
+ * Returns: Allocated buffer of out_len bytes of encoded data,
+ * or %NULL on failure
+ *
+ * Caller is responsible for freeing the returned buffer. Returned buffer is
+ * nul terminated to make it easier to use as a C string. The nul terminator is
+ * not included in out_len.
+ */
+unsigned char * base64_encode(const unsigned char *src, size_t len,
+ size_t *out_len)
+{
+ unsigned char *out, *pos;
+ const unsigned char *end, *in;
+ size_t olen;
+ int line_len;
+
+ olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
+ olen += olen / 72; /* line feeds */
+ olen++; /* nul termination */
+ if (olen < len)
+ return NULL; /* integer overflow */
+ out = malloc(olen);
+ if (out == NULL)
+ return NULL;
+
+ end = src + len;
+ in = src;
+ pos = out;
+ line_len = 0;
+ while (end - in >= 3) {
+ *pos++ = base64_table[in[0] >> 2];
+ *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+ *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
+ *pos++ = base64_table[in[2] & 0x3f];
+ in += 3;
+ line_len += 4;
+ if (line_len >= 72) {
+ *pos++ = '\n';
+ line_len = 0;
+ }
+ }
+
+ if (end - in) {
+ *pos++ = base64_table[in[0] >> 2];
+ if (end - in == 1) {
+ *pos++ = base64_table[(in[0] & 0x03) << 4];
+ *pos++ = '=';
+ } else {
+ *pos++ = base64_table[((in[0] & 0x03) << 4) |
+ (in[1] >> 4)];
+ *pos++ = base64_table[(in[1] & 0x0f) << 2];
+ }
+ *pos++ = '=';
+ line_len += 4;
+ }
+
+ if (line_len)
+ *pos++ = '\n';
+
+ *pos = '\0';
+ if (out_len)
+ *out_len = pos - out;
+ return out;
+}
+
+
+/**
+ * base64_decode - Base64 decode
+ * @src: Data to be decoded
+ * @len: Length of the data to be decoded
+ * @out_len: Pointer to output length variable
+ * Returns: Allocated buffer of out_len bytes of decoded data,
+ * or %NULL on failure
+ *
+ * Caller is responsible for freeing the returned buffer.
+ */
+unsigned char * base64_decode(const unsigned char *src, size_t len,
+ size_t *out_len)
+{
+ unsigned char dtable[256], *out, *pos, block[4], tmp;
+ size_t i, count, olen;
+ int pad = 0;
+
+ memset(dtable, 0x80, 256);
+ for (i = 0; i < sizeof(base64_table) - 1; i++)
+ dtable[base64_table[i]] = (unsigned char) i;
+ dtable['='] = 0;
+
+ count = 0;
+ for (i = 0; i < len; i++) {
+ if (dtable[src[i]] != 0x80)
+ count++;
+ }
+
+ if (count == 0 || count % 4)
+ return NULL;
+
+ olen = count / 4 * 3;
+ pos = out = malloc(olen);
+ if (out == NULL)
+ return NULL;
+
+ count = 0;
+ for (i = 0; i < len; i++) {
+ tmp = dtable[src[i]];
+ if (tmp == 0x80)
+ continue;
+
+ if (src[i] == '=')
+ pad++;
+ block[count] = tmp;
+ count++;
+ if (count == 4) {
+ *pos++ = (block[0] << 2) | (block[1] >> 4);
+ *pos++ = (block[1] << 4) | (block[2] >> 2);
+ *pos++ = (block[2] << 6) | block[3];
+ count = 0;
+ if (pad) {
+ if (pad == 1)
+ pos--;
+ else if (pad == 2)
+ pos -= 2;
+ else {
+ /* Invalid padding */
+ free(out);
+ return NULL;
+ }
+ break;
+ }
+ }
+ }
+
+ *out_len = pos - out;
+ return out;
+}
diff --git a/base64.h b/base64.h
new file mode 100644
index 0000000..5ea7f13
--- /dev/null
+++ b/base64.h
@@ -0,0 +1,19 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005, Jouni Malinen
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef BASE64_H
+#define BASE64_H
+
+#include
+
+unsigned char * base64_encode(const unsigned char *src, size_t len,
+ size_t *out_len);
+unsigned char * base64_decode(const unsigned char *src, size_t len,
+ size_t *out_len);
+
+#endif /* BASE64_H */
diff --git a/configure.py b/configure.py
index 5c61055..eb07d10 100644
--- a/configure.py
+++ b/configure.py
@@ -4203,6 +4203,9 @@ class RadioRemote(BaseWindow): # The Remote page in the second-level notebook f
heading = None
self.NextRow()
self.col = 2
+ if not heading:
+ heading = self.AddTextHeading("TCI", text,
+'Rig control commands and digital sample streams are sent to multiple clients using the TCI protocol.')
if not heading:
heading = self.AddTextHeading("Hamlib Rig 2", text,
'Rig control commands using "Hamlib Rig 2" are sent to a Quisk TCP port from multiple clients.')
diff --git a/docs.html b/docs.html
index 4e3f2bb..7483689 100644
--- a/docs.html
+++ b/docs.html
@@ -1434,9 +1434,35 @@ is no longer needed
Digital Modes
+
+Digital Modes with the TCI Protocol
+Using the sound devices in a PC to transfer digital sample streams has always caused problems.
+Several new methods are now becoming available. These are the TCI protocol
+used by Expert Electronics for
+their SunSDR hardware
+and Vita49 used by FlexRadio.
+And Hamlib is planning to add digital stream support.
+
+
+
+Quisk can now connect to WSJT-X version 3.0.0 with the TCI protocol.
+On the Quisk Config/radio/Remote screen enter IP address 127.0.0.1 and port 40001.
+Use USB for digital modes. The DGT-* modes are for sound cards.
+In WSJT-X choose the radio "TCI Client RX1", and for audio choose "TCI audio".
+And enter the same IP and port. Everything then just works.
+If your WSJT-X does not have "TCI Client RX1" you need to update to a more recent version.
+This feature is new. To see the TCI conversation, use the "Debug level" on the Config/radio/Options screen.
+
+
+Digital Modes with Sound Cards
+
+Digital modes need to receive the audio from the radio to decode it.
+They need to send audio to the radio to transmit.
+These audio streams have traditionally been sent using PC sound cards or
+an artificial sound card known as a Virtual Audio Cable.
Quisk has a number of modes "DGT-" to receive and transmit digital
-signals. The modes "DGT-U" and "DGT-L" decode the signal as upper
+signals on sound devices. The modes "DGT-U" and "DGT-L" decode the signal as upper
or lower sideband, and send the stereo audio to the digital sound
device. The left and right channel are the same.
The bandwidth is set with the filter buttons as
diff --git a/handshake.c b/handshake.c
new file mode 100644
index 0000000..5124315
--- /dev/null
+++ b/handshake.c
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016-2024 Davidson Francis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ */
+#define _POSIX_C_SOURCE 200809L
+#include "base64.h"
+#include "sha1.h"
+#include "ws.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * @dir src/
+ * @brief Handshake routines directory
+ *
+ * @file handshake.c
+ * @brief Handshake routines.
+ */
+
+/**
+ * @brief Gets the field Sec-WebSocket-Accept on response, by
+ * an previously informed key.
+ *
+ * @param wsKey Sec-WebSocket-Key
+ * @param dest source to be stored the value.
+ *
+ * @return Returns 0 if success and a negative number
+ * otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+int get_handshake_accept(char *wsKey, unsigned char **dest)
+{
+ unsigned char hash[SHA1HashSize]; /* SHA-1 Hash. */
+ SHA1Context ctx; /* SHA-1 Context. */
+ char *str; /* WebSocket key + magic string. */
+
+ /* Invalid key. */
+ if (!wsKey)
+ return (-1);
+
+ str = calloc(1, sizeof(char) * (WS_KEY_LEN + WS_MS_LEN + 1));
+ if (!str)
+ return (-1);
+
+ strncpy(str, wsKey, WS_KEY_LEN);
+ strcat(str, MAGIC_STRING);
+
+ SHA1Reset(&ctx);
+ SHA1Input(&ctx, (const uint8_t *)str, WS_KEYMS_LEN);
+ SHA1Result(&ctx, hash);
+
+ *dest = base64_encode(hash, SHA1HashSize, NULL);
+ *(*dest + strlen((const char *)*dest) - 1) = '\0';
+ free(str);
+ return (0);
+}
+
+/**
+ * @brief Finds the ocorrence of @p needle in @p haystack, case
+ * insensitive.
+ *
+ * @param haystack Target string to be searched.
+ * @param needle Substring to search for.
+ *
+ * @returns If found, returns a pointer at the beginning of the
+ * found substring. Otherwise, returns NULL.
+ */
+static const char *strstricase(const char *haystack, const char *needle)
+{
+ size_t length;
+ for (length = strlen(needle); *haystack; haystack++)
+ if (!strncasecmp(haystack, needle, length))
+ return haystack;
+ return (NULL);
+}
+
+/**
+ * @brief Gets the complete response to accomplish a succesfully
+ * handshake.
+ *
+ * @param hsrequest Client request.
+ * @param hsresponse Server response.
+ *
+ * @return Returns 0 if success and a negative number
+ * otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+int get_handshake_response(char *hsrequest, char **hsresponse)
+{
+ unsigned char *accept; /* Accept message. */
+ char *saveptr; /* strtok_r() pointer. */
+ char *s; /* Current string. */
+ int ret; /* Return value. */
+
+ saveptr = NULL;
+ for (s = strtok_r(hsrequest, "\r\n", &saveptr); s != NULL;
+ s = strtok_r(NULL, "\r\n", &saveptr))
+ {
+ if (strstricase(s, WS_HS_REQ) != NULL)
+ break;
+ }
+
+ /* Ensure that we have a valid pointer. */
+ if (s == NULL)
+ return (-1);
+
+ saveptr = NULL;
+ s = strtok_r(s, " ", &saveptr);
+ s = strtok_r(NULL, " ", &saveptr);
+
+ ret = get_handshake_accept(s, &accept);
+ if (ret < 0)
+ return (ret);
+
+ *hsresponse = malloc(sizeof(char) * WS_HS_ACCLEN);
+ if (*hsresponse == NULL)
+ return (-1);
+
+ strcpy(*hsresponse, WS_HS_ACCEPT);
+ strcat(*hsresponse, (const char *)accept);
+ strcat(*hsresponse, "\r\n\r\n");
+
+ free(accept);
+ return (0);
+}
diff --git a/microphone.c b/microphone.c
index cc8a99c..ba66d2e 100644
--- a/microphone.c
+++ b/microphone.c
@@ -1219,6 +1219,10 @@ int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int
count *= interp;
transmit_mic_carrier(cSamples, count, quiskSpotLevel / 1000.0);
}
+ else if (tci_tx_audio_client) {
+ count = tx_filter_digital(cSamples, count); // filter samples, minimal processing
+ process_alc(cSamples, count, &tx_alc, DGT_U);
+ }
else switch (rxMode) {
case LSB: // LSB
case USB: // USB
diff --git a/quisk.c b/quisk.c
index b511599..743571b 100644
--- a/quisk.c
+++ b/quisk.c
@@ -6188,6 +6188,8 @@ static PyMethodDef QuiskMethods[] = {
{"watfall_GetPixels", watfall_GetPixels, METH_VARARGS, "Write the Waterfall image to be displayed."},
{"write_fftw_wisdom", write_fftw_wisdom, METH_VARARGS, "Write the current fftw wisdom to the wisdom file."},
{"read_fftw_wisdom", read_fftw_wisdom, METH_VARARGS, "Return the current fftw wisdom as a byte array."},
+ {"tci_get_params", (PyCFunction)quisk_tci_get_params, METH_VARARGS, "Return parameters from TCI."},
+ {"tci_set_params", (PyCFunction)quisk_tci_set_params, METH_VARARGS|METH_KEYWORDS, "Set parameters for TCI."},
// Remote Quisk control head and slave by Ben, AC2YD
{"start_control_head_remote_sound", quisk_start_control_head_remote_sound, METH_VARARGS, "Start running UDP remote sound on control_head."},
{"stop_control_head_remote_sound", quisk_stop_control_head_remote_sound, METH_VARARGS, "Stop running UDP remote sound on control_head."},
diff --git a/quisk.h b/quisk.h
index d22f73f..9df9b97 100644
--- a/quisk.h
+++ b/quisk.h
@@ -260,6 +260,13 @@ extern PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject
extern int receive_graph_data(double * fft_avg);
extern void send_graph_data(double * fft_avg, int fft_size, double zoom, double deltaf, int fft_sample_rate, double scale);
+// TCI server
+PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds);
+PyObject * quisk_tci_get_params(PyObject * self, PyObject * args);
+void tci_send_audio(complex double * cSamples, int nSamples);
+int tci_get_mic(complex double * cSamples, int mic_count);
+extern uint64_t tci_tx_audio_client; // This is ws_cli_conn_t defined in ws.h.
+
void QuiskWavClose(struct QuiskWav *);
int QuiskWavWriteOpen(struct QuiskWav *, char *, short, short, short, int, double);
void QuiskWavWriteC(struct QuiskWav *, complex double *, int);
@@ -394,6 +401,7 @@ int quisk_play_sidetone(struct sound_dev *);
void quisk_set_play_state(void);
void quisk_poll_hardware_key(void);
void PreDistort(complex double * amp_in_samples, complex double * amp_out_samples, int nSamples, complex double * tx_samples, int num_tx);
+int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite);
// Driver function definitions=================================================
int quisk_read_alsa(struct sound_dev *, complex double *);
diff --git a/quisk.py b/quisk.py
index 623d650..cf44bec 100644
--- a/quisk.py
+++ b/quisk.py
@@ -3938,6 +3938,7 @@ class App(wx.App):
self.hot_key_ptt_active = False
self.serial_ptt_active = False
self.vox_ptt_active = False
+ self.tci_ptt_active = False
self.hermes_LNA_dB = 20
self.hermes_atten_dB = 0
if hasattr(Hardware, "OnChangeRxTx"):
@@ -4028,6 +4029,8 @@ class App(wx.App):
self.file_name_play_samples = ''
self.file_name_play_cq = ''
self.midiControls = {} # Control object and associated function for Midi control name
+ self.tci_vfo = 0
+ self.tci_started = False
# get the screen size - thanks to Lucian Langa
x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() # Using display index 0
self.Bind(wx.EVT_IDLE, self.OnIdle)
@@ -4353,6 +4356,19 @@ class App(wx.App):
if hasattr(bw, "sliderLNA"):
self.midiControls["RfLna"] = (bw.sliderLNA, bw.OnLNA)
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyHook)
+ # Start the TCI server
+ if conf.tci_ip and conf.tci_port:
+ value = getattr(conf, "pulse_audio_verbose_output")
+ QS.tci_set_params(verbose=value)
+ QS.tci_set_params(start=1);
+ self.tci_started = True
+ QS.tci_set_params(tci_dds=self.VFO)
+ QS.tci_set_params(tci_if=self.txFreq)
+ self.tci_vfo = self.txFreq + self.VFO
+ QS.tci_set_params(tci_vfo=self.tci_vfo)
+ QS.tci_set_params(tci_modulation=self.mode)
+ QS.tci_set_params(tci_trx=0)
+ QS.tci_set_params(tci_split_enable=0)
return True
def OnStartWsjtx(self, ctrl):
method = ctrl.GetValue()
@@ -4762,6 +4778,7 @@ class App(wx.App):
def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output":
value = getattr(conf, name)
+ QS.tci_set_params(verbose=value)
if value == 0:
self.std_out_err.Show(False)
else:
@@ -5765,6 +5782,7 @@ class App(wx.App):
QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay)
def OnBtnSplit(self, event): # Called when the Split check button is pressed
self.split_rxtx = self.splitButton.GetValue()
+ QS.tci_set_params(tci_split_enable=self.split_rxtx)
if self.split_rxtx:
if self.split_offset == 0:
if self.mode in ("CWL", "CWU"):
@@ -6150,6 +6168,7 @@ class App(wx.App):
self.ChangeRxTxFrequency(self.rxFreq + delta + self.VFO, self.txFreq + delta + self.VFO)
Hardware.ChangeMode(mode)
self.mode = mode
+ QS.tci_set_params(tci_modulation=mode)
self.MakeFilterButtons(self.Mode2Filters(mode))
QS.set_rx_mode(Mode2Index.get(mode, 3))
if mode == 'CWL':
@@ -6677,11 +6696,13 @@ class App(wx.App):
if not self.tx_indicator:
self.tx_indicator = True
self.pttButton.Tx.TurnOn(True)
+ QS.tci_set_params(tci_trx=1)
else:
if self.tx_indicator:
self.tx_indicator = False
self.pttButton.Tx.TurnOn(False)
- if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT
+ QS.tci_set_params(tci_trx=0)
+ if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT, TCI
ptt_button_down = self.pttButton.GetValue()
ptt = None
if conf.quisk_serial_cts[0:4] == "PTT " or conf.quisk_serial_dsr[0:4] == "PTT ":
@@ -6736,6 +6757,14 @@ class App(wx.App):
self.hot_key_ptt_pressed = False
if self.tx_inhibit:
ptt = False
+ tci = QS.tci_get_params("tci_trx")
+ if tci is not None:
+ if tci:
+ self.tci_ptt_active = True
+ ptt = True
+ elif self.tci_ptt_active:
+ self.tci_ptt_active = False
+ ptt = False
if ptt is True and not ptt_button_down:
self.SetPTT(True)
elif ptt is False and ptt_button_down:
@@ -6803,6 +6832,19 @@ class App(wx.App):
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
self.heart_time0 = self.timer
Hardware.HeartBeat()
+ if self.tci_started:
+ if self.tci_vfo != self.txFreq + self.VFO: # limit the speed of frequency updates
+ self.tci_vfo = self.txFreq + self.VFO
+ QS.tci_set_params(tci_vfo=self.tci_vfo)
+ freq = QS.tci_get_params("tci_vfo")
+ if freq is not None: # TCI frequency
+ self.ChangeRxTxFrequency(freq, None)
+ mode = QS.tci_get_params("tci_modulation")
+ if mode is not None: # TCI mode
+ self.modeButns.SetLabel(mode, True)
+ split = QS.tci_get_params("tci_split_enable")
+ if split is not None: # TCI split_enable
+ self.splitButton.SetValue(split, True)
if self.is_HermesLite2:
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
else:
diff --git a/quisk_conf_defaults.py b/quisk_conf_defaults.py
index 4302c6c..2f88e77 100644
--- a/quisk_conf_defaults.py
+++ b/quisk_conf_defaults.py
@@ -1423,6 +1423,19 @@ fixed_tune_offset = 0
################ Remote
+## tci_ip IP address for TCI, text
+# Quisk can connect to WSJT-X and other digital programs using the TCI protocol.
+# Enter the TCI server host name.
+tci_ip = "127.0.0.1"
+
+## tci_port IP port for TCI, integer
+# Quisk can connect to WSJT-X and other digital programs using the TCI protocol.
+# Enter the TCI port number. If the port number is zero, TCI is turned off.
+# The preferred port is 40001.
+tci_port = 0
+#tci_port = 40001
+#tci_port = 50001
+
## hamlib_ip IP address for Hamlib Rig 2, text
# You can control Quisk from Hamlib. Set the Hamlib rig to 2 and the device for rig 2 to
# localhost:4575. Or choose a different name and port here. Set the same name and port
diff --git a/setup.py b/setup.py
index f3ab525..eb5ef92 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py.
-Version = '4.2.50'
+Version = '4.2.51'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)
@@ -16,7 +16,8 @@ fp.close()
sources = ['quisk.c', 'sound.c', 'is_key_down.c', 'microphone.c', 'utility.c',
'sound_alsa.c', 'sound_pulseaudio.c', 'sound_portaudio.c', 'sound_directx.c', 'sound_wasapi.c',
- 'filter.c', 'extdemod.c', 'freedv.c', 'quisk_wdsp.c', 'ac2yd/remote.c']
+ 'filter.c', 'extdemod.c', 'freedv.c', 'quisk_wdsp.c', 'ac2yd/remote.c',
+ 'tci.c', 'base64.c', 'handshake.c', 'sha1.c', 'utf8.c', 'ws.c']
# Afedri hardware support added by Alex, Alex@gmail.com
mAfedri = Extension ('quisk.afedrinet.afedrinet_io',
diff --git a/sha1.c b/sha1.c
new file mode 100644
index 0000000..824c170
--- /dev/null
+++ b/sha1.c
@@ -0,0 +1,389 @@
+/*
+ * sha1.c
+ *
+ * Description:
+ * This file implements the Secure Hashing Algorithm 1 as
+ * defined in FIPS PUB 180-1 published April 17, 1995.
+ *
+ * The SHA-1, produces a 160-bit message digest for a given
+ * data stream. It should take about 2**n steps to find a
+ * message with the same digest as a given message and
+ * 2**(n/2) to find any two messages with the same digest,
+ * when n is the digest size in bits. Therefore, this
+ * algorithm can serve as a means of providing a
+ * "fingerprint" for a message.
+ *
+ * Portability Issues:
+ * SHA-1 is defined in terms of 32-bit "words". This code
+ * uses (included via "sha1.h" to define 32 and 8
+ * bit unsigned integer types. If your C compiler does not
+ * support 32 bit unsigned integers, this code is not
+ * appropriate.
+ *
+ * Caveats:
+ * SHA-1 is designed to work with messages less than 2^64 bits
+ * long. Although SHA-1 allows a message digest to be generated
+ * for messages of any number of bits less than 2^64, this
+ * implementation only works with messages with a length that is
+ * a multiple of the size of an 8-bit character.
+ *
+ */
+
+#include "sha1.h"
+
+/*
+ * Define the SHA1 circular left shift macro
+ */
+#define SHA1CircularShift(bits,word) \
+ (((word) << (bits)) | ((word) >> (32-(bits))))
+
+/* Local Function Prototyptes */
+void SHA1PadMessage(SHA1Context *);
+void SHA1ProcessMessageBlock(SHA1Context *);
+
+/*
+ * SHA1Reset
+ *
+ * Description:
+ * This function will initialize the SHA1Context in preparation
+ * for computing a new SHA1 message digest.
+ *
+ * Parameters:
+ * context: [in/out]
+ * The context to reset.
+ *
+ * Returns:
+ * sha Error Code.
+ *
+ */
+int SHA1Reset(SHA1Context *context)
+{
+ if (!context)
+ {
+ return shaNull;
+ }
+
+ context->Length_Low = 0;
+ context->Length_High = 0;
+ context->Message_Block_Index = 0;
+
+ context->Intermediate_Hash[0] = 0x67452301;
+ context->Intermediate_Hash[1] = 0xEFCDAB89;
+ context->Intermediate_Hash[2] = 0x98BADCFE;
+ context->Intermediate_Hash[3] = 0x10325476;
+ context->Intermediate_Hash[4] = 0xC3D2E1F0;
+
+ context->Computed = 0;
+ context->Corrupted = 0;
+
+ return shaSuccess;
+}
+
+/*
+ * SHA1Result
+ *
+ * Description:
+ * This function will return the 160-bit message digest into the
+ * Message_Digest array provided by the caller.
+ * NOTE: The first octet of hash is stored in the 0th element,
+ * the last octet of hash in the 19th element.
+ *
+ * Parameters:
+ * context: [in/out]
+ * The context to use to calculate the SHA-1 hash.
+ * Message_Digest: [out]
+ * Where the digest is returned.
+ *
+ * Returns:
+ * sha Error Code.
+ *
+ */
+int SHA1Result( SHA1Context *context,
+ uint8_t Message_Digest[SHA1HashSize])
+{
+ int i;
+
+ if (!context || !Message_Digest)
+ {
+ return shaNull;
+ }
+
+ if (context->Corrupted)
+ {
+ return context->Corrupted;
+ }
+
+ if (!context->Computed)
+ {
+ SHA1PadMessage(context);
+ for(i=0; i<64; ++i)
+ {
+ /* message may be sensitive, clear it out */
+ context->Message_Block[i] = 0;
+ }
+ context->Length_Low = 0; /* and clear length */
+ context->Length_High = 0;
+ context->Computed = 1;
+
+ }
+
+ for(i = 0; i < SHA1HashSize; ++i)
+ {
+ Message_Digest[i] = context->Intermediate_Hash[i>>2]
+ >> 8 * ( 3 - ( i & 0x03 ) );
+ }
+
+ return shaSuccess;
+}
+
+/*
+ * SHA1Input
+ *
+ * Description:
+ * This function accepts an array of octets as the next portion
+ * of the message.
+ *
+ * Parameters:
+ * context: [in/out]
+ * The SHA context to update
+ * message_array: [in]
+ * An array of characters representing the next portion of
+ * the message.
+ * length: [in]
+ * The length of the message in message_array
+ *
+ * Returns:
+ * sha Error Code.
+ *
+ */
+int SHA1Input( SHA1Context *context,
+ const uint8_t *message_array,
+ unsigned length)
+{
+ if (!length)
+ {
+ return shaSuccess;
+ }
+
+ if (!context || !message_array)
+ {
+ return shaNull;
+ }
+
+ if (context->Computed)
+ {
+ context->Corrupted = shaStateError;
+
+ return shaStateError;
+ }
+
+ if (context->Corrupted)
+ {
+ return context->Corrupted;
+ }
+ while(length-- && !context->Corrupted)
+ {
+ context->Message_Block[context->Message_Block_Index++] =
+ (*message_array & 0xFF);
+
+ context->Length_Low += 8;
+ if (context->Length_Low == 0)
+ {
+ context->Length_High++;
+ if (context->Length_High == 0)
+ {
+ /* Message is too long */
+ context->Corrupted = 1;
+ }
+ }
+
+ if (context->Message_Block_Index == 64)
+ {
+ SHA1ProcessMessageBlock(context);
+ }
+
+ message_array++;
+ }
+
+ return shaSuccess;
+}
+
+/*
+ * SHA1ProcessMessageBlock
+ *
+ * Description:
+ * This function will process the next 512 bits of the message
+ * stored in the Message_Block array.
+ *
+ * Parameters:
+ * None.
+ *
+ * Returns:
+ * Nothing.
+ *
+ * Comments:
+
+ * Many of the variable names in this code, especially the
+ * single character names, were used because those were the
+ * names used in the publication.
+ *
+ *
+ */
+void SHA1ProcessMessageBlock(SHA1Context *context)
+{
+ const uint32_t K[] = { /* Constants defined in SHA-1 */
+ 0x5A827999,
+ 0x6ED9EBA1,
+ 0x8F1BBCDC,
+ 0xCA62C1D6
+ };
+ int t; /* Loop counter */
+ uint32_t temp; /* Temporary word value */
+ uint32_t W[80]; /* Word sequence */
+ uint32_t A, B, C, D, E; /* Word buffers */
+
+ /*
+ * Initialize the first 16 words in the array W
+ */
+ for(t = 0; t < 16; t++)
+ {
+ W[t] = (uint32_t)context->Message_Block[t * 4] << 24;
+ W[t] |= (uint32_t)context->Message_Block[t * 4 + 1] << 16;
+ W[t] |= (uint32_t)context->Message_Block[t * 4 + 2] << 8;
+ W[t] |= (uint32_t)context->Message_Block[t * 4 + 3];
+ }
+
+ for(t = 16; t < 80; t++)
+ {
+ W[t] = SHA1CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]);
+ }
+
+ A = context->Intermediate_Hash[0];
+ B = context->Intermediate_Hash[1];
+ C = context->Intermediate_Hash[2];
+ D = context->Intermediate_Hash[3];
+ E = context->Intermediate_Hash[4];
+
+ for(t = 0; t < 20; t++)
+ {
+ temp = SHA1CircularShift(5,A) +
+ ((B & C) | ((~B) & D)) + E + W[t] + K[0];
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30,B);
+
+ B = A;
+ A = temp;
+ }
+
+ for(t = 20; t < 40; t++)
+ {
+ temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1];
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30,B);
+ B = A;
+ A = temp;
+ }
+
+ for(t = 40; t < 60; t++)
+ {
+ temp = SHA1CircularShift(5,A) +
+ ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30,B);
+ B = A;
+ A = temp;
+ }
+
+ for(t = 60; t < 80; t++)
+ {
+ temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3];
+ E = D;
+ D = C;
+ C = SHA1CircularShift(30,B);
+ B = A;
+ A = temp;
+ }
+
+ context->Intermediate_Hash[0] += A;
+ context->Intermediate_Hash[1] += B;
+ context->Intermediate_Hash[2] += C;
+ context->Intermediate_Hash[3] += D;
+ context->Intermediate_Hash[4] += E;
+
+ context->Message_Block_Index = 0;
+}
+
+/*
+ * SHA1PadMessage
+ *
+
+ * Description:
+ * According to the standard, the message must be padded to an even
+ * 512 bits. The first padding bit must be a '1'. The last 64
+ * bits represent the length of the original message. All bits in
+ * between should be 0. This function will pad the message
+ * according to those rules by filling the Message_Block array
+ * accordingly. It will also call the ProcessMessageBlock function
+ * provided appropriately. When it returns, it can be assumed that
+ * the message digest has been computed.
+ *
+ * Parameters:
+ * context: [in/out]
+ * The context to pad
+ * ProcessMessageBlock: [in]
+ * The appropriate SHA*ProcessMessageBlock function
+ * Returns:
+ * Nothing.
+ *
+ */
+
+void SHA1PadMessage(SHA1Context *context)
+{
+ /*
+ * Check to see if the current message block is too small to hold
+ * the initial padding bits and length. If so, we will pad the
+ * block, process it, and then continue padding into a second
+ * block.
+ */
+ if (context->Message_Block_Index > 55)
+ {
+ context->Message_Block[context->Message_Block_Index++] = 0x80;
+ while(context->Message_Block_Index < 64)
+ {
+ context->Message_Block[context->Message_Block_Index++] = 0;
+ }
+
+ SHA1ProcessMessageBlock(context);
+
+ while(context->Message_Block_Index < 56)
+ {
+ context->Message_Block[context->Message_Block_Index++] = 0;
+ }
+ }
+ else
+ {
+ context->Message_Block[context->Message_Block_Index++] = 0x80;
+ while(context->Message_Block_Index < 56)
+ {
+
+ context->Message_Block[context->Message_Block_Index++] = 0;
+ }
+ }
+
+ /*
+ * Store the message length as the last 8 octets
+ */
+ context->Message_Block[56] = context->Length_High >> 24;
+ context->Message_Block[57] = context->Length_High >> 16;
+ context->Message_Block[58] = context->Length_High >> 8;
+ context->Message_Block[59] = context->Length_High;
+ context->Message_Block[60] = context->Length_Low >> 24;
+ context->Message_Block[61] = context->Length_Low >> 16;
+ context->Message_Block[62] = context->Length_Low >> 8;
+ context->Message_Block[63] = context->Length_Low;
+
+ SHA1ProcessMessageBlock(context);
+}
diff --git a/sha1.h b/sha1.h
new file mode 100644
index 0000000..eb51ed5
--- /dev/null
+++ b/sha1.h
@@ -0,0 +1,73 @@
+/*
+ * sha1.h
+ *
+ * Description:
+ * This is the header file for code which implements the Secure
+ * Hashing Algorithm 1 as defined in FIPS PUB 180-1 published
+ * April 17, 1995.
+ *
+ * Many of the variable names in this code, especially the
+ * single character names, were used because those were the names
+ * used in the publication.
+ *
+ * Please read the file sha1.c for more information.
+ *
+ */
+
+#ifndef _SHA1_H_
+#define _SHA1_H_
+
+#include
+/*
+ * If you do not have the ISO standard stdint.h header file, then you
+ * must typdef the following:
+ * name meaning
+ * uint32_t unsigned 32 bit integer
+ * uint8_t unsigned 8 bit integer (i.e., unsigned char)
+ * int_least16_t integer of >= 16 bits
+ *
+ */
+
+#ifndef _SHA_enum_
+#define _SHA_enum_
+enum
+{
+ shaSuccess = 0,
+ shaNull, /* Null pointer parameter */
+ shaInputTooLong, /* input data too long */
+ shaStateError /* called Input after Result */
+};
+#endif
+#define SHA1HashSize 20
+
+/*
+ * This structure will hold context information for the SHA-1
+ * hashing operation
+ */
+typedef struct SHA1Context
+{
+ uint32_t Intermediate_Hash[SHA1HashSize/4]; /* Message Digest */
+
+ uint32_t Length_Low; /* Message length in bits */
+ uint32_t Length_High; /* Message length in bits */
+
+ /* Index into message block array */
+ int_least16_t Message_Block_Index;
+ uint8_t Message_Block[64]; /* 512-bit message blocks */
+
+ int Computed; /* Is the digest computed? */
+ int Corrupted; /* Is the message digest corrupted? */
+} SHA1Context;
+
+/*
+ * Function Prototypes
+ */
+
+int SHA1Reset( SHA1Context *);
+int SHA1Input( SHA1Context *,
+ const uint8_t *,
+ unsigned int);
+int SHA1Result( SHA1Context *,
+ uint8_t Message_Digest[SHA1HashSize]);
+
+#endif
diff --git a/sound.c b/sound.c
index aab2415..153e214 100644
--- a/sound.c
+++ b/sound.c
@@ -113,7 +113,7 @@ void quisk_sample_level(const char * msg, complex double * cSamples, int nSample
level = d;
}
if (QuiskTimeSec() - time0 > 0.1) {
- printf ("sample_level %s: %10.6lf count %8d\n", msg, level / scale, count);
+ printf ("peak sample level %s: %14.6lf count %8d\n", msg, level / scale, count);
level = 0;
count = 0;
time0 = QuiskTimeSec();
@@ -1019,7 +1019,9 @@ int quisk_read_sound(void) // Called from sound thread
// Play digital if required
//if (is_DGT)
- play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level);
+ play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level);
+ // Send radio sound to TCI
+ tci_send_audio(cSamples, nSamples);
// Perhaps record the speaker audio to a file
if ( ! key_state && file_rec_audio.fp)
@@ -1065,7 +1067,10 @@ int quisk_read_sound(void) // Called from sound thread
for (i = 0; i < mic_count; i++)
cSamples[i] = 0;
}
- //quisk_sample_level("read mic or DGT", cSamples, mic_count, CLIP16);
+ // Receive microphone sound from TCI
+ //quisk_sample_level("read mic or DGT", cSamples, mic_count, CLIP32);
+ mic_count = tci_get_mic(cSamples, mic_count);
+ //quisk_sample_level("quisk_tci", cSamples, mic_count, CLIP32);
// Perhaps record the microphone audio to the speaker audio file
if (key_state && file_rec_audio.fp)
quisk_record_audio(&file_rec_audio, cSamples, mic_count);
diff --git a/tci.c b/tci.c
new file mode 100644
index 0000000..6da2bbc
--- /dev/null
+++ b/tci.c
@@ -0,0 +1,651 @@
+#include
+#include
+#include
+#include
+#include "ws.h"
+#include
+#include
+#include
+#include "quisk.h"
+
+#define TCI_RX_BUF_SIZE 1024
+#define TCI_TX_BUFFER_MSEC 30 // Buffer delay in milliseconds
+#define TCI_COMMAND_SIZE 32
+
+static int verbose; // non-zero for verbose log messages
+static int tci_port;
+
+// Implement the ExpertSDR2 Version 1.4 protocol:
+// The only sample rate is 48000. The Tx audio parameters are copied from the Rx audio stream.
+// The only sample type is FLOAT32, type 4. The FLOAT32 type is 3 in ExpertSDR3. We will always use 3.
+// WSJT-X RX_AUDIO_STREAM must be two channels and Stream.length is the number of floats (not samples).
+// WSJT-X TX_AUDIO_STREAM always returns two channels but the data is one channel in half the data length.
+// It is unclear whether stream.length is the number of floats, or half that for two channels. And version 1.4 lacks stream.channels.
+// The protocol name for TCI version 1.4 is "protocol:ESDR,1.4;" and the port is 40001.
+// The protocol name for TCI version 2.0 is "protocol:ExpertSDR3,2.0;" and the port is 50001.
+// The TCI modulations are: AM, SAM, DSB, LSB, USB, CW, NFM, WFM, SPEC, DIGL, DIGU, DRM.
+
+// These are TCI parameters:
+//static int quisk_dds;
+//static int quisk_if;
+static int quisk_vfo;
+static int client_vfo=-1;
+static int quisk_trx;
+static int client_trx=-1;
+static int quisk_split_enable;
+static int client_split_enable=-1;
+static char quisk_modulation[TCI_COMMAND_SIZE];
+static char client_modulation[TCI_COMMAND_SIZE]={0};
+
+static ws_cli_conn_t tci_clients_list[MAX_CLIENTS]; // list of TCI clients
+static int tci_clients_count;
+ws_cli_conn_t tci_tx_audio_client; // single client providing Tx audio
+static int tci_started; // Did we start the TCI server?
+
+static pthread_mutex_t clients_list_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t tx_buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+enum StreamType // ExpertSDR3
+{
+IQ_STREAM = 0, // Receiver IQ signal stream
+RX_AUDIO_STREAM = 1, // Receiver audio stream
+TX_AUDIO_STREAM = 2, // Audio stream for transmitter
+TX_CHRONO = 3, // Time marker flow for audio signal transmission
+LINEOUT_STREAM = 4 // Linear audio output audio stream
+} ;
+
+enum SampleType // ExpertSDR3
+{
+TCI_INT16 = 0, // 16-bit integral character type
+TCI_INT24 = 1, // 24-bit integral character type
+TCI_INT32 = 2, // 32-bit integral character type
+TCI_FLOAT32 = 3, // 32-bit type with a floating point (was 4 in SDR2)
+} ;
+
+struct ClientData { // data for each client
+ char msg_buf[TCI_RX_BUF_SIZE];
+ int msg_length;
+ int send_Rx_audio_stream;
+ int audio_stream_samplerate;
+ int audio_stream_sample_type;
+ int audio_stream_channels;
+ int audio_stream_samples;
+ int audio_stream_bytes_per_sample;
+} ;
+
+struct _Stream {
+ uint32_t receiver; // receiver number
+ uint32_t sample_rate; // sampling rate
+ uint32_t format; // sample type determined in SampleType
+ uint32_t codec; // compression algorithm (not implemented), always 0
+ uint32_t crc; // checksum (not implemented), always 0
+ int32_t length; // number of samples
+ uint32_t type; // stream type determined in StreamType
+ uint32_t channels; // number of channels for ExpertSDR3
+ uint32_t reserv[8]; // reserved
+ uint8_t data[16384]; // samples
+} ;
+
+struct _TxChrono {
+ uint32_t receiver; // receiver number
+ uint32_t sample_rate; // sampling rate
+ uint32_t format; // sample type determined in SampleType
+ uint32_t codec; // compression algorithm (not implemented), always 0
+ uint32_t crc; // checksum (not implemented), always 0
+ int32_t length; // number of samples
+ uint32_t type; // stream type determined in StreamType
+ uint32_t channels; // number of channels for ExpertSDR3
+ uint32_t reserv[8]; // reserved
+} ;
+
+static struct _TxChrono TxChrono = {
+ .receiver = 0,
+ .sample_rate = 48000,
+ .format = TCI_FLOAT32,
+ .codec = 0,
+ .crc = 0,
+ .length = 0,
+ .type = TX_CHRONO,
+ .channels = 1,
+ .reserv = {0, 0, 0, 0, 0, 0, 0, 0}
+} ;
+
+struct _StreamTypes {
+ char * name;
+ time_t when;
+} ;
+
+#define NUM_STREAM_TYPES 5
+struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = {
+ {"iq ", 0},
+ {"rx_audio ", 0},
+ {"tx_audio ", 0},
+ {"tx_chrono", 0},
+ {"lineout ", 0}
+} ;
+
+static void PrintStream(const char * msg, size_t size)
+{
+ struct _Stream * pt = (struct _Stream *)msg;
+
+ if (time(NULL) - StreamTypes[pt->type].when > 5) {
+ StreamTypes[pt->type].when = time(NULL);
+ if (pt->type == TX_AUDIO_STREAM)
+ QuiskPrintf("TCI Receiving %slength %5d:", StreamTypes[pt->type].name, (int)size);
+ else
+ QuiskPrintf("TCI Sending %s length %5d:", StreamTypes[pt->type].name, (int)size);
+ QuiskPrintf("%d %5d %d %d %d %5d %d %d\n", (int)(pt->receiver), (int)(pt->sample_rate), (int)(pt->format), (int)(pt->codec),
+ (int)(pt->crc), (int)(pt->length), (int)(pt->type), (int)(pt->channels));
+ }
+}
+
+static int sendframe_txt(ws_cli_conn_t client, const char * msg)
+{
+ if (verbose)
+ QuiskPrintf("TCI Send text %s\n", msg);
+ return ws_sendframe_txt(client, msg);
+}
+
+static int sendframe_txt_bcast(uint16_t port, const char * msg)
+{
+ if (verbose)
+ QuiskPrintf("TCI Broadcast text %s\n", msg);
+ return ws_sendframe_txt_bcast(port, msg);
+}
+
+static size_t sendframe_bin(ws_cli_conn_t client, const char * msg, size_t size)
+{
+ if (verbose)
+ PrintStream(msg, size);
+ return ws_sendframe_bin(client, msg, size);
+}
+
+static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
+{ // This function alters the message ctx->msg_buf.
+ int i;
+ char char_buf[TCI_COMMAND_SIZE];
+ char * saveptr, * command;
+ char * arg1, * arg2, * arg3;
+
+ if (verbose)
+ QuiskPrintf("TCI Received text %s\n", ctx->msg_buf);
+ for (i = 0; ctx->msg_buf[i]; i++)
+ ctx->msg_buf[i] = tolower(ctx->msg_buf[i]);
+ command = strtok_r(ctx->msg_buf, ":", &saveptr);
+ if (command == NULL)
+ return 0;
+ arg1 = strtok_r(NULL, ",;", &saveptr);
+ arg2 = strtok_r(NULL, ",;", &saveptr);
+ arg3 = strtok_r(NULL, ",;", &saveptr);
+ // The command will be broadcast to all clients unless the return value is zero.
+ switch (command[0]) {
+ case 'a':
+ if (strcmp(command, "audio_start") == 0) {
+ pthread_mutex_lock(&clients_list_mutex);
+ for (i = 0; i < tci_clients_count; i++) {
+ if (tci_clients_list[i] == client) {
+ ctx->send_Rx_audio_stream = 1;
+ break;
+ }
+ }
+ pthread_mutex_unlock(&clients_list_mutex);
+ }
+ else if (strcmp(command, "audio_stop") == 0) {
+ pthread_mutex_lock(&clients_list_mutex);
+ for (i = 0; i < tci_clients_count; i++) {
+ if (tci_clients_list[i] == client) {
+ ctx->send_Rx_audio_stream = 0;
+ break;
+ }
+ }
+ pthread_mutex_unlock(&clients_list_mutex);
+ }
+ else if (strcmp(command, "audio_stream_sample_type") == 0) {
+ if (strcmp(arg1, "float32") == 0) {
+ ctx->audio_stream_sample_type = TCI_FLOAT32;
+ ctx->audio_stream_bytes_per_sample = 4;
+ }
+ else {
+ return 0;
+ }
+ }
+ else if (strcmp(command, "audio_samplerate") == 0) {
+ if (atoi(arg1) != 48000)
+ return 0;
+ }
+ else if (strcmp(command, "audio_stream_channels") == 0) {
+ if (strcmp(arg1, "1") == 0)
+ ctx->audio_stream_channels = 1;
+ else if (strcmp(arg1, "2") == 0)
+ ctx->audio_stream_channels = 2;
+ else
+ return 0;
+ }
+ else if (strcmp(command, "audio_stream_samples") == 0) {
+ return 0; // do not send
+ }
+ break;
+ case 'i':
+ if (strcmp(command, "iq_start") == 0)
+ return 0; // do not send
+ else if (strcmp(command, "iq_stop") == 0)
+ return 0; // do not send
+ else if (strcmp(command, "iq_samplerate") == 0)
+ return 0; // do not send
+ break;
+ case 'm':
+ if (strcmp(command, "modulation") == 0) {
+ if (arg2) {
+ for (i = 0; arg2[i]; i++)
+ arg2[i] = toupper(arg2[i]);
+ strncpy(client_modulation, arg2, TCI_COMMAND_SIZE - 1);
+ }
+ else {
+ snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
+ sendframe_txt(client, char_buf);
+ }
+ return 0;
+ }
+ break;
+ case 's':
+ if (strcmp(command, "split_enable") == 0) {
+ if (arg2) {
+ if (strcmp(arg2, "true") == 0)
+ client_split_enable = 1;
+ else
+ client_split_enable = 0;
+ }
+ else {
+ if (quisk_split_enable)
+ sendframe_txt(client, "split_enable:0,true;");
+ else
+ sendframe_txt(client, "split_enable:0,false;");
+ }
+ return 0;
+ }
+ break;
+ case 't':
+ if (strcmp(command, "trx") == 0) {
+ if (arg2) {
+ if (strcmp(arg2, "true") == 0) {
+ if (quisk_trx == 0) {
+ client_trx = 1;
+ CircularBuffer(0, NULL, TCI_TX_BUFFER_MSEC * ctx->audio_stream_samplerate / 1000, 0);
+ tci_tx_audio_client = client;
+ }
+ }
+ else if (client == tci_tx_audio_client) {
+ client_trx = 0;
+ tci_tx_audio_client = 0;
+ }
+ }
+ else {
+ if (quisk_trx)
+ sendframe_txt(client, "trx:0,true;");
+ else
+ sendframe_txt(client, "trx:0,false;");
+ }
+ return 0;
+ }
+ else if (strcmp(command, "tx_stream_audio_buffering") == 0) {
+ return 0; // do not send
+ }
+ break;
+ case 'v':
+ if (strcmp(command, "vfo") == 0) {
+ if (arg3) {
+ client_vfo = atoi(arg3);
+ }
+ else {
+ snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%d;", quisk_vfo);
+ sendframe_txt(client, char_buf);
+ }
+ return 0;
+ }
+ break;
+ }
+ return 1; // send to all clients
+}
+
+static void onopen(ws_cli_conn_t client)
+{
+ char *cli;
+ char command[TCI_COMMAND_SIZE];
+ struct ClientData * ctx;
+
+ // Make parameters for each client:
+ ctx = malloc(sizeof(struct ClientData));
+ ctx->msg_length = 0;
+ ctx->send_Rx_audio_stream = 0;
+ ctx->audio_stream_samplerate = 48000;
+ ctx->audio_stream_sample_type = TCI_FLOAT32;
+ ctx->audio_stream_channels = 2;
+ ctx->audio_stream_samples = 0;
+ ctx->audio_stream_bytes_per_sample = 4;
+ ws_set_connection_context(client, ctx);
+
+ pthread_mutex_lock(&clients_list_mutex);
+ tci_clients_list[tci_clients_count++] = client;
+ pthread_mutex_unlock(&clients_list_mutex);
+
+ if (verbose) {
+ cli = ws_getaddress(client);
+ QuiskPrintf("TCI *Connection opened, addr: %s\n", cli);
+ }
+
+ sendframe_txt(client, "protocol:ESDR,1.4;");
+ sendframe_txt(client, "vfo_limits:30000,30000000;");
+ sendframe_txt(client, "if_limits:-48000,48000;");
+ sendframe_txt(client, "trx_count:1;");
+ sendframe_txt(client, "channel_count:1;");
+ sendframe_txt(client, "device:QuiskSDR;");
+ sendframe_txt(client, "receive_only:false;");
+ sendframe_txt(client, "modulations_list:USB,LSB,CW,AM,FM,DIGL,DIGU;");
+ sendframe_txt(client, "ready;");
+
+ sendframe_txt(client, "start;");
+ sendframe_txt(client, "tx_enable:0,true;");
+ //snprintf(command, TCI_COMMAND_SIZE, "dds:0,%d;", quisk_dds);
+ //sendframe_txt(client, command);
+ //snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
+ //sendframe_txt(client, command);
+ snprintf(command, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
+ sendframe_txt(client, command);
+ snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%d;", quisk_vfo);
+ sendframe_txt(client, command);
+ if (quisk_trx)
+ sendframe_txt(client, "trx:0,true;");
+ else
+ sendframe_txt(client, "trx:0,false;");
+ if (quisk_split_enable)
+ sendframe_txt(client, "split_enable:0,true;");
+ else
+ sendframe_txt(client, "split_enable:0,false;");
+}
+
+static void onclose(ws_cli_conn_t client)
+{
+ int i, count;
+
+ pthread_mutex_lock(&clients_list_mutex);
+ count = tci_clients_count;
+ tci_clients_count = 0;
+ for (i = 0; i < count; i++)
+ if (tci_clients_list[i] != client)
+ tci_clients_list[tci_clients_count++] = tci_clients_list[i];
+ pthread_mutex_unlock(&clients_list_mutex);
+
+ free(ws_get_connection_context(client));
+ if (verbose) {
+ char * cli = ws_getaddress(client);
+ QuiskPrintf("TCI *Connection closed, addr: %s\n", cli);
+ }
+}
+
+static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t size, int type)
+{
+ char echo_copy[TCI_RX_BUF_SIZE];
+ complex double sample;
+
+ if (type == WS_FR_OP_TXT) {
+ if (size > TCI_RX_BUF_SIZE / 2)
+ return; // prevent overflow
+ struct ClientData * ctx = ws_get_connection_context(client);
+ if (ctx->msg_length + size >= TCI_RX_BUF_SIZE - 2)
+ ctx->msg_length = 0; // prevent overflow
+ memcpy(ctx->msg_buf + ctx->msg_length, msg, size); // append msg to buffer
+ ctx->msg_length += size;
+ ctx->msg_buf[ctx->msg_length] = '\0';
+ char * semicolon;
+ char save;
+ while ((semicolon = strchr(ctx->msg_buf, ';')) != NULL) {
+ save = *(semicolon + 1);
+ *(semicolon + 1) = '\0';
+ strncpy(echo_copy, ctx->msg_buf, TCI_RX_BUF_SIZE);
+ if (text_message(client, ctx)) // should we send to all clients?
+ sendframe_txt_bcast(tci_port, echo_copy);
+ *(semicolon + 1) = save;
+ size_t processed_len = (semicolon - ctx->msg_buf) + 1;
+ size_t remaining_len = ctx->msg_length - processed_len;
+ memmove(ctx->msg_buf, ctx->msg_buf + processed_len, remaining_len);
+ ctx->msg_length = remaining_len;
+ ctx->msg_buf[ctx->msg_length] = '\0';
+ }
+ }
+ else if (type == WS_FR_OP_BIN) {
+ struct _Stream * pt = (struct _Stream *)msg;
+ int count = 0;
+ float re, im;
+ int channels = TxChrono.channels;
+ int two_channels = channels == 2;
+ int header = 16 * sizeof(uint32_t);
+ const void * vpt = msg + header;
+ const void * vpt_end = vpt + (size - header);
+
+ if (pt->type == TX_AUDIO_STREAM) {
+ if (pt->length > 0 && client == tci_tx_audio_client) {
+ pthread_mutex_lock(&tx_buffer_mutex);
+ switch (TxChrono.format) {
+ case TCI_INT16:
+ break;
+ case TCI_FLOAT32: // FLOAT32 in version 2.0
+ case 4: // FLOAT32 in version 1.4
+ // Version 1.4 does not return the number of channels. We assume one channel.
+ while (vpt < vpt_end && count < pt->length) {
+ re = *(float *)vpt;
+ vpt += sizeof(float);
+ if (two_channels) {
+ im = *(float *)vpt;
+ vpt += sizeof(float);
+ }
+ else {
+ im = re;
+ }
+ sample = (re + I * im) * (CLIP32 / 2);
+ count += CircularBuffer(0, &sample, 0, 1);
+ }
+ if (verbose && count != pt->length)
+ QuiskPrintf("TCI TX count %d %d\n", count, pt->length);
+ break;
+ }
+ pthread_mutex_unlock(&tx_buffer_mutex);
+ }
+ }
+ if (verbose)
+ PrintStream((const char *)msg, size);
+
+ }
+
+}
+
+static void tci_startup(void)
+{
+ struct ws_server tci = {
+ .host = NULL,
+ .port = 0,
+ .thread_loop = 1,
+ .timeout_ms = 1000,
+ .evs.onopen = &onopen,
+ .evs.onclose = &onclose,
+ .evs.onmessage = &onmessage
+ };
+ char host32[32];
+
+ tci_clients_count = 0;
+ tci_port = QuiskGetConfigInt("tci_port", 40001);
+ strncpy(host32, QuiskGetConfigString("tci_ip", "127.0.0.1"), 31);
+ tci.host = host32;
+ tci.port = tci_port;
+ ws_socket(&tci);
+ if (verbose)
+ QuiskPrintf("TCI Start server on host %s port %d\n", tci.host, tci.port);
+ return;
+}
+
+void tci_send_audio(complex double * cSamples, int nSamples) // called from the sound thread
+{
+ int i, n;
+ struct _Stream stream;
+ struct ClientData * ctx;
+
+ if (tci_clients_count <= 0)
+ return;
+
+ if (nSamples <= 0)
+ return;
+
+ pthread_mutex_lock(&clients_list_mutex);
+ for (n = 0; n < tci_clients_count; n++) {
+ ctx = ws_get_connection_context(tci_clients_list[n]);
+ if (ctx->send_Rx_audio_stream == 0)
+ continue;
+ memset(&stream, 0, 16 * sizeof(uint32_t));
+ stream.sample_rate = ctx->audio_stream_samplerate;
+ stream.format = ctx->audio_stream_sample_type;
+ stream.type = RX_AUDIO_STREAM;
+ stream.channels = ctx->audio_stream_channels;
+ int two_channels = stream.channels == 2;
+ int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
+ void * vpt = &stream.data;
+ void * vpt_end = vpt + 16384;
+ switch (ctx->audio_stream_sample_type) {
+ case TCI_FLOAT32:
+ stream.length = 0; // make a frame
+ for (i = 0; i < nSamples; i++) {
+ *(float *)vpt = (float)(creal(cSamples[i]) / 2147483648);
+ vpt += bytes_per_sample;
+ stream.length++;
+ if (two_channels) {
+ *(float *)vpt = (float)(cimag(cSamples[i]) / 2147483648);
+ vpt += bytes_per_sample;
+ stream.length++;
+ }
+ if (vpt >= vpt_end || i == nSamples - 1) {
+ sendframe_bin(tci_clients_list[n], (const char *)&stream,
+ (16 * sizeof(uint32_t) + stream.length * bytes_per_sample));
+ stream.length = 0;
+ }
+ }
+ break;
+ }
+ }
+ pthread_mutex_unlock(&clients_list_mutex);
+}
+
+int tci_get_mic(complex double * cSamples, int mic_count) // called from the sound thread
+{
+ if (tci_tx_audio_client) {
+ TxChrono.length = mic_count;
+ sendframe_bin(tci_tx_audio_client, (const char *)&TxChrono, 16 * sizeof(uint32_t));
+ pthread_mutex_lock(&tx_buffer_mutex);
+ mic_count = CircularBuffer(0, cSamples, mic_count, 0);
+ pthread_mutex_unlock(&tx_buffer_mutex);
+ }
+ return mic_count;
+}
+
+PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds) // called from the GUI thread
+{ /* Call with keyword arguments ONLY.
+ Sent from Quisk when parameters change. Broadcast the change to clients.*/
+ static char * kwlist[] = {"start", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable",
+ "tci_modulation", NULL} ;
+ int start=-1, new_dds=-1, new_if=-1, new_vfo=-1, new_trx=-1, new_split_enable=-1;
+ char * mode=NULL;
+ char char_buf[TCI_COMMAND_SIZE];
+
+ if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiis", kwlist,
+ &start, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable,
+ &mode))
+ return NULL;
+ if (start == 1) {
+ tci_started = 1;
+ tci_startup(); // start the TCP server for TCI
+ }
+ if (tci_started == 0) {
+ Py_INCREF (Py_None);
+ return Py_None;
+ }
+ if (new_vfo != -1) {
+ quisk_vfo = new_vfo;
+ snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%d;", quisk_vfo);
+ sendframe_txt_bcast(tci_port, char_buf);
+ }
+ if (new_split_enable != -1) {
+ quisk_split_enable = new_split_enable;
+ if (quisk_split_enable)
+ sendframe_txt_bcast(tci_port, "split_enable:0,true;");
+ else
+ sendframe_txt_bcast(tci_port, "split_enable:0,false;");
+ }
+ if (new_trx != -1) {
+ quisk_trx = new_trx;
+ if (quisk_trx) {
+ sendframe_txt_bcast(tci_port, "trx:0,true;");
+ }
+ else {
+ tci_tx_audio_client = 0;
+ sendframe_txt_bcast(tci_port, "trx:0,false;");
+ }
+ }
+ if (mode) {
+ if (strcmp(mode, "DGT-U") == 0)
+ strcpy(quisk_modulation, "DIGU");
+ else if (strcmp(mode, "DGT-L") == 0)
+ strcpy(quisk_modulation, "DIGL");
+ else if (strcmp(mode, "CWL") == 0 || strcmp(mode, "CWU") == 0)
+ strcpy(quisk_modulation, "CW");
+ else if (strcmp(mode, "LSB") == 0 || strcmp(mode, "USB") == 0 ||
+ strcmp(mode, "AM") == 0 || strcmp(mode, "FM") == 0)
+ strncpy(quisk_modulation, mode, TCI_COMMAND_SIZE - 1);
+ else
+ strcpy(quisk_modulation, "USB");
+ snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
+ sendframe_txt_bcast(tci_port, char_buf);
+ }
+ Py_INCREF (Py_None);
+ return Py_None;
+}
+
+PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from the GUI thread
+{ /* Return client request for a parameter change */
+ const char * name;
+ int i;
+ char char_buf[TCI_COMMAND_SIZE];
+
+ if (!PyArg_ParseTuple (args, "s", &name))
+ return NULL;
+ if (tci_started == 0) {
+ }
+ else if (strcmp(name, "tci_vfo") == 0 && client_vfo >= 0) {
+ i = client_vfo;
+ client_vfo = -1;
+ return PyLong_FromLong(i);
+ }
+ else if (strcmp(name, "tci_split_enable") == 0 && client_split_enable >= 0) {
+ i = client_split_enable;
+ client_split_enable = -1;
+ return PyLong_FromLong(i);
+ }
+ else if (strcmp(name, "tci_trx") == 0 && client_trx >= 0) {
+ i = client_trx;
+ client_trx = -1;
+ return PyLong_FromLong(i);
+ }
+ else if (strcmp(name, "tci_modulation") == 0 && client_modulation[0] != 0) {
+ char_buf[0] = 0;
+ if (strcmp(client_modulation, "DIGU") == 0)
+ strcpy(char_buf, "DGT-U");
+ else if (strcmp(client_modulation, "DIGL") == 0)
+ strcpy(char_buf, "DGT-L");
+ else if (strcmp(client_modulation, "CW") == 0)
+ strcpy(char_buf, "CWU");
+ else if (strcmp(client_modulation, "LSB") == 0 || strcmp(client_modulation, "USB") == 0 ||
+ strcmp(client_modulation, "AM") == 0 || strcmp(client_modulation, "FM") == 0)
+ strncpy(char_buf, client_modulation, TCI_COMMAND_SIZE);
+ client_modulation[0] = 0;
+ if (char_buf[0])
+ return PyString_FromString(char_buf);
+ }
+ Py_INCREF (Py_None);
+ return Py_None;
+}
diff --git a/utf8.c b/utf8.c
new file mode 100644
index 0000000..824060f
--- /dev/null
+++ b/utf8.c
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2008-2009 Bjoern Hoehrmann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/*
+ * Amazing utf8 decoder & validator grabbed from:
+ * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+ *
+ * All rights goes to the original author.
+ */
+
+#include "utf8.h"
+
+static const uint8_t utf8d[] = {
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
+ 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
+ 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
+ 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
+ 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
+ 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
+ 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
+ 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
+ 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
+};
+
+static
+uint32_t decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
+ uint32_t type = utf8d[byte];
+
+ *codep = (*state != UTF8_ACCEPT) ?
+ (byte & 0x3fu) | (*codep << 6) :
+ (0xff >> type) & (byte);
+
+ *state = utf8d[256 + *state*16 + type];
+ return *state;
+}
+
+int is_utf8(uint8_t *s) {
+ uint32_t codepoint, state = 0;
+
+ while (*s)
+ decode(&state, &codepoint, *s++);
+
+ return state == UTF8_ACCEPT;
+}
+
+int is_utf8_len(uint8_t *s, size_t len) {
+ uint32_t codepoint, state = 0;
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ decode(&state, &codepoint, *s++);
+
+ return state == UTF8_ACCEPT;
+}
+
+uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state) {
+ uint32_t codepoint;
+ size_t i;
+
+ codepoint = 0;
+ for (i = 0; i < len; i++)
+ decode(&state, &codepoint, *s++);
+
+ return state;
+}
diff --git a/utf8.h b/utf8.h
new file mode 100644
index 0000000..b30a62b
--- /dev/null
+++ b/utf8.h
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2008-2009 Bjoern Hoehrmann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef UTF8_DECODE_H
+#define UTF8_DECODE_H
+
+ #include
+ #include
+
+ /* UTF8 return state. */
+ #define UTF8_ACCEPT 0
+ #define UTF8_REJECT 1
+
+ extern int is_utf8(uint8_t* s);
+ extern int is_utf8_len(uint8_t *s, size_t len);
+ extern uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state);
+
+#endif
diff --git a/utility.c b/utility.c
index a9b5d08..1fecd9c 100644
--- a/utility.c
+++ b/utility.c
@@ -321,3 +321,65 @@ PyObject * QuiskPrintf(char * format, ...)
return NULL;
}
#endif
+
+#define MAX_CHANNELS 8
+int CircularBuffer(int channel, complex double * cSamples, int nRead, int nWrite)
+{
+// Initialize with cSamples NULL and nRead equal to the buffer delay in samples.
+// Read with nRead, write with nWrite. Only one can be non-zero.
+// Return the number of samples read or written.
+ static struct _channel {
+ complex double * cBuf; // circular sample buffer; save samples until the buffer fills to nDelay
+ int sizeBuf; // current size of cBuf in complex samples
+ int nDelay; // number of samples needed to start reading the buffer
+ int nBuf; // current number of complex samples in cBuf
+ int filling; // if true, we are filling the buffer and not returning samples
+ int Windex; // index of write
+ int Rindex; // index of read
+ } Channels[MAX_CHANNELS] = {0};
+ struct _channel * ptChannel = Channels + channel;
+ int i, num;
+
+ if ( ! cSamples) { // initialize
+ ptChannel->nDelay = nRead;
+ ptChannel->sizeBuf = nRead * 8;
+ ptChannel->cBuf = (complex double *)realloc(ptChannel->cBuf, ptChannel->sizeBuf * sizeof(complex double));
+ ptChannel->filling = 1;
+ ptChannel->Windex = 0;
+ ptChannel->Rindex = 0;
+ ptChannel->nBuf = 0;
+ return 0;
+ }
+ if ( ! ptChannel->cBuf)
+ return 0;
+ num = 0; // number of samples read or written
+ if (nWrite > 0) { // Copy samples from cSamples to cBuf
+ for (i = 0; i < nWrite && ptChannel->nBuf < ptChannel->sizeBuf; i++) {
+ ptChannel->cBuf[ptChannel->Windex++] = cSamples[i];
+ if (ptChannel->Windex >= ptChannel->sizeBuf)
+ ptChannel->Windex = 0;
+ ptChannel->nBuf++;
+ num++;
+ }
+ if (ptChannel->nBuf >= ptChannel->nDelay)
+ ptChannel->filling = 0;
+ }
+ else if (nRead > 0) {
+ if (ptChannel->filling) { // return zero-valued samples
+ for (num = 0; num < nRead; num++)
+ cSamples[num] = 0;
+ }
+ else { // Copy samples from cBuf to cSamples
+ for (i = 0; i < nRead && ptChannel->nBuf > 0; i++) {
+ cSamples[i] = ptChannel->cBuf[ptChannel->Rindex++];
+ if (ptChannel->Rindex >= ptChannel->sizeBuf)
+ ptChannel->Rindex = 0;
+ ptChannel->nBuf--;
+ num++;
+ }
+ if (ptChannel->nBuf <= 0)
+ ptChannel->filling = 1;
+ }
+ }
+ return num;
+}
diff --git a/ws.c b/ws.c
new file mode 100644
index 0000000..9a02f54
--- /dev/null
+++ b/ws.c
@@ -0,0 +1,2101 @@
+/*
+ * Copyright (C) 2016-2023 Davidson Francis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ */
+#define _POSIX_C_SOURCE 200809L
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* clang-format off */
+#ifndef _WIN32
+#include
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+typedef int socklen_t;
+#endif
+/* clang-format on */
+
+/* Windows and macOS seems to not have MSG_NOSIGNAL */
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+#include
+
+#include "utf8.h"
+#include "ws.h"
+
+/**
+ * @dir src/
+ * @brief wsServer source code
+ *
+ * @file ws.c
+ * @brief wsServer main routines.
+ */
+
+/**
+ * @brief Client socks.
+ */
+struct ws_connection
+{
+ int client_sock; /**< Client socket FD. */
+ int state; /**< WebSocket current state. */
+
+ /* wsServer structure copy. */
+ struct ws_server ws_srv;
+
+ /* Timeout thread and locks. */
+ pthread_mutex_t mtx_state;
+ pthread_cond_t cnd_state_close;
+ pthread_t thrd_tout;
+ bool close_thrd;
+
+ /* Send lock. */
+ pthread_mutex_t mtx_snd;
+
+ /* IP address and port. */
+ char ip[1025]; /* NI_MAXHOST. */
+ char port[32]; /* NI_MAXSERV. */
+
+ /* Ping/Pong IDs and locks. */
+ int32_t last_pong_id;
+ int32_t current_ping_id;
+ pthread_mutex_t mtx_ping;
+
+ /* Connection context */
+ void *connection_context;
+
+ ws_cli_conn_t client_id;
+};
+
+static struct ws_connection *get_client_by_cid(ws_cli_conn_t cid);
+
+/**
+ * @brief Clients list.
+ */
+static struct ws_connection client_socks[MAX_CLIENTS];
+
+/**
+ * @brief Timeout to a single send().
+ */
+static uint32_t timeout;
+
+/**
+ * @brief Client validity macro
+ */
+#define CLIENT_VALID(cli) \
+ ((cli) != NULL && (cli) >= &client_socks[0] && \
+ (cli) <= &client_socks[MAX_CLIENTS - 1] && \
+ (cli)->client_sock > -1)
+
+
+/**
+ * @brief Get server context.
+ * Assumed to be set once, when initializing `.context` in `struct ws_server`.
+ */
+void *ws_get_server_context(ws_cli_conn_t cli)
+{
+ struct ws_connection *client = get_client_by_cid(cli);
+ if (!CLIENT_VALID(client))
+ return NULL;
+ return client->ws_srv.context;
+}
+
+/**
+ * @brief Set connection context.
+ */
+void ws_set_connection_context(ws_cli_conn_t client, void *ptr)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return;
+ cli->connection_context = ptr;
+}
+
+/**
+ * @brief Get connection context.
+ */
+void *ws_get_connection_context(ws_cli_conn_t client)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return NULL;
+ return cli->connection_context;
+}
+
+/**
+ * @brief WebSocket frame data
+ */
+struct ws_frame_data
+{
+ /**
+ * @brief Frame read.
+ */
+ unsigned char frm[MESSAGE_LENGTH];
+ /**
+ * @brief Processed message at the moment.
+ */
+ unsigned char *msg;
+ /**
+ * @brief Control frame payload
+ */
+ unsigned char msg_ctrl[125];
+ /**
+ * @brief Current byte position.
+ */
+ size_t cur_pos;
+ /**
+ * @brief Amount of read bytes.
+ */
+ size_t amt_read;
+ /**
+ * @brief Frame type, like text or binary.
+ */
+ int frame_type;
+ /**
+ * @brief Frame size.
+ */
+ uint64_t frame_size;
+ /**
+ * @brief Error flag, set when a read was not possible.
+ */
+ int error;
+ /**
+ * @brief Client connection structure.
+ */
+ struct ws_connection *client;
+};
+
+/**
+ * @brief Global mutex.
+ */
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+
+/**
+ * @brief Issues an error message and aborts the program.
+ *
+ * @param s Error message.
+ */
+#define panic(s) \
+ do \
+ { \
+ perror(s); \
+ exit(-1); \
+ } while (0);
+
+static struct ws_connection *get_client_by_cid(ws_cli_conn_t cid)
+{
+ pthread_mutex_lock(&mutex);
+ for (int i = 0; i < MAX_CLIENTS; i++)
+ {
+ if (client_socks[i].client_id == cid)
+ {
+ pthread_mutex_unlock(&mutex);
+ return &client_socks[i];
+ }
+ }
+ pthread_mutex_unlock(&mutex);
+ return NULL;
+}
+/**
+ * @brief Shutdown and close a given socket.
+ *
+ * @param fd Socket file descriptor to be closed.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void close_socket(int fd)
+{
+#ifndef _WIN32
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+#else
+ closesocket(fd);
+#endif
+}
+
+
+static uint64_t cid_generator = 1;
+
+static pthread_mutex_t cid_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static uint64_t get_next_cid()
+{
+ uint64_t next_cid;
+
+ pthread_mutex_lock(&cid_mutex);
+ next_cid = cid_generator++;
+ pthread_mutex_unlock(&cid_mutex);
+
+ return next_cid;
+}
+
+/**
+ * @brief Returns the current client state for a given
+ * client @p client.
+ *
+ * @param client Client structure.
+ *
+ * @return Returns the client state, -1 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int get_client_state(struct ws_connection *client)
+{
+ int state;
+
+ if (!CLIENT_VALID(client))
+ return (-1);
+
+ pthread_mutex_lock(&client->mtx_state);
+ state = client->state;
+ pthread_mutex_unlock(&client->mtx_state);
+ return (state);
+}
+
+/**
+ * @brief Set a state @p state to the client index
+ * @p client.
+ *
+ * @param client Client structure.
+ * @param state State to be set.
+ *
+ * @return Returns 0 if success, -1 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int set_client_state(struct ws_connection *client, int state)
+{
+ if (!CLIENT_VALID(client))
+ return (-1);
+
+ if (state < 0 || state > 3)
+ return (-1);
+
+ pthread_mutex_lock(&client->mtx_state);
+ client->state = state;
+ pthread_mutex_unlock(&client->mtx_state);
+ return (0);
+}
+
+/**
+ * @brief Send a given message @p buf on a socket @p sockfd.
+ *
+ * @param client Target client.
+ * @param buf Message to be sent.
+ * @param len Message length.
+ * @param flags Send flags.
+ *
+ * @return If success (i.e: all message was sent), returns
+ * the amount of bytes sent. Otherwise, -1.
+ *
+ * @note Technically this shouldn't be necessary, since send() should
+ * block until all content is sent, since _we_ don't use 'O_NONBLOCK'.
+ * However, it was reported (issue #22 on GitHub) that this was
+ * happening, so just to be cautious, I will keep using this routine.
+ */
+static ssize_t send_all(
+ struct ws_connection *client, const void *buf, size_t len, int flags)
+{
+ const char *p;
+ ssize_t ret;
+ ssize_t r;
+
+ ret = 0;
+
+ /* Sanity check. */
+ if (!CLIENT_VALID(client))
+ return (-1);
+
+ p = buf;
+ /* clang-format off */
+ pthread_mutex_lock(&client->mtx_snd);
+ while (len)
+ {
+ r = send(client->client_sock, p, len, flags);
+ if (r == -1)
+ {
+ pthread_mutex_unlock(&client->mtx_snd);
+ return (-1);
+ }
+ p += r;
+ len -= r;
+ ret += r;
+ }
+ pthread_mutex_unlock(&client->mtx_snd);
+ /* clang-format on */
+ return (ret);
+}
+
+/**
+ * @brief Close client connection (no close handshake, this should
+ * be done earlier), set appropriate state and destroy mutexes.
+ *
+ * @param client Client connection.
+ * @param lock Should lock the global mutex?.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void close_client(struct ws_connection *client, int lock)
+{
+ if (!CLIENT_VALID(client))
+ return;
+
+ set_client_state(client, WS_STATE_CLOSED);
+
+ close_socket(client->client_sock);
+
+ /* Destroy client mutexes and clear fd 'slot'. */
+ /* clang-format off */
+ if (lock)
+ pthread_mutex_lock(&mutex);
+ client->client_sock = -1;
+ pthread_cond_destroy(&client->cnd_state_close);
+ pthread_mutex_destroy(&client->mtx_state);
+ pthread_mutex_destroy(&client->mtx_snd);
+ pthread_mutex_destroy(&client->mtx_ping);
+ if (lock)
+ pthread_mutex_unlock(&mutex);
+ /* clang-format on */
+}
+
+/**
+ * @brief Close time-out thread.
+ *
+ * For a given client, this routine sleeps until
+ * TIMEOUT_MS and closes the connection or returns
+ * sooner if already closed connection.
+ *
+ * @param p ws_connection/ws_cli_conn_t Structure Pointer.
+ *
+ * @return Always NULL.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void *close_timeout(void *p)
+{
+ struct ws_connection *conn = p;
+ struct timespec ts;
+ int state;
+
+ pthread_mutex_lock(&conn->mtx_state);
+
+ clock_gettime(CLOCK_REALTIME, &ts);
+ ts.tv_nsec += MS_TO_NS(TIMEOUT_MS);
+
+ /* Normalize the time. */
+ while (ts.tv_nsec >= 1000000000)
+ {
+ ts.tv_sec++;
+ ts.tv_nsec -= 1000000000;
+ }
+
+ while (conn->state != WS_STATE_CLOSED &&
+ pthread_cond_timedwait(&conn->cnd_state_close, &conn->mtx_state, &ts) !=
+ ETIMEDOUT)
+ ;
+
+ state = conn->state;
+ pthread_mutex_unlock(&conn->mtx_state);
+
+ /* If already closed. */
+ if (state == WS_STATE_CLOSED)
+ goto quit;
+
+ DEBUG("Timer expired, closing client %d\n", conn->client_sock);
+
+ close_client(conn, 1);
+quit:
+ return (NULL);
+}
+
+/**
+ * @brief For a valid client index @p client, starts
+ * the timeout thread and set the current state
+ * to 'CLOSING'.
+ *
+ * @param client Client connection.
+ *
+ * @return Returns 0 if success, -1 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int start_close_timeout(struct ws_connection *client)
+{
+ if (!CLIENT_VALID(client))
+ return (-1);
+
+ pthread_mutex_lock(&client->mtx_state);
+
+ if (client->state != WS_STATE_OPEN)
+ goto out;
+
+ client->state = WS_STATE_CLOSING;
+
+ if (pthread_create(&client->thrd_tout, NULL, close_timeout, client))
+ {
+ pthread_mutex_unlock(&client->mtx_state);
+ panic("Unable to create timeout thread\n");
+ }
+ client->close_thrd = true;
+out:
+ pthread_mutex_unlock(&client->mtx_state);
+ return (0);
+}
+
+/**
+ * @brief Sets the IP address relative to a client connection opened
+ * by the server and save inside the client structure.
+ *
+ * @param client Client connection.
+ */
+static void set_client_address(struct ws_connection *client)
+{
+ struct sockaddr_storage addr;
+ socklen_t hlen = sizeof(addr);
+
+ if (!CLIENT_VALID(client))
+ return;
+
+ memset(client->ip, 0, sizeof(client->ip));
+ memset(client->port, 0, sizeof(client->port));
+
+ if (getpeername(client->client_sock, (struct sockaddr *)&addr, &hlen) < 0)
+ return;
+
+ getnameinfo((struct sockaddr *)&addr, hlen,
+ client->ip, sizeof(client->ip),
+ client->port, sizeof(client->port),
+ NI_NUMERICHOST|NI_NUMERICSERV);
+}
+
+/**
+ * @brief Gets the IP address relative to a client connection opened
+ * by the server.
+ *
+ * @param client Client connection.
+ *
+ * @return Pointer the ip address, or NULL if fails.
+ *
+ * @note The returned string is static, no need to free up memory.
+ */
+char *ws_getaddress(ws_cli_conn_t client)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return (NULL);
+
+ return (cli->ip);
+}
+
+/**
+ * @brief Gets the IP port relative to a client connection opened
+ * by the server.
+ *
+ * @param client Client connection.
+ *
+ * @return Pointer the port, or NULL if fails.
+ *
+ * @note The returned string is static, no need to free up memory.
+ */
+char *ws_getport(ws_cli_conn_t client)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return (NULL);
+
+ return (cli->port);
+}
+
+/**
+ * @brief Creates and send an WebSocket frame with some payload data.
+ *
+ * This routine is intended to be used to create a websocket frame for
+ * a given type e sending to the client. For higher level routines,
+ * please check @ref ws_sendframe_txt and @ref ws_sendframe_bin.
+ *
+ * @param client Target to be send. If NULL, broadcast the message.
+ * @param msg Message to be send.
+ * @param size Binary message size.
+ * @param type Frame type.
+ * @param port Server listen port to broadcast message (if any).
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ *
+ * @note If @p size is -1, it is assumed that a text frame is being sent,
+ * otherwise, a binary frame. In the later case, the @p size is used.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int ws_sendframe_internal(struct ws_connection *client, const char *msg,
+ uint64_t size, int type, uint16_t port)
+{
+ unsigned char *response; /* Response data. */
+ unsigned char frame[10]; /* Frame. */
+ uint8_t idx_first_rData; /* Index data. */
+ struct ws_connection *cli; /* Client. */
+ int idx_response; /* Index response. */
+ ssize_t send_ret; /* Ret send function */
+ uint64_t length; /* Message length. */
+ ssize_t output; /* Bytes sent. */
+ uint64_t i; /* Loop index. */
+
+ /*
+ * Check if there is a valid condition before proceeding.
+ *
+ * Valid ones:
+ * client == true && port == 0
+ * -> send to single client
+ *
+ * client == false && port != 0
+ * -> send to all clients within single port
+ *
+ * Other options are invalid!
+ */
+
+ if (client)
+ {
+ if (port != 0)
+ return (-1);
+ }
+ else
+ {
+ if (port == 0)
+ return (-1);
+ }
+
+ frame[0] = (WS_FIN | type);
+ length = (uint64_t)size;
+
+ /* Split the size between octets. */
+ if (length <= 125)
+ {
+ frame[1] = length & 0x7F;
+ idx_first_rData = 2;
+ }
+
+ /* Size between 126 and 65535 bytes. */
+ else if (length >= 126 && length <= 65535)
+ {
+ frame[1] = 126;
+ frame[2] = (length >> 8) & 255;
+ frame[3] = length & 255;
+ idx_first_rData = 4;
+ }
+
+ /* More than 65535 bytes. */
+ else
+ {
+ frame[1] = 127;
+ frame[2] = (unsigned char)((length >> 56) & 255);
+ frame[3] = (unsigned char)((length >> 48) & 255);
+ frame[4] = (unsigned char)((length >> 40) & 255);
+ frame[5] = (unsigned char)((length >> 32) & 255);
+ frame[6] = (unsigned char)((length >> 24) & 255);
+ frame[7] = (unsigned char)((length >> 16) & 255);
+ frame[8] = (unsigned char)((length >> 8) & 255);
+ frame[9] = (unsigned char)(length & 255);
+ idx_first_rData = 10;
+ }
+
+ /* Add frame bytes. */
+ idx_response = 0;
+ response = malloc(sizeof(unsigned char) * (idx_first_rData + length + 1));
+ if (!response)
+ return (-1);
+
+ for (i = 0; i < idx_first_rData; i++)
+ {
+ response[i] = frame[i];
+ idx_response++;
+ }
+
+ /* Add data bytes. */
+ for (i = 0; i < length; i++)
+ {
+ response[idx_response] = msg[i];
+ idx_response++;
+ }
+
+ response[idx_response] = '\0';
+
+ /* Send to the client if there is one. */
+ output = 0;
+ if (client && port == 0)
+ {
+ output = SEND(client, response, idx_response);
+ goto skip_broadcast;
+ }
+
+ /* clang-format off */
+ pthread_mutex_lock(&mutex);
+ /* Do broadcast. */
+ for (i = 0; i < MAX_CLIENTS; i++)
+ {
+ cli = &client_socks[i];
+
+ if ((cli->client_sock > -1) &&
+ get_client_state(cli) == WS_STATE_OPEN &&
+ (cli->ws_srv.port == port))
+ {
+ if ((send_ret = SEND(cli, response, idx_response)) != -1)
+ output += send_ret;
+ else
+ {
+ output = -1;
+ break;
+ }
+ }
+ }
+ pthread_mutex_unlock(&mutex);
+ /* clang-format on */
+
+skip_broadcast:
+ free(response);
+ return ((int)output);
+}
+
+/**
+ * @brief Send an WebSocket frame with some payload data.
+ *
+ * @param client Target to be send. If NULL, broadcast the message.
+ * @param msg Message to be send.
+ * @param size Binary message size.
+ * @param type Frame type.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ *
+ * @note If @p size is -1, it is assumed that a text frame is being sent,
+ * otherwise, a binary frame. In the later case, the @p size is used.
+ */
+int ws_sendframe(ws_cli_conn_t client, const char *msg, uint64_t size, int type)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return (-1);
+ return ws_sendframe_internal(cli, msg, size, type, 0);
+}
+
+/**
+ * @brief Send an WebSocket frame with some payload data to all clients
+ * connected into the same port.
+ *
+ * @param port Server listen port to broadcast message.
+ * @param msg Message to be send.
+ * @param size Binary message size.
+ * @param type Frame type.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ *
+ * @note If @p size is -1, it is assumed that a text frame is being sent,
+ * otherwise, a binary frame. In the later case, the @p size is used.
+ */
+int ws_sendframe_bcast(uint16_t port, const char *msg, uint64_t size, int type)
+{
+ return ws_sendframe_internal(NULL, msg, size, type, port);
+}
+
+/**
+ * @brief Given a PONG message, decodes the content
+ * as a int32_t number that corresponds to our
+ * PONG id.
+ *
+ * @param msg Content to be decoded.
+ *
+ * @return Returns the PONG id.
+ */
+static inline int32_t pong_msg_to_int32(uint8_t *msg)
+{
+ int32_t pong_id;
+ /* Decodes as big-endian. */
+ pong_id = (msg[3] << 0) | (msg[2] << 8) | (msg[1] << 16) | (msg[0] << 24);
+ return (pong_id);
+}
+
+/**
+ * @brief Given a PING id, encodes the content to be sent
+ * as payload of a PING frame.
+ *
+ * @param ping_id PING id to be encoded.
+ * @param msg Target buffer.
+ */
+static inline void int32_to_ping_msg(int32_t ping_id, uint8_t *msg)
+{
+ /* Encodes as big-endian. */
+ msg[0] = (ping_id >> 24);
+ msg[1] = (ping_id >> 16);
+ msg[2] = (ping_id >> 8);
+ msg[3] = (ping_id >> 0);
+}
+
+/**
+ * @brief Send a ping message and close if the client surpasses
+ * the threshold imposed.
+ *
+ * @param cli Client to be sent.
+ * @param threshold How many pings can miss?.
+ * @param lock Should lock global mutex or not?.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void send_ping_close(struct ws_connection *cli, int threshold, int lock)
+{
+ uint8_t ping_msg[4];
+
+ if (!CLIENT_VALID(cli) || get_client_state(cli) != WS_STATE_OPEN)
+ return;
+
+ /* clang-format off */
+ pthread_mutex_lock(&cli->mtx_ping);
+
+ cli->current_ping_id++;
+ int32_to_ping_msg(cli->current_ping_id, ping_msg);
+
+ /* Send PING. */
+ ws_sendframe_internal(cli, (const char*)ping_msg, sizeof(ping_msg),
+ WS_FR_OP_PING, 0);
+
+ /* Check previous PONG: if greater than threshold, abort. */
+ if ((cli->current_ping_id - cli->last_pong_id) > threshold) {
+ DEBUG("Closing, reason: many unanswered PINGs\n");
+ close_client(cli, lock);
+ }
+
+ pthread_mutex_unlock(&cli->mtx_ping);
+ /* clang-format on */
+}
+
+/**
+ * @brief Sends a PING frame to the client @p cli with threshold
+ * @p threshold.
+ *
+ * This routine sends a PING to a single client pointed to by
+ * @p cli or a broadcast PING if @p cli is NULL. If the specified
+ * client does not respond up to @p threshold PINGs, the connection
+ * is aborted.
+ *
+ * ws_ping() is not automatic: the user who wants to send keep-alive
+ * PINGs *must* call this routine in a timely manner, whether on
+ * a different thread or inside an event.
+ *
+ * See examples/ping/ping.c for a minimal example usage.
+ *
+ * @param cli Client to be sent, if NULL, broadcast.
+ * @param threshold How many ignored PINGs should tolerate? (should be
+ * positive and greater than 0).
+ *
+ * @note It should be noted that the time between each call to
+ * ws_ping() is the timeout itself for receiving the PONG.
+ *
+ * It is also important to note that for devices with unstable
+ * connections (such as a weak WiFi signal or 3/4/5G from a cell phone),
+ * a threshold greater than 1 is advisable.
+ */
+void ws_ping(ws_cli_conn_t client, int threshold)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ int i;
+
+ /* Sanity check. */
+ if (threshold <= 0)
+ return;
+
+ /* PING a single client. */
+ if (cli)
+ send_ping_close(cli, threshold, 1);
+
+ /* PING broadcast. */
+ else
+ {
+ /* clang-format off */
+ pthread_mutex_lock(&mutex);
+ for (i = 0; i < MAX_CLIENTS; i++)
+ send_ping_close(&client_socks[i], threshold, 0);
+ pthread_mutex_unlock(&mutex);
+ /* clang-format on */
+ }
+}
+
+/**
+ * @brief Sends a WebSocket text frame.
+ *
+ * @param client Target to be send.
+ * @param msg Message to be send, null terminated.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ */
+int ws_sendframe_txt(ws_cli_conn_t client, const char *msg)
+{
+ return ws_sendframe(client, msg, (uint64_t)strlen(msg), WS_FR_OP_TXT);
+}
+
+/**
+ * @brief Sends a broadcast WebSocket text frame.
+ *
+ * @param port Server listen port to broadcast message.
+ * @param msg Message to be send, null terminated.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ */
+int ws_sendframe_txt_bcast(uint16_t port, const char *msg)
+{
+ return ws_sendframe_bcast(port, msg, (uint64_t)strlen(msg), WS_FR_OP_TXT);
+}
+
+/**
+ * @brief Sends a WebSocket binary frame.
+ *
+ * @param client Target to be send.
+ * @param msg Message to be send.
+ * @param size Binary message size.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ */
+int ws_sendframe_bin(ws_cli_conn_t client, const char *msg, uint64_t size)
+{
+ return ws_sendframe(client, msg, size, WS_FR_OP_BIN);
+}
+
+/**
+ * @brief Sends a broadcast WebSocket binary frame.
+ *
+ * @param port Server listen port to broadcast message.
+ * @param msg Message to be send.
+ * @param size Binary message size.
+ *
+ * @return Returns the number of bytes written, -1 if error.
+ */
+int ws_sendframe_bin_bcast(uint16_t port, const char *msg, uint64_t size)
+{
+ return ws_sendframe_bcast(port, msg, size, WS_FR_OP_BIN);
+}
+
+/**
+ * @brief For a given @p client, gets the current state for
+ * the connection, or -1 if invalid.
+ *
+ * @param client Client connection.
+ *
+ * @return Returns the connection state or -1 if
+ * invalid @p client.
+ *
+ * @see WS_STATE_CONNECTING
+ * @see WS_STATE_OPEN
+ * @see WS_STATE_CLOSING
+ * @see WS_STATE_CLOSED
+ */
+int ws_get_state(ws_cli_conn_t client)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+ if (!CLIENT_VALID(cli))
+ return -1;
+ return (get_client_state(cli));
+}
+
+/**
+ * @brief Close the client connection for the given @p
+ * client with normal close code (1000) and no reason
+ * string.
+ *
+ * @param client Client connection.
+ *
+ * @return Returns 0 on success, -1 otherwise.
+ *
+ * @note If the client did not send a close frame in
+ * TIMEOUT_MS milliseconds, the server will close the
+ * connection with error code (1002).
+ */
+int ws_close_client(ws_cli_conn_t client)
+{
+ struct ws_connection *cli = get_client_by_cid(client);
+
+ unsigned char clse_code[2];
+ int cc;
+
+ /* Check if client is a valid and connected client. */
+ if (!CLIENT_VALID(cli) || cli->client_sock == -1)
+ return (-1);
+
+ /*
+ * Instead of using do_close(), we use this to avoid using
+ * msg_ctrl buffer from wfd and avoid a race condition
+ * if this is invoked asynchronously.
+ */
+ cc = WS_CLSE_NORMAL;
+ clse_code[0] = (cc >> 8);
+ clse_code[1] = (cc & 0xFF);
+ if (ws_sendframe(
+ client, (const char *)clse_code, sizeof(char) * 2, WS_FR_OP_CLSE) < 0)
+ {
+ DEBUG("An error has occurred while sending closing frame!\n");
+ return (-1);
+ }
+
+ /*
+ * Starts the timeout thread: if the client did not send
+ * a close frame in TIMEOUT_MS milliseconds, the server
+ * will close the connection with error code (1002).
+ */
+ start_close_timeout(cli);
+ return (0);
+}
+
+/**
+ * @brief Checks is a given opcode @p frame
+ * belongs to a control frame or not.
+ *
+ * @param frame Frame opcode to be checked.
+ *
+ * @return Returns 1 if is a control frame, 0 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static inline int is_control_frame(int frame)
+{
+ return (
+ frame == WS_FR_OP_CLSE || frame == WS_FR_OP_PING || frame == WS_FR_OP_PONG);
+}
+
+/**
+ * @brief Checks is a given opcode @p opcode is valid or not.
+ *
+ * @param opcode Frame opcode to be checked.
+ *
+ * @return Returns 1 if valid, 0 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static inline int is_valid_frame(int opcode)
+{
+ return (
+ opcode == WS_FR_OP_TXT || opcode == WS_FR_OP_BIN ||
+ opcode == WS_FR_OP_CONT || opcode == WS_FR_OP_PING ||
+ opcode == WS_FR_OP_PONG || opcode == WS_FR_OP_CLSE
+ );
+}
+
+/**
+ * @brief Do the handshake process.
+ *
+ * @param wfd Websocket Frame Data.
+ *
+ * @return Returns 0 if success, a negative number otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int do_handshake(struct ws_frame_data *wfd)
+{
+ char *response; /* Handshake response message. */
+ char *p; /* Last request line pointer. */
+ ssize_t n; /* Read/Write bytes. */
+
+ /* Read the very first client message. */
+ if ((n = RECV(wfd->client, wfd->frm, sizeof(wfd->frm) - 1)) < 0)
+ return (-1);
+
+ /* Advance our pointers before the first next_byte(). */
+ p = strstr((const char *)wfd->frm, "\r\n\r\n");
+ if (p == NULL)
+ {
+ DEBUG("An empty line with \\r\\n was expected!\n");
+ return (-1);
+ }
+ wfd->amt_read = n;
+ wfd->cur_pos = (size_t)((ptrdiff_t)(p - (char *)wfd->frm)) + 4;
+
+ /* Get response. */
+ if (get_handshake_response((char *)wfd->frm, &response) < 0)
+ {
+ DEBUG("Cannot get handshake response, request was: %s\n", wfd->frm);
+ return (-1);
+ }
+
+ /* Valid request. */
+ DEBUG("Handshaked, response: \n"
+ "------------------------------------\n"
+ "%s"
+ "------------------------------------\n",
+ response);
+
+ /* Send handshake. */
+ if (SEND(wfd->client, response, strlen(response)) < 0)
+ {
+ free(response);
+ DEBUG("As error has occurred while handshaking!\n");
+ return (-1);
+ }
+
+ /* Change state. */
+ set_client_state(wfd->client, WS_STATE_OPEN);
+
+ /* Trigger events and clean up buffers. */
+ wfd->client->ws_srv.evs.onopen(wfd->client->client_id);
+ free(response);
+ return (0);
+}
+
+/**
+ * @brief Sends a close frame, accordingly with the @p close_code
+ * or the message inside @p wfd.
+ *
+ * @param wfd Websocket Frame Data.
+ * @param close_code Websocket close code.
+ *
+ * @return Returns 0 if success, a negative number otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int do_close(struct ws_frame_data *wfd, int close_code)
+{
+ int cc; /* Close code. */
+
+ /* If custom close-code. */
+ if (close_code != -1)
+ {
+ cc = close_code;
+ goto custom_close;
+ }
+
+ /* If empty or have a close reason, just re-send. */
+ if (wfd->frame_size == 0 || wfd->frame_size > 2)
+ goto send;
+
+ /* Parse close code and check if valid, if not, we issue an protocol error.
+ */
+ if (wfd->frame_size == 1)
+ cc = wfd->msg_ctrl[0];
+ else
+ cc = ((int)wfd->msg_ctrl[0]) << 8 | wfd->msg_ctrl[1];
+
+ /* Check if it's not valid, if so, we send a protocol error (1002). */
+ if ((cc < 1000 || cc > 1003) && (cc < 1007 || cc > 1011) &&
+ (cc < 3000 || cc > 4999))
+ {
+ cc = WS_CLSE_PROTERR;
+
+ custom_close:
+ wfd->msg_ctrl[0] = (cc >> 8);
+ wfd->msg_ctrl[1] = (cc & 0xFF);
+
+ if (ws_sendframe(wfd->client->client_id, (const char *)wfd->msg_ctrl, sizeof(char) * 2,
+ WS_FR_OP_CLSE) < 0)
+ {
+ DEBUG("An error has occurred while sending closing frame!\n");
+ return (-1);
+ }
+ return (0);
+ }
+
+ /* Send the data inside wfd->msg_ctrl. */
+send:
+ if (ws_sendframe(wfd->client->client_id, (const char *)wfd->msg_ctrl, wfd->frame_size,
+ WS_FR_OP_CLSE) < 0)
+ {
+ DEBUG("An error has occurred while sending closing frame!\n");
+ return (-1);
+ }
+ return (0);
+}
+
+/**
+ * @brief Send a pong frame in response to a ping frame.
+ *
+ * Accordingly to the RFC, a pong frame must have the same
+ * data payload as the ping frame, so we just send a
+ * ordinary frame with PONG opcode.
+ *
+ * @param wfd Websocket frame data.
+ *
+ * @param frame_size Pong frame size.
+ *
+ * @return Returns 0 if success and a negative number
+ * otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int do_pong(struct ws_frame_data *wfd, uint64_t frame_size)
+{
+ if (ws_sendframe(
+ wfd->client->client_id, (const char *)wfd->msg_ctrl, frame_size, WS_FR_OP_PONG) < 0)
+ {
+ wfd->error = 1;
+ DEBUG("An error has occurred while ponging!\n");
+ return (-1);
+ }
+ return (0);
+}
+
+/**
+ * @brief Read a chunk of bytes and return the next byte
+ * belonging to the frame.
+ *
+ * @param wfd Websocket Frame Data.
+ *
+ * @return Returns the byte read, or -1 if error.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static inline int next_byte(struct ws_frame_data *wfd)
+{
+ ssize_t n;
+
+ /* If empty or full. */
+ if (wfd->cur_pos == 0 || wfd->cur_pos == wfd->amt_read)
+ {
+ if ((n = RECV(wfd->client, wfd->frm, sizeof(wfd->frm))) <= 0)
+ {
+ wfd->error = 1;
+ DEBUG("An error has occurred while trying to read next byte\n");
+ return (-1);
+ }
+ wfd->amt_read = (size_t)n;
+ wfd->cur_pos = 0;
+ }
+ return (wfd->frm[wfd->cur_pos++]);
+}
+
+/**
+ * @brief Skips @p frame_size bytes of the current frame.
+ *
+ * @param wfd Websocket Frame Data.
+ * @param frame_size Amount of bytes to be skipped.
+ *
+ * @return Returns 0 if success, a negative number
+ * otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int skip_frame(struct ws_frame_data *wfd, uint64_t frame_size)
+{
+ uint64_t i;
+ for (i = 0; i < frame_size; i++)
+ {
+ if (next_byte(wfd) == -1)
+ {
+ wfd->error = 1;
+ return (-1);
+ }
+ }
+ return (0);
+}
+
+/**
+ * Frame state data
+ *
+ * This structure holds the current data for handling the
+ * received frames.
+ */
+struct frame_state_data
+{
+ unsigned char *msg_data; /* Data frame. */
+ unsigned char *msg_ctrl; /* Control frame. */
+ uint8_t masks_data[4]; /* Masks data frame array. */
+ uint8_t masks_ctrl[4]; /* Masks control frame array. */
+ uint64_t msg_idx_data; /* Current msg index. */
+ uint64_t msg_idx_ctrl; /* Current msg index. */
+ uint64_t frame_length; /* Frame length. */
+ uint64_t frame_size; /* Current frame size. */
+#ifdef VALIDATE_UTF8
+ uint32_t utf8_state; /* Current UTF-8 state. */
+#endif
+ int32_t pong_id; /* Current PONG id. */
+ uint8_t opcode; /* Frame opcode. */
+ uint8_t is_fin; /* Is FIN frame flag. */
+ uint8_t mask; /* Mask. */
+ int cur_byte; /* Current frame byte. */
+};
+
+/**
+ * @brief Validates TXT frames if UTF8 validation is enabled.
+ * If the content is not valid, the connection is aborted.
+ *
+ * @param wfd WebSocket frame data.
+ * @param fsd Frame state data.
+ *
+ * @return Always 0.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int validate_utf8_txt(struct ws_frame_data *wfd,
+ struct frame_state_data *fsd)
+{
+#ifdef VALIDATE_UTF8
+ /* UTF-8 Validate partial (or not) frame. */
+ if (wfd->frame_type != WS_FR_OP_TXT)
+ return (0);
+
+ if (fsd->is_fin)
+ {
+ if (is_utf8_len_state(
+ fsd->msg_data + (fsd->msg_idx_data - fsd->frame_length),
+ fsd->frame_length, fsd->utf8_state) != UTF8_ACCEPT)
+ {
+ DEBUG("Dropping invalid complete message!\n");
+ wfd->error = 1;
+ do_close(wfd, WS_CLSE_INVUTF8);
+ }
+
+ return (0);
+ }
+
+ /* Check current state for a CONT or initial TXT frame. */
+ fsd->utf8_state =
+ is_utf8_len_state(fsd->msg_data +
+ (fsd->msg_idx_data - fsd->frame_length),
+ fsd->frame_length, fsd->utf8_state);
+
+ /* We can be in any state, except reject. */
+ if (fsd->utf8_state == UTF8_REJECT)
+ {
+ DEBUG("Dropping invalid cont/initial frame!\n");
+ wfd->error = 1;
+ do_close(wfd, WS_CLSE_INVUTF8);
+ }
+#endif
+ return (0);
+}
+
+/**
+ * @brief Handle PONG frames in response to our PING
+ * (or not, unsolicited is possible too).
+ *
+ * @param wfd WebSocket frame data.
+ * @param fsd Frame state data.
+ *
+ * @return Always 0.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int handle_pong_frame(struct ws_frame_data *wfd,
+ struct frame_state_data *fsd)
+{
+ fsd->is_fin = 0;
+
+ /* If there is no content and/or differs the size, ignore it. */
+ if (fsd->frame_size != sizeof(wfd->client->last_pong_id))
+ return (0);
+
+ /*
+ * Our PONG id should be positive and smaller than our
+ * current PING id. If not, ignore.
+ */
+ /* clang-format off */
+ pthread_mutex_lock(&wfd->client->mtx_ping);
+ fsd->pong_id = pong_msg_to_int32(fsd->msg_ctrl);
+ if (fsd->pong_id < 0 || fsd->pong_id > wfd->client->current_ping_id)
+ {
+ pthread_mutex_unlock(&wfd->client->mtx_ping);
+ return (0);
+ }
+ wfd->client->last_pong_id = fsd->pong_id;
+ pthread_mutex_unlock(&wfd->client->mtx_ping);
+ /* clang-format on */
+
+ return (0);
+}
+
+/**
+ * @brief Handle PING frames sending a PONG response.
+ *
+ * @param wfd WebSocket frame data.
+ * @param fsd Frame state data.
+ *
+ * @return Returns 0 if success, -1 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int handle_ping_frame(struct ws_frame_data *wfd,
+ struct frame_state_data *fsd)
+{
+ if (do_pong(wfd, fsd->frame_size) < 0)
+ return (-1);
+
+ /* Quick hack to keep our loop. */
+ fsd->is_fin = 0;
+ return (0);
+}
+
+/**
+ * @brief Handle close frames while checking for UTF8
+ * in the close reason.
+ *
+ * @param wfd WebSocket frame data.
+ * @param fsd Frame state data.
+ *
+ * @return Returns 0 if success, -1 otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int handle_close_frame(struct ws_frame_data *wfd,
+ struct frame_state_data *fsd)
+{
+#ifdef VALIDATE_UTF8
+ /* If there is a close reason, check if it is UTF-8 valid. */
+ if (fsd->frame_size > 2 &&
+ !is_utf8_len(fsd->msg_ctrl + 2, fsd->frame_size - 2))
+ {
+ DEBUG("Invalid close frame payload reason! (not UTF-8)\n");
+ wfd->error = 1;
+ return (-1);
+ }
+#endif
+
+ /*
+ * Since we're aborting, we can scratch the 'data'-related
+ * vars here.
+ */
+ wfd->frame_size = fsd->frame_size;
+ wfd->frame_type = WS_FR_OP_CLSE;
+ free(fsd->msg_data);
+ return (0);
+}
+
+/**
+ * @brief Reads the current frame isolating data from control frames.
+ * The parameters are changed in order to reflect the current state.
+ *
+ * @param wfd Websocket Frame Data.
+ * @param fsd Frame state data.
+ *
+ * @return Returns 0 if success, a negative number otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int read_single_frame(struct ws_frame_data *wfd,
+ struct frame_state_data *fsd)
+{
+ uint64_t *frame_size; /* Curr frame size. */
+ unsigned char *tmp; /* Tmp message. */
+ unsigned char *msg; /* Current message. */
+ uint64_t *msg_idx; /* Message index. */
+ uint8_t *masks; /* Current mask. */
+ int cur_byte; /* Curr byte read. */
+ uint64_t i; /* Loop index. */
+
+ /* Decide which mask and msg to use. */
+ if (is_control_frame(fsd->opcode)) {
+ frame_size = &fsd->frame_size;
+ msg_idx = &fsd->msg_idx_ctrl;
+ masks = fsd->masks_ctrl;
+ msg = fsd->msg_ctrl;
+ }
+ else {
+ frame_size = &wfd->frame_size;
+ msg_idx = &fsd->msg_idx_data;
+ masks = fsd->masks_data;
+ msg = fsd->msg_data;
+ }
+
+ /* Decode masks and length for 16-bit messages. */
+ if (fsd->frame_length == 126)
+ fsd->frame_length = (((uint64_t)next_byte(wfd)) << 8) | next_byte(wfd);
+
+ /* 64-bit messages. */
+ else if (fsd->frame_length == 127)
+ {
+ fsd->frame_length =
+ (((uint64_t)next_byte(wfd)) << 56) | /* frame[2]. */
+ (((uint64_t)next_byte(wfd)) << 48) | /* frame[3]. */
+ (((uint64_t)next_byte(wfd)) << 40) |
+ (((uint64_t)next_byte(wfd)) << 32) |
+ (((uint64_t)next_byte(wfd)) << 24) |
+ (((uint64_t)next_byte(wfd)) << 16) |
+ (((uint64_t)next_byte(wfd)) << 8) |
+ (((uint64_t)next_byte(wfd))); /* frame[9]. */
+ }
+
+ *frame_size += fsd->frame_length;
+
+ /*
+ * Check frame size
+ *
+ * We need to limit the amount supported here, since if
+ * we follow strictly to the RFC, we have to allow 2^64
+ * bytes. Also keep in mind that this is still true
+ * for continuation frames.
+ */
+ if (*frame_size > MAX_FRAME_LENGTH)
+ {
+ DEBUG("Current frame from client %d, exceeds the maximum\n"
+ "amount of bytes allowed (%" PRId64 "/%d)!",
+ wfd->client->client_sock, *frame_size + fsd->frame_length,
+ MAX_FRAME_LENGTH);
+
+ wfd->error = 1;
+ return (-1);
+ }
+
+ /* Read masks. */
+ masks[0] = next_byte(wfd);
+ masks[1] = next_byte(wfd);
+ masks[2] = next_byte(wfd);
+ masks[3] = next_byte(wfd);
+
+ /*
+ * Abort if error.
+ *
+ * This is tricky: we may have multiples error codes from the
+ * previous next_bytes() calls, but, since we're only setting
+ * variables and flags, there is no major issue in setting
+ * them wrong _if_ we do not use their values, thing that
+ * we do here.
+ */
+ if (wfd->error)
+ return (-1);
+
+ /*
+ * Allocate memory.
+ *
+ * The statement below will allocate a new chunk of memory
+ * if msg is NULL with size total_length. Otherwise, it will
+ * resize the total memory accordingly with the message index
+ * and if the current frame is a FIN frame or not, if so,
+ * increment the size by 1 to accommodate the line ending \0.
+ */
+ if (fsd->frame_length > 0)
+ {
+ if (!is_control_frame(fsd->opcode))
+ {
+ tmp = realloc(msg, *msg_idx + fsd->frame_length + fsd->is_fin);
+ if (!tmp)
+ {
+ DEBUG("Cannot allocate memory, requested: % " PRId64 "\n",
+ (*msg_idx + fsd->frame_length + fsd->is_fin));
+
+ wfd->error = 1;
+ return (-1);
+ }
+ msg = tmp;
+ fsd->msg_data = msg;
+ }
+
+ /* Copy to the proper location. */
+ for (i = 0; i < fsd->frame_length; i++, (*msg_idx)++)
+ {
+ /* We were able to read? .*/
+ cur_byte = next_byte(wfd);
+ if (cur_byte == -1)
+ return (-1);
+
+ msg[*msg_idx] = cur_byte ^ masks[i % 4];
+ }
+ }
+
+ /* If we're inside a FIN frame, lets... */
+ if (fsd->is_fin && *frame_size > 0)
+ {
+ /* Increase memory if our FIN frame is of length 0. */
+ if (!fsd->frame_length && !is_control_frame(fsd->opcode))
+ {
+ tmp = realloc(msg, *msg_idx + 1);
+ if (!tmp)
+ {
+ DEBUG("Cannot allocate memory, requested: %" PRId64 "\n",
+ (*msg_idx + 1));
+
+ wfd->error = 1;
+ return (-1);
+ }
+ msg = tmp;
+ fsd->msg_data = msg;
+ }
+ msg[*msg_idx] = '\0';
+ }
+
+ return (0);
+}
+
+/**
+ * @brief Reads the next frame, whether if a TXT/BIN/CLOSE
+ * of arbitrary size.
+ *
+ * @param wfd Websocket Frame Data.
+ *
+ * @return Returns 0 if success, a negative number otherwise.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static int next_complete_frame(struct ws_frame_data *wfd)
+{
+ struct frame_state_data fsd = {0};
+ fsd.msg_data = NULL;
+ fsd.msg_ctrl = wfd->msg_ctrl;
+
+#ifdef VALIDATE_UTF8
+ fsd.utf8_state = UTF8_ACCEPT;
+#endif
+
+ wfd->frame_size = 0;
+ wfd->frame_type = -1;
+ wfd->msg = NULL;
+
+ /* Read until find a FIN or a unsupported frame. */
+ do
+ {
+ fsd.cur_byte = next_byte(wfd);
+ if (fsd.cur_byte == -1)
+ return (-1);
+
+ fsd.is_fin = (fsd.cur_byte & 0xFF) >> WS_FIN_SHIFT;
+ fsd.opcode = (fsd.cur_byte & 0xF);
+
+ /* Check for RSV field. */
+ if (fsd.cur_byte & 0x70)
+ {
+ DEBUG("RSV is set while wsServer do not negotiate extensions!\n");
+ wfd->error = 1;
+ break;
+ }
+
+ /*
+ * Check if the current opcode makes sense:
+ * a) If we're inside a cont frame but no previous data frame
+ *
+ * b) If we're handling a data-frame and receive another data
+ * frame. (it's expected to receive only CONT or control
+ * frames).
+ *
+ * It is worth to note that in a), we do not need to check
+ * if the previous frame was FIN or not: if was FIN, an
+ * on_message event was triggered and this function returned;
+ * so the only possibility here is a previous non-FIN data
+ * frame, ;-).
+ */
+ if ((wfd->frame_type == -1 && fsd.opcode == WS_FR_OP_CONT) ||
+ (wfd->frame_type != -1 && !is_control_frame(fsd.opcode) &&
+ fsd.opcode != WS_FR_OP_CONT))
+ {
+ DEBUG("Unexpected frame was received!, opcode: %d, previous: %d\n",
+ fsd.opcode, wfd->frame_type);
+ wfd->error = 1;
+ break;
+ }
+
+ /* Check if one of the valid opcodes. */
+ if (!is_valid_frame(fsd.opcode))
+ {
+ DEBUG("Unsupported frame opcode: %d\n", fsd.opcode);
+ /* We should consider as error receive an unknown frame. */
+ wfd->frame_type = fsd.opcode;
+ wfd->error = 1;
+ break;
+ }
+
+ /* Check our current state: if CLOSING, we only accept close frames. */
+ if (get_client_state(wfd->client) == WS_STATE_CLOSING &&
+ fsd.opcode != WS_FR_OP_CLSE)
+ {
+ DEBUG("Unexpected frame received, expected CLOSE (%d), "
+ "received: (%d)",
+ WS_FR_OP_CLSE, fsd.opcode);
+ wfd->error = 1;
+ break;
+ }
+
+ /* Only change frame type if not a CONT frame. */
+ if (fsd.opcode != WS_FR_OP_CONT && !is_control_frame(fsd.opcode))
+ wfd->frame_type = fsd.opcode;
+
+ fsd.mask = next_byte(wfd);
+ fsd.frame_length = fsd.mask & 0x7F;
+ fsd.frame_size = 0;
+ fsd.msg_idx_ctrl = 0;
+
+ /*
+ * We should deny non-FIN control frames or that have
+ * more than 125 octets.
+ */
+ if (is_control_frame(fsd.opcode) &&
+ (!fsd.is_fin || fsd.frame_length > 125))
+ {
+ DEBUG("Control frame bigger than 125 octets or not a FIN "
+ "frame!\n");
+ wfd->error = 1;
+ break;
+ }
+
+ /* Read a single frame, and then handle accordingly. */
+ if (read_single_frame(wfd, &fsd) < 0)
+ break;
+
+ /* Handle each frame
+ * Obs: If BIN, nothing should be done unless we got
+ * a FIN-frame.
+ */
+ switch (fsd.opcode) {
+ /* UTF-8 Validate partial (or not) frame. */
+ case WS_FR_OP_CONT:
+ case WS_FR_OP_TXT: {
+ validate_utf8_txt(wfd, &fsd);
+ break;
+ }
+ /*
+ * We _may_ send a PING frame if the ws_ping() routine was invoked.
+ *
+ * If the content is invalid and/or differs the size, ignore it.
+ * (maybe unsolicited PONG).
+ */
+ case WS_FR_OP_PONG: {
+ handle_pong_frame(wfd, &fsd);
+ goto next_it;
+ break;
+ }
+ /* We should answer to a PING frame as soon as possible. */
+ case WS_FR_OP_PING: {
+ if (handle_ping_frame(wfd, &fsd) < 0)
+ goto done;
+ break;
+ }
+ /* We interrupt the loop as soon as we find a CLOSE frame. */
+ case WS_FR_OP_CLSE: {
+ if (handle_close_frame(wfd, &fsd) < 0)
+ goto done;
+ return (0);
+ break;
+ }
+ }
+
+next_it:;
+
+ } while (!fsd.is_fin && !wfd->error);
+
+done:
+ /* Check for error. */
+ if (wfd->error)
+ {
+ free(fsd.msg_data);
+ wfd->msg = NULL;
+ return (-1);
+ }
+
+ wfd->msg = fsd.msg_data;
+ return (0);
+}
+
+/**
+ * @brief Establishes to connection with the client and trigger
+ * events when occurs one.
+ *
+ * @param vclient Client connection.
+ *
+ * @return Returns @p vclient.
+ *
+ * @note This will be run on a different thread.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void *ws_establishconnection(void *vclient)
+{
+ struct ws_frame_data wfd; /* WebSocket frame data. */
+ struct ws_connection *client; /* Client structure. */
+ int clse_thrd; /* Time-out close thread. */
+
+ client = vclient;
+
+ /* Prepare frame data. */
+ memset(&wfd, 0, sizeof(wfd));
+ wfd.client = client;
+
+ /* Do handshake. */
+ if (do_handshake(&wfd) < 0)
+ goto closed;
+
+ /* Read next frame until client disconnects or an error occur. */
+ while (next_complete_frame(&wfd) >= 0)
+ {
+ /* Text/binary event. */
+ if ((wfd.frame_type == WS_FR_OP_TXT ||
+ wfd.frame_type == WS_FR_OP_BIN) && !wfd.error)
+ {
+ client->ws_srv.evs.onmessage(client->client_id, wfd.msg, wfd.frame_size,
+ wfd.frame_type);
+ }
+
+ /* Close event. */
+ else if (wfd.frame_type == WS_FR_OP_CLSE && !wfd.error)
+ {
+ /*
+ * We only send a CLOSE frame once, if we're already
+ * in CLOSING state, there is no need to send.
+ */
+ if (get_client_state(client) != WS_STATE_CLOSING)
+ {
+ set_client_state(client, WS_STATE_CLOSING);
+
+ /* We only send a close frameSend close frame */
+ do_close(&wfd, -1);
+ }
+
+ free(wfd.msg);
+ break;
+ }
+
+ free(wfd.msg);
+ }
+
+ /*
+ * on_close events always occur, whether for client closure
+ * or server closure, as the server is expected to
+ * always know when the client disconnects.
+ */
+ client->ws_srv.evs.onclose(client->client_id);
+
+closed:
+ clse_thrd = client->close_thrd;
+
+ /* Wait for timeout thread if necessary. */
+ if (clse_thrd)
+ {
+ pthread_cond_signal(&client->cnd_state_close);
+ pthread_join(client->thrd_tout, NULL);
+ }
+
+ /* Close connectin properly. */
+ if (get_client_state(client) != WS_STATE_CLOSED) {
+ DEBUG("Closing: normal close\n");
+ close_client(client, 1);
+ }
+
+ return (vclient);
+}
+
+/**
+ * Accept parameters.
+ */
+struct ws_accept_params
+{
+ int sock;
+ struct ws_server ws_srv;
+};
+
+/**
+ * @brief Main loop that keeps accepting new connections.
+ *
+ * @param data Server socket.
+ *
+ * @return Returns @p data.
+ *
+ * @note This may be run on a different thread.
+ *
+ * @attention This is part of the internal API and is documented just
+ * for completeness.
+ */
+static void *ws_accept(void *data)
+{
+ struct ws_accept_params *ws_prm; /* wsServer parameters. */
+ struct sockaddr_storage sa; /* Client. */
+ pthread_t client_thread; /* Client thread. */
+ struct timeval time; /* Client socket timeout. */
+ socklen_t salen; /* Length of sockaddr. */
+ int new_sock; /* New opened connection. */
+ int sock; /* Server sock. */
+ int i; /* Loop index. */
+
+ ws_prm = data;
+ sock = ws_prm->sock;
+ salen = sizeof(sa);
+
+ while (1)
+ {
+ /* Accept. */
+ new_sock = accept(sock, (struct sockaddr *)&sa, &salen);
+ if (new_sock < 0)
+ panic("Error on accepting connections..");
+
+ if (timeout)
+ {
+ time.tv_sec = timeout / 1000;
+ time.tv_usec = (timeout % 1000) * 1000;
+
+ /*
+ * Socket timeout
+ * This feature seems to be supported on Linux, Windows,
+ * macOS and FreeBSD.
+ *
+ * See:
+ * https://linux.die.net/man/3/setsockopt
+ */
+ setsockopt(new_sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&time,
+ sizeof(struct timeval));
+ }
+
+ /* Adds client socket to socks list. */
+ pthread_mutex_lock(&mutex);
+ for (i = 0; i < MAX_CLIENTS; i++)
+ {
+ if (client_socks[i].client_sock == -1)
+ {
+ memcpy(&client_socks[i].ws_srv, &ws_prm->ws_srv,
+ sizeof(struct ws_server));
+
+ client_socks[i].client_sock = new_sock;
+ client_socks[i].state = WS_STATE_CONNECTING;
+ client_socks[i].close_thrd = false;
+ client_socks[i].last_pong_id = -1;
+ client_socks[i].current_ping_id = -1;
+ client_socks[i].client_id = get_next_cid();
+ set_client_address(&client_socks[i]);
+
+ if (pthread_mutex_init(&client_socks[i].mtx_state, NULL))
+ panic("Error on allocating close mutex");
+ if (pthread_cond_init(&client_socks[i].cnd_state_close, NULL))
+ panic("Error on allocating condition var\n");
+ if (pthread_mutex_init(&client_socks[i].mtx_snd, NULL))
+ panic("Error on allocating send mutex");
+ if (pthread_mutex_init(&client_socks[i].mtx_ping, NULL))
+ panic("Error on allocating ping/pong mutex");
+ break;
+ }
+ }
+ pthread_mutex_unlock(&mutex);
+
+ /* Client socket added to socks list ? */
+ if (i != MAX_CLIENTS)
+ {
+ if (pthread_create(
+ &client_thread, NULL, ws_establishconnection, &client_socks[i]))
+ panic("Could not create the client thread!");
+
+ pthread_detach(client_thread);
+ }
+ else
+ close_socket(new_sock);
+ }
+
+ free(data);
+ return (data);
+}
+
+/**
+ * @brief By using the server parameters provided in @p ws_srv,
+ * create a socket and bind it accordingly with the server
+ * configurations.
+ *
+ * @param ws_srv Web Socket configurations.
+ *
+ * @return Returns the socket file descriptor.
+ */
+static int do_bind_socket(struct ws_server *ws_srv)
+{
+ struct addrinfo hints, *results, *try;
+ char port[8] = {0};
+ int reuse;
+ int sock;
+
+ reuse = 1;
+
+ /* Prepare the getaddrinfo structure. */
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_flags = AI_PASSIVE;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ /* Port. */
+ snprintf(port, sizeof port - 1, "%d", ws_srv->port);
+
+ if (getaddrinfo(ws_srv->host, port, &hints, &results) != 0)
+ panic("getaddrinfo() failed");
+
+ /* Try to create a socket with one of the returned addresses. */
+ for (try = results; try != NULL; try = try->ai_next)
+ {
+ /* try to make a socket with this setup */
+ if ((sock = socket(try->ai_family, try->ai_socktype,
+ try->ai_protocol)) < 0)
+ {
+ continue;
+ }
+
+ /* Reuse previous address. */
+ if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse,
+ sizeof(reuse)) < 0)
+ {
+ panic("setsockopt(SO_REUSEADDR) failed");
+ }
+
+ /* Bind. */
+ if (bind(sock, try->ai_addr, try->ai_addrlen) < 0)
+ panic("Bind failed");
+
+ /* if it worked, we're done. */
+ break;
+ }
+
+ freeaddrinfo(results);
+
+ /* Check if binded with success. */
+ if (try == NULL)
+ panic("couldn't find a port to bind to");
+
+ return (sock);
+}
+
+/**
+ * @brief Main loop for the server.
+ *
+ * @param ws_srv Web Socket server parameters.
+ *
+ * @return If @p thread_loop != 0, returns 0. Otherwise, never
+ * returns.
+ */
+int ws_socket(struct ws_server *ws_srv)
+{
+ struct ws_accept_params *ws_prm; /* Accept parameters. */
+ pthread_t accept_thread; /* Accept thread. */
+ int sock; /* Client sock. */
+
+ timeout = ws_srv->timeout_ms;
+
+ /* Ignore 'unused functions' warnings. */
+ ((void)skip_frame);
+
+ /* Allocates our parameters data and copy the ws_server structure. */
+ ws_prm = malloc(sizeof(*ws_prm));
+ if (!ws_prm)
+ panic("Unable to allocate ws parameters, out of memory!\n");
+
+ memcpy(&ws_prm->ws_srv, ws_srv, sizeof(*ws_srv));
+
+#ifdef _WIN32
+ WSADATA wsaData;
+ if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
+ panic("WSAStartup failed!");
+
+ /**
+ * Sets stdout to be non-buffered.
+ *
+ * According to the docs from MSDN (setvbuf page), Windows do not
+ * really supports line buffering but full-buffering instead.
+ *
+ * Quote from the docs:
+ * "... _IOLBF For some systems, this provides line buffering.
+ * However, for Win32, the behavior is the same as _IOFBF"
+ */
+ setvbuf(stdout, NULL, _IONBF, 0);
+#endif
+
+ /* Create socket and bind. */
+ sock = do_bind_socket(ws_srv);
+
+ /* Listen. */
+ if (listen(sock, MAX_CLIENTS) < 0)
+ panic("Unable to listen!\n");
+
+ /* Wait for incoming connections. */
+ printf("Waiting for incoming connections...\n");
+ memset(client_socks, -1, sizeof(client_socks));
+
+ /* Accept connections. */
+ ws_prm->sock = sock;
+
+ if (!ws_srv->thread_loop)
+ ws_accept(ws_prm);
+ else
+ {
+ if (pthread_create(&accept_thread, NULL, ws_accept, (void *)ws_prm))
+ panic("Could not create the client thread!");
+ pthread_detach(accept_thread);
+ }
+
+ return (0);
+}
+
+#ifdef AFL_FUZZ
+/**
+ * @brief WebSocket fuzzy test routine
+ *
+ * @param evs Events structure.
+ *
+ * @param file File to be read.
+ *
+ * @return Returns 0, or crash.
+ *
+ * @note This is a 'fuzzy version' of the function @ref ws_socket.
+ * This routine do not listen to any port nor accept multiples
+ * connections. It is intended to read a stream of frames through a
+ * file and process it as if they are coming from a socket.
+ *
+ * This behavior enables us to test wsServer against fuzzers, like
+ * AFL, and see if it crashes, hangs or behaves normally, even under
+ * weird conditions.
+ */
+int ws_file(struct ws_events *evs, const char *file)
+{
+ int sock;
+ sock = open(file, O_RDONLY);
+ if (sock < 0)
+ panic("Invalid file\n");
+
+ /* Copy events. */
+ memcpy(&cli_events, evs, sizeof(struct ws_events));
+
+ /* Clear client socks list. */
+ memset(client_socks, -1, sizeof(client_socks));
+
+ /* Set client settings. */
+ client_socks[0].client_sock = sock;
+ client_socks[0].state = WS_STATE_CONNECTING;
+ client_socks[0].close_thrd = false;
+
+ /* Initialize mutexes. */
+ if (pthread_mutex_init(&client_socks[0].mtx_state, NULL))
+ panic("Error on allocating close mutex");
+ if (pthread_cond_init(&client_socks[0].cnd_state_close, NULL))
+ panic("Error on allocating condition var\n");
+ if (pthread_mutex_init(&client_socks[0].mtx_snd, NULL))
+ panic("Error on allocating send mutex");
+ if (pthread_mutex_init(&client_socks[0].mtx_ping, NULL))
+ panic("Error on allocating ping/pong mutex");
+
+ ws_establishconnection(&client_socks[0]);
+ return (0);
+}
+#endif
diff --git a/ws.h b/ws.h
new file mode 100644
index 0000000..900d03b
--- /dev/null
+++ b/ws.h
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2016-2022 Davidson Francis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ */
+
+/**
+ * @dir include/
+ * @brief wsServer include directory
+ *
+ * @file ws.h
+ * @brief wsServer constants and functions.
+ */
+#ifndef WS_H
+#define WS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ #include
+ #include
+ #include
+
+ /**
+ * @name Global configurations
+ */
+ /**@{*/
+ /**
+ * @brief Max clients connected simultaneously.
+ */
+#ifndef MAX_CLIENTS
+ #define MAX_CLIENTS 8
+#endif
+
+ /**
+ * @name Key and message configurations.
+ */
+ /**@{*/
+ /**
+ * @brief Message buffer length.
+ */
+ #define MESSAGE_LENGTH 2048
+ /**
+ * @brief Maximum frame/message length.
+ */
+ #define MAX_FRAME_LENGTH (16*1024*1024)
+ /**
+ * @brief WebSocket key length.
+ */
+ #define WS_KEY_LEN 24
+ /**
+ * @brief Magic string length.
+ */
+ #define WS_MS_LEN 36
+ /**
+ * @brief Accept message response length.
+ */
+ #define WS_KEYMS_LEN (WS_KEY_LEN + WS_MS_LEN)
+ /**
+ * @brief Magic string.
+ */
+ #define MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+ /**@}*/
+
+ /**
+ * @name Handshake constants.
+ */
+ /**@{*/
+ /**
+ * @brief Alias for 'Sec-WebSocket-Key'.
+ */
+ #define WS_HS_REQ "Sec-WebSocket-Key"
+
+ /**
+ * @brief Handshake accept message length.
+ */
+ #define WS_HS_ACCLEN 130
+
+ /**
+ * @brief Handshake accept message.
+ */
+ #define WS_HS_ACCEPT \
+ "HTTP/1.1 101 Switching Protocols\r\n" \
+ "Upgrade: websocket\r\n" \
+ "Connection: Upgrade\r\n" \
+ "Sec-WebSocket-Accept: "
+ /**@}*/
+
+ /**
+ * @name Frame types.
+ */
+ /**@{*/
+ /**
+ * @brief Frame FIN.
+ */
+ #define WS_FIN 128
+
+ /**
+ * @brief Frame FIN shift
+ */
+ #define WS_FIN_SHIFT 7
+
+ /**
+ * @brief Continuation frame.
+ */
+ #define WS_FR_OP_CONT 0
+
+ /**
+ * @brief Text frame.
+ */
+ #define WS_FR_OP_TXT 1
+
+ /**
+ * @brief Binary frame.
+ */
+ #define WS_FR_OP_BIN 2
+
+ /**
+ * @brief Close frame.
+ */
+ #define WS_FR_OP_CLSE 8
+
+ /**
+ * @brief Ping frame.
+ */
+ #define WS_FR_OP_PING 0x9
+
+ /**
+ * @brief Pong frame.
+ */
+ #define WS_FR_OP_PONG 0xA
+
+ /**
+ * @brief Unsupported frame.
+ */
+ #define WS_FR_OP_UNSUPPORTED 0xF
+ /**@}*/
+
+ /**
+ * @name Close codes
+ */
+ /**@{*/
+ /**
+ * @brief Normal close
+ */
+ #define WS_CLSE_NORMAL 1000
+ /**
+ * @brief Protocol error
+ */
+ #define WS_CLSE_PROTERR 1002
+ /**@}*/
+ /**
+ * @brief Inconsistent message (invalid utf-8)
+ */
+ #define WS_CLSE_INVUTF8 1007
+
+ /**
+ * @name Connection states
+ */
+ /**@{*/
+ /**
+ * @brief Connection not established yet.
+ */
+ #define WS_STATE_CONNECTING 0
+ /**
+ * @brief Communicating.
+ */
+ #define WS_STATE_OPEN 1
+ /**
+ * @brief Closing state.
+ */
+ #define WS_STATE_CLOSING 2
+ /**
+ * @brief Closed.
+ */
+ #define WS_STATE_CLOSED 3
+ /**@}*/
+
+ /**
+ * @name Timeout util
+ */
+ /**@{*/
+ /**
+ * @brief Nanoseconds macro converter
+ */
+ #define MS_TO_NS(x) ((x)*1000000)
+ /**
+ * @brief Timeout in milliseconds.
+ */
+ #define TIMEOUT_MS (500)
+ /**@}*/
+
+ /**
+ * @name Handshake constants.
+ */
+ /**@{*/
+ /**
+ * @brief Debug
+ */
+ #ifdef VERBOSE_MODE
+ #define DEBUG(...) fprintf(stderr, __VA_ARGS__)
+ #else
+ #define DEBUG(...)
+ #endif
+ /**@}*/
+
+ #ifndef AFL_FUZZ
+ #define SEND(client,buf,len) send_all((client), (buf), (len), MSG_NOSIGNAL)
+ #define RECV(fd,buf,len) recv((fd)->client_sock, (buf), (len), 0)
+ #else
+ #define SEND(client,buf,len) write(fileno(stdout), (buf), (len))
+ #define RECV(fd,buf,len) read((fd)->client_sock, (buf), (len))
+ #endif
+
+ /* Opaque client connection type. */
+ typedef uint64_t ws_cli_conn_t;
+
+ /* Opaque server instance type. */
+ typedef struct ws_server ws_server_t;
+
+ /**
+ * @brief Get server context.
+ * Set when initializing `.context` in `struct ws_server`.
+ */
+ void *ws_get_server_context(ws_cli_conn_t client);
+
+ /**
+ * @brief Set connection context.
+ */
+ void ws_set_connection_context(ws_cli_conn_t client, void *ptr);
+
+ /**
+ * @brief Get connection context.
+ */
+ void *ws_get_connection_context(ws_cli_conn_t client);
+
+ /**
+ * @brief events Web Socket events types.
+ */
+ struct ws_events
+ {
+ /**
+ * @brief On open event, called when a new client connects.
+ */
+ void (*onopen)(ws_cli_conn_t client);
+ /**
+ * @brief On close event, called when a client disconnects.
+ */
+ void (*onclose)(ws_cli_conn_t client);
+ /**
+ * @brief On message event, called when a client sends a text
+ * or binary message.
+ */
+ void (*onmessage)(ws_cli_conn_t client,
+ const unsigned char *msg, uint64_t msg_size, int type);
+ };
+
+ /**
+ * @brief server Web Socket server parameters
+ */
+ struct ws_server
+ {
+ /**
+ * @brief Required hostname that the wsServer will bind to
+ */
+ const char *host;
+ /**
+ * @brief Listening port
+ */
+ uint16_t port;
+ /**
+ * @brief Whether if the ws_socket() should create a new thread
+ * and be non-blocking (1) or not (0).
+ */
+ int thread_loop;
+ /**
+ * @brief Ping timeout in milliseconds
+ */
+ uint32_t timeout_ms;
+ /**
+ * @brief Server events.
+ */
+ struct ws_events evs;
+ /**
+ * @brief Server context.
+ * Provided by the user, can be accessed via `ws_get_server_context` from `onopen`.
+ */
+ void* context;
+ };
+
+ /* Forward declarations. */
+
+ /* Internal usage. */
+ extern int get_handshake_accept(char *wsKey, unsigned char **dest);
+ extern int get_handshake_response(char *hsrequest, char **hsresponse);
+
+ /* External usage. */
+ extern char *ws_getaddress(ws_cli_conn_t client);
+ extern char *ws_getport(ws_cli_conn_t client);
+ extern int ws_sendframe(
+ ws_cli_conn_t client, const char *msg, uint64_t size, int type);
+ extern int ws_sendframe_bcast(
+ uint16_t port, const char *msg, uint64_t size, int type);
+ extern int ws_sendframe_txt(ws_cli_conn_t client, const char *msg);
+ extern int ws_sendframe_txt_bcast(uint16_t port, const char *msg);
+ extern int ws_sendframe_bin(ws_cli_conn_t client, const char *msg,
+ uint64_t size);
+ extern int ws_sendframe_bin_bcast(uint16_t port, const char *msg,
+ uint64_t size);
+ extern int ws_get_state(ws_cli_conn_t client);
+ extern int ws_close_client(ws_cli_conn_t client);
+ extern int ws_socket(struct ws_server *ws_srv);
+
+ /* Ping routines. */
+ extern void ws_ping(ws_cli_conn_t cid, int threshold);
+
+#ifdef AFL_FUZZ
+ extern int ws_file(struct ws_events *evs, const char *file);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* WS_H */