TCP Remote Shell Server in C

Autor: Andre Adrian
Version 27.Juni.2007

Zusammenfassung

Nach zwei Versionen eines Internet Relay Chat (IRC) Servers gibt es nun einen Remote Shell Server mit Remote Shell Client. Ein Remote Shell Client wie z.B. rsh oder remsh sendet einen ASCII String zum Remote Shell Server. Dort wird dieser ASCII String als Shell Kommando ausgeführt. Die Ausgabe des Shell Kommandos wird über das Netzwerk wieder an den Remote Shell Client zurückgesendet und dort angezeigt.
Wie durch viele Exploits bekannt ist Entfernte Programm Ausführung (remote program execution) eine gefährliche Sache. Die hier vorgestellte Software hat keinen Schutz gegen Remote Exploits und ist mindestens so unsicher wie rsh! Profis verwenden ssh oder setzen sich zur Wartung vor den Server.
In der Linux Version wird popen() und fork() verwendet. Unter MS-Windows gibt es _popen() aber kein fork(). Deshalb wird unter MS-Windows wieder mit Threads gearbeitet.
Die erste Linux Implementierung arbeitet mit gepufferter, zeilenorientierter Ein-/Ausgabe auf der Socket Schnittstelle. Die zweite Linux Implementierung arbeitet mit recv() und send(). Die MS-Windows Implementierung funktioniert nur mit recv() und send().

Inhaltsverzeichnis

UNIX Prozesse

IO-Multiplexing mit select() wurde 1983 mit 4.2BSD eingeführt. POSIX Threads gibt es seit 1995. Seit den Anfangstagen von UNIX werden Prozesse mit fork() kopiert. Das Betriebssystem selbst benötigt fork() und exec() um in der Shell ein Programm auszuführen. Die drei Alternativen select(), Threads und fork() unterscheiden sich in der Anzahl der Programmfäden und im Umgang mit dem globalen Speicher.

Systemaufruf
Programmfäden
Globaler Speicher
select()
einer
gemeinsam
Threads
mehrere
gemeinsam
fork()
mehrere
gemeinsam vor fork(), getrennt nach fork()

Bei select() sind keine Mutex Variablen oder Semaphore nötig. Bei Threads sind fast immer Mutexe oder ähnliches nötig. Im Remote Shell Server arbeiten die Kindprozesse nach dem fork() unabhängig voneinander. Es ist daher keine Inter Prozess Kommunikation (IPC) nötig und somit auch keine Mutexe.

fork() und Speicher

Der Systemaufruf fork() kopiert den Prozess-Speicher. Nach dem fork() haben Vaterprozess und Kindprozess zuerst die gleichen Variablenwerte und unterscheiden sich nur im Returnwert des fork() Aufrufes. Im weiteren Programmlauf  erhalten die Variablen gleichen Names im Vaterprozess und im Kindprozess üblicherweise unterschiedliche Werte. Für einen Datenaustausch zwischen Vater und Kind nach dem fork() ist Inter Prozess Kommunikation (IPC) nötig.

UNIX und MS-Windows Systemaufrufe

Unter MS-Windows gibt es keinen fork(). Soll fork() zusammen mit exec() eine Programmdatei im Kindprozess ausführen, dann bietet MS-Windows _spawn() an. Ein Kindprozess ohne Programmdatei muß unter MS-Windows als Thread programmiert werden. Die Abkürzung CRT steht für C Run-Time. Eine Pipe ist eine Ein-Wege-Verbindung zwischen zwei Prozessen.
Unter MS-Windows kann die _fdopen() Funktion nicht zusammen mit Sockets verwendet werden. Die Stream IO Funktionen fgets() und fputs() benutzen die Systemaufrufe read() und write() für die eigentliche Arbeit. Die Systemaufrufe read() und write() sind bei MS-Windows aber nicht für Socket Filedeskriptoren geeignet.

Funktion
UNIX
MS-Windows CRT
Programmdatei im Kindprozess starten
fork() und exec()
_spawn()
Kind Prozess (Thread) starten
fork()
_beginthread()
Kind Prozess (Thread) beenden
exit()
_endthread()
Pipe erzeugen und Shell Kommando ausführen
popen()
_popen()
Pipe schließen
pclose()
_pclose()
Gepufferte Ein-/Ausgabe ermöglichen
fdopen()
_fdopen() - nicht für Sockets

Compile und Ausführen

Der Server Quelltext wird unter Linux mit  cc -o remshd remshd.c  übersetzt. Der Server wird mit   ./remshd &   im Hintergrund gestartet. Der Client Quelltext wird mit  cc -o remsh remsh.c  übersetzt. Für den ersten Test können ein oder mehr Client Programme mit  remsh 127.0.0.1  auf dem gleichen Rechner gestartet werden. Eine Eingabe im remsh Programm wird per TCP Verbindung zum remshd Programm gesendet. Dort wird die Eingabe als Kommando ausgeführt und das Ergebnis (stdout und stderr) an den Client zurückgesendet.

Der Quelltext mit Stream IO

Der Quelltext besteht aus den drei Dateien
Im Client Programm werden alle Aufgaben in der Funktion main() ausgeführt. Im Server Programm werden die Aufgaben des Vater-Prozesses in der Funktion main() ausgeführt. Die Aufgaben der Kindprozesse werden in der Funktion worker() ausgeführt. Die Funktion sigchld_handler() wird beim Auftreten eines SIGCHLD Signals aufgerufen. Der Exit Wert des Kindprozesses wird gelesen und damit der Zombie Status des Kindprozesses beendet.
Die genaue Funktion des Quelltextes erschließt sich durch aufmerksames Lesen des Quelltextes. Das   man   UNIX Kommando ist sehr hilfreich beim Verständnis der Betriebssystem Aufrufe. Falls Lesen nicht mehr weiterhilft, sollte man mit dem Programm "spielen": Quelltext ändern, printf() Kommandos einfügen, das Programm im Debugger ausführen, usw.
Falls auch Spielen nicht dem Verständnis hilft dann sollte man vielleicht zuerst das select() Beispiel und das Thread Beispiel durcharbeiten.

remsh Header Datei

/* remsh.h
 * TCP Remote Shell gemeinsame Headerdatei
 * Andre Adrian
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <netdb.h>
 
#define REMSH_PORT 26001    // Der Remote Shell well known Port
#define BUFSIZE 1400        // etwas kleiner als Ethernet MTU von 1500 Bytes

#define exit_if(expr) \
if(expr) { \
  fprintf(stderr, "%s(%d) %s\n", __FILE__, __LINE__, strerror(errno)); \
  exit(1); \
}

remsh Server Quelltext mit fdopen()

/* remshd.c
 * TCP Remote Shell Server mit fdopen()
 * Andre Adrian
 */

#include "remsh.h"

void sigchld_handler(int s)
/* Zombie Child Signal Handler */
{
  while (waitpid(-1, NULL, WNOHANG) > 0);
}

void worker(int ausgabefd)
/* Worker Child Prozess */
{
  FILE *ausgabefp;
 
  ausgabefp = fdopen(ausgabefd, "r+");  // buffered IO verwenden
  exit_if(NULL == ausgabefp);
  // Achtung: nach fdopen() nicht read() und fgets() gemischt verwenden!

  for (;;) {              // Endlos Schleife
    int rv;
    FILE *eingabefp;
    char buf[BUFSIZE];
    char *rvp;

    rvp = fgets(buf, sizeof(buf), ausgabefp);
    if (NULL == rvp) {
      break;              // Gegenseite hat Verbindung beendet oder Fehler
    }
   
    eingabefp = popen(buf, "r");  // Shell aufrufen
    exit_if(NULL == eingabefp);

    for (;;) {
      rvp = fgets(buf, sizeof(buf), eingabefp);
      if (NULL == rvp) {
        break;                    // End of File EOF oder Fehler
      }
     
      rv = fputs(buf, ausgabefp);
      exit_if(rv < 0);      
    }
    rv = fputs("\003\n", ausgabefp);  // ETX als Ende Ausgabe Zeichen senden
    exit_if(rv < 0);
   
    fflush(ausgabefp);
   
    pclose(eingabefp);      
  }
  shutdown(ausgabefd, SHUT_RDWR); // vermeide FIN_WAIT2, CLOSE_WAIT TCP States
  fclose(ausgabefp);              // schliesst auch ausgabefd
  exit(0);                        // beende Child
}

int main(void)
{
  struct sockaddr_in local; // Server IPv4 Adress und Port Struktur
  int localfd;              // Server Filedeskriptor
  int yes = 1;
  int rv;
  struct sigaction sa;

  localfd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(localfd < 0);

  // Address already in use Fehlermeldung vermeiden
  rv = setsockopt(localfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
  exit_if(rv < 0);
 
  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(REMSH_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  exit_if(rv < 0);

  rv = listen(localfd, 5);  // TCP Server Verbindungs Warteschlange setzen
  exit_if(rv < 0);
 
  sa.sa_handler = sigchld_handler;  // Signal Handler fuer Zombie Prozesse
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;
  rv = sigaction(SIGCHLD, &sa, NULL);
  exit_if(rv < 0);

  for (;;) {
    int connectfd;
   
    connectfd = accept(localfd, NULL, NULL);  // Verbindung entgegennehmen
    if (connectfd < 0) {
      perror("remshd1.c accept");
      continue;                         // for Schleife erneut durchlaufen
    }
          
    rv = fork();
    if (rv < 0) {
      perror("remshd1.c fork");
      continue;
    }
    if (0 == rv) worker(connectfd);        
  }
  return 0;
}

remsh Client Quelltext mit fdopen()

/* remsh.c
 * TCP Remote Shell Client mit fdopen()
 * Andre Adrian
 */
 
#include "remsh.h"

int main(int argc, char *argv[])
{
  struct sockaddr_in remote;    // remote Server IPv4 Adress und Port Struktur
  int remotefd;                 // remote Server Filedeskriptor
  FILE *remotefp;
  int rv;
  struct hostent *he;

  if (argc != 2) {
    fprintf(stderr, "usage: remsh hostname\n");
    exit(1);
  }

  he = gethostbyname(argv[1]);  // hole Server IPv4 Adresse
  exit_if(NULL == he);

  remotefd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(remotefd < 0);
 
  memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
  remote.sin_family = AF_INET;
  remote.sin_port = htons(REMSH_PORT);
  remote.sin_addr = *((struct in_addr *) he->h_addr);

  // remote Server Verbindung aufbauen
  rv = connect(remotefd, (struct sockaddr *) &remote, sizeof(remote));
  exit_if(rv < 0);
 
  remotefp = fdopen(remotefd, "r+");  // mit buffered IO arbeiten
  exit_if(NULL == remotefp);
  // Achtung: nach fdopen() nicht read() und fgets() gemischt verwenden!

  for(;;) {               // Endlos Schleife
    char buf[BUFSIZE];
    char *p;
   
    fputs("> ", stdout);                // Prompt ausgeben
    fgets(buf, sizeof(buf), stdin);     // Kommando holen
    if (0 == strcmp(buf, "QUIT\n")) {
      break;                            // verlasse for() Schleife
    }
    rv = fputs(buf, remotefp);          // zum Server senden
    exit_if(rv < 0);
    fflush(remotefp);
   
    for (;;) {
      char *rvp;

      rvp = fgets(buf, sizeof(buf), remotefp);
      exit_if(NULL == rvp); // Gegenseite hat Verbindung beendet oder Fehler

      p = strchr(buf, '\003');
      if (p != NULL) {
        break;              // End of Transmission ETX gefunden
      }
      fputs(buf, stdout);   // Antwort ausgeben (ohne zusaetzliches \n)
    }
  }
  fclose(remotefp);

  return 0;
}

Der Quelltext mit UNIX IO

Die zweite Linux Lösung arbeitet mit recv() und send(). Diese Lösung ist nicht so elegant wie die Stream IO Lösung oben. Die Header Datei remsh.h ist identisch.

remsh Server Quelltext ohne fdopen()

/* remshd.c
 * TCP Remote Shell Server ohne fdopen()
 * Andre Adrian
 */

#include "remsh.h"

void sigchld_handler(int s)
/* Zombie Child Signal Handler */
{
  while (waitpid(-1, NULL, WNOHANG) > 0);
}

void worker(int fd)
/* Worker Child Prozess */
{
  for (;;) {                // Endlos Schleife
    int len;
    int rv;
    FILE *fp;
    char buf[BUFSIZE];
    char etx[] = "\003\n";  // Ausgabe Ende String ist ETX und LF

    len = recv(fd, buf, sizeof(buf)-1, 0);
    exit_if(len < 0);
    if (0 == len) {    
      break;              // Verbindung geschlossen von Gegenseite
    }     
    buf[len] = 0;         // Null-Byte anhaengen an Kommando

    fp = popen(buf, "r"); // Shell aufrufen
    exit_if(NULL == fp);

    do {
      int items;
     
      items = fread(buf, 1, sizeof(buf), fp);  
      rv = send(fd, buf, items, 0);
      exit_if(rv != items);
     
    } while(!feof(fp));
    rv = send(fd, etx, strlen(etx), 0); // Ausgabe Ende String senden
    exit_if(rv != strlen(etx));
   
    pclose(fp);      
  }
  shutdown(fd, SHUT_RDWR);  // vermeide FIN_WAIT2 und CLOSE_WAIT TCP States
  close(fd);
  exit(0);                  // beende Child
}

int main(void)
{
  struct sockaddr_in local; // Server IPv4 Adress und Port Struktur
  int localfd;              // Server Filedeskriptor
  int yes = 1;
  int rv;
  struct sigaction sa;

  localfd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(localfd < 0);

  // Address already in use Fehlermeldung vermeiden
  rv = setsockopt(localfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
  exit_if(rv < 0);
 
  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(REMSH_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  exit_if(rv < 0);

  rv = listen(localfd, 5);  // TCP Server Verbindungs Warteschlange setzen
  exit_if(rv < 0);
 
  sa.sa_handler = sigchld_handler;  // Signal Handler fuer Zombie Prozesse
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;
  rv = sigaction(SIGCHLD, &sa, NULL);
  exit_if(rv < 0);

  for (;;) {
    int connectfd;
   
    connectfd = accept(localfd, NULL, NULL);  // Verbindung entgegennehmen
    if (connectfd < 0) {
      perror("remshd.c accept");
      continue;                         // for Schleife erneut durchlaufen
    }
          
    rv = fork();
    if (rv < 0) {
      perror("remshd.c fork");
      continue;
    }
    if (0 == rv) worker(connectfd);        
  }
  return 0;
}

remsh Client Quelltext ohne fdopen()

/* remsh.c
 * TCP Remote Shell Client ohne fdopen()
 * Andre Adrian
 */
 
#include "remsh.h"

int main(int argc, char *argv[])
{
  struct sockaddr_in remote;    // remote Server IPv4 Adress und Port Struktur
  int remotefd;                 // remote Server Filedeskriptor
  int rv;
  struct hostent *he;

  if (argc != 2) {
    fprintf(stderr, "usage: remsh hostname\n");
    exit(1);
  }

  he = gethostbyname(argv[1]);  // hole Server IPv4 Adresse
  exit_if(NULL == he);

  remotefd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(remotefd < 0);
 
  memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
  remote.sin_family = AF_INET;
  remote.sin_port = htons(REMSH_PORT);
  remote.sin_addr = *((struct in_addr *) he->h_addr);

  // remote Server Verbindung aufbauen
  rv = connect(remotefd, (struct sockaddr *) &remote, sizeof(remote));
  exit_if(rv < 0);

  for(;;) {               // Endlos Schleife
    char buf[BUFSIZE];
    char *p;
   
    printf("> ");                           // Prompt ausgeben
    fgets(buf, sizeof(buf), stdin);         // Kommando holen
    if (0 == strcmp(buf, "QUIT\n")) {
      break;                                // verlasse for() Schleife
    }
    rv = send(remotefd, buf, strlen(buf), 0); // zum Server senden
    exit_if(rv != strlen(buf));
   
    do {
      int len;
     
      len = recv(remotefd, buf, sizeof(buf)-1, 0);  // Antwort holen
      exit_if(len < 0);
      if (0 == len) {
        close(remotefd);        // Verbindung geschlossen von Gegenseite
        return 0;
      }
      buf[len] = 0;             // Null-Byte anhaengen

      p = strchr(buf, '\003');
      if (p != NULL) *p = 0;    // ersetze ETX Zeichen durch NUL Zeichen
      printf("%s", buf);        // Antwort ausgeben
    } while (NULL == p);        // solange kein ETX gefunden
  }
  close(remotefd);

  return 0;
}

MS-Windows TCP Remote Shell Quelltext

Die Unterschiede bei der Socket Schnittstelle zwischen Linux und MS-Windows werden im select() Beispiel dargestellt. Im Linux Worker Prozess kann exit() eingesetzt werden. Im MS-Windows Worker Thread beendet exit() den kompletten Server. Deshalb wird return_if() anstelle von exit_if() eingesetzt.
Bei der stdio Schnittstelle gibt es auch einige Unterschiede:
Nach drei Server Beispielen mit Linux und MS-Windows kann man sagen: UNIX ist das Original, MS-Windows ist die Kopie. Unter UNIX ist alles eine Datei und kann mit Funktionen der gepufferten Ein-/Ausgabe bearbeitet werden. Unter MS-Windows gibt es Unterschiede zwischen Files und Sockets. Die von MS-Windows implementierten Standards ANSI C, POSIX und Berkeley Sockets machen die Portierung von UNIX nach MS-Windows recht einfach. Das fork() in MS-Windows fehlt ist leicht zu kompensieren.

MS-Windows Header Datei

/* winremsh.h
 * TCP Remote Shell gemeinsame Headerdatei
 * Andre Adrian
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <process.h>
 
#define REMSH_PORT 26001    // Der Remote Shell well known Port
#define BUFSIZE 1400        // etwas kleiner als Ethernet MTU von 1500 Bytes

#define exit_if(expr) \
if(expr) { \
  fprintf(stderr, "exit_if: %s(%d) %s() WinError %d\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  exit(1); \
}

#define return_if(expr) \
if(expr) { \
  fprintf(stderr, "return_if: %s(%d) %s() WinError %d\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  return; \
}

MS-Windows Server Quelltext

/* winremshd.c
 * TCP Remote Shell Server fuer MS-Windows
 * Andre Adrian
 */

#include "winremsh.h"

typedef struct {
  SOCKET fd;
} THREAD_ARG;

void worker(THREAD_ARG *arg)
/* Worker Thread */
{
  SOCKET ausgabefd = arg->fd;
  for (;;) {                  // Endlos Schleife
    int len;
    int rv;
    FILE *eingabefp;
    char buf[BUFSIZE];
    char etx[] = "\003\n";    // Ausgabe Ende String ist ETX und LF
    char *p;

    len = recv(ausgabefd, buf, sizeof(buf)-7, 0);
    return_if(len < 0);
    if (0 == len) {
      break;                  // Verbindung geschlossen von Gegenseite
    }
    buf[len] = 0;             // Null-Byte anhaengen an Kommando
    p = strchr(buf, '\n');    // \n suchen
    if (p != NULL) *p = 0;    // \n durch NUL Zeichen ersetzen
    strcat_s(buf, sizeof(buf), " 2>&1\n");  // stderr auf stdout abbilden

    eingabefp = _popen(buf, "r"); // Shell aufrufen
    return_if(NULL == eingabefp);

    do {
      size_t items;
     
      items = fread(buf, 1, sizeof(buf), eingabefp);  
      rv = send(ausgabefd, buf, (int)items, 0);
      return_if(rv < 0);

    } while(!feof(eingabefp));
    rv = send(ausgabefd, etx, (int)strlen(etx), 0); // Ausgabe Ende String senden
    return_if(rv != strlen(etx));
    _pclose(eingabefp);
  }
  shutdown(ausgabefd, SD_BOTH);    // vermeide FIN_WAIT2 und CLOSE_WAIT TCP States
  closesocket(ausgabefd);
  _endthread();
}

int main(void)
{
  struct sockaddr_in local; // Server IPv4 Adress und Port Struktur
  SOCKET localfd;           // Server Filedeskriptor
  int rv;
  WSADATA wsaData;

  /* MS-Windows spezielle Socket Initialisierung */
  rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
  exit_if(rv != 0);

  localfd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(localfd < 0);

  memset(&local, 0, sizeof(local)); // local mit 0 vollschreiben
  local.sin_family = AF_INET;
  local.sin_port = htons(REMSH_PORT);
  local.sin_addr.s_addr = INADDR_ANY;

  // local Server IPv4 Adresse eintragen
  rv = bind(localfd, (struct sockaddr *) &local, sizeof(local));
  exit_if(rv < 0);

  rv = listen(localfd, 5);  // TCP Server Verbindungs Warteschlange setzen
  exit_if(rv < 0);

  for (;;) {
    SOCKET connectfd;
    THREAD_ARG arg;
   
    connectfd = accept(localfd, NULL, NULL);  // Verbindung entgegennehmen
    if (connectfd < 0) {
      perror("remshd.c accept");
      continue;                         // for Schleife erneut durchlaufen
    }
   
    arg.fd = connectfd;
    rv = (int)_beginthread(worker, 0, &arg);
    exit_if(rv < 0);
  }

  /* MS-Windows spezielle Socket Terminierung */
  WSACleanup();

  return 0;
}

MS-Windows Client Quelltext

/* winremsh.c
 * TCP Remote Shell Client fuer MS-Windows
 * Andre Adrian
 */
 
#include "winremsh.h"

int main(int argc, char *argv[])
{
  struct sockaddr_in remote;    // remote Server IPv4 Adress und Port Struktur
  SOCKET remotefd;              // remote Server Filedeskriptor
  int rv;
  struct hostent *he;

  WSADATA wsaData;

  /* MS-Windows spezielle Socket Initialisierung */
  rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
  exit_if(rv != 0);

  if (argc != 2) {
    fprintf(stderr, "usage: remsh hostname\n");
    exit(1);
  }

  he = gethostbyname(argv[1]);  // hole Server IPv4 Adresse
  exit_if(NULL == he);

  remotefd = socket(AF_INET, SOCK_STREAM, 0); // Socket Filedeskriptor anlegen
  exit_if(remotefd < 0);
 
  memset(&remote, 0, sizeof(remote)); // remote mit 0 fuellen
  remote.sin_family = AF_INET;
  remote.sin_port = htons(REMSH_PORT);
  remote.sin_addr = *((struct in_addr *) he->h_addr);

  // remote Server IPv4 Adresse Verbindung aufbauen
  rv = connect(remotefd, (struct sockaddr *) &remote, sizeof(remote));
  exit_if(rv < 0);

  for(;;) {               // Endlos Schleife
    char buf[BUFSIZE];
    char *p;
  
    printf("> ");                     // Prompt ausgeben
    fgets(buf, sizeof(buf), stdin);   // Kommando holen
    if (0 == strcmp(buf, "QUIT\n")) {
      break;                          // verlasse for() Schleife
    }
    rv = send(remotefd, buf, (int)strlen(buf), 0);  // zum Server senden
    exit_if(rv < 0);
   
    do {
      int len;
     
      len = recv(remotefd, buf, sizeof(buf)-1, 0);  // Antwort holen
      exit_if(len < 0);
      if (0 == len) {
        closesocket(remotefd);        // Gegenseite hat Verbindung beendet
        return 0;
      }
      buf[len] = 0;                   // Null-Byte anhaengen

      p = strchr(buf, '\003');
      if (p != NULL) *p = 0;          // ersetze ETX Zeichen durch NUL Zeichen
      printf("%s", buf);              // Antwort ausgeben
    } while (NULL == p);              // solange kein ETX gefunden
  }
  closesocket(remotefd);

  /* MS-Windows spezielle Socket Terminierung */
  WSACleanup();

  return 0;
}

Programmierstil

Die deutsche Übersetzung von "The International Obfuscated C Code Contest" ist Internationaler C Quelltext Vernebelungs Wettbewerb. Wenn man C lernen will sind vernebelte Quelltexte keine grosse Hilfe. Einige Fallstricke ergeben sich aus der Kombination von Kontroll-Anweisung (control statement) und Variablen-Zuweisung (variable assignment).  In C bindet der Vergleich (==, !=, <, >, ..) stärker als die Zuweisung. Ohne Klammern rund um die Zuweisung wird der Variablen das Ergebnis des Vergleiches und nicht der Returnwert der Funktion zugewiesen! Meistens vernebelt man sich selbst...

Schlechte Lesbarkeit
Gute Lesbarkeit
if ((connectfd = accept(localfd, NULL, NULL)) < 0) {
  perror("remshd.c accept");
  continue;
}
connectfd = accept(localfd, NULL, NULL);
if (connectfd < 0) {
  perror("remshd.c accept");
  continue;
}
exit_if(listen(localfd, 5) < 0); rv = listen(localfd, 5);
exit_if(rv < 0);
while ((len = recv(fd, eingabe, sizeof(eingabe)-7,0)) > 0) {
  // weitere Anweisungen
}
for (;;) {
  len = recv(fd, eingabe, sizeof(eingabe)-7,0);
  if (len <= 0) {
    break;  // for Schleife verlassen
  }
  // weitere Anweisungen
}

Eine weitere Problemquelle ist der Einsatz von zuviel Konstanten und zuwenig sizeof(). Immer wenn möglich sollte die Größe eines Datenobjektes mit sizeof() festgestellt werden. Wenn die Grösse des Datenobjektes geändert wird, dann passt sich sizeof() automatisch an. Das Argument von sizeof() ist eine Typeangabe oder ein Variablenname. Ein Variablenname ist weniger fehleranfällig als eine Typeangabe.
Beim Senden von Strings (Zeichenketten) wird oft strlen() anstelle von sizeof() verwendet.

Stark Fehleranfällig
Wenig Fehleranfällig
char buf[1024];
     
items = fread(buf, 1,
1024, fp);
char buf[1024];
     
items = fread(buf, 1, sizeof(buf), fp);  
struct sockaddr_in local;

bind(localfd, (struct sockaddr *) &local,
  sizeof(
struct sockaddr_in));
struct sockaddr_in local;

bind(localfd, (struct sockaddr *) &local, sizeof(local));
rv = send(ausgabefd, "\003\n", 2, 0);
exit_if(rv != 2);
char etx[] = "\003\n";

rv = send(ausgabefd, etx, strlen(etx), 0);
exit_if(rv != strlen(etx));


Variablen sollten so lokal wie möglich angelegt werden. Dann ist die Variablen Definition nahe bei der Variablen Verwendung. Der Quelltext Leser muß nicht soweit zurückblättern.

Schlechte Lesbarkeit Gute Lesbarkeit
int len;
int rv;
int items;
FILE *fp;
char eingabe[1025];
char ausgabe[1024];

for (;;) {
  len = recv(fd, eingabe, sizeof(eingabe)-1, 0);
  exit_if(len < 0);
  if (0 == len) {    
    break;
  }     
  eingabe[len] = 0;

  fp = popen(eingabe, "r");
  exit_if(NULL == fp);

  do {   
    items = fread(ausgabe, 1, sizeof(ausgabe), fp);  
    rv = send(fd, ausgabe, items, 0);
    exit_if(rv != items);
for (;;) {
  int len;
  int rv;
  FILE *fp;
  char eingabe[1025];

  len = recv(fd, eingabe, sizeof(eingabe)-1, 0);
  exit_if(len < 0);
  if (0 == len) {    
    break;
  }     
  eingabe[len] = 0;

  fp = popen(eingabe, "r");
  exit_if(NULL == fp);

  do {
    int items;
    char ausgabe[1024];
     
    items = fread(ausgabe, 1, sizeof(ausgabe), fp);  
    rv = send(fd, ausgabe, items, 0);
    exit_if(rv != items);

Die vorgestellten Tipps zum Programmierstil machen die Programme ein wenig länger. Vielleicht erzeugt der Compiler auch etwas grössere Programmdateien die etwas langsamer laufen. Die Vorteile eines gut lesbaren Quelltextes wiegen diese Nachteile bei weitem auf. Es sei denn man will den nächsten Vernebelungs Wettbewerb gewinnen...

Literatur

POSIX ist definiert in IEEE Std 1003.1. Diese Definition wurde nach ISO/IEC 9945 übernommen. Die POSIX Standard Dokumente gibt es von IEEE oder online - im Moment kostenfrei nach Registrierung - von The Open Group als Single UNIX Specification.

Von Brian "Beej Jorgensen" Hall gibt es eine gute Einführung in Netzwerk Programmierung mit Sockets unter Beej's Guide to Network Programming.

Vom Lawrence Livermore National Laboratory gibt es ein gutes POSIX Threads Programming Tutorial.

Im Buch "Programmieren von UNIX-Netzen" von Richard Stevens wird im Kapitel 14 die BSD Implementierung von rshd.c besprochen. Der rshd benutzt zwei TCP Verbindungen zwischen Server und Client. Die erste TCP Verbindung für stdin und stdout, die zweite für Signale und stderr.

Das Buch "C von A bis Z" gibt es im Internet online oder in der Buchhandlung.
Jürgen Wolf; C von A bis Z. Das umfassende Handbuch für Linux, Unix und Windows; Galileo Press; 2.Auflage 2006; ISBN 3898426432

Der Klassiker zur Programmiersprache C ist "Programmieren in C" von Kernighan und Ritchie.
Kernighan, Ritchie; Programmieren in C. ANSI C; Hanser Fachbuch; 2.Auflage; 1990; ISBN 3446154973
Kernighan, Ritchie; The C Programming Language, Second Edition, ANSI C; Bell Telephone Labs; 1988