CGI Scripting and Perl

In this activity, we’ll be experimenting with the cgi-bin and writing web scripts in BASH and Perl.

Environment Variables

Apache provides a large number of variables about the request and server into a CGI script using environment variables. The HTTP Request’s GET variables are also provided in the environment. Each language we consider will have a slightly different method for accessing them:

  • C: getenv("VARIABLE_NAME") returns the variable’s value as a string
  • BASH: variables are accessible using $ as $VARIABLE_NAME
  • Perl: variables are set in the %ENV hash and can be accessed as $ENV{VARIABLE_NAME}

Review the example.pl Perl script and example.sh BASH script in your Docker container’s cgi-bin.

Open up each file through Apache, by navigating to them:

http://localhost:8080/cgi-bin/example.pl and
http://localhost:8080/cgi-bin/example.sh

Notice what variables are provided to the script.

Question: Should we present all of these variables to the output on a production system?

Question: How can these variables help us to tailor our responses to a particular user?

Writing Perl

Now let’s experiment with writing some Perl.

Creating a Form

We will first create an HTML page that prompts the user for some information. You are welcome to use the HTML below or create your own.

<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
  <head>
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="author" content="your name">
    <meta name="description" content="include some description about your page">  

    <title>Perl Activity</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"  integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"  crossorigin="anonymous"> 
  </head>
  <body>
    <main class="container">
      <h1>Perl Activity</h1>
      <form method="GET" action="/cgi-bin/perlactivity.pl">
        <div class="mb-3">
          <label for="firstname" class="form-label">First Name</label>
          <input type="text" class="form-control" id="firstname" name="firstname">
        </div>
        <div class="mb-3">
          <label for="lastname" class="form-label">Last Name</label>
          <input type="text" class="form-control" id="lastname" name="lastname">
        </div>
        <div class="mb-3">
          <label for="food" class="form-label">Guess</label>
          <input type="text" class="form-control" id="guess" name="guess" placeholder="Guess our secret word!">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
      </form>
      <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
  </body>
</html>

Save the above HTML into a file, perlactivity.html in your www directory. We will use this file to send data to our Perl script in cgi-bin.

Note that we are not saving the html file above directly in the cgi-bin with our Perl scripts.

Try saving the html file into the cgi-bin directory and accessing it through Apache.

Question: Does Apache serve the file as expected? What happens?

Review the Apache log file in your Docker container. What does Apache report.

This file provides form input for the user to enter their first and last name as well as their favorite food. When the user submits the form, it sends an HTTP GET request to a Perl script in our cgi-bin.

Question: in this GET request, how are the data passed to the backend? Is this a security vulnerability?

Perl Script

Save the following code as a new file perlactivity.pl inside your cgi-bin directory. This file will respond to the user’s form submission. We will add additional logic as we go.

#!/usr/bin/env perl
# Use the CGI module
use CGI;

# Create a new CGI object
my $q = CGI->new;

# Access the firstname GET parameter
my $firstname = $q->param('firstname');

# Print out the HTTP header
# What happens if we leave this out?
# print "Content-Type: text/html\n\n";

# Print out the initial HTML
print "<!DOCTYPE html>\n<html><head><title>Our CS4640 Perl Wordle Game</title></head>";
print "<body><h1>CS4640 Perl Wordle</h1>";

# Put additional code here


# Close out the body
print "</body></html>";

Next, we are ready to test our Perl script. It should print out the heading in the <h1> above. Open your perlactivity.html through Apache, fill out the form, and submit.

It looks like we have another Internal Server Error. Review the Apache log in Docker to determine the cause.

The Apache logs in Docker will be very helpful moving forward. If you are using Docker Desktop, open the Dev Environments tab, then click on the web container listed under the course’s environment. The Logs tab should be open (by default) and you will see the Apache logs there. If you ran docker compose up on the command line, then the Apache logs are printed to standard output in that terminal.

By default, any server-side errors will be printed in the error log file displayed here. On a production system, these logs are typically stored in a file: /var/log/apache2/error.log.

Additionally, Docker is also showing access logs in the same output stream. Access logs have the following format:

172.18.0.1 - - [18/Feb/2025:15:01:27 -0500] "GET /activities/perlactivity.html HTTP/1.1" 200 827 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" 

Access logs are helpful for understanding traffic to our site and any potential attacks. We can see the IP address of the incoming request, the timestamp, the HTTP request, and the HTTP response code, among other data points for this access. Some attacks target systems looking for vulnerable web applications; if our system does not have that code, Apache will return a 404. On a production system, access logs are typically stored in /var/log/apache2/access.log.

Our error indicates that the server does not have access to execute our Perl script. For any script in the cgi-bin directory, Apache must have execute permissions so that it can fork and exec the script. Additionally, our Docker container follows Linux file permissions, so we will need to allow the Apache user to execute the script.

Opening a Terminal in Docker

We will need to run a command in our Docker environment to continue. The process may be slightly different depending on how you opened Docker.

  1. If you are using Docker Desktop, open the Dev Environments tab, then click on the web container listed under the course’s environment. The Logs tab should be open and you will see the Apache logs. Open the Exec tab, which will display a prompt as follows:
  #
  1. If you are using Docker on the command line, open a new terminal window and connect to the Docker instance as:
  docker exec -it web_2025 bash

Then, you will be presented with a bash terminal.

Open the web_2025 Docker container’s terminal. Our cgi-bin directory is mapped to /usr/lib/cgi-bin in Docker. Change directory to the cgi-bin and allow users to execute our new Perl script:

cd /usr/lib/cgi-bin
chmod a+x perlactivity.pl

Now that Apache can execute our Perl script, return to the browser and reload the page. Note what happens next! Review the example.pl file to fix your Perl script (hint: look for something commented out in your script). Once you have fixed all errors, you should see the following when submitting our form:

CS4640 Perl Wordle

Next, add the following function to the end of your Perl script (just before the </body> tag is printed):

sub printGreeting {
  # Note: String interpolation! The variables are directly in the string
  my $msg = "<p>Hello $firstname $lastname!</p>";
  print $msg;
}

Update your Perl to call this function so that it correctly prints with the first and last name variables we passed in through the HTTP GET request. Hint: you may need to create additional variables, similar to our $firstname variable.

Last, let’s make this fun. We’ve written most of the code for you so that you can take a look at Perl. Add the following code to the end of your Perl script and resolve the remaining todo items:

# Get a random word from an array of words
my @wordlist = ("computer", "science", "wordle", "perl", "network", "game");
my $word = $q->param('word');
if (!$word) {
  $word = $wordlist[rand @wordlist];
}
# Read out the "guess" form input from GET
my $guess = $q->param('guess');

# Check if the guess is correct
my $success = $guess eq $word; # string equality with eq

# Function to check the guess
sub checkGuess {
  # Turn the strings into arrays of characters
  my @gchar = split(undef, $guess); 
  my @wchar = split(undef, $word); 

  # todo: print out the guess below (inside an <h2>) tag:
  
  
  # Print any characters that the user got in the right position
  print "<h3>Characters in the correct position</h3>\n<p>";
  my $any = 0;
  # Loop over each character in the guess array
  while (my ($i, $gc) = each @gchar) {
    # Check equality with the character in the word array
    if ($wchar[$i] eq $gc) {
      print "$gc correct at index $i<br>\n";
      $any += 1;
    } 
  }
  # If no letters correct, print
  if ($any == 0) {
    print("No characters in correct position\n");
  }

  print("</p>\n\n");
  print("<h3>Characters in any position of the word</h3>\n<p>");

  # Get unique array of characters in guess
  my @uchar = do { my %seen; grep { !$seen{$_}++ } @gchar};
  # foreach loop over the unique characters
  foreach (@uchar) {
    # Check if the character is in the $word
    # Note: the current loop variable is denoted by $_
    # In Perl, we can check if a substring matches with the =~ operator
    if ($word =~ m/$_/) {
      print("$_ is in the word<br>\n");
    }
  }
  
  print("</p>");

}

# todo: call the function above to check the guess

if ($success) {
  # If they got it correct, state it
  print "<h3>Congrats!</h3>";
} else {
  # If they don't have it correct, provide a text box to guess again
  # Note: 1) This is an interesting multi-line string!
  #       2) The form has no action! The browser will submit to the
  #          same URL/script!
  #       3) Hidden inputs are not displayed to the user
  print <<end_of_string
  <h3>Enter another guess</h3>
  <form method="POST">
  <input type="hidden" name="firstname" value="$firstname">
  <input type="hidden" name="lastname" value="$lastname">
  <input type="hidden" name="word" value="$word">
  <label>
    Guess:
    <input type="text" name="guess" placeholder="Enter another guess">
  </label>
  <input type="submit" name="submit" value="Submit">
  </form>
end_of_string
}

After fixing your file, you may consider how to style it with Bootstrap so that it matches the initial form submission page. For more Perl practice, you could also try adding the following functionality:

  • Let the user know if their word is longer (or shorter) than the target word
  • Let the user know if any letters they guessed show up twice
  • Clean up the output in some way, such as changing the output to depict the guessed word with correct letters bolded and incorrect letters italicized

Note: you do not need to do this today!

Submission

Answer the questions and submit your Perl script on Gradescope in today’s activity submission form. If you worked in a group, please submit once for the entire group, but be sure to include everyone in the Gradescope submission.

For a different adventure, try re-writing this example in BASH scripting! You may wish to start with our example.sh BASH script.