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