next up previous
Next: 3.4 UDP Sockets Up: 3. Internet Sockets Previous: 3.2 Concurrent Servers


3.3 Defensive Servers

The server in Section 3.2.4 [Pump-Aided Server] protects itself quite well against careless or malicious clients by handing them off to a child process immediately after they are accepted, but does not guard against the buildup of clients that somehow never get around to closing their connections. An example of a service which does time out in this way after 15 minutes of idle client time is FTP. If it did not do this, a popular FTP server would soon swamp its own host with idle TCP connections to its command port, since users naturally tend to leave such tedious housekeeping details as closing connections to the software rather than disconnecting explicitly.

3.3.1 Time-Monitoring Server

For a concurrent line-length server, there is not only the TCP connection but also the child process that consumes space on the server's host. What we want to do, if there is no client activity for, say, 15 minutes, is drop the client, forcibly if necessary.

In the following version of the line-length server, like the shell-aided server of Section 3.2.2 [Shell-Aided Server], the overall parent process does nothing more than instantiate some external program for each new client, using the shell for convenience: *

-- Line-length server, version 5 (impatient)
server_fd := open (`54005', `server-socket');                 -- listen
loop                                              -- cycle indefinitely
  if (fd := accept (server_fd)) /= om then                -- new client
    -- Form command using fd converted to string, run in background:
    system (`setl impatient.setl  --  ' + str fd + ` &');
    close (fd);                 -- this has been inherited by the child
  end if;
end loop;
Here, the program impatient.setl replaces the program lengths.setl of Section 3.2.2 [Shell-Aided Server], but as we shall now see, prevents clients from ``hanging'' it indefinitely. If clients could be trusted to send and receive entire lines, it would be a simple matter of using select with a timeout argument of 15 minutes, like this: *
-- ``impatient.setl'' (too-trusting version)
fd := open (val command_line(1), `rw');    -- open inherited fd as fd
loop                             -- cycle until EOF on fd, or timeout
  [ready] := select ([{fd}], 15 * 60 * 1000);        -- 15-minute limit
  if fd in ready then                              -- fd input or EOF
    if (line := getline fd/= om then
      printa (fd, #line);                  -- send length of input line
      stop;                                       -- exit on client EOF
    end if;
  else                     -- timeout (nothing from fd in 15 minutes)
    stop;                                     -- exit on client timeout
  end if;
end loop;
But a devious client could send part of a line, and then the getline call would block indefinitely.

The solution to this problem, shown below, has impatient.setl fork itself into (1) a worker process which deals with the client, and (2) a monitor process which kills the first process if 15 minutes of continuous client inactivity occurs. The way this works is that the worker process sends the monitor process an empty line after each cycle of interaction with the client. If the monitor does not receive such a line within 15 minutes initially or after the previous cycle, it sends the worker a termination signal: *

-- ``impatient.setl'' (highly cautious version)
if (pump_fd := pump()) = -1 then
  -- Child (worker), deals with inherited client stream
  fd := open (val command_line(1), `rw');           -- inherited stream
  while (line := getline fd/= om loop
    printa (fd, #line);                   -- send line length to client
    printflush(stdout);               -- make monitoring parent happy
  end loop;
  stop;                                                     -- all done
elseif pump_fd /= om then
  -- Parent (monitor) continues here
  loop                       -- cycle until EOF on pump_fd or timeout
    [ready] := select ([{pump_fd}], 15 * 60 * 1000);
    if pump_fd in ready then
      if getline pump_fd = om then
        stop;                    -- child exited normally, and so do we
      end if;
    else              -- timeout (nothing from pump_fd in 15 minutes)
      kill (pid (pump_fd));                -- send TERM signal to child
      stop;                    -- presume child exited, and do likewise
    end if;
  end loop;
  -- No child was created
  printa (stderr, `pump() failed, dropping client');
end if;
If the monitoring parent process here had responsibilities other than closing pump_fd, the stop statements would become quit loop statements, and close would be called explicitly on the pump file descriptor. But to avoid clutter, the closing is left to be done automatically in this version, and the logging of information (cf. the server in Section 3.2.4 [Pump-Aided Server]) is omitted.

3.3.2 Identity-Sensitive Server

For security reasons, it is often important to know the identity of clients, so the following primitives have been introduced into SETL: *

name    := peer_name fd;
address := peer_address fd;
portnum := peer_port fd;
Both name and address are returned as strings; the only difference between them is that address is the customary external representation of an IP address (4 decimal fields in the range 0-255, beginning with the high-order part of the address, and having the fields separated by dots), and name is an Internet primary host name if one can be found for the peer connected to fd, otherwise om. Portnum is the integer-valued port number of the peer connected to fd. Although the argument in all three of these cases is shown as fd to suggest an integer-valued file descriptor, this can as usual be the argument originally passed to open if that is known to the current process. These functions are primarily intended for servers to obtain information about clients, but are also available for client sockets, where they return information about servers. In the case of peer_port, of course, this is merely the number that was originally after the colon in the original argument to open, whereas when peer_port is used by a server to inquire about a client, the ephemeral port number it returns can be a useful way to distinguish among multiple clients from a single host.

The primary name and IP address of the host on which the current process is running can be obtained through the following string-valued nullary primitives: *

name    := hostname;
address := hostaddr;

Finally, since a single host can have more than one name and/or IP address, the following primitives return sets of strings: *

names     := ip_names (name_or_address);
addresses := ip_addresses (name_or_address);
The name_or_address argument to both of these is optional, and is understood to be that of the local host if omitted.

Just as a single host name can map to multiple IP addresses when the host has multiple network interfaces, a single IP address can almost always be reached by both a ``local'' name and a ``fully qualified'' name, and often also some further aliases. For example, consider this program: *

print (ip_names (`birch'));
print (ip_addresses (`birch'));
print (ip_names (`'));
print (ip_names (`genie'));
print (ip_addresses (`genie'));
It produces the following output when executed on host birch:

{birch `' `'}
{birch `' `'}
{`' `' `' `'

The association between Internet host names and IP addresses is maintained by the Domain Name System (DNS) [116]. SETL currently supports only the familiar IP version 4 (IPv4) and not the emerging IP version 6 (IPv6) forms of host names and addresses [194]. The DNS service is typically provided by a combination of information local to the host on which a particular request is made and knowledge maintained by nameservers. This is why the output of the above program when ip_names was applied to `birch' was more extensive than when it was applied to `genie', though the opposite was true for the application of ip_addresses because of the multiple network interfaces on `genie'.

Without modifying impatient.setl, we can now implement a ``blacklist'' of clients that are to be denied service by the line-length server of Section 3.3.1 [Time-Monitoring Server]. This, our sixth and final version of the line-length server, also shows how the HUP signal is conventionally interpreted by servers as a command to re-read configuration data. Here, it causes the server to re-read the file blacklist: *

-- Line-length server, version 6 (prejudiced)
server_fd := open (`54006', `server-socket');                 -- listen
assert server_fd /= om;                                  -- or we crash
hup_fd := open (`HUP', `signal');                      -- catch SIGHUPs
nasty := get_blacklist();            -- read set of names from database
loop                                              -- cycle indefinitely
  -- Await connection requests and HUP signals
  [ready] := select ([{server_fdhup_fd}]);
  if server_fd in ready and
     (fd := accept (server_fd)) /= om then
    -- If client is not blacklisted, spawn background command
    if {peer_name fdpeer_address fd} * nasty = {} then
      system (`setl impatient.setl  --  ' + str fd + ` &');
    end if;
    close (fd);
  end if;
  if hup_fd in ready then                                 -- HUP caught
    dummy := getline hup_fd;                      -- receive the signal
    nasty := get_blacklist();               -- re-read the set of names
  end if;
end loop;
proc get_blacklist();        -- read names of naughty clients from file
  return {id : while (id := getline `blacklist') /= om};
end proc;

next up previous
Next: 3.4 UDP Sockets Up: 3. Internet Sockets Previous: 3.2 Concurrent Servers
David Bacon