Let’s ‘C’ integration with security

The previous post started a Server process which could receive ASCII based input, although it was pretty limited in terms of what it did it did show some of the basics required to build a Server process which listens to a TCP/IP port. The fact that it did nothing meant that there was no real security issues around having it sit there running and sending back a simple message, but in reality we should add some security around the process so that when it does something in the future it encapsulates a level of security appropriate for the environment.

This post is going to add a level of security to that process, it is probably too little for an application that is accessed from outside your own network (particularly as the password we send in will be text based) but it is a starting point which we can work with. The method we have chosen to add this level of security requires the job adopts the profile of the user requesting access which is achieved by using the Profile Handle API’s. Each time a user requests access we will extract a link to that profile and set the job up to use it, once the user signs off we will set the profile back to the original profile (the one the job was loaded with). If the user send in incorrect data the process is stopped and the connection closed, the only way the user can communicate again is by starting a new connection request.

We are going to create a new Service Program which will be utilized by the WORKER program, its a good fit for this because we could have lots of WORKER programs running to respond to outside requests. When the user needs to sign on we will have to communicate with the client to pull in the profile and password to be used, this will be our first function in the Service Program.

Add a new member to OSLIB/H called SRVFUNC, this will be the header we will include for all of the Server Functions we are going to build. The following code is to be added to the member.
#ifndef SRVFUNC_h
   #define SRVFUNC_h
   #include <H/COMMON>                      // common header
   #include <H/MSGFUNC>                     // message functions
   #include <qsyphandle.h>                  // profile handles
   #include <netdb.h>                       // Socket DB
   #include <sys/socket.h>                  // Sockets
   #include <netinet/in.h>                  // Socket net
   #include <arpa/inet.h>                   // ARPA structs
   #include <errno.h>                       // Error return header
   #include <netdb.h>                       // net DB header
   #include <sys/errno.h>                   // sys error header
   #include <arpa/inet.h>                   // inet header
   #include <signal.h>                      // signal funcs
   #include <except.h>                      // exception funcs
   #include <iconv.h>                       // conversion header
   #include <qtqiconv.h>                    // iconv header

   int Handle_SO(int,char *,iconv_t,iconv_t);
   #endif       
A lot of the includes have been removed from the WORKER program code with a few additions, we have also defined the call requirements for the Handle_SO function we are going to build for the Service Program..

Next we are going to create the function which will communicate with the client and get the profile and password information. if all works well it will change the job to use the profile handle the API returns.
#include <H/SRVFUNC>                        // Server Functions Header

// Function Handle_SO()
// purpose: handle a sign on request
// @parms
//      socket
//      Prf Handle
// returns 1 on success

int Handle_SO(int accept_sd,
              char *Usr_hdl,
              iconv_t a_e_ccsid,
              iconv_t e_a_ccsid) {
int rc = 0;                                 // return code
int len = 0;                                // counter
int ret = 0;                                // return code
size_t insz;                                // input len
size_t outsz;                               // output size
char recv_buf[_32K];                        // recv buffer
char msg_dta[_MAX_MSG];                     // message buffer
char *out_ptr;                              // buffer ptr
char *in_ptr;                               // buffer ptr
char Profile[10] = {' '};                   // Profile
char Pwd[128] = {' '};                      // password
char convBuf[132];                          // conversion buffer
Os_EC_t Error_Code = {0};                   // error struct

Error_Code.EC.Bytes_Provided = _ERR_REC;
// first we need to retieve the profile
sprintf(msg_dta,"Please enter your Profile name : ");
len = strlen(msg_dta);
in_ptr = msg_dta;
out_ptr = convBuf;
insz = len;
outsz = 132;
ret = (iconv(e_a_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
rc = send(accept_sd,convBuf,len,0);
// make sure the data was sent
if(rc != len) {
   return -1;
   }
// make sure not too long. Our test server sends a '0A' at the end of the input so
// it needs to be dropped. In a future implementation we should consider alternatives
rc = recv(accept_sd,recv_buf,_32K,0);
// copy to the Profile holder which is initialized with all blanks after conversion
in_ptr = recv_buf;
out_ptr = convBuf;
insz = rc;
outsz = 132;
ret = (iconv(a_e_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
// strip off the 'A0' byte.
len = rc -1;
if(len > 10) {
   sprintf(msg_dta,"Profile too long Sign On aborted");
   snd_msg("GEN0001",msg_dta,strlen(msg_dta));
   return -1;
   }
memcpy(Profile,convBuf,len);
// now get the password
sprintf(msg_dta,"Please enter your Password : ");
len = strlen(msg_dta);
in_ptr = msg_dta;
out_ptr = convBuf;
insz = len;
outsz = 132;
ret = (iconv(e_a_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
rc = send(accept_sd,convBuf,len,0);
if(rc != len) {
   return -1;
   }
// recv the password and convert
rc = recv(accept_sd,recv_buf,_32K,0);
in_ptr = recv_buf;
out_ptr = convBuf;
insz = rc;
outsz = 132;
ret = (iconv(a_e_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
// strip off the 'A0' byte.
len = rc -1;
if(len > 128) {
   sprintf(msg_dta,"Password too long");
   snd_msg("GEN0001",msg_dta,strlen(msg_dta));
   return -1;
   }
memcpy(Pwd,convBuf,len);
// now check the password against the profile, CCSID of 0 means use the job ccsid
QsyGetProfileHandle(Usr_hdl,
                    Profile,
                    Pwd,
                    rc,
                    0,
                    &Error_Code);
if(Error_Code.EC.Bytes_Available) {
   snd_error_msg(Error_Code);
   return -1;
   }
return 1;
} 
We are going to convert the text used for the communication so we need the conversion tables from the WORKER program and the socket that is to be used. We will pass a holder for the Profile handle which is created by the API as these are finite resources in the system, its important that we release any profile handles as soon as they are finished with. As this function will be doing some conversions between ASCII and EBCDIC as part of the communication stream we pass in the tables already created by the WORKER program.

The socket has been accepted so we first send off a request to the client to enter the Profile name they wish to use, this has to be converted to ASCII before being sent. The data we received is terminated by a ‘0A’ combination, we need to ensure we do not recognize that as part of the Profile so we simply decrement the counter which states how much data was received ( this works in our case but may be different dependent on the client platform test application) . We also make sure the data is not longer than we can accept (10 characters for a Profile). Next we send off a request for the password which follows the same process and is stored ready to be passed into the API we are going to use to verify the input and get the profile handle. You will see we have set the maximum length of the password to 128 which is the maximum length on the IBM i. However you could adjust that based on the relevant system settings. the Profile and Password buffers are passed into the API, if the profile exists and the password matches a 12 byte handle is returned which can be used to set the jobs profile. 

We had to make a number of changes to the WORKER program to allow this to work, we have implemented a switch process that will work against a converted 4 byte number (passed as a character array). We have to copy this to a temp holder which is NULL terminated for the atoi() function to convert to an integer, we will use this integer to decide what request the user is sending in. Here is the code we will use, later we will explain some of the changes.
/**
  *
  *  Revision log.:
  *  Date     Author    Revision
  *  2018     Chris H   1.0
  *
  *  Copyright (C) <2018>  <Chris Hird>
  *
  *  This program is free software; 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 program 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.
  *
  *  You should have received a copy of the GNU General Public License
  *  with this program; if not, write to the Free Software Foundation,
  *  Inc.,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  *  You can contact the author via Email chrish@shieldadvanced.com
  */
#include <H/SRVFUNC>                        // Server functions
#include <qsyphandle.h>                     // profile handles

#define _QSIZE 256
#define _CPYRGHT "Copyright @ Shield Advanced Solutions Ltd 1997-2018"
#pragma comment(copyright,_CPYRGHT)

// Signal catcher

void catcher(int sig) {
// you can do some messaging etc in this function, it is called
// when the kill signal is received from the TESTSVR program
exit(0);
}

int main(int argc, char **argv) {
int listen_sd, accept_sd = 0;               // file descriptors
int rc = 0;                                 // return codes
int ret = 0;                                // return vals
int Stop = 0;                               // stop flag
int i = 0;                                  // counter
int j = 0;                                  // counter
int offset = 0;                             // offset marker
int total_bytes = 0;                        // counter
int bytes_processed = 0;                    // counter
int valid_user = 0;                         // valid user flag
int req = 0;                                // request flag
size_t insz;                                // input len
size_t outsz = _32K;                        // outbuf size
char recv_buf[_32K];                        // recv buffer
char convbuf[_32K];                         // conversion buffer
char msg_dta[_MAX_MSG];                     // message buffer
char Usr_hdl[12];                           // Usr profile handle
char Cur_hdl[12];                           // Current profile handle
char key[5] = {'\0'};                       // request key
char *out_ptr;                              // buffer ptr
char *in_ptr;                               // buffer ptr
struct sockaddr_in addr;                    // socket struct
struct sigaction sigact;                    // Signal Action Struct
QtqCode_T jobCode = {0,0,0,0,0,0};          // (Job) CCSID to struct
QtqCode_T asciiCode = {819,0,0,0,0,0};      // (ASCII) CCSID from struct
iconv_t a_e_ccsid;                          // convert table struct
iconv_t e_a_ccsid;                          // convert table struct
Os_EC_t Error_Code = {0};                   // error struct

Error_Code.EC.Bytes_Provided = _ERR_REC;

// Set up the signal handler
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigact.sa_handler = catcher;
sigaction(SIGTERM, &sigact, NULL);
// create the conversion tables
// ASCII to EBCDIC
a_e_ccsid = QtqIconvOpen(&jobCode,&asciiCode);
if(a_e_ccsid.return_value == -1) {
   sprintf(msg_dta,"QtqIconvOpen Failed %s",strerror(errno));
   snd_msg("GEN0001",msg_dta,strlen(msg_dta));
   exit(-1);
   }
// EBCDIC to ASCII
e_a_ccsid = QtqIconvOpen(&asciiCode,&jobCode);
if(e_a_ccsid.return_value == -1) {
   iconv_close(a_e_ccsid);
   sprintf(msg_dta,"QtqIconvOpen Failed %s",strerror(errno));
   snd_msg("GEN0001",msg_dta,strlen(msg_dta));
   exit(-1);
   }
// get the current profile handle for the job
QsyGetProfileHandleNoPwd(Cur_hdl,
                         "*CURRENT  ",
                         "*NOPWDCHK ",
                         &Error_Code);
if(Error_Code.EC.Bytes_Available) {
   snd_error_msg(Error_Code);
   exit(-1);
   }
// connect to the socket passed as argv[1]
listen_sd = atoi(argv[1]);
do {
   // only accept new connection if not already set
   if(valid_user == 0) {
      accept_sd = accept(listen_sd, NULL, NULL);
      setsockopt(accept_sd,SOL_SOCKET,SO_RCVBUF,CHAR_32K,sizeof(CHAR_32K));
      if(accept_sd < 0) {
         sprintf(msg_dta,"accept() failed",strerror(errno));
         snd_msg("GEN0001",msg_dta,strlen(msg_dta));
         close(listen_sd);
         exit(-1);
         }
      }
   memset(recv_buf,'\0',_32K);
   rc = recv(accept_sd, recv_buf, _32K, 0);
   // at this point we need to get the key information and switch to sort
   // the first 4 bytes will be the key and it must be sent separately the enter key is also passed
   if(rc == 5) {
      in_ptr = recv_buf;
      out_ptr = convbuf;
      insz = rc;
      outsz = _32K;
      ret = (iconv(a_e_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
      memcpy(key,convbuf,4);
      sprintf(msg_dta,"Key : %s",key);
      snd_msg("GEN0001",msg_dta,strlen(msg_dta));
      req = atoi(key);
      switch(req) {
         case  0 :
            // signon request
            if(Handle_SO(accept_sd,Usr_hdl,a_e_ccsid,e_a_ccsid) != 1) {
               sprintf(msg_dta,"Sign On failed : ");
               snd_msg("GEN0001",msg_dta,strlen(msg_dta));
               in_ptr = msg_dta;
               out_ptr = convbuf;
               insz = strlen(msg_dta);
               outsz = _32K;
               ret = (iconv(e_a_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
               rc = send(accept_sd,convbuf,strlen(msg_dta),0);
               close(accept_sd);
               }
            valid_user = 1;
            break;
         case 1 :
            // sign off request
            if(valid_user == 1) {
               QsySetToProfileHandle(Cur_hdl,
                                     &Error_Code);
               if(Error_Code.EC.Bytes_Available > 0) {
                  snd_error_msg(Error_Code);
                  }
               // release the profile handle for the user
               QsyReleaseProfileHandle(Usr_hdl,
                                       &Error_Code);
               // send closing message
               sprintf(msg_dta,"OK signing off and closing socket : ");
               in_ptr = msg_dta;
               out_ptr = convbuf;
               insz = strlen(msg_dta);
               outsz = _32K;
               ret = (iconv(e_a_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
               rc = send(accept_sd,convbuf,strlen(msg_dta),0);
               // close the socket
               close(accept_sd);
               valid_user = 0;
               }
            break;
         default :
               sprintf(msg_dta,"Unknown Request : ");
               snd_msg("GEN0001",msg_dta,strlen(msg_dta));
               in_ptr = msg_dta;
               out_ptr = convbuf;
               insz = strlen(msg_dta);
               outsz = _32K;
               ret = (iconv(e_a_ccsid,(char **)&(in_ptr),&insz,(char **)&(out_ptr),&outsz));
               rc = send(accept_sd,convbuf,strlen(msg_dta),0);
            break;
         } // switch
      } // key request
   if(valid_user == 0) {
      close(accept_sd);
      }
   } while(Stop == 0);
}
As this program will be assuming the attributes of any number of profiles it is important that we can revert it back to the original profile once the conversation has ended between the client and it. To do this we will retrieve a profile handle for the *CURRENT user at program start (this is the profile the job was loaded with) and store it ready to use later when we need to switch back.

We need to determine when the existing conversation has ended, we do this by setting a variable called valid_user. If the valid_user variable is not set (== 1) we will wait for a connection request on the socket we have been passed. Once the connection has been made we will receive the initial request which will be used to determine the action we need to take, this will be a 4 byte character array which is converted to an integer to be used in the switch process. If the user does not follow the required process or it sends in an invalid request we will drop into the default : process which is to send a message stating its an invalid request. If it converts the input characters to 0 we know its a sign on request and we will call the Service Program function to collect the data, a positive return means it worked and so we set valid_user to 1. The only other action we will accept from there is a 0001 request which will switch the profile back to the original, destroy the profile handle, send a message, close the connection and set valid_user to 0 before going back to the accept state. It will continue to go through that loop until the TESTSVR program sends the kill signal.

There are still a number of changes we can add to improve the security and clean up on the kill signal being received, but for now this is enough. To create the programs we are going to need to bring the SRVFUNC Service Program into the mix. This requires a couple of commands to be run that will allow it to be created and seen by any programs that need it exported functions.

First we will create the Module from the source.
CRTCMOD MODULE(OSLIB/SRVFUNC) SRCFILE(OSLIB/QCSRC) SRCMBR(SRVFUNC) DBGVIEW(*SOURCE) REPLACE(*YES) TGTRLS(V7R1M0)
Next we need to update the export source file we use to add the new Service Program exports and add the Service program to the binding directory.
RTVBNDSRC MODULE(OSLIB/SRVFUNC) SRCFILE(OSLIB/QSRVSRC)
ADDBNDDIRE BNDDIR(OSLIB/OS) OBJ((SRVFUNC *SRVPGM *DEFER))
The Service Program can now be created ready for use.
CRTSRVPGM SRVPGM(OSPGM/SRVFUNC) MODULE(OSLIB/SRVFUNC) SRCFILE(OSLIB/QSRVSRC) BNDDIR(OSLIB/OS) TGTRLS(V7R1M0)
We have made a number of changes to the WORKER program plus it now relies on the SRVFUNC Service program so we will recreate the program.
CRTCMOD MODULE(OSLIB/WORKER) SRCFILE(OSLIB/QCSRC) SRCMBR(WORKER) DBGVIEW(*SOURCE) REPLACE(*YES) TGTRLS(V7R1M0)
CRTPGM PGM(OSPGM/WORKER) MODULE(OSLIB/WORKER) BNDDIR(OSLIB/OS) TGTRLS(V7R1M0)
Go to the SVRMAIN menu with OSPGM in your library list and restart the jobs. Once they are up and running you can test the process using your test program in our case its NetCat running under Linux. Here is a sample of the communication from a terminal session.
chrish@webserver:~$ nc sas4.shield.local 12345
0000
Please enter your Profile name : CHIRD
Please enter your Password : *******
0003
Unknown Request : 0001
OK signing off and closing socket : 
One important point here is the IBM i does not like the Profile to be sent in as anything but upper case, the password does not matter but sending in ‘chird’ results in the Profile not being found.

This is only one of many ways to handle a communication process with the IBM i via TCP/IP and ASCII clients, it is limited by the fact that is holds any further connections to this program until the current conversation ends. We will look at how to improve the scalability in a future post but for now this is where we will stop.

In this post we have looked at how we can add security to the comminications using some of the API’s available, we can take this further by adding some encryption particularly for the password but that will require a client application that is specific for this server. Next post will look at how we can take additional actions using the profile security we have added.

If you have any questions etc please feel free to contact us. If you would like help in setting up your own in house server we provide services for that.

Chris…