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';
}


Subscribe to Newsletter here

WEBINAR 12 AUGUST - Why is Cyber Security PR different?

This webinar is an introduction for cyber security companies and communication professionals on the nuances of cyber security public relations in the Asia Pacific.

Join Code Red Security PR Network for a virtual conversation with leading cyber security and ICT journalists, Victor Ng and Stuart Corner, on PR best practices and key success factors for effective communication in the Asian Pacific cyber security market.

You will also hear a success story testimonial from Claroty and what Code Red Security PR has achieved for the brand.

Please register here by 11 August 2020 and a confirmation email, along with instructions on how to join the webinar will be sent to you after registration.

Aug 12, 2020 01:00 PM in Canberra, Melbourne, Sydney. We look forward to seeing you there!

REGISTER NOW!

PROMOTE YOUR WEBINAR ON ITWIRE

It's all about Webinars.

These days our customers Advertising & Marketing campaigns are mainly focussed on Webinars.

If you wish to promote a Webinar we recommend at least a 2 week campaign prior to your event.

The iTWire campaign will include extensive adverts on our News Site itwire.com and prominent Newsletter promotion https://www.itwire.com/itwire-update.html and Promotional News & Editorial.

For covid-19 assistance we have extended terms, a Webinar Business Booster Pack and other supportive programs.

We look forward to discussing your campaign goals with you. Please click the button below.

MORE INFO HERE!

BACK TO HOME PAGE
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.

BACK TO HOME PAGE

WEBINARS ONLINE & DEMAND

GUEST ARTICLES

VENDOR NEWS

Guest Opinion

Guest Interviews

Guest Reviews

Guest Research & Case Studies

Channel News

Comments