Author's Opinion

The views in this column are those of the author and do not necessarily reflect the views of iTWire.

Have your say and comment below.

Tuesday, 11 December 2007 03:05

Real world Linux programming

By
Here’s a genuine app that I wrote for an ISP. They needed a way to let their help desk staff perform system administration functions in a controlled manner, through a simple menu of choices. The solution was pretty simple: a controlled environment with no shell access plus a series of programs and scripts that were elevated if required by sudo.

The help desk staff would use telnet or ssh or any other means to login to the Linux server as a special menu user account, instead of their ordinary accounts. The shell in /etc/passwd was set to be the menu program. This meant their environment was locked.

It wasn’t desirable to recompile the application whenever a new menu option was needed, so the first thing it does is read a configuration file and dynamically builds up its list of features that the user is offered. Additional programs and shell scripts perform the actual tasks, and these are referenced in the config file.

Also, some functions require privileged access – ie superuser access – and it’s best to run programs with the least permissions required. So, by hiving off the functionality out of the main app, the app itself need just not have any special permission.

Not all users are equal, so the config file also specifies a minimum access level required to perform each task, and another list of users stipulates the access level each user has. If a user doesn’t meet the minimum criteria for any option they simply do not see that option in the list – no point teasing people!

The first file in our program, constants.h, simply defines – as you might guess – some constant values which will be used by the rest of the program. This lets the behaviour be changed in one spot.

#define BANNER "ISP Help-Desk operations menu\nPlease log in\n\n"

#define LOGGING_ENABLED
#undef DISABLE_INTERRUPTS
#define HIDE_PASSWORD
#define CLEAR_SCREEN
#define USE_EXECL
#undef DEBUGGING

#define LOGFILE "/usr/local/menu/menu.log"
#define USERLIST "/usr/local/menu/user.list"
#define MENULIST "/usr/local/menu/menu.list"
#define SCRIPTSDIR "/usr/local/menu/scripts"
#define SUDO  "/usr/local/bin/sudo"


The next file, menu.h, defines a data structure to hold the menu in memory as well as declare the functions the program will implement.

#include "constants.h"

#include <crypt.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <wait.h>


typedef struct
{
 char MenuText [75];
 char ScriptName [80];
 int RunAsRoot;
 void *Next;
 void *Submenu;
} ScriptNode;


typedef ScriptNode *ScriptTree;


void clrscrn ();
void login (char *username, char *password);
void noAccess (char *username);
int verify (char *username, char *password, int *access);
void buildMenu (int access, ScriptTree *theMenu);
void DoMenu (char *username, ScriptTree theMenu, int InSub);
ScriptTree AddMenu (ScriptTree *theMenu, char *ItemText);
void AddNode (ScriptTree *theMenu, char *ItemText,
 char *ScriptCommand, int UseSudo);
void Destroy (ScriptTree theMenu);
void DoCommand (char *username, char *ScriptName, int UseSudo);
void log (const char *format, ...);
int DisplayMenu (ScriptTree theMenu, int InSub);
int getChoice (int MenuItems);
void ProcessChoice (int userChoice, ScriptTree theMenu, char *username, int InSub);
void DumpMenu (int level, ScriptTree theMenu);
void indent (int level);


CONTINUED





I’ll leave the main program code – menu.c – right to the last page. The two configuration files needed are the list of users and authorisation levels, and the menu definition itself.

The list of users is defined in user.list with a username, an encrypted password, and a privilege level.

#
# username  password  access (0 is lowest)
#
# Blank lines and lines beginning with a '#' are treated as comments

davidw daDDC5rpKNN5c 5
fred frL8GUupUeU6A 1


The passwords can be generated using this simple program which calls the Linux crypt API function. As this is intended only for internal use it is extremely simple and doesn’t have any error checking. If you require usernames of more than 50 characters or passwords of more than 10 characters then you ought to change the code below or else a buffer overrun will occur.

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


main ()
{
 char *username;
 char *password;

 username = (char *) malloc (sizeof (char) * 50);
 password = (char *) malloc (sizeof (char) * 10);

 printf ("   Login: ");
 scanf ("%s", username);

 strcpy (password, getpass ("Password: "));

 printf ("Encrypted password is %s\n", crypt (password, username));

 free (username);
 free (password);
}


The menu is defined in menu.list and this has a more complex structure. Each line defines either a submenu or a genuine command. Commands provide the name of a program to be invoked as well as a flag indicating if super-user access is necessary for that program. Whether a submenu or a command, a minimum access level is given.

# Menu configuration file
#
# Blank lines and lines beginning with a '#' are treated as comments
#
# Lines beginning with 'Menu' indicate a new main menu entry
# These lines will also specify the access level required for entry to
# that menu option, and the following line will contain a textual description
# to be displayed.
#
# Lines that begin with 'Option' indicate an entry for the current menu
# item. These lines will also specify the access level required to use the
# item, whether the item requires root privileges (1 = yes) and the name of
# the script to run. The following line contains the textual description to
# be displayed.
#
# The scripts are all prefixed with the SCRIPTDIR prefix (from menu.h)
#
# Scripts must handle parameters and crashes themselves


Menu 3
Manage mail boxes

Option 3 1 viEditMailBox
Edit a specified mail box using vi

Option 3 1 EditMailBox
Read (and manage) a specified mail box


Menu 1
Testing stuff

Option 1 0 sayHi
Say Hi

Option 1 0 testRead
Read a number


Menu 1
Passwords

Option 1 1 chmenupass
Change password for this menu system


From here on in the rest is simple. All functionality can be implemented as simple, individual, stand-alone scripts. For instance, to allow staff to view a mailbox the following can be used:

#!/bin/sh

MAILPATH=/var/spool/mail

echo -n "What is the name of the mailbox to list? "
read MailBoxName

if [ -r $MAILPATH/$MailBoxName ]; then
 Mail -f $MAILPATH/$MailBoxName
else
 echo No such mailbox
fi


Instead, however, to let the help desk users edit and modify a mailbox, the following will work:

#!/bin/sh

MAILPATH=/var/spool/mail

echo -n "What is the name of the mailbox to edit? "
read MailBoxName

if [ -r $MAILPATH/$MailBoxName ]; then
 /bin/vi $MAILPATH/$MailBoxName
else
 echo No such mailbox
fi


The functionality need not be constrained to shell scripts, or indeed trite programs. Here’s one example of something more complex, namely adding new items to the end of the reverse DNS ARPA file, which is something web hosting services routinely require.

/*
 * arg 1 is the name of the Web node to create,
 * arg 2 is the full path of the ARPA file
 * returns an IP address
*/

#define SENTINEL "# New entries will be placed here - do not remove this line\n"

#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <time.h>


int main (int argc, char **argv)
{
 FILE *Infp;
 FILE *Outfp;

 char line [80];
 char ArpaFile [PATH_MAX];
 char TmpFile [PATH_MAX];
 int LastIp = 0;
 int NewIp = -1;


 if (argc != 3)
 {
  fprintf (stderr, "Bad parameters\n");
  exit (1);
 }


// Open the ARPA file and make a temporary file for writing

 strcpy (ArpaFile, argv [2]);
 strcpy (TmpFile, ArpaFile);
 strcat (TmpFile, ".tmp");


 if ((Infp = fopen (ArpaFile, "r")) <= 0)
 {
  fprintf (stderr, "Cannot open %s\n", ArpaFile);
  exit (1);
 }


 if ((Outfp = fopen (TmpFile, "w")) <= 0)
 {
  fprintf (stderr, "Cannot write to %s\n", TmpFile);
  fclose (Infp);
  exit (1);
 }


// Copy every line from the ARPA file to the temporary file
// If the line begins with a number, grab it as it is an IP address
// If the line is our SENTINEL line then insert a new line with a new IP
// address for the Web node specified.


 while (!feof (Infp))
 {
  fgets (line, 80, Infp);

  if (strcmp (line, SENTINEL) == 0) // Add our line
  {
   NewIp = LastIp + 1;
   fprintf (Outfp, "%-d\t\tIN\tPTR\t%s\n", NewIp,argv [1]);
  }
  else if (isdigit (line [0]))
   sscanf (line, "%d", &LastIp);

  fprintf (Outfp, "%s", line);
 }


 fclose (Infp);
 fclose (Outfp);


 if (NewIp == -1)
 {
  fprintf (stderr, "Could not find sentinel line.\n");
  unlink (TmpFile);
  exit (0);
 }


// Copy the new file back over the original arpa file

 rename (TmpFile, ArpaFile);

 printf ("192.168.1.%-d", NewIp);
 exit (0);
}



CONTINUED





Finally, here is menu.c, the main program file for the menu application. This reads in the menu configuration file, displays a text-based menu on screen, prompts the user for input, then performs the appropriate command with privilege elevation if required.


/*
 * Simple menu program
 *
 *  David M. Williams
*/

#include "menu.h"

// ----------------------------------------------------------------------------
int main (int argc, char **argv)
{
 char  *username;
 char  *password;
 int  access = -1;
 ScriptTree theMenu = NULL;

#ifdef DISABLE_INTERRUPTS
 signal (SIGINT, SIG_IGN);
#endif

 username = (char *) malloc (sizeof (char) * 50);
 password = (char *) malloc (sizeof (char) * 10);

// Ask for a login

 login (username, password);

// Verify username and password, getting access level

 if (verify (username, password, &access) < 0)
  noAccess (username);
 else
 {

// Generate a list of menu options

  buildMenu (access, &theMenu);

// Repeatedly display menu and process input

  DoMenu (username, theMenu, 0);

// Tidy up

 }

 printf ("\nBye.\n");

 free (username);
 free (password);
 Destroy (theMenu);
}

// ----------------------------------------------------------------------------
void clrscrn ()
{
#ifdef CLEAR_SCREEN
 system ("/bin/clear");
#endif
}

// ----------------------------------------------------------------------------
void login (char *username, char *password)
{
 char tempPassword [10];

 clrscrn ();

#ifdef BANNER
 if (strlen (BANNER) > 0)
  puts (BANNER);
#endif

 printf ("   Login: ");
 scanf ("%s", username);

#ifdef HIDE_PASSWORD
 strcpy (tempPassword, getpass ("Password: "));
#else
 printf ("Password: ");
 scanf ("%s", tempPassword);
#endif

 log ("Login attempt by %s\t%s\n", username, tempPassword);

 strcpy (password, crypt (tempPassword, username));
}

// ----------------------------------------------------------------------------
void noAccess (char *username)
{
 clrscrn ();
 printf ("%s, you do not have permission to access this system\n"
  "or your password was wrong.\n", username);
}

// ----------------------------------------------------------------------------
int verify (char *username, char *password, int *access)
{
 FILE *fp;
 int Found = 0;
 char line [80];
 char tempUser [50];
 char tempPass [10];
 int tempAccess;

 *access = -1;

 if ((fp = fopen (USERLIST, "r")) > 0)
 {
  while ((!Found) && (!feof (fp)))
  {
   fgets (line, 80, fp);
   if ((line [0] != '\n') && (line [0] != '#'))
   {
    sscanf (line, "%s%s%d", tempUser, tempPass,
     &tempAccess);

    if ((strcmp (tempUser, username) == 0) &&
     (strcmp (tempPass, password) == 0))
    {
     Found = 1;
     *access = tempAccess;
    }
   }
  }

  fclose (fp);
 }
 else
  log ("Cannot open %s\n", USERLIST);

 log ("%s has an access level of %d\n", username, *access);

 return *access;
}

// ----------------------------------------------------------------------------
void buildMenu (int access, ScriptTree *theMenu)
{
 FILE  *fp;
 char  *line;
 char  *line2;
 char  *scriptName;
 char  *LineType;
 int  userLevel;
 int  useSudo;
 ScriptTree subMenu;

 line = (char *) malloc (sizeof (char) * 80);
 line2 = (char *) malloc (sizeof (char) * 80);
 scriptName = (char *) malloc (sizeof (char) * 80);
 LineType = (char *) malloc (sizeof (char) * 10);

 subMenu = *theMenu;

 if ((fp = fopen (MENULIST, "r")) > 0)
 {
  while (!feof (fp))
  {
   line [0] = '#';
   while (((line [0] == '\n') || (line [0] == '#')) &&
     (!feof (fp)))
    fgets (line, 80, fp);

   line2 [0] = '#';
   while (((line2 [0] == '\n') || (line2 [0] == '#')) &&
     (!feof (fp)))
    fgets (line2, 80, fp);

   if (!feof (fp))
   {
    sscanf (line, "%s", LineType);

    if (strcmp (LineType, "Menu") == 0)
    {
     sscanf (line, "%s%d", LineType,
      &userLevel);
     if (userLevel <= access)
      subMenu = AddMenu
       (theMenu, line2);
    }
    else if (strcmp (LineType, "Submenu") == 0)
    {
     sscanf (line, "%s%d", LineType,
      &userLevel);
     if (userLevel <= access)
      subMenu = AddMenu
       (&subMenu, line2);
    }
    else if (strcmp (LineType, "Option") == 0)
    {
     sscanf (line, "%s%d%d%s", LineType,
      &userLevel, &useSudo,
      scriptName);
     if (userLevel <= access)
      AddNode (&subMenu, line2,
       scriptName, useSudo);
    }
   }
  }

  fclose (fp);
 }
 else
  log ("Cannot open %s\n", MENULIST);

#ifdef DEBUGGING
 DumpMenu (0, *theMenu);
 exit (0);
#endif

 free (line);
 free (line2);
 free (scriptName);
 free (LineType);
}

// ----------------------------------------------------------------------------
void DoMenu (char *username, ScriptTree theMenu, int InSub)
{
 int  DoExit = 0;
 int  userChoice;
 int  MenuItems;

 while (!DoExit)
 {

// Display the menu

  MenuItems = DisplayMenu (theMenu, InSub);

// Get input

  userChoice = getChoice (MenuItems);

// Process command

  if (userChoice == 0)
   DoExit = 1;
  else
   ProcessChoice (userChoice, theMenu, username, InSub);
 }
}

// ----------------------------------------------------------------------------
ScriptTree AddMenu (ScriptTree *theMenu, char *ItemText)
{
// Make a new node
 ScriptTree aNode;

 aNode = (ScriptTree) malloc (sizeof (ScriptNode));
 strcpy (aNode->MenuText, ItemText);
 strcpy (aNode->ScriptName, "");
 aNode->Next = NULL;
 aNode->Submenu = NULL;

// Find the last node in the list and set its next pointer to this node.

 if (*theMenu == NULL)
  *theMenu = aNode;
 else
 {
  ScriptTree iterator = *theMenu;

  while (iterator->Submenu != NULL)
   iterator = (void *) iterator->Submenu;

  iterator->Submenu = aNode;
 }

 return aNode;
}

// ----------------------------------------------------------------------------
void AddNode (ScriptTree *theMenu, char *ItemText,
        char *ScriptCommand, int UseSudo)
{
// Make a new node
 ScriptTree aNode;

 aNode = (ScriptTree) malloc (sizeof (ScriptNode));
 strcpy (aNode->MenuText, ItemText);
 strcpy (aNode->ScriptName, ScriptCommand);
 aNode->RunAsRoot = UseSudo;
 aNode->Next = NULL;
 aNode->Submenu = NULL;

// Find the last node in the list and set its next pointer to this node.

 if (*theMenu == NULL)
  *theMenu = aNode;
 else
 {
  ScriptTree iterator = *theMenu;

  while (iterator->Next != NULL)
   iterator = (void *) iterator->Next;

  iterator->Next = aNode;
 }
}

// ----------------------------------------------------------------------------
void Destroy (ScriptTree theMenu)
{
 ScriptTree iterator = theMenu;

 if (iterator != NULL)
 {
  Destroy (iterator->Next);
  Destroy (iterator->Submenu);
  free (iterator);
 }
}

// ----------------------------------------------------------------------------
void DoCommand (char *username, char *ScriptName, int UseSudo)
{
 char c = ' ';
 char FullPath [PATH_MAX];

 puts ("\n\n");

 log ("%s is running %s %s sudo\n", username, ScriptName,
  (UseSudo ? "with" : "without"));

 if (strlen (ScriptName) > 0)
 {
  sprintf (FullPath, "%s/%s", SCRIPTSDIR, ScriptName);

  if (fork () == 0)
  {
#ifdef USE_EXECL
   if (UseSudo)
    execl (SUDO, SUDO, FullPath, NULL);
   else
    execl (FullPath, FullPath, NULL);
#else
   if (UseSudo)
   {
    char SudoCmnd [PATH_MAX];
    sprintf (SudoCmnd, "%s %s", SUDO, FullPath);
    system (SudoCmnd);
   }
   else
    system (FullPath);
#endif

   exit (0);
  }
#ifdef USE_EXECL
  else
  {
   int status;
          wait3 (&status, NULL, (struct rusage *) 0);
  }
#endif
 }

// Wait for a key to be pushed

 printf ("\n\n\tPlease press return : ");
 while (c != '\n')
  c = getchar ();
}

// ----------------------------------------------------------------------------
void log (const char *format, ...)
{
#ifdef LOGGING_ENABLED
        va_list args;
        char logmsg [255];
 FILE *fp;
 time_t now;
 char nowtime [30];
 
        va_start (args, format);
        vsprintf (logmsg, format, args);

 if ((fp = fopen (LOGFILE, "a")) > 0)
 {
  time (&now);
  strcpy (nowtime, ctime (&now));
  nowtime [strlen (nowtime) - 1] = '\0';
  fprintf (fp, "%s\t%s", nowtime, logmsg);
  fclose (fp);
 }

        va_end (args);
#endif
}

// ----------------------------------------------------------------------------
int DisplayMenu (ScriptTree theMenu, int InSub)
{
 int   MenuItems = 0;
 ScriptTree iterator;

 clrscrn ();

 if (!InSub)
  puts ("\tAdministrative services main menu");
 else
  printf ("\tAdministative sub-menu\n\t%s", theMenu->MenuText);

 puts ("\n\n");

 if (!InSub)
  puts ("\t0)\tExit the program\n");
 else
  puts ("\t0)\tExit this menu level\n");

 if (theMenu != NULL)
 {
  if (!InSub)
  {
   MenuItems = 1;
   printf ("\t1)\t%s", theMenu->MenuText);
  }

  if (!InSub)
  {
   iterator = (void *) theMenu->Submenu;
   while (iterator != NULL)
   {
    MenuItems++;
    printf ("\t%d)\t%s", MenuItems,
     iterator->MenuText);
    iterator = (void *) iterator->Submenu;
   }
  }
  else
  {
   iterator = (void *) theMenu->Next;
   while (iterator != NULL)
   {
    MenuItems++;
    printf ("\t%d)\t%s", MenuItems,
     iterator->MenuText);
    iterator = (void *) iterator->Next;
   }
  }
 }

 return MenuItems;
}

// ----------------------------------------------------------------------------
int getChoice (int MenuItems)
{
 int userChoice = -1;
 char temp [5];
 char c = ' ';

 do
 {
  printf ("\n\t\tChoice (0-%d) : ", MenuItems);
  scanf ("%s", temp);
  userChoice = atoi (temp);
 } while ((userChoice < 0) || (userChoice > MenuItems));

// Consume all remaining input
 while (c != '\n')
  c = getchar ();

 return userChoice;
}

// ----------------------------------------------------------------------------
void ProcessChoice (int userChoice, ScriptTree theMenu, char *username,
  int InSub)
{
 int  itemNum = 0;
 ScriptTree iterator = theMenu;
 ScriptTree theChoice = NULL;

 if (!InSub)
 {
  if (userChoice == 1)
   theChoice = theMenu;
  else
   itemNum = 1;
 }

 if ((theChoice == NULL) && (!InSub))
 {
  iterator = (void *) theMenu->Submenu;
  while ((iterator != NULL) && (theChoice == NULL))
  {
   itemNum++;
   if (userChoice == itemNum)
    theChoice = iterator;
   else
    iterator = (void *) iterator->Submenu;
  }
 }

 if ((theChoice == NULL) && (InSub))
 {
  iterator = (void *) theMenu->Next;
  while ((iterator != NULL) && (theChoice == NULL))
  {
   itemNum++;
   if (userChoice == itemNum)
    theChoice = iterator;
   else
    iterator = (void *) iterator->Next;
  }
 }

 if (theChoice == NULL)
  puts ("\nInvalid choice.\n");
 else
 {
  if (strcmp (theChoice->ScriptName, "") != 0)
   DoCommand (username, theChoice->ScriptName,
    theChoice->RunAsRoot);
  else
   DoMenu (username, theChoice, 1);
 }
}

// ----------------------------------------------------------------------------
void DumpMenu (int level, ScriptTree theMenu)
{
 ScriptTree iterator;

 if (theMenu != NULL)
 {
  indent (level);
  printf ("%d\t%s", level, theMenu->MenuText);

  level++;
  iterator = (void *) theMenu->Next;
  while (iterator != NULL)
  {
   indent (level);
   printf ("%d\t%s", level, iterator->MenuText);
   iterator = (void *) iterator->Next;
  }
  level--;

  iterator = (void *) theMenu->Submenu;
  while (iterator != NULL)
  {
   DumpMenu (level, iterator);
   iterator = (void *) iterator->Submenu;
  }
 }
}

// ----------------------------------------------------------------------------
void indent (int level)
{
 int i;

 for (i = 0; i < level * 2; i++)
  putchar (' ');
}


Subscribe to ITWIRE UPDATE Newsletter here

GRAND OPENING OF THE ITWIRE SHOP

The much awaited iTWire Shop is now open to our readers.

Visit the iTWire Shop, a leading destination for stylish accessories, gear & gadgets, lifestyle products and everyday portable office essentials, drones, zoom lenses for smartphones, software and online training.

PLUS Big Brands include: Apple, Lenovo, LG, Samsung, Sennheiser and many more.

Products available for any country.

We hope you enjoy and find value in the much anticipated iTWire Shop.

ENTER THE SHOP NOW!

INTRODUCING ITWIRE TV

iTWire TV offers a unique value to the Tech Sector by providing a range of video interviews, news, views and reviews, and also provides the opportunity for vendors to promote your company and your marketing messages.

We work with you to develop the message and conduct the interview or product review in a safe and collaborative way. Unlike other Tech YouTube channels, we create a story around your message and post that on the homepage of ITWire, linking to your message.

In addition, your interview post message can be displayed in up to 7 different post displays on our the iTWire.com site to drive traffic and readers to your video content and downloads. This can be a significant Lead Generation opportunity for your business.

We also provide 3 videos in one recording/sitting if you require so that you have a series of videos to promote to your customers. Your sales team can add your emails to sales collateral and to the footer of their sales and marketing emails.

See the latest in Tech News, Views, Interviews, Reviews, Product Promos and Events. Plus funny videos from our readers and customers.

SEE WHAT'S ON ITWIRE TV NOW!

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.

Share News tips for the iTWire Journalists? Your tip will be anonymous

WEBINARS ONLINE & ON-DEMAND

GUEST ARTICLES

VENDOR NEWS

Guest Opinion

Guest Interviews

Guest Reviews

Guest Research

Guest Research & Case Studies

Channel News

Comments