diff --git a/labs/lab04-busybox.pdf b/labs/lab04-busybox.pdf
index c6c3cfa6b614417c61546167598a60fa85fbd352..47307af2425447a8662e54049baed7577c3c4a1b 100644
Binary files a/labs/lab04-busybox.pdf and b/labs/lab04-busybox.pdf differ
diff --git a/labs/lab04-resources/www/cgi-bin/cpu.sh b/labs/lab04-resources/www/cgi-bin/cpu.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e7397b4328eb02cec1b32a0f319e84033f7f5f3a
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/cpu.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+echo "Content-type: text/html"
+echo
+echo "<html>"
+echo "<body>"
+echo "<h1>CPU information</h1>"
+echo "The embedded system is based on the following CPU:<pre>"
+cat /proc/cpuinfo
+echo "</pre>"
+echo "<p><a href=\"/\"><img src=\"/gohome.png\" border=\"0\"></a></p>"
+echo "</body></html>"
diff --git a/labs/lab04-resources/www/cgi-bin/kernel.sh b/labs/lab04-resources/www/cgi-bin/kernel.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0645a144120794707dbca12519469dc412f08b28
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/kernel.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+echo "Content-type: text/html"
+echo
+echo "<html>"
+echo "<body>"
+echo "<h1>Kernel information</h1>"
+echo "The embedded system runs the following kernel:<font color=Red>"
+uname -srm
+echo "</font>"
+echo "<p><a href=\"/\"><img src=\"/gohome.png\" border=\"0\"></a></p>"
+echo "</body></html>"
diff --git a/labs/lab04-resources/www/cgi-bin/list_files.sh b/labs/lab04-resources/www/cgi-bin/list_files.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a403434051d77441e71eabafdde53d8dfaa9667f
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/list_files.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+UPLOAD_DIR="/www/upload/files"
+URL_PATH="/upload/files"
+
+echo "Content-type: text/html"
+echo
+echo "<html>"
+echo "<body>"
+echo "<h1>Uploaded files</h1></p>"
+
+cd $UPLOAD_DIR
+for f in *; do
+    echo "<p><a href=\"$URL_PATH/$f\">$f</a></p>"
+done
+
+echo "</p><p><a href=\"/\"><img src=\"/gohome.png\" border=\"0\"></a></p>"
+echo "</body></html>"
diff --git a/labs/lab04-resources/www/cgi-bin/upload.c b/labs/lab04-resources/www/cgi-bin/upload.c
new file mode 100644
index 0000000000000000000000000000000000000000..247beff66ed24b39d9298d87ebb4e7eaaced45d7
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/upload.c
@@ -0,0 +1,963 @@
+/*
+
+   upload.exe  --  Upload a file to a server using forms.
+
+
+   DESCRIPTION
+
+       This is a CGI program to upload one or more files to a WWW
+       server, using standard HTML forms instead of FTP. It works with
+       Netscape 3.0 and 4.0, and Internet Explorer 4.0.
+
+       See the manpage for more information.
+
+   AUTHOR
+
+       Jeroen C. Kessels
+       Internet Engineer
+       mailto:jeroen@kessels.com       http://www.kessels.com/
+       Tel: +31(0)654 744 702
+
+
+   COPYRIGHT
+
+       Jeroen C. Kessels
+       9 december 2000
+
+
+   VERSION 2.6
+
+*/
+
+
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#ifdef unix
+#include <sys/stat.h>
+#else
+#include <io.h>
+#ifdef _MSC_VER
+#include <direct.h>
+#else
+#include <dir.h>
+#endif
+#endif
+#include <time.h>
+
+
+
+
+#ifdef unix
+#define stricmp         strcasecmp
+#define strnicmp        strncasecmp
+#endif
+
+
+
+#define COPYRIGHT  "<center><font size=6><b>Upload v2.6</b></font><br>&copy; 2000 <a href='http://www.kessels.com/'>Jeroen C. Kessels</a></center>\n<hr>"
+#define NO   0
+#define YES  1
+#define MUST 2
+
+
+
+/* Define DIRSEP, the character that will be used to separate directories. */
+#ifdef unix
+#define DIRSEP "/\\"
+#else
+#define DIRSEP "\\/"
+#endif
+
+
+
+
+/* Configuration parameters from the configuration file. */
+char Root[BUFSIZ];
+char FileMask[BUFSIZ];
+int IgnoreSubdirs;
+int OverWrite;
+char LogFile[BUFSIZ];
+char OkPage[BUFSIZ];
+char OkUrl[BUFSIZ];
+char BadPage[BUFSIZ];
+char BadUrl[BUFSIZ];
+
+char UpFileName[BUFSIZ];
+int Debug;
+
+long FileCount;
+long ByteCount;
+char LastFileName[BUFSIZ];
+
+
+
+
+/* Translate character to lowercase. */
+char clower(char c) {
+  if ((c >= 'A') && (c <= 'Z')) return((c - 'A') + 'a');
+  return(c);
+  }
+
+
+
+
+/* Compare a string with a mask, case-insensitive. If it matches then return
+   YES, otherwise NO. The mask may contain wildcard characters '?' (any
+   character) '*' (any characters). */
+int MatchMask(char *String, char *Mask) {
+  char *m;
+  char *s;
+
+  if (String == NULL) return NO;                /* Just to speed up things. */
+  if (Mask == NULL) return NO;
+  if (strcmp(Mask,"*") == 0) return YES;
+
+  m = Mask;
+  s = String;
+
+  while ((*m != '\0') && (*s != '\0')) {
+    if ((clower(*m) != clower(*s)) && (*m != '?')) {
+      if (*m != '*') return NO;
+      m++;
+      if (*m == '\0') return YES;
+      while (*s != '\0') {
+        if (MatchMask(s,m) == YES) return YES;
+        s++;
+        }
+      return NO;
+      }
+    m++;
+    s++;
+    }
+
+  while (*m == '*') m++;
+  if ((*s == '\0') && (*m == '\0')) return YES;
+  return NO;
+  }
+
+
+
+
+
+/* Translate the URL separator '/' into the directory separator ('/' for
+   Unix, '\\' for DOS), making a proper pathname. */
+void Url2Dir(char *Dir, char *Url) {
+  char *p1;
+  char *p2;
+
+  p1 = Url;
+  p2 = Dir;
+  while (*p1 != '\0') {
+    if (*p1 == '/') {
+        *p2++ = *DIRSEP;
+        p1++;
+      } else {
+        *p2++ = *p1++;
+        }
+    }
+  *p2 = '\0';
+  }
+
+
+
+
+/* Return the last position in a string of any character from
+   collection. The haystack string is scanned from end to begin until a
+   character is found from the needle string. The pointer to the found
+   character is returned, or NULL if not found. This subroutine is
+   designed to find a directory separator in a path, where the directory
+   separator can be '\\' (DOS) or '/' (Unix). */
+char *strrchrs(char *haystack,char *needle) {
+  char *h_here;
+  char *n_here;
+  long h_length;
+
+  if ((haystack == NULL) || (needle == NULL)) return(NULL);
+
+  for (h_length = 0, h_here = haystack; *h_here != '\0'; h_here++, h_length++);
+  while (h_length > 0) {
+    h_here--;
+    h_length--;
+    n_here = needle;
+    while (*n_here != '\0') {
+      if (*n_here == *h_here) return(h_here);
+      n_here++;
+      }
+    }
+
+  return(NULL);
+  }
+
+
+
+
+
+/* Create all directories in the path. The permissions of the directory at
+   From are used for the new directory, leave NULL for default permissions.
+   If IsAdir=NO then the path points to a filename, otherwise it is a
+   directoryname. */
+void CreatePath(char *To, char *From, int IsAdir) {
+#ifdef unix
+  struct stat statbuf;
+#endif
+  char s1[BUFSIZ];
+  char s2[BUFSIZ];
+  char *p1;
+
+  if (To == NULL) return;
+
+  /* If the Path contains subdirectories, then strip the last directory and
+     iterate. */
+  strcpy(s1,To);
+  if (From != NULL) {
+      strcpy(s2,From);
+    } else {
+      *s2 ='\0';
+      }
+  p1 = strrchrs(s1,DIRSEP);
+  if (p1 != NULL) {
+    *p1 = '\0';
+    p1 = strrchrs(s2,DIRSEP);
+    if (p1 == NULL) p1 = s2;
+    *p1 = '\0';
+    CreatePath(s1,s2,YES);
+    }
+
+  /* Create the directory. */
+  if (IsAdir == YES) {
+#ifdef unix
+    if ((From != NULL) && (*From != '\0') && (stat(From,&statbuf) == 0)) {
+        mkdir(To,statbuf.st_mode & 0777);
+      } else {
+        mkdir(To,0755);
+        }
+#else
+    if ((strlen(To) > 2) || (To[1] != ':')) mkdir(To);
+#endif
+    }
+  }
+
+
+
+
+
+/* Show the BadPage, with macro MESSAGE. */
+void ShowBadPage(char *Message) {
+  FILE *Fin;
+  char Line[BUFSIZ];
+  char s1[BUFSIZ];
+  char *p1;
+
+  if (*BadUrl != '\0') {
+    fprintf(stdout,"Location: %s\n\n",BadUrl);
+    return;
+    }
+
+  if (Debug == 0) fprintf(stdout,"Content-type: text/html\n\n");
+
+  Fin = fopen(BadPage,"r");
+  if (Fin == NULL) {
+    fprintf(stdout,"<html>\n<body>\n",Message);
+    if (*BadPage != '\0') {
+      fprintf(stdout,"Error: could not open the BadPage: %s<p>\n",BadPage);
+      fprintf(stdout,"The original error message was:<br>\n");
+      }
+    fprintf(stdout,"%s\n",Message);
+    fprintf(stdout,"</body>\n</html>\n",Message);
+    exit(0);
+    }
+
+  while (fgets(Line,BUFSIZ,Fin) != NULL) {
+    p1 = Line;
+    while (*p1 != '\0') {
+      if (strnicmp(p1,"<insert message>",15) == 0) {
+        *p1 = '\0';
+        sprintf(s1,"%s%s%s",Line,Message,p1 + 16);
+        strcpy(Line,s1);
+        }
+      p1++;
+      }
+    fprintf(stdout,"%s",Line);
+    }
+
+  exit(0);
+  }
+
+
+
+
+/* Write a line to the logging file. */
+void WriteLogLine(char *Line) {
+  FILE *Fout;
+  char s1[BUFSIZ];
+  time_t Now;
+
+  if (*LogFile == '\0') return;
+
+  Fout = fopen(LogFile,"a");
+  if (Fout == NULL) {
+    sprintf(s1,"I could not open the logfile: %s",LogFile);
+    ShowBadPage(s1);
+    }
+  Now = time(NULL);
+  strcpy(s1,ctime(&Now));
+  s1[24] = '\0';
+  fprintf(Fout,"%s %s\n",s1,Line);
+  fclose(Fout);
+  }
+
+
+
+
+/* Show the OkPage, with statistics about the file. */
+void ShowOkPage(void) {
+  FILE *Fin;
+  char Line[BUFSIZ];
+  char s1[BUFSIZ];
+  char *p1;
+
+  if (*OkUrl != '\0') {
+    fprintf(stdout,"Location: %s\n\n",OkUrl);
+    return;
+    }
+
+  Fin = fopen(OkPage,"r");
+  if (Fin == NULL) {
+    sprintf(s1,"I could not open the OkPage: %s",OkPage);
+    ShowBadPage(s1);
+    }
+  if (Debug == 0) fprintf(stdout,"Content-type: text/html\n\n");
+  while (fgets(Line,BUFSIZ,Fin) != NULL) {
+    p1 = Line;
+    while (*p1 != '\0') {
+      if (strnicmp(p1,"<insert filecount>",18) == 0) {
+        *p1 = '\0';
+        sprintf(s1,"%s%lu%s",Line,FileCount,p1 + 18);
+        strcpy(Line,s1);
+        }
+      if (strnicmp(p1,"<insert bytecount>",18) == 0) {
+        *p1 = '\0';
+        sprintf(s1,"%s%lu%s",Line,ByteCount,p1 + 18);
+        strcpy(Line,s1);
+        }
+      if (strnicmp(p1,"<insert lastfilename>",21) == 0) {
+        *p1 = '\0';
+        sprintf(s1,"%s%s%s",Line,LastFileName,p1 + 21);
+        strcpy(Line,s1);
+        }
+      p1++;
+      }
+    fprintf(stdout,"%s",Line);
+    }
+
+  exit(0);
+  }
+
+
+
+
+/* Load the proper segment from the configuration file. */
+void LoadConfig(char *ProgramPath, char *ConfigID) {
+  FILE *Fin;
+  char Path[BUFSIZ];
+  char Line[BUFSIZ];
+  char Name[BUFSIZ];
+  char Value[BUFSIZ];
+  int Accept;
+  int NewDebug;
+  char *p1;
+  char *p2;
+
+  strcpy(Path,ProgramPath);
+  p1 = strrchrs(Path,"./\\");
+  if (p1 != NULL) {
+      strcpy(p1,".cfg");
+    } else {
+      strcat(Path,".cfg");
+      }
+  Fin = fopen(Path,"rt");
+  if (Fin == NULL) {
+    p1 = getenv("PATH");
+    while ((p1 != NULL) && (*p1 != '\0')) {
+      p2 = Value;
+      while ((*p1 != '\0') && (*p1 != ';')) *p2++ = *p1++;
+      *p2 = '\0';
+      if (*p1 == ';') p1++;
+      sprintf(Name,"%s%cupload.cfg",Value,*DIRSEP);
+      Fin = fopen(Name,"rt");
+      if (Fin != NULL) break;
+      }
+    }
+  if (Fin == NULL) ShowBadPage("I could not open the configuration file.");
+
+  if (Debug > 0) fprintf(stdout,"<center>\n");
+  Accept = NO;
+  NewDebug = Debug;
+  while (fgets(Line,BUFSIZ,Fin) != NULL) {
+    *Name = *Value = '\0';
+    if (sscanf(Line,"%[^ \t=]%*[ \t=]%[^\n\r]",Name,Value) != 2) {
+      sscanf(Line,"%*[ \t]%[^ \t=]%*[ \t=]%[^\n\r]",Name,Value);
+      }
+    if (stricmp(Name,"Config") == 0) {
+      if (Accept == YES) {
+        if (Debug > 0) fprintf(stdout,"</table>\n</center>\n<p>\n");
+        if ((Debug == 0) && (NewDebug > 0)) {
+          fprintf(stdout,"Content-type: text/html\n\n");
+          fprintf(stdout,"<html>\n<head>\n");
+          fprintf(stdout,"<meta http-equiv=expires content=\"0\">\n");
+          fprintf(stdout,"</head>\n<body>\n");
+          fprintf(stdout,"%s\n",COPYRIGHT);
+          }
+        Debug = NewDebug;
+        return;
+        }
+      if ((ConfigID == NULL) || (*ConfigID == '\0')) {
+          Accept = YES;
+        } else {
+          if (stricmp(ConfigID,Value) == 0) Accept = YES;
+          }
+      if (Debug > 0) {
+        if (Accept == YES) {
+          fprintf(stdout,"<h2>Loading configuration</h2>\"%s\"\n",Value);
+          fprintf(stdout,"<table border=1>\n");
+          }
+        }
+      continue;
+      }
+    if (Accept != NO) {
+      if (Debug > 0) {
+        fprintf(stdout,"<tr><td>%s</td><td>%s</td></tr>\n",Name,Value);
+        }
+      if (stricmp(Name,"Debug") == 0) NewDebug = atoi(Value);
+      if (stricmp(Name,"Root") == 0) {
+        Url2Dir(Root,Value);
+        p1 = strchr(Root,'\0');
+        if (p1 != Root) p1--;
+        if ((p1 != Root) && (strchr(DIRSEP,*p1) == NULL)) {
+          p1++;
+          *p1++ = *DIRSEP;
+          *p1 = '\0';
+          }
+        }
+      if (stricmp(Name,"FileMask") == 0) strcpy(FileMask,Value);
+      if (stricmp(Name,"LogFile") == 0) Url2Dir(LogFile,Value);
+      if (stricmp(Name,"OkPage") == 0) Url2Dir(OkPage,Value);
+      if (stricmp(Name,"OkUrl") == 0) strcpy(OkUrl,Value);
+      if (stricmp(Name,"BadPage") == 0) Url2Dir(BadPage,Value);
+      if (stricmp(Name,"BadUrl") == 0) strcpy(BadUrl,Value);
+      if (stricmp(Name,"OverWrite") == 0) {
+        if (stricmp(Value,"no") == 0) OverWrite = NO;
+        if (stricmp(Value,"yes") == 0) OverWrite = YES;
+        if (stricmp(Value,"must") == 0) OverWrite = MUST;
+        }
+      if (stricmp(Name,"IgnoreSubdirs") == 0) {
+        if (stricmp(Value,"no") == 0) IgnoreSubdirs = NO;
+        if (stricmp(Value,"yes") == 0) IgnoreSubdirs = YES;
+        }
+      }
+    }
+  if (Debug > 0) {
+    if (Accept == YES) {
+        fprintf(stdout,"</table>\n</center>\n<p>\n");
+      } else {
+        fprintf(stdout,
+          "</center>\nConfiguration \"%s\" not found, using defaults instead.<p>\n",
+          ConfigID);
+        }
+    }
+  if ((Debug == 0) && (NewDebug > 0)) {
+    fprintf(stdout,"Content-type: text/html\n\n");
+    fprintf(stdout,"<html>\n<head>\n");
+    fprintf(stdout,"<meta http-equiv=expires content=\"0\">\n");
+    fprintf(stdout,"</head>\n<body>\n");
+    fprintf(stdout,"%s\n",COPYRIGHT);
+    }
+  Debug = NewDebug;
+  }
+
+
+
+
+
+/* Skip a line in the input stream. */
+void SkipLine(char **Input,            /* Pointer into the incoming stream. */
+    long *InputLength) {              /* Bytes left in the incoming stream. */
+
+  while ((**Input != '\0') && (**Input != '\r') && (**Input != '\n')) {
+    *Input = *Input + 1;
+    *InputLength = *InputLength - 1;
+    }
+  if (**Input == '\r') {
+    *Input = *Input + 1;
+    *InputLength = *InputLength - 1;
+    }
+  if (**Input == '\n') {
+    *Input = *Input + 1;
+    *InputLength = *InputLength - 1;
+    }
+  }
+
+
+
+
+
+/* Accept a single segment from the incoming mime stream. Each field in the
+   form will generate a mime segment. Return a pointer to the beginning of
+   the Boundary, or NULL if the stream is exhausted. */
+void AcceptSegment(char **Input,            /* Pointer into the incoming stream. */
+    long *InputLength,                /* Bytes left in the incoming stream. */
+    char *Boundary,             /* Character string that delimits segments. */
+    char *ProgramPath) {
+  char FieldName[BUFSIZ];            /* Name of the variable from the form. */
+  char FileName[BUFSIZ];          /* The filename, as selected by the user. */
+  char *ContentStart;                       /* Pointer to the file content. */
+  char *ContentEnd;
+  char ContentAsString[BUFSIZ];
+  long ContentLength;                          /* Bytecount of the content. */
+  char Key1[BUFSIZ];
+  char Key2[BUFSIZ];
+  char Path[BUFSIZ];
+  FILE *Fout;
+  long Result;
+  char s1[BUFSIZ];
+  char *p1;
+  int i;
+
+  /* The input stream should begin with a Boundary line. Error-exit if not
+     found. */
+  if (strncmp(*Input,Boundary,strlen(Boundary)) != 0) {
+    sprintf(s1,"Missing boundary in input.");
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+
+  /* Skip the Boundary line. */
+  *Input = *Input + strlen(Boundary);
+  *InputLength = *InputLength - strlen(Boundary);
+  SkipLine(Input,InputLength);
+
+  /* Return NULL if the stream is exhausted (no more segments). */
+  if ((**Input == '\0') || (strncmp(*Input,"--",2) == 0)) {
+    *InputLength = 0;
+    return;
+    }
+
+  /* The first line of a segment must be a "Content-Disposition" line. It
+     contains the fieldname, and optionally the original filename. Error-exit
+     if the line is not recognised. */
+  if (sscanf(*Input,"%[^:]: %[^;]; name=\"%[^\"]\"; filename=\"%[^\"]\"",
+      Key1,Key2,FieldName,FileName) != 4) {
+    *FileName = '\0';
+    if (sscanf(*Input,"%[^:]: %[^;]; name=\"%[^\"]\"",Key1,Key2,
+        FieldName) != 3) {
+      *FieldName = '\0';
+      if (sscanf(*Input,"%[^:]: %[^;]; filename=\"%[^\"]\"",Key1,Key2,
+          FileName) != 3) {
+        sscanf(*Input,"%[^\r\n]",Key1);
+        sprintf(s1,"Disposition line not recognised: %s",Key1);
+        WriteLogLine(s1);
+        ShowBadPage(s1);
+        }
+      }
+    }
+  if (stricmp(Key1,"content-disposition") != 0) {
+    sprintf(s1,"\"Content-Disposition\" expected, but I got \"%s\" instead.",
+      Key1);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+  if (stricmp(Key2,"form-data") != 0) {
+    sprintf(s1,"\"form-data\" expected, but I got \"%s\" instead.",Key2);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+
+  /* Skip the Disposition line and one or more mime lines, until an empty
+     line is found. */
+  SkipLine(Input,InputLength);
+  while ((**Input != '\r') && (**Input != '\n')) SkipLine(Input,InputLength);
+  SkipLine(Input,InputLength);
+
+  /* The following data in the stream is binary. The Boundary string is the
+     end of the data. There may be a CRLF just before the Boundary, which
+     must be stripped. */
+  ContentStart = *Input;
+  ContentLength = 0;
+  while ((*InputLength > 0) && (memcmp(*Input,Boundary,strlen(Boundary)) != 0)) {
+    *Input = *Input + 1;
+    *InputLength = *InputLength - 1;
+    ContentLength = ContentLength + 1;
+    }
+  ContentEnd = *Input - 1;
+  if ((ContentLength > 0) && (*ContentEnd == '\n')) {
+    ContentEnd--;
+    ContentLength = ContentLength - 1;
+    }
+  if ((ContentLength > 0) && (*ContentEnd == '\r')) {
+    ContentEnd--;
+    ContentLength = ContentLength - 1;
+    }
+  i = BUFSIZ - 1;
+  if (ContentLength < i) i = ContentLength;
+  strncpy(ContentAsString,ContentStart,i);
+  ContentAsString[i] = '\0';
+
+  if (ContentEnd) {};                                 /* Keep lint happy... */
+
+  /* Show debugging information. */
+  if (Debug > 0) {
+    fprintf(stdout,"<tr>\n");
+    fprintf(stdout,"  <td valign=top rowspan=2>");
+    if (*FieldName != '\0') fprintf(stdout,"%s",FieldName);
+    fprintf(stdout,"</td>\n");
+    fprintf(stdout,"  <td valign=top>");
+    if (*FileName != '\0') {
+        fprintf(stdout,"%s",FileName);
+      } else {
+        fprintf(stdout,"%s",ContentAsString);
+        }
+    fprintf(stdout,"</td>\n");
+    fprintf(stdout,"  <td valign=top>%ld</td>\n",ContentLength);
+    fprintf(stdout,"  </tr>\n");
+    }
+
+  /* If this field is the "Config" field, then load the configuration and
+     leave. */
+  if ((stricmp(FieldName,"Config") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    if (Debug > 0) fprintf(stdout,"<tr><td colspan=2>");
+    LoadConfig(ProgramPath,ContentAsString);
+    if (Debug > 0) fprintf(stdout,"</td></tr>\n");
+    return;
+    }
+
+  /* If this field is the "FileName" field, then store it and leave. */
+  if ((stricmp(FieldName,"FileName") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    strcpy(UpFileName,ContentAsString);
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>New filename stored.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* If this field is the "OkPage" field, then store it and leave. */
+  if ((stricmp(FieldName,"OkPage") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    Url2Dir(OkPage,ContentAsString);
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>New OkPage stored.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* If this field is the "OkUrl" field, then store it and leave. */
+  if ((stricmp(FieldName,"OkUrl") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    strcpy(OkUrl,ContentAsString);
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>New OkUrl stored.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* If this field is the "BadPage" field, then store it and leave. */
+  if ((stricmp(FieldName,"BadPage") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    Url2Dir(BadPage,ContentAsString);
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>New BadPage stored.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* If this field is the "BadUrl" field, then store it and leave. */
+  if ((stricmp(FieldName,"BadUrl") == 0) &&
+      (*FileName == '\0') &&
+      (*ContentStart != '\0')) {
+    strcpy(BadUrl,ContentAsString);
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>New BadUrl stored.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* Do nothing if this is not a file, but some other kind of field. */
+  if ((FileName == NULL) || (*FileName == '\0')) {
+    if (Debug > 0) {
+      fprintf(stdout,"<tr><td colspan=2>FieldName not recognised.</td></tr>\n");
+      }
+    return;
+    }
+
+  /* Determine the filename to store the file. If the UpFileName field was
+     defined then use it, otherwise use the name of the incoming file. The
+     ROOT is always prepended. */
+  if (*UpFileName == '\0') strcpy(UpFileName,FileName);
+  if (IgnoreSubdirs == YES) {
+    p1 = strrchrs(UpFileName,DIRSEP);
+    if (p1 != NULL) {
+      p1++;
+      strcpy(UpFileName,p1);
+      }
+    }
+  sprintf(Path,"%s%s",Root,UpFileName);
+
+  /* Test if the filename matches the mask. */
+  if (MatchMask(Path,FileMask) == NO) {
+    sprintf(s1,
+      "The file is rejected because it does not match the mask.<br>Filename: %s<br>Mask: %s",
+      Path,FileMask);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+
+  /* Test if the file already exists. */
+  if (OverWrite != YES) {
+    Fout = fopen(Path,"r");
+    if ((OverWrite == NO) && (Fout != NULL)) {
+      fclose(Fout);
+      sprintf(s1,
+        "The file could not be uploaded because it already exists.<br>Filename: %s",
+        Path);
+      WriteLogLine(s1);
+      ShowBadPage(s1);
+      }
+    if ((OverWrite == MUST) && (Fout == NULL)) {
+      sprintf(s1,
+        "The file could not be uploaded because it does not exist already.<br>Filename: %s",
+        Path);
+      WriteLogLine(s1);
+      ShowBadPage(s1);
+      }
+    if (Fout != NULL) fclose(Fout);
+    }
+
+  /* If needed then create directories for the file. */
+  CreatePath(Path,NULL,NO);
+
+  /* Open the file for writing. */
+#ifndef unix
+  Fout = fopen(Path,"w");
+#else
+  Fout = fopen(Path,"wb");
+#endif
+  if (Fout == NULL) {
+    sprintf(s1,
+      "The file could not be uploaded because write permission is denied.<br>Filename: %s",
+      Path);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+#ifndef unix
+  setmode(fileno(Fout),O_BINARY);
+#endif
+
+  /* Write the file to disk. */
+  Result = fwrite(ContentStart,1,ContentLength,Fout);
+  fclose(Fout);
+
+  /* If the wrong number of bytes were written to disk, then show an error
+     message. */
+  if (Result != ContentLength) {
+    sprintf(s1,
+      "The wrong number of bytes were written.<br>Written: %ld<br>Bytes in input: %ld",
+      Result,ContentLength);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    }
+
+  /* Show debugging information. */
+  if (Debug > 0) {
+    fprintf(stdout,"<tr>\n");
+    fprintf(stdout,"  <td colspan=2>File written: %s</td>\n",Path);
+    fprintf(stdout,"  </tr>\n");
+    }
+
+  FileCount = FileCount + 1;
+  ByteCount = ByteCount + ContentLength;
+  strcpy(LastFileName,UpFileName);
+
+  sprintf(s1,"File uploaded succesfully: %s (%lu bytes)",
+    UpFileName,ContentLength);
+  WriteLogLine(s1);
+
+  /* Clear the filename. */
+  *UpFileName = '\0';
+
+  return;
+  }
+
+
+
+
+
+int main(int argc, char *argv[], char *environment[]) {
+  char *Method;           /* Pointer to REQUEST_METHOD environment variable. */
+  char *ContentLength;    /* Pointer to CONTENT_LENGTH environment variable. */
+  char *ContentType;        /* Pointer to CONTENT_TYPE environment variable. */
+  long InCount;                    /* The supposed number of incoming bytes. */
+  long RealCount;                        /* Actual number of incoming bytes. */
+  char *Content;                           /* Copy in memory of the Content. */
+  char *Here;                                  /* Position into the Content. */
+  char Boundary[BUFSIZ];            /* The boundary string between segments. */
+
+  char Char;
+  long MoreCount;
+  char *p1;
+  char s1[BUFSIZ];
+
+  if (argc > 1) {
+    fprintf(stdout,"Error: %s is a cgi program, and should not be ",argv[0]);
+    fprintf(stdout,"started from the command line.\n");
+    exit(1);
+    }
+
+  *Root = '\0';                                          /* Setup defaults. */
+  strcpy(FileMask,"*");
+  IgnoreSubdirs = YES;
+  OverWrite = YES;
+  *LogFile = '\0';
+  *OkPage = '\0';
+  *OkUrl = '\0';
+  *BadPage = '\0';
+  *BadUrl = '\0';
+  Debug = 0;
+  *UpFileName = '\0';
+  FileCount = 0;
+  ByteCount = 0;
+  *LastFileName = '\0';
+
+  if (environment) {};                                /* Keep lint() happy. */
+
+  if (Debug > 0) {
+    fprintf(stdout,"Content-type: text/html\n\n");
+    fprintf(stdout,"<html>\n<head>\n");
+    fprintf(stdout,"<meta http-equiv=expires content=\"0\">\n");
+    fprintf(stdout,"</head>\n<body>\n");
+    fprintf(stdout,"%s\n",COPYRIGHT);
+    }
+
+  LoadConfig(argv[0],"");                    /* Load default configuration. */
+
+  /* Test if the program was started by a METHOD=POST form. */
+  Method = getenv("REQUEST_METHOD");
+  if ((Method == NULL) || (*Method == '\0') || (stricmp(Method,"post") != 0)) {
+    ShowBadPage("Sorry, this program only supports METHOD=POST.");
+    }
+  if (Debug > 0) fprintf(stdout,"<center><h2>Loading input</h2></center>\n",Method);
+  if (Debug > 0) fprintf(stdout,"Method = %s<br>\n",Method);
+
+  /* Test if the program was started with ENCTYPE="multipart/form-data". */
+  ContentType = getenv("CONTENT_TYPE");
+  if ((ContentType == NULL) ||
+      (strnicmp(ContentType,"multipart/form-data; boundary=",30) != 0)) {
+    ShowBadPage("Sorry, this program only supports ENCTYPE=\"multipart/form-data\".");
+    }
+  if (Debug > 0) fprintf(stdout,"Enctype = %s<br>\n",ContentType);
+
+  /* Determine the Boundary, the string that separates the segments in the
+     stream. The boundary is available from the CONTENT_TYPE environment
+     variable. */
+  Here = strchr(ContentType,'=') + 1;
+  sprintf(Boundary,"--%s",Here);
+  if (Debug > 0) fprintf(stdout,"Boundary = %s<br>\n",Boundary);
+
+  /* Get the total number of bytes in the input stream from the
+     CONTENT_LENGTH environment variable. */
+  ContentLength = getenv("CONTENT_LENGTH");
+  if (ContentLength == NULL) {
+    WriteLogLine("Error: no CONTENT_LENGTH found.");
+    ShowBadPage("Error: no CONTENT_LENGTH found.");
+    }
+  InCount = atol(ContentLength);
+  if (InCount == 0) {
+    WriteLogLine("Error: CONTENT_LENGTH is zero.");
+    ShowBadPage("Error: CONTENT_LENGTH is zero.");
+    }
+  if (Debug > 0) fprintf(stdout,"Content_Length = %d<br>\n",InCount);
+
+  /* Allocate sufficient memory for the incoming data. */
+  Content = (char *)malloc(InCount + 1);
+  if (Content == NULL) {
+    WriteLogLine("Error: malloc() returned NULL.");
+    ShowBadPage("Error: malloc() returned NULL.");
+    }
+
+  /* Load the data from standard input into memory. */
+#ifndef unix
+  setmode(fileno(stdin),O_BINARY);      /* Make sure the input is binary... */
+#endif
+  p1 = Content;
+  RealCount = 0;
+  /* For some reason fread() of Borland C 4.52 barfs if the bytecount is
+     bigger than 2.5Mb, so I have to do it like this. */
+  while (fread(p1++,1,1,stdin) == 1) {
+    RealCount++;
+    if (RealCount >= InCount) break;
+    }
+  *p1 = '\0';
+  /* Ignore any extra caracters. We have to read them, or the server
+     will give an error message. */
+  if (RealCount < InCount) {
+    MoreCount = InCount - RealCount;
+    while ((MoreCount > 0) && (fread(&Char,1,1,stdin) == 1)) MoreCount--;
+    }
+  if (RealCount != InCount) {
+    free(Content);
+    sprintf(s1,
+      "Error: The number of bytes received (%ld) is not what the CONTENT_LENGTH environment variable says it sould be (%ld).",
+      RealCount + MoreCount, InCount);
+    WriteLogLine(s1);
+    ShowBadPage(s1);
+    return(0);
+    }
+  if (Debug > 0) fprintf(stdout,"Input succesfully loaded into memory.<br>\n");
+
+  /* Handle all segments in the incoming data, that has been stored in
+     memory. */
+  Here = Content;
+  if (Debug > 0) {
+    fprintf(stdout,"<p>\n<center>\n<h2>Parsing input</h2>\n");
+    fprintf(stdout,"<table border=1>\n");
+    fprintf(stdout,"<tr>\n");
+    fprintf(stdout,"  <th>Fieldname</th>\n");
+    fprintf(stdout,"  <th>Contents</th>\n");
+    fprintf(stdout,"  <th>Size</th>\n");
+    fprintf(stdout,"  </tr>\n");
+    }
+  while (RealCount > 0) AcceptSegment(&Here,&RealCount,Boundary,argv[0]);
+  if (Debug > 0) fprintf(stdout,"</table>\n</center>\n");
+
+  /* Cleanup. */
+  free(Content);
+
+  if (*LastFileName == '\0') ShowBadPage("You have not specified a file, so nothing was uploaded.");
+
+  /* Display the OkPage. */
+  if (Debug > 0) {
+    fprintf(stdout,"<p><center><h2>Finished</h2></center>\n");
+    }
+  ShowOkPage();
+  return(0);
+  }
+
+
+/*
+Ideeen:
+
+- Multiple filemasks.
+- Maximum directory size.
+- Maximum and Minimum file size.
+- Result-macros as parameters to OkUrl.
+
+*/
\ No newline at end of file
diff --git a/labs/lab04-resources/www/cgi-bin/upload.cfg b/labs/lab04-resources/www/cgi-bin/upload.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..f0df701442322d4c887e2051e000e3d1988bcc27
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/upload.cfg
@@ -0,0 +1,9 @@
+Config          = Default
+  Root          = /www/upload/files
+  FileMask      = *
+  IgnoreSubdirs = YES
+  Overwrite     = YES
+  LogFile       = /www/upload/logs/upload.log
+  OkPage        = /www/upload/OkPage.html
+  BadPage       = /www/upload/BadPage.html
+  Debug         = 0
diff --git a/labs/lab04-resources/www/cgi-bin/uptime.sh b/labs/lab04-resources/www/cgi-bin/uptime.sh
new file mode 100755
index 0000000000000000000000000000000000000000..dc7f83fe278767f5ead73a36f8ecaec2cd98d967
--- /dev/null
+++ b/labs/lab04-resources/www/cgi-bin/uptime.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+echo "Content-type: text/html"
+echo
+echo "<html>"
+echo "<meta http-equiv=\"refresh\" content=\"5\">"
+echo "<body>"
+echo "<h1>System uptime</h1>"
+echo "System time and uptime since last boot: <font color=Red>"
+echo `uptime | awk -F"," '{print $1}'`
+echo "</font>"
+echo "<p><a href=\"/\"><img src=\"/gohome.png\" border=\"0\"></a></p>"
+echo "</body></html>"
diff --git a/labs/lab04-resources/www/gohome.png b/labs/lab04-resources/www/gohome.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae0520383f1308a0fbbbb307691965368cd8dcbe
Binary files /dev/null and b/labs/lab04-resources/www/gohome.png differ
diff --git a/labs/lab04-resources/www/index.html b/labs/lab04-resources/www/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..3a40470dd1966a4e7aa130dcca7253eec69b40bb
--- /dev/null
+++ b/labs/lab04-resources/www/index.html
@@ -0,0 +1,20 @@
+<html>
+    <head>
+    <title>Tiny Web Server</title>
+    </head>
+    <body>
+        <h1>Tiny Web Server</h1>
+        <h3>Available pages</h1>
+        <ul>
+            <li><a href="/cgi-bin/uptime.sh">Uptime</a></li>
+            <li><a href="/cgi-bin/kernel.sh">Kernel info</a></li>
+            <li><a href="/cgi-bin/cpu.sh">CPU info</a></li>
+            <li><a href="/cgi-bin/list_files.sh">Uploaded files</a></li>
+        </ul>
+        <h3>Upload a file</h3>
+        <form action="/cgi-bin/upload" enctype="multipart/form-data" method=post>
+            <input type=file name="File">
+            <input type=submit value="Upload">
+        </form>
+    </body>
+</html>
diff --git a/labs/lab04-resources/www/upload/BadPage.html b/labs/lab04-resources/www/upload/BadPage.html
new file mode 100644
index 0000000000000000000000000000000000000000..7f6a292a458bcf08144cc2c6501fd1efa55c073f
--- /dev/null
+++ b/labs/lab04-resources/www/upload/BadPage.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+<h1>Upload failed!</h1>
+<hr>
+
+<p><insert MESSAGE></p>
+
+<p><a href="/"><img src="/gohome.png" border="0"></a></p>
+</body>
+</html>
diff --git a/labs/lab04-resources/www/upload/OkPage.html b/labs/lab04-resources/www/upload/OkPage.html
new file mode 100644
index 0000000000000000000000000000000000000000..b845121d1ebfc870ce069fd94bdc69d69ff4b023
--- /dev/null
+++ b/labs/lab04-resources/www/upload/OkPage.html
@@ -0,0 +1,12 @@
+<html>
+<body>
+<h1>Upload successful!</h1>
+
+<p>Uploaded file: <insert lastfilename><br>
+Number of uploaded bytes: <insert bytecount>
+</p>
+
+<p><a href="/"><img src="/gohome.png" border="0"></a></p>
+
+</body>
+</html>
diff --git a/labs/lab04-resources/www/upload/files/Embedded_Linux_Primer.jpg b/labs/lab04-resources/www/upload/files/Embedded_Linux_Primer.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..93ab27dbd2791c64a591ae38f81111f353291351
Binary files /dev/null and b/labs/lab04-resources/www/upload/files/Embedded_Linux_Primer.jpg differ
diff --git a/labs/lab04-resources/www/upload/files/linux_distribs.jpg b/labs/lab04-resources/www/upload/files/linux_distribs.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f9af90f65a024a99ba95e435403da2b1200d1362
Binary files /dev/null and b/labs/lab04-resources/www/upload/files/linux_distribs.jpg differ
diff --git a/labs/lab04-resources/www/upload/files/preinternet_chat_rooms.jpg b/labs/lab04-resources/www/upload/files/preinternet_chat_rooms.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1aeedc3c9b5571b0564422bb76259f880cc1e370
Binary files /dev/null and b/labs/lab04-resources/www/upload/files/preinternet_chat_rooms.jpg differ
diff --git a/labs/lab04-resources/www/upload/files/sudo.png b/labs/lab04-resources/www/upload/files/sudo.png
new file mode 100644
index 0000000000000000000000000000000000000000..d148872fb01ff8d05e116a90fdca954ecbc72760
Binary files /dev/null and b/labs/lab04-resources/www/upload/files/sudo.png differ
diff --git a/labs/lab04-resources/www/upload/files/upgrading.png b/labs/lab04-resources/www/upload/files/upgrading.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf9210fd2f8ea8d5995b5bd0c2e1c1a971a5074f
Binary files /dev/null and b/labs/lab04-resources/www/upload/files/upgrading.png differ
diff --git a/labs/lab04-resources/www/upload/logs/upload.log b/labs/lab04-resources/www/upload/logs/upload.log
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391