A Place Where No Dreams Come True...

Current Topic: The Raspberry Pi 3 provides a rich set of interfaces combined with a powerful quad core 64-bit ARM processor and a Gig of RAM. Running a Linux operating system it becomes a first class single board computer solution...

Using the Raspberry Pi As A Simple WebSocket Echo Server...

In this demonstration is presented a basic echo server using the HTML5 WebSockets interface. It offers only a small set of login requirements which has not been problematic for this example which operates for private use even though it has public access. The use of the WebSocket interface allows for a low overhead persistant connection between a remote client browser and the server. It does however encapsulate the data within a packet. The packet additionally uses a header that allow for differentiation of data, or control, types.

In this example the distinction between binary and text packet types is used to determine the destination(s) for each client update packet. As a slight (very) optimization binary packets are echoed to all WebSocket clients except the source client. Otherwise packets are echoed to all WebSocket clients. Additionally there is some minimal control packet management.

Running The Server As A Linux System Daemon (Service)...

Blah blah!

//-----------------------------------------------------------------------------
// smegchat.c
//
//  WebSocket Whiteboard Chat (echo) Server Implementation.
//
// Copyright (c) 2015 - No Fun Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; 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 2 of the License, or
//  (at your option) any later version.
//
//  This software 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.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//-----------------------------------------------------------------------------

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#include <wsutils.h>

#include "interpret.h"
#include "util.h"

//-----------------------------------------------------------------------------

#define MAXCONNECTIONS   8
#define SERVERMSGLENGTH  4096
#define SOCKETBUFFERSIZE 2048
#define MAXCLIENTMSGSIZE (SOCKETBUFFERSIZE - 48)
#define NICKNAMELEN      48

#define INVALID_NICKNAME "Invalid user name for this session!\r\n"

#define WSMASKDEFAULT 0X23659487

#define WEBSOCKSERVER   0
#define LOCALCONSOLE    1

// Server protocols accepted.
#define WSPROT_NONE     0
#define WSPROT_WBCHAT   1
#define WSPROT_AUTOMATE 2
#define WSPROT_CAMERA   4
#define WSPROT_WEATHER  8
#define WSPROT_ALL     -1
#define WSPROT_BROADCAST WSPROT_ALL

//-----------------------------------------------------------------------------

struct _client {
  int sock;
  int wsctl;
  int connected;
  int type;
  int octin;
  int octout;
  int pntin;
  int pntout;
  int paylen;
  struct sockaddr_in cliaddr;
  char nick[NICKNAMELEN];
  unsigned char in[SOCKETBUFFERSIZE + 2];
  unsigned char out[SOCKETBUFFERSIZE + 2];
};

struct _server {
  int ctlsock;
  int srvsock;
  socklen_t size;
  int serving;
  struct sockaddr_in srvaddr;
  struct sockaddr_in ctladdr;
  fd_set read_fd_set;
  fd_set active_read_set;
  int msglen;
  int octets;
  unsigned char *message;
  struct _client client[MAXCONNECTIONS];
  struct _client *conn;
};

//-----------------------------------------------------------------------------

static uint32_t wsmask;

//----------------------------------------------------------------------------
// Get new websocket mask.

static uint32_t get_next_mask(void)
{
  wsmask++;
  //return wsmask;
  // According to the Mozilla WebSocket Implementation guide the server to client
  //  packet should always be unmasked since its purpose is not for security
  //  but instead to help reduce the possibility of 'cache polution' especially
  //  in regard to proxies. While in return the client to server packet must
  //  always be masked.
  return 0;
}

//----------------------------------------------------------------------------
// Extract username (if any) from request URL.

static uint8_t get_nickname(const char *request, char *nick)
{
  uint8_t i;
  uint8_t p = 0;

  memset(nick, 0, NICKNAMELEN);
  for(i = 0; i < strlen(request); i++)
  {
    if(request[i] == '?')
    {
      i++;
      while(request[i] && (request[i] != ' '))
      {
        nick[p++] = request[i++];
        if(p == NICKNAMELEN - 1)
        {
          p = 0;
          break;
        }
      }
      break;
    }
  }
  return p;
}

//-----------------------------------------------------------------------------
// Try to find an empty client slot.

static int server_find_available(struct _server *server)
{
  int i;

  for(i = 0; i < MAXCONNECTIONS; i++)
  {
    if(server->client[i].sock == 0)
    {
      break;
    }
  }
  if(i >= MAXCONNECTIONS)
  {
    i = -1;
  }
  return i;
}

//-----------------------------------------------------------------------------
// Find the client object which 'sock' belongs to.

static int server_find_client(struct _server *server, int sock)
{
  int i;

  for(i = 0; i < MAXCONNECTIONS; i++)
  {
    if(server->client[i].sock == sock)
    {
      break;
    }
  }
  if(i >= MAXCONNECTIONS)
  {
    i = -1;
  }
  return i;
}

//----------------------------------------------------------------------------
// Exit the daemon gracefully.

static int daemon_quit(void *object, char *params)
{
  struct _server *server = object;

  server_log(dbglevl_msg, "smegchat client request - shutdown.\n");
  server->serving = 0;
  return 0;
}

//----------------------------------------------------------------------------
// Close the local console connection.

static int daemon_close(void *object, char *params)
{
  struct _server *server = object;

  server_log(dbglevl_msg, "smegchat client request - exit.\n");
  FD_CLR(server->conn->sock, &server->active_read_set);
  close(server->conn->sock);
  server->conn->sock = 0;
  server->conn->connected = 0;
  server->conn->nick[0] = '\0';
  return 0;
}

//----------------------------------------------------------------------------
// Display a list of connected chat clients.

static int daemon_show_clients(void *object, char *params)
{
  int cli;
  struct _server *server = object;

  server_log(dbglevl_msg, "smegchat client request - show users...\n");
  console_send(server->conn->sock, "Smegchat daemon current client list...\n");
  for(cli = 0;  cli < MAXCONNECTIONS; cli++)
  {
    if(server->client[cli].sock > 0)
    {
      sprintf((char*)server->client[cli].out, " client %s@%s:%i.\n",
              server->client[cli].nick,
              inet_ntoa(server->client[cli].cliaddr.sin_addr),
              ntohs(server->client[cli].cliaddr.sin_port));
      console_send(server->conn->sock, (char*)server->client[cli].out);
    }
  }
  return 0;
}

//----------------------------------------------------------------------------
// Parse comand from the clients console input buffer.

static int console_parse(struct _client *client)
{
  int i;

  client->octin = recv(client->sock, client->in, MAXCLIENTMSGSIZE, 0);
  if(client->octin == -1)
  {
    server_error("error : control_parse():recv(%i) failed - ", client->sock);
  }
  else
  {
    for(i = 0; i < client->octin; i++)
    {
      // Remove any trailing LF/CR chars.
      if((client->in[i] == '\n') || (client->in[i] == '\r'))
      {
        client->in[i] = '\0';
      }
    }
    // Ensure text delimiter.
    client->in[i] = '\0';
  }
  return client->octin;
}

//----------------------------------------------------------------------------
// Console interpreter command lookup table.

static const struct _command cmdtable[] = {
  { "clients",     daemon_show_clients },
  { "exit",        daemon_close },
  { "shutdown",    daemon_quit }
};

static const size_t size_cmdtable = sizeof(cmdtable) / sizeof(struct _command);

//----------------------------------------------------------------------------
// Interpret a line from the clients console input buffer.

static int console_interpret(struct _server *server, struct _client *client)
{
  int rtn;

  rtn = console_parse(client);
  if(rtn > 0)
  {
    server->conn = client;
    rtn = interpret(server, (char*)client->in, cmdtable, size_cmdtable);
    console_prompt(client->sock);
  }
  else if(rtn == 0)
  {
    // Asume socket closed from other side.
    server_log(dbglevl_msg, "control_interpret(%s:%i-%i) parse error assuming closed socket.\n",
               inet_ntoa(client->cliaddr.sin_addr),
               ntohs(client->cliaddr.sin_port),
               client->sock);
    FD_CLR(client->sock, &server->active_read_set);
    close(client->sock);
    client->sock = 0;
    client->connected = 0;
    client->nick[0] = '\0';
    rtn = -1;
  }
  return rtn;
}

//----------------------------------------------------------------------------
// Gracefully close (hopefuly) an existing connection.

static void client_close(struct _client *client, unsigned char *msg)
{
  ws_set_frame(client->out, msg, WS_FRAME_CLOSE, get_next_mask(), strlen((char*)msg));
  send(client->sock, client->out, ws_get_frame_size(client->out), 0);
}

//----------------------------------------------------------------------------
// Transmit global message to all connected chat clients.

static void client_global_message(struct _server *server, uint8_t *packet)
{
  int cli;

  for(cli = 0; cli < MAXCONNECTIONS; cli++)
  {
    if((server->client[cli].sock > 0))
    {
      if(server->client[cli].wsctl == WEBSOCKSERVER)
      {
        send(server->client[cli].sock, packet, ws_get_frame_size(packet), 0);
      }
    }
  }
}

//----------------------------------------------------------------------------
// Transmit binary message to all connected chat clients other then 'client'.

static void client_binary_message(struct _server *server, struct _client *client)
{
  int cli;

  for(cli = 0; cli < MAXCONNECTIONS; cli++)
  {
    if((server->client[cli].sock > 0))
    {
      if(server->client[cli].wsctl == WEBSOCKSERVER)
      {
        if(server->client[cli].sock != client->sock)
        {
          send(server->client[cli].sock, client->out, client->octout, 0);
        }
      }
    }
  }
}

//-----------------------------------------------------------------------------
// Send the list of connected users to all chat clients.

static int server_update_users(struct _server *server)
{
  int cli;
  int update = 0;
  char user[NICKNAMELEN * 2];

  sprintf((char*)server->message, "$users online...\n");
  for(cli = 0; cli < MAXCONNECTIONS; cli++)
  {
    if(server->client[cli].connected && server->client[cli].nick[0])
    {
      sprintf(user, " %s\n", server->client[cli].nick);
      strcat((char*)server->message, user);
      update = 1;
    }
  }
  if(update)
  {
    ws_set_frame(server->message, server->message, WS_FRAME_TEXT,
                 get_next_mask(), strlen((char*)server->message));
    client_global_message(server, server->message);
  }
  return 0;
}

//-----------------------------------------------------------------------------
// Parse a packet from a chat client.

static int client_parse(struct _client *client)
{
  client->pntin = 0;
  client->octin = recv(client->sock, client->in, MAXCLIENTMSGSIZE, 0);
  if(client->octin == -1)
  {
    server_error("error : client_parse():recv(%i) failed - ", client->sock);
  }
  return client->octin;
}

//-----------------------------------------------------------------------------
// Make sure the new chat client meets minimum 'join' requirements.

static void client_validate(struct _server *server, struct _client *client)
{
  int cli;

  if(strlen(client->nick) == 0)
  {
    cli = 0;
  }
  else
  {
    for(cli = 0; cli < MAXCONNECTIONS; cli++)
    {
      if(server->client[cli].sock != client->sock)
      {
        if(strlen(server->client[cli].nick) && (strcmp(server->client[cli].nick, client->nick) == 0))
        {
          break;
        }
      }
    }
  }
  if(cli >= MAXCONNECTIONS)
  {
    sprintf((char*)client->in, "%s@%s:%u logged-in.\n", client->nick,
            inet_ntoa(client->cliaddr.sin_addr), ntohs(client->cliaddr.sin_port));
    server_log(dbglevl_msg, (char*)client->in);
    ws_set_frame(client->out, client->in, WS_FRAME_TEXT, get_next_mask(),
                 strlen((char*)client->in));
    client_global_message(server, client->out);
  }
  else
  {
    // Nickname invalid or already in use... Kick client.
    server_log(dbglevl_msg, "client_validate(%s) failed - Invalid user session name.\n",
               client->nick);
    ws_set_frame(client->out, (unsigned char*)INVALID_NICKNAME, WS_FRAME_TEXT,
                 get_next_mask(), strlen(INVALID_NICKNAME));
    send(client->sock, client->out, ws_get_frame_size(client->out), 0);
    // FIXME: Definite Kludge!
    usleep(1000);
    client_close(client, (unsigned char*)INVALID_NICKNAME);
    client->nick[0] = '\0';
  }
}

//-----------------------------------------------------------------------------
// New WebSocket connections require a proper handshake to establish.

static int client_connect(struct _client *client)
{
  if(!client->connected)
  {
    server_log(dbglevl_msg, "client_connect(%i) connecting...\n", client->sock);
    pWsHeader hdr = ws_parse_handshake((char*)client->in, client->octin);
    get_nickname(ws_get_handshake_request(hdr), client->nick);
    ws_reply_handshake(hdr, (char*)client->out);
    report(dbglevl_debug, (char*)client->out);
    send(client->sock, client->out, strlen((char*)client->out), 0);
    client->connected = 1;
  }
  return client->connected;
}

//-----------------------------------------------------------------------------
// Interpret WebSocket protocol packets.

static int client_interpret(struct _server *server, struct _client *client)
{
  int rtn = 0;
  int paylen;
  WsHead head;

  if(client_parse(client) > 0)
  {
    if(!client->connected)
    {
      client_connect(client);
      if(client->connected)
      {
        client_validate(server, client);
        server_update_users(server);
      }
    }
    else
    {
      ws_get_frame_header(client->in, &head);
      // FIXME: Include frame head in buffer max calculation.
      paylen = head.paylen >= MAXCLIENTMSGSIZE ? (MAXCLIENTMSGSIZE - 1) : head.paylen;
      ws_mask_unmask(head.data, head.data, head.mask, paylen);
      client->type = head.opcode;
      switch(client->type)
      {
        case WS_FRAME_TEXT:
          head.data[paylen] = '\0';
          ws_set_frame(client->out, head.data, WS_FRAME_TEXT, get_next_mask(), paylen);
          client->octout = ws_get_frame_size(client->out);
          client_global_message(server, client->out);
          break;

        case WS_FRAME_BINARY:
          ws_set_frame(client->out, head.data, WS_FRAME_BINARY, get_next_mask(), paylen);
          client->octout = ws_get_frame_size(client->out);
          client_binary_message(server, client);
          break;

        case WS_FRAME_PING:
          // Client PING... Relay client message back as PONG (response).
          server_log(dbglevl_msg, "interpret_client(%i) : PING frame received.\n", client->sock);
          ws_mask_unmask(head.data, head.data, head.mask, paylen);
          ws_set_frame(client->out, head.data, WS_FRAME_PONG, get_next_mask(), paylen);
          send(client->sock, client->out, ws_get_frame_size(client->out), 0);
          break;

        case WS_FRAME_PONG:
          // Spec says Ignore any PONG replies from Invalid (non) PING requests.
          server_log(dbglevl_msg, "interpret_client(%i) : PONG frame received went Ignored.\n", client->sock);
          break;

        case WS_FRAME_CLOSE:
          server_log(dbglevl_msg, "interpret_client(%s:%i):close(%i) - requested.\n",
                     inet_ntoa(client->cliaddr.sin_addr),
                     ntohs(client->cliaddr.sin_port), client->sock);
          sprintf((char*)client->out, "%s@%s:%i logged out.", client->nick,
                  inet_ntoa(client->cliaddr.sin_addr), ntohs(client->cliaddr.sin_port));
          ws_set_frame(client->out, client->out,
                       WS_FRAME_TEXT, get_next_mask(), strlen((char*)client->out));
          client_global_message(server, client->out);
          // Fall thru.
        default:
          if(client->type != WS_FRAME_CLOSE)
          {
            server_log(dbglevl_msg, "interpret_client(%s:%i):close(%i) - protocol violation.\n",
                       inet_ntoa(client->cliaddr.sin_addr),
                       ntohs(client->cliaddr.sin_port), client->sock);
          }
          FD_CLR(client->sock, &server->active_read_set);
          close(client->sock);
          client->sock = 0;
          client->connected = 0;
          client->nick[0] = '\0';
          server_update_users(server);
          break;
      }
      client->pntin += head.size;
      rtn = client->octin - client->pntin;
    }
  }
  if(client->octin == 0)
  {
    // Assume socket closed from other side.
    server_log(dbglevl_msg, "interpret_client(%s:%i-%i) parse error assuming closed socket.\n",
               inet_ntoa(client->cliaddr.sin_addr),
               ntohs(client->cliaddr.sin_port),
               client->sock);
    FD_CLR(client->sock, &server->active_read_set);
           close(client->sock);
           client->sock = 0;
           client->connected = 0;
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// Connect a client to the proper server.

static int server_connect(struct _server *server, int wsctl)
{
  int sock = 0;
  int port = 0;
  int rtn = -1;
  int cli = server_find_available(server);
  if(cli > -1)
  {
    switch(wsctl)
    {
      case LOCALCONSOLE:
        sock = server->ctlsock;
        port = ntohs(server->ctladdr.sin_port);
        break;

      case WEBSOCKSERVER:
      default:
        sock = server->srvsock;
        port = ntohs(server->srvaddr.sin_port);
        break;
    }
    // New connection.
    server->client[cli].wsctl     = wsctl;
    server->client[cli].nick[0]   = '\0';
    server->client[cli].connected = 0;
    server->size = sizeof(server->client[cli].cliaddr);
    server->client[cli].sock = accept(sock, (struct sockaddr*)&server->client[cli].cliaddr,
                                      &server->size);
    if(server->client[cli].sock > 0)
    {
      server_log(dbglevl_msg, "server_connect(%i):accept(%i)=%i : address=%s port=%i.\n",
                 port, sock, server->client[cli].sock,
                 inet_ntoa(server->client[cli].cliaddr.sin_addr),
                 ntohs(server->client[cli].cliaddr.sin_port));
      FD_SET(server->client[cli].sock, &server->active_read_set);
      rtn = 0;
    }
    else
    {
      server_error("error : server_connect(%i):accept(%i) failed - ", port, sock);
      server->client[cli].sock = 0;
    }
  }
  else
  {
    server_log(dbglevl_msg, "server_connect() - Max connections reached Unable to add new client.\n");
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// Establish the server as a network listener on this machine.

int server_listen(struct sockaddr_in *addr, const char *name)
{
  int sock;

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if(sock != -1)
  {
    if(bind(sock, (struct sockaddr*)addr, sizeof(struct sockaddr_in)) != -1)
    {
      if(listen(sock, 1) != -1)
      {
        server_log(dbglevl_msg, "smegchat %s server started : %i@%s:%i.\n", name,
                   sock, inet_ntoa(addr->sin_addr), ntohs(addr->sin_port));
      }
      else
      {
        server_error("error : server_listen(%i):listen(%i) failed  - ",
                     ntohs(addr->sin_port), sock);
        close(sock);
        sock = -1;
      }
    }
    else
    {
      server_error("error : server_listen(%i):bind(%i) failed  - ",
                   ntohs(addr->sin_port), sock);
      close(sock);
      sock = -1;
    }
  }
  else
  {
    server_error("error : server_listen(%i):socket() failed  - ",
                 ntohs(addr->sin_port));
  }
  return sock;
}

//-----------------------------------------------------------------------------
// Serve...

int smegchat_server(int port)
{
  int i;
  int cli;
  int rtn = 0;

  struct _server *server = malloc(sizeof(struct _server));
  if(server)
  {
    wsmask = WSMASKDEFAULT;
    memset(server, 0, sizeof(struct _server));
    server->message = malloc(SERVERMSGLENGTH + 256);
    if(server->message)
    {
      server->srvaddr.sin_family      = AF_INET;
      server->srvaddr.sin_port        = htons(port);
      server->srvaddr.sin_addr.s_addr = INADDR_ANY;
      server->srvsock = server_listen(&server->srvaddr, "WebSocket");
      if(server->srvsock != -1)
      {
        server->ctladdr.sin_family      = AF_INET;
        server->ctladdr.sin_port        = htons(CONSOLEPORT);
        server->ctladdr.sin_addr.s_addr = inet_addr(CONSOLESERVER);
        server->ctlsock = server_listen(&server->ctladdr, "Console");
        if(server->ctlsock != -1)
        {
            // Initialize the set of active sockets.
          FD_ZERO(&server->active_read_set);
          FD_SET(server->srvsock, &server->active_read_set);
          FD_SET(server->ctlsock, &server->active_read_set);
          report_flush();
          server->serving = 1;
          while(server->serving)
          {
            // Block until input arrives on one or more active sockets.
            server->read_fd_set = server->active_read_set;
            if(select(FD_SETSIZE, &server->read_fd_set, NULL, NULL, NULL) < 0)
            {
              server_error("error : smegchat_server(%i):select() failed ",
                           ntohs(server->srvaddr.sin_port));
              break;
            }
            for(i = 0; i < FD_SETSIZE; i++)
            {
              if(FD_ISSET(i, &server->read_fd_set))
              {
                if(i == server->ctlsock)
                {
                  // Connection on WebSocket server.
                  server_connect(server, LOCALCONSOLE);
                  break;
                }
                else if(i == server->srvsock)
                {
                  // Connection on Console server.
                  server_connect(server, WEBSOCKSERVER);
                }
                else
                {
                  cli = server_find_client(server, i);
                  if(cli > -1)
                  {
                    switch(server->client[cli].wsctl)
                    {
                      case LOCALCONSOLE:
                        rtn = console_interpret(server, &server->client[cli]);
                        break;

                      case WEBSOCKSERVER:
                        rtn = client_interpret(server, &server->client[cli]);
                        break;

                      default:
                        break;
                    }
                  }
                }
              }
            }
            // Update log.
            report_flush();
          }
        }
      }
      server_log(dbglevl_msg, "smegchat daemon exiting...\n");
      // Close any client connections.
      server_log(dbglevl_msg, " purging client connections...\n");
      for(cli = 0; cli < MAXCONNECTIONS; cli++)
      {
        if(server->client[cli].sock > 0)
        {
          server_log(dbglevl_msg, "  terminating client session %s@%s:%i.\n",
                     server->client[cli].nick,
                     inet_ntoa(server->client[cli].cliaddr.sin_addr),
                     ntohs(server->client[cli].cliaddr.sin_port));
          close(server->client[cli].sock);
        }
      }
      // Release minor resources.
      server_log(dbglevl_msg, " smegchat_server():free(buffers) - releasing resources.\n");
      free(server->message);
      // Close servers.
      if(server->ctlsock != -1)
      {
        server_log(dbglevl_msg, " close Console server : %i@%s:%i.\n",
                   server->ctlsock, inet_ntoa(server->ctladdr.sin_addr),
                   ntohs(server->ctladdr.sin_port));
        close(server->ctlsock);
      }
      if(server->srvsock != -1)
      {
        server_log(dbglevl_msg, " close WebSocket server : %i@%s:%i.\n",
                   server->srvsock, inet_ntoa(server->srvaddr.sin_addr),
                   ntohs(server->srvaddr.sin_port));
        close(server->srvsock);
      }
    }
    else
    {
      server_error("error : smegchat_server(%i):message = malloc(%i) failed  - ",
                   server->msglen + 256);
    }
    server_log(dbglevl_msg, " smegchat_server():free(websocket_server) exit.\n");
    free(server);
    rtn = 0;
  }
  else
  {
    report_error("error : smegchat_server(%i):server = malloc(%i) failed  - ",
                 sizeof(struct _server));
  }
  server_log(dbglevl_msg, "smegchat daemon dead.\n");
  report_flush();
  return rtn;
}

//-----------------------------------------------------------------------------
// end: smegchat.c

General Command Line Interpreter For The Local Console Server...

Blah blah!

//-----------------------------------------------------------------------------
// interpret.c
//
//  General purpose command line interpreter.
//
// Copyright (c) 1989-2018 - Dysfunctional Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; 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 2 of the License, or
//  (at your option) any later version.
//
//  This software 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.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//
//-----------------------------------------------------------------------------

#include <stdlib.h>
#include <string.h>

#include "interpret.h"

//----------------------------------------------------------------------------

int interpret(void *object, char *console, const struct _command *cmdtable, int cmdsize)
{
  int rtn;
  int i;
  char *tok;
  char *params;

  rtn = -1;
  tok = strtok_r(console, " ", &params);
  for(i = 0; i < cmdsize; i++)
  {
    if(strcmp(tok, cmdtable[i].cmd) == 0)
    {
      rtn = (cmdtable[i].funct)(object, params);
    }
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// end: interpret.c

There Are Some Additional Shared Helper Utilities...

Blah blah!

//-----------------------------------------------------------------------------
// util.c
//
//  Web Whiteboard/Chat Server Example Utilities.
//
// Copyright (c) 2018 - Dysfunctional Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; 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 2 of the License, or
//  (at your option) any later version.
//
//  This software 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.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//
//-----------------------------------------------------------------------------

#include <time.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>

#include "util.h"

#define NETBUFFERSIZE 512

//-----------------------------------------------------------------------------
// Create time stamp.

const char *smegchat_get_time(char *stime, int slen)
{
  time_t ltime;
  struct tm ltm;

  ltime = time(NULL);
  localtime_r(&ltime, &ltm);
  strftime(stime, slen, "%d/%m/%g:%H:%M:%S", &ltm);
  return stime;
}

//-----------------------------------------------------------------------------
// Line > Log file with variable args.

void server_log(int levl, const char *msg, ...)
{
  va_list pval;
  char sometime[24];

  report(levl, "%s - ", smegchat_get_time(sometime, sizeof(sometime) - 1));
  va_start(pval, msg);
  vreport(levl, msg, pval);
  va_end(pval);
}

//-----------------------------------------------------------------------------
// Error line > Log file with variable args.

void server_error(const char *msg, ...)
{
  va_list pval;
  char sometime[24];

  report(dbglevl_none, "%s - ", smegchat_get_time(sometime, sizeof(sometime) - 1));
  va_start(pval, msg);
  vreport(dbglevl_none, msg, pval);
  va_end(pval);
  report(dbglevl_none, "%s.\n", strerror(errno));
}

//-----------------------------------------------------------------------------
// Local console prompt.

int console_prompt(int sock)
{
  return send(sock, ">", 1, 0);
}

//-----------------------------------------------------------------------------
// Connect using the console (debug) interface.

int console_connect(void)
{
  struct sockaddr_in ctladdr;
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if(sock != -1)
  {
    ctladdr.sin_family      = AF_INET;
    ctladdr.sin_port        = htons(CONSOLEPORT);
    ctladdr.sin_addr.s_addr = inet_addr(CONSOLESERVER);
    if(connect(sock, (struct sockaddr*)&ctladdr, sizeof(struct sockaddr_in)) == -1)
    {
      server_error("error console_connect(%i@%s:%i):connect() failed - ",
                   sock, inet_ntoa(ctladdr.sin_addr), ntohs(ctladdr.sin_port));
      close(sock);
      sock = -1;
    }
    else
    {
      console_prompt(sock);
    }
  }
  else
  {
    server_error("error : console_connect():socket() failed - ");
  }
  return sock;
}

//-----------------------------------------------------------------------------
// Parse line from console.

int console_read(int sock, char *in, size_t len)
{
  int rtn = -1;

  in[0] = '\0';
  rtn = recv(sock, in, len - 1, 0);
  if((rtn > 0) && (rtn < len))
  {
    in[rtn] = '\0';
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// Raw push to server.

int console_write(int sock, const char *out, size_t len)
{
  return send(sock, out, len, 0);
}

//----------------------------------------------------------------------------
// Console line to server.

int console_send(int sock, const char *msg)
{
  return send(sock, msg, strlen(msg), 0);
}

//----------------------------------------------------------------------------
// Formatted console line to server.

#define MAXCONSOLEMSG 128

int console_print(int sock, const char *fmt, ...)
{
  va_list pval;
  char msg[MAXCONSOLEMSG + 2];

  va_start(pval, fmt);
  vsnprintf(msg, MAXCONSOLEMSG - 1, fmt, pval);
  va_end(pval);
  msg[MAXCONSOLEMSG - 1] = '\0';
  return send(sock, msg, strlen(msg), 0);
}

//-----------------------------------------------------------------------------
// One stop shopping - shutdown chat server; Bye bye Now!

void stop_smegchat(void)
{
  int sock;

  sock = console_connect();
  if(sock != -1)
  {
    server_log(dbglevl_msg, "stop_smegchat() - requesting shutdown.\n");
    console_send(sock, "shutdown\n");
    close(sock);
  }
}

//-----------------------------------------------------------------------------

struct _interp {
  int       tty;
  int       sock;
  int       result;
  int       live;
  int       running;
  int       stop;
  fd_set    fds;
  size_t    lplen;
  char     *lp;
  char      in[NETBUFFERSIZE + 2];
};

//-----------------------------------------------------------------------------
// Console (debug) server.

void interactive_smegchat(void)
{
  struct _interp *interp = malloc(sizeof(struct _interp));
  if(interp)
  {
    report(dbglevl_debug, "Created smegchat interpreter context.\n");
    memset(interp, 0, sizeof(struct _interp));
    interp->sock = console_connect();
    if(interp->sock != -1)
    {
      report(dbglevl_msg, "smegchat interpreter connected...\n");
      interp->live = 1;
      while(interp->live)
      {
        interp->tty = STDIN_FILENO;
        FD_ZERO(&interp->fds);
        FD_SET(interp->tty, &interp->fds);
        FD_SET(interp->sock, &interp->fds);
        if(select(interp->sock + 1, &interp->fds, NULL, NULL, NULL) != -1)
        {
          if(FD_ISSET(interp->tty, &interp->fds))
          {
            // Prompt.
            interp->result = getline(&interp->lp, &interp->lplen, stdin);
            // Hmm! Maybe check error here ?
            interp->result = console_send(interp->sock, interp->lp);
            // Here too!
          }
          else if(FD_ISSET(interp->sock, &interp->fds))
          {
            interp->result = console_read(interp->sock, interp->in, NETBUFFERSIZE);
            if(interp->result == 0)
            {
              report(dbglevl_msg, "smegchat daemon closed the connection.\n");
              interp->live = 0;
            }
            else if(interp->result == -1)
            {
              report_error("error : interactive_smegchat():control_read() failed - ");
            }
            else
            {
              report(dbglevl_none, interp->in);
              report_flush();
            }
          }
        }
        else
        {
          report_error("error : interactive_smegchat():select() failed - ");
          interp->live = 0;
        }
      }
      close(interp->sock);
    }
    free(interp);
  }
  report(dbglevl_msg, "smegchat interpreter exiting...\n");
}

//-----------------------------------------------------------------------------
// end:util.c

Arch Linux 'systemd' Requires A '.service' File To Describe The Service Behaviour...

Blah blah!

[Unit]
Description=Smegware Networked WhiteBoard Chat Demonstration Server

[Service]
Type=forking
PIDFile=/run/smegchatd.pid
ExecStart=/usr/bin/smegchatd -S -m
ExecStop=/usr/bin/smegchatd -Q

[Install]
WantedBy=multi-user.target

10625