Monday, 20 August 2007 19:33

Write your own Linux server part one

By
One of the great strengths of Linux is its multi-faceted network server capabilities, reaching back to its rich UNIX history and the development of TCP/IP on that platform. If you’re a software developer, it’s dead simple to network-enable your own apps too, making them act consistently with other server processes. Here’s how to do it, in two parts.

The task at hand

Firstly, a real world story: I was called to do some work for a local ISP. They used a database system for administration and billing purposes, and a Linux server for user accounts and subscriber Web publishing.


This ISP was reasonably small. Its two system administrators were making all the Linux accounts by hand. They wanted the help desk staff to take over the job in some automated fashion, but didn’t want to give them actual privileged logins to the Linux server. These guys didn’t know Linux either, so sudo wasn’t really a solution.


What they requested was a Web page that the help desk staff could access on their local Intranet. The form allowed new registrations to be manually entered or uploaded from a file. When the staff member clicked OK the user details entered on the form were to be added to both the database and the Linux server, creating an account in the process.


The intranet was not running on the public Linux server; it was on a private machine – so a CGI script wasn’t an option. This called for a client/server solution. The key was to make a server process running on the Linux box which would receive commands to make accounts. It had to be always available because it would be impossible to predict when it may be needed, and thus could not be launched at pre-determined times. The server could then be invoked from a script on the intranet, or through other means. The server process – otherwise known as a daemon – was written in C and used the Berkeley socket implementation to listen for incoming requests and handle them. A username and password was funneled down the network connection and the daemon duly created the account.


This was agreed as the way forward, and the resulting code worked for them. I wanted the daemon to act as similarly as any other Linux daemon. This meant it had to be capable of processing multiple simultaneous requests, and it had to have an rc2.d script to start and stop gracefully.


The functionality of our client/server system

The starting point was to determine what the system ought to do. Obviously, it had to create user accounts. However, feature creep reared its head and the ISP thought some other features would prove useful, namely

  1. executing an arbitrary process on the server
  2. returning the user group of an arbitrary login ID
  3. returning the mail aliases for an arbitrary login ID
  4. testing the network connection
  5. returning the daemon's version number


The code

With this in mind, let’s now walk through the code files, annotating the major parts. First, we’ll consider how to configure and compile the server.


Open the Makefile and set CFLAGS to compile with optimisation or with debugging information as preferred. Optimisation is the best option when you are certain the program performs as you require. Comment out the line which is not required.


CFLAGS = $(INC) -O2
#CFLAGS = $(INC) -g -DDEBUG


There are also some necessary constants which have to be set in the header file dwserv.h according to your preferences. The ALIASFILE and GROUPFILE constants are the disk locations of your e-mail aliases and user groups, respectively. These are almost always in /etc/aliases (but sometimes /etc/mail/aliases) and /etc/group. The HOME_PATH constant is the directory prefix for where you store user accounts. Be sure to include the trailing slash (/). On your system, this directory will likely be /home/.


#define ALIASFILE "/etc/aliases"
#define GROUPFILE "/etc/group"
#define HOME_PATH "/export/home/"


You next need to specify the full path to some external programs the daemon requires, namely useradd and grep. useradd is called to actually create accounts, conforming with policies in place on your system such as copying skeleton files, making directories and so forth. Further, useradd is a known and tested and debugged program; it is sensible to use it to perform the work. You can usually find where your programs are by typing a command like which useradd on the command line at a shell prompt.


#define USERADD_PATH "/usr/sbin/useradd"
#define GREP_PATH "/usr/bin/grep"


Finally, we need to specify the listening port the daemon will use. The default value of 6000 can be changed by editing a line in dwserv.cpp.


int port = 6000;


Ports below 1024 are reserved for system use; ports from there through 65,535 are free for other purposes. Every TCP/IP protocol runs over a port. Port 80 is the standard port for HTTP web browsing and port 25 is the standard port for SMTP e-mail. Similarly, this daemon needs to choose a port on which it will listen for requests.


Once you have configured the server, run make at the command line. The source code will be compiled and the executable program produced.

Creating an account

The main task of the daemon is to create accounts. This work is performed by the method CreateAccount located in file pwroutines.cpp. This accepts a data string parameter which consists of a space-separated login ID, password and user group. The output message indicates the success or otherwise of the operation.

The string is decoded and then useradd is called, with appropriate parameters passed in - namely, the home directory, login ID, user group and password. One of our ancillary features was to execute arbitrary Linux commands so we have, by necessity, provided a routine called StartProcess. We can thus piggy-back on this to call useradd for the requirement at this point.

void CreateAccount (char *data, char *OutMsg)
// break Data into login, password and group
{
  int i, j;
 
char login [LINELEN];
  char pword [LINELEN];
  char group [LINELEN];
  char buf [LINELEN];

  login[0] = '\0';
  pword[0] = '\0';
  group[0] = '\0';
  i = j = 0;

  while ((i < strlen (data)) && (data[i] != ' '))
    login [j++] = data [i++];
  login [j] = '\0';

  j = 0; i++;
  while ((i < strlen (data)) && (data[i] != ' '))
    pword [j++] = data [i++];
  pword [j] = '\0';

  j = 0; i++;
  while ((i < strlen (data)) && (data[i] != ' '))
    group [j++] = data [i++];
  group [j] = '\0';

  sprintf (buf, "%s -m -d %s%s -G %s –p %s %s", USERADD_PATH, HOME_PATH,
           login, group, pword, login);

  StartProcess (buf, OutMsg);
}


Most all modern versions of useradd let a password be set at the time the account is created. This is not always so; it is important you determine if your version of useradd has this restriction. man useradd should quickly reveal the answer. If your useradd is lacking you will need extra code to set the password after useradd has performed its work. This means editing the shadow password file and updating the appropriate record.


  if (strcmp (OutMsg, SUCCESS_MSG) == 0)
    SetPassword (login, pword, OutMsg);


Fortunately, the Linux kernel provides routines to assist in this endeavour. getspnam locates the appropriate shadow entry. crypt then encrypts the password. putspent stores a memory-based shadow password record. There’s just one catch; putspent won’t overwrite an already existing record but merely appends to the end of the shadow password file thus creating a second entry. This is unfortunate but far from disastrous. It does mean, though, the shadow password file has to be directly operated on and this needs great care. Another kernel routine, lckpwdf, indicates our desire to lock the shadow password file. Any other well-behaved software will honour this request and keep its hands off until we’re done. Once the existing shadow entry has been replaced, ulckpwdf releases the file for other processes to use.  This is all performed in the routine SetPassword.


Stay tuned for part two where we finish off, detailing the socket-handling routines and the rc2.d script to start and stop the daemon as well as explain how to use it. Go to the next page to see the code in its entirety.



Code listings


Makefile



# -----------------------------------------------------------------------------
# Makefile for DWSERV
# -----------------------------------------------------------------------------

CC = g++
BINDIR = .

# -- Includes and links -------------------------------------------------------

#CFLAGS = $(INC) -O2
CFLAGS = $(INC) -g -DDEBUG

# Note - use the appropriate LNFLAGS lines below for your OS

LNFLAGS = -O2   #Linux
#LNFLAGS = -O2 -lnsl -lsocket #Solaris

# -- Objects ----------------------------------------------------------------
APP  = dwserv.o pwroutines.o grep.o

$(BINDIR)/dwserv: $(APP)
  $(CC) -o $(BINDIR)/dwserv $(APP) $(LNFLAGS)

dwserv.o: dwserv.cpp dwserv.h StringVector.h grep.o pwroutines.o
  $(CC) $(CFLAGS) -c dwserv.cpp

pwroutines.o: pwroutines.cpp dwserv.h StringVector.h
  $(CC) $(CFLAGS) -c pwroutines.cpp

grep.o:  grep.cpp dwserv.h StringVector.h
  $(CC) $(CFLAGS) -c grep.cpp

# -- housekeeping -----------------------------------------------------------
clean:
  rm -f *.o $(BINDIR)/dwserv


dwserv.h


/*
 *  DWSERV
 */

#ifndef __DWSERV_H
#define __DWSERV_H

// You may need to change some of these ...

#define SECRET_PASSWORD "allyourbasearebelongtous"
#define SUCCESS_MSG "Successful"

#define ALIASFILE "/etc/aliases"
#define GROUPFILE "/etc/group"

#define USERADD_PATH "/usr/sbin/useradd"
#define GREP_PATH "/usr/bin/grep"
#define PASSWD_PATH "/bin/passwd"
#define HOME_PATH "/home/"   // Be sure to include the trailing /

#include <arpa/inet.h>
#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <shadow.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/errno.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/termios.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <time.h>

#include "StringVector.h"

// Constants and variables

#define MAX_STRING 1024
#define LINELEN 1024
#ifndef BUFSIZ
#define BUFSIZ 1024
#endif
#define MAXPWWAITS 5

// Socket and interface handling routines

void log (const char *format, ...);
int errexit (const char *format, ...);
void process (int ssock, char *Remote, int connections);
void DisplayHelpMessage (int port, int qlen);
void WriteToFD (int filedesc, char *s);
const char *IPtoAddress (struct in_addr ipA);
void reaper (int sig);
char *CurrentDateTime (char *nowtime);
void StripString (char *InMsg, char *Command, char *Data);

// Account handling routines

void GetAliases (char *login, char *OutMsg);
void GetGroup (char *login, char *OutMsg);
void CreateAccount (char *data, char *OutMsg);
void StartProcess (char *command, char *OutMsg);
void SetPassword (char *szUser, char *szPlainPass, char *OutMsg);

// File manipulation routines

bool grep (char *pattern, char *file, StringVector &svec);

#endif


dwserv.cpp


/*
 *  DWSERV
 */

#include "dwserv.h"
#include <unistd.h>

#define VERSION "1.2\n"

bool logging = false;
bool verbose = false;
char FileName [PATH_MAX];

// ---------------------------------------------------------------------------
int main (int argc, char *argv [])
{
  struct sockaddr_in sin, fsin;
  struct protoent *ppe;
  int sock, ssock;
  socklen_t alen;
  int port = 6000;
  int qlen = 5;
  int pid;
  int fd;
  char nowtime [26];
  char Remote [80];
  int connections = 0;
  FILE *logfp = NULL;

  if (geteuid () != 0)
  {
    printf ("\n%s must be run with super-user privileges.\n", argv [0]);
    return 0;
  }

  FileName [0] = '\0';

// Process command line arguments.

  for (int i = 1; i < argc; i++)
  {
    if (strncmp (argv[i], "-p", 2) == 0)
      port = atoi (argv[i] + 2);

    else if (strncmp (argv[i], "-q", 2) == 0)
      qlen = atoi (argv[i] + 2);

    else if (strncmp (argv[i], "-f", 2) == 0)
    {
      strcpy (FileName, argv[i] + 2);
      logging = true;
    }
    else if (strncmp (argv[i], "-v", 2) == 0)
      verbose = logging = true;

    else if (strncmp (argv[i], "-l", 2) == 0)
      logging = true;

    else
    {
      DisplayHelpMessage (port, qlen);
      return 0;
    }
  }

// Set up the socket.

  sin.sin_family = AF_INET;
  sin.sin_addr.s_addr = INADDR_ANY;
  sin.sin_port = htons (port);

  if ((ppe = getprotobyname ("tcp")) == 0)
    errexit ("Can't find tcp: %s\n", strerror (errno));

  if ((sock = socket (PF_INET, SOCK_STREAM, ppe->p_proto)) < 0)
    errexit ("Can't create socket: %s\n", strerror (errno));

  if (bind (sock, (struct sockaddr *) &sin, sizeof (sin)) < 0)
    errexit ("Can't bind to port: %s\n", strerror (errno));

  if (listen (sock, qlen) < 0)
    errexit ("Can't listen on port: %s\n", strerror (errno));

// Print a welcome banner.

  printf ("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
                  "   DWServ Server\n"
                  "      Port: %4d\n"
                  "   Version: %s"
                  "-----------------------------\n"
                  "by  David M. Williams\n"
                  "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n", port, VERSION);

  log ("Listening on port %d (qlen = %d).\n", port, qlen);

// Set up the server safely.

// 1. Run the server in the background ...

  if ((pid = fork ()) < 0)
    errexit ("Error setting up server: %s\n", strerror (errno));

  if (pid)  // Non-zero is parent.
  {
    printf ("Server pid is %d.\n", pid);
    if (strlen (FileName) > 0)
      log ("Server pid is %d.\n\n", pid);
    exit (0);
  }

// 2. Detach from controlling tty ...

  fd = open ("/dev/tty", O_RDWR);
  ioctl (fd, TIOCNOTTY, 0);
  close (fd);

// 3. Miscellaneous commands ...

  umask (027);
  chdir ("/tmp");

// Be the server !

  while (1)
  {
// Wait for connections. If one is made, then get a slave socket to
// process it, in a child process. This way the server remains free
// to accept more connections.

    alen = sizeof (fsin);
    ssock = accept (sock, (struct sockaddr *) &fsin, &alen);
    if (ssock < 0)
    {
      if (errno == EINTR)
        continue;
      else
        errexit ("accept: %s\n", strerror (errno));
    }

    strcpy (Remote, IPtoAddress (fsin.sin_addr));
    log ("%s: connect from %s\n", CurrentDateTime (nowtime), Remote);

    signal (SIGCHLD, reaper);
    connections++;
    if (fork () == 0)
    {
      process (ssock, Remote, connections);
      exit (0);
    }
    else
      close (ssock);
  }
}

// ---------------------------------------------------------------------------
void DisplayHelpMessage (int port, int qlen)
{
  printf ("\nDWServ Server\n\n");
  printf ("\t\tSyntax:\tdwserv [flags]\n\n");
  printf ("\t-pPORT\t\tto specify the port to use.\n");
  printf ("\t\t\t(default = %d)\n", port);
  printf ("\t-qQLEN\t\tto specify the queue length.\n");
  printf ("\t\t\t(default = %d)\n", qlen);
  printf ("\t-l\t\tto perform logging.\n");
  printf ("\t-fFILENAME\tto specify a log file.\n");
  printf ("\t\t\t(default = stdout)\n");
  printf ("\t-v\t\tto specify verbose logging.\n");
  printf ("\t\t\t(default = off)\n\n");
}

// ---------------------------------------------------------------------------
char *CurrentDateTime (char *nowtime)
{
  time_t now;

  time (&now);
  strcpy (nowtime, ctime (&now));
  nowtime [strlen (nowtime) - 1] = '\0';

  return nowtime;
}

// ---------------------------------------------------------------------------
void WriteToFD (int filedesc, char *s)
{
  write (filedesc, s, strlen (s));
}

// ---------------------------------------------------------------------------
void log (const char *format, ...)
{
  va_list  args;
  FILE  *fp;

  if (logging)
  {
    fp = stdout;

    if (strlen (FileName) > 0)
    {
      if ((fp = fopen (FileName, "a")) < 0)
        fp = stdout;
    }

    va_start (args, format);
    vfprintf (fp, format, args);
    va_end (args);

    if (fp != stdout)
      fclose (fp);
  }
}

// ---------------------------------------------------------------------------
int errexit (const char *format, ...)
{
  va_list args;
  char errstr [255];

  va_start (args, format);
  vsprintf (errstr, format, args);
  log (errstr);
  fprintf (stderr, errstr);
  va_end (args);

  exit (1);
}

// ---------------------------------------------------------------------------
const char *IPtoAddress (struct in_addr ipA)
{
  unsigned long hostname;
  struct hostent *ip;

  hostname = inet_addr (inet_ntoa (ipA));

  if ((ip = gethostbyaddr ((char *) &hostname, sizeof (long), AF_INET))<0)
    return "unknown";
  else
    return ip->h_name;
}

// ---------------------------------------------------------------------------
void reaper (int sig)
// The reaper cleans up zombie children processes.
// In Unix, when a child process terminates it sends a message back to
// the parent process.  Unless this message is handled, the child process
// will wait around forever taking up resources.
{
  int status;

  wait3 (&status, WNOHANG, (struct rusage *) 0);
}

// ---------------------------------------------------------------------------
void process (int ssock, char *Remote, int connections)
// This function handles the processing of a socket connection.
{
  StringVector *svec = new StringVector;
  int count = 0, n;
  char TmpBuf [LINELEN], InMsg [LINELEN];
  char OutMsg [MAX_STRING];
  char Command [5], Data [97];
  char nowtime [26];
  bool keepgoing = true;

// Display an innocent banner (anything to hide the real purpose).

  while (count < 3)
  {
    sprintf (OutMsg, "%s\n", CurrentDateTime (nowtime));
    WriteToFD (ssock, OutMsg);

    if ((n = read (ssock, InMsg, LINELEN - 1)) > 0)
    {
      InMsg [n] = '\0';

// Strip CR's and LF's
      if ((InMsg [strlen (InMsg) - 1] == 10) ||
         (InMsg [strlen (InMsg) - 1] == 13))
        InMsg [strlen (InMsg) - 1] = '\0';
      if ((InMsg [strlen (InMsg) - 1] == 10) ||
         (InMsg [strlen (InMsg) - 1] == 13))
        InMsg [strlen (InMsg) - 1] = '\0';

      if (strcmp (InMsg, SECRET_PASSWORD) == 0)
        count = 999;
    }

    count++;
  }

  if (count < 900)
  {
    close (ssock);
    return;
  }

// Loop until exit or connection lost.

  keepgoing = true;
  while (keepgoing)
  {

// Read input.
    WriteToFD (ssock, "");      // Send no prompt.

    if ((n = read (ssock, InMsg, LINELEN - 1)) <= 0)
      keepgoing = false;  // No more.
    else
      InMsg [n] = '\0';

// Process input.
    strcpy (OutMsg, "");

    if (keepgoing)
    {
      StripString (InMsg, Command, Data);

// Here are the main functions ...

// Aliases
      if (strcmp (Command, "ALIA") == 0)
        GetAliases (Data, OutMsg);

// Groups
      else if (strcmp (Command, "GROU") == 0)
        GetGroup (Data, OutMsg);

// Make new account
      else if (strcmp (Command, "MAKE") == 0)
        CreateAccount (Data, OutMsg);

// Start a process
      else if (strcmp (Command, "STRT") == 0)
        StartProcess (Data, OutMsg);

// Return the version number
      else if (strcmp (Command, "VERS") == 0)
        strcpy (OutMsg, VERSION);

// Miscellaneous commands ...

      else if (strcmp (Command, "QUIT") == 0)
      {
        keepgoing = false;
        strcpy (OutMsg, "");
      }

      else if (strcmp (Command, "STAT") == 0)
        sprintf (OutMsg, "%d connections.\n", connections);

      else
        sprintf (OutMsg, "");

      if (strlen (OutMsg) > 0)
        WriteToFD (ssock, OutMsg);

      if (verbose)
        log ("%d  %s\t%s\t%s\t%s\n", connections, Remote, Command,
                  Data, OutMsg);
    }

// And loop again!
  }

// Close the socket and finish !
  close (ssock);
}

// ---------------------------------------------------------------------------
void StripString (char *InMsg, char *Command, char *Data)
{
  int i, count;

  while (strlen (InMsg) < 4)
    strcat (InMsg, " ");

  for (i = 0; i < 4; i++)
    Command [i] = toupper (InMsg [i]);
  Command [i] = '\0';

  while ((InMsg [i] != ' ') && (InMsg [i] != '\0'))
    i++;

  while ((InMsg [i] == ' ') && (InMsg [i] != '\0'))
    i++;

  count = 0;
  while ((InMsg [i] != '\0') && (InMsg [i] != 13) && (InMsg [i] != 10))
    Data [count++] = InMsg [i++];
  Data [count] = '\0';
}

CHIEF DATA & ANALYTICS OFFICER BRISBANE 2020

26-27 February 2020 | Hilton Brisbane

Connecting the region’s leading data analytics professionals to drive and inspire your future strategy

Leading the data analytics division has never been easy, but now the challenge is on to remain ahead of the competition and reap the massive rewards as a strategic executive.

Do you want to leverage data governance as an enabler?Are you working at driving AI/ML implementation?

Want to stay abreast of data privacy and AI ethics requirements? Are you working hard to push predictive analytics to the limits?

With so much to keep on top of in such a rapidly changing technology space, collaboration is key to success. You don't need to struggle alone, network and share your struggles as well as your tips for success at CDAO Brisbane.

Discover how your peers have tackled the very same issues you face daily. Network with over 140 of your peers and hear from the leading professionals in your industry. Leverage this community of data and analytics enthusiasts to advance your strategy to the next level.

Download the Agenda to find out more

DOWNLOAD NOW!

David M Williams

David has been computing since 1984 where he instantly gravitated to the family Commodore 64. He completed a Bachelor of Computer Science degree from 1990 to 1992, commencing full-time employment as a systems analyst at the end of that year. David subsequently worked as a UNIX Systems Manager, Asia-Pacific technical specialist for an international software company, Business Analyst, IT Manager, and other roles. David has been the Chief Information Officer for national public companies since 2007, delivering IT knowledge and business acumen, seeking to transform the industries within which he works. David is also involved in the user group community, the Australian Computer Society technical advisory boards, and education.

VENDOR NEWS & EVENTS

REVIEWS

Recent Comments