Home | History | Annotate | Download | only in core
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.core;
     18 
     19 import android.util.Log;
     20 
     21 import java.io.*;
     22 import java.lang.Thread;
     23 import java.net.*;
     24 import java.util.*;
     25 
     26 /**
     27  * TestWebServer is a simulated controllable test server that
     28  * can respond to requests from HTTP clients.
     29  *
     30  * The server can be controlled to change how it reacts to any
     31  * requests, and can be told to simulate various events (such as
     32  * network failure) that would happen in a real environment.
     33  */
     34 class TestWebServer implements HttpConstants {
     35 
     36     /* static class data/methods */
     37 
     38     /* The ANDROID_LOG_TAG */
     39     private final static String LOGTAG = "httpsv";
     40 
     41     /* Where worker threads stand idle */
     42     Vector threads = new Vector();
     43 
     44     /* List of all active worker threads */
     45     Vector activeThreads = new Vector();
     46 
     47     /* timeout on client connections */
     48     int timeout = 0;
     49 
     50     /* max # worker threads */
     51     int workers = 5;
     52 
     53     /* Default port for this server to listen on */
     54     final static int DEFAULT_PORT = 8080;
     55 
     56     /* Default socket timeout value */
     57     final static int DEFAULT_TIMEOUT = 5000;
     58 
     59     /* Version string (configurable) */
     60     protected String HTTP_VERSION_STRING = "HTTP/1.1";
     61 
     62     /* Indicator for whether this server is configured as a HTTP/1.1
     63      * or HTTP/1.0 server
     64      */
     65     private boolean http11 = true;
     66 
     67     /* The thread handling new requests from clients */
     68     private AcceptThread acceptT;
     69 
     70     /* timeout on client connections */
     71     int mTimeout;
     72 
     73     /* Server port */
     74     int mPort;
     75 
     76     /* Switch on/off logging */
     77     boolean mLog = false;
     78 
     79     /* If set, this will keep connections alive after a request has been
     80      * processed.
     81      */
     82     boolean keepAlive = true;
     83 
     84     /* If set, this will cause response data to be sent in 'chunked' format */
     85     boolean chunked = false;
     86 
     87     /* If set, this will indicate a new redirection host */
     88     String redirectHost = null;
     89 
     90     /* If set, this indicates the reason for redirection */
     91     int redirectCode = -1;
     92 
     93     /* Set the number of connections the server will accept before shutdown */
     94     int acceptLimit = 100;
     95 
     96     /* Count of number of accepted connections */
     97     int acceptedConnections = 0;
     98 
     99     public TestWebServer() {
    100     }
    101 
    102     /**
    103      * Initialize a new server with default port and timeout.
    104      * @param log Set true if you want trace output
    105      */
    106     public void initServer(boolean log) throws Exception {
    107         initServer(DEFAULT_PORT, DEFAULT_TIMEOUT, log);
    108     }
    109 
    110     /**
    111      * Initialize a new server with default timeout.
    112      * @param port Sets the server to listen on this port
    113      * @param log Set true if you want trace output
    114      */
    115     public void initServer(int port, boolean log) throws Exception {
    116         initServer(port, DEFAULT_TIMEOUT, log);
    117     }
    118 
    119     /**
    120      * Initialize a new server with default port and timeout.
    121      * @param port Sets the server to listen on this port
    122      * @param timeout Indicates the period of time to wait until a socket is
    123      *                closed
    124      * @param log Set true if you want trace output
    125      */
    126     public void initServer(int port, int timeout, boolean log) throws Exception {
    127         mPort = port;
    128         mTimeout = timeout;
    129         mLog = log;
    130         keepAlive = true;
    131 
    132         if (acceptT == null) {
    133             acceptT = new AcceptThread();
    134             acceptT.init();
    135             acceptT.start();
    136         }
    137     }
    138 
    139     /**
    140      * Print to the log file (if logging enabled)
    141      * @param s String to send to the log
    142      */
    143     protected void log(String s) {
    144         if (mLog) {
    145             Log.d(LOGTAG, s);
    146         }
    147     }
    148 
    149     /**
    150      * Set the server to be an HTTP/1.0 or HTTP/1.1 server.
    151      * This should be called prior to any requests being sent
    152      * to the server.
    153      * @param set True for the server to be HTTP/1.1, false for HTTP/1.0
    154      */
    155     public void setHttpVersion11(boolean set) {
    156         http11 = set;
    157         if (set) {
    158             HTTP_VERSION_STRING = "HTTP/1.1";
    159         } else {
    160             HTTP_VERSION_STRING = "HTTP/1.0";
    161         }
    162     }
    163 
    164     /**
    165      * Call this to determine whether server connection should remain open
    166      * @param value Set true to keep connections open after a request
    167      *              completes
    168      */
    169     public void setKeepAlive(boolean value) {
    170         keepAlive = value;
    171     }
    172 
    173     /**
    174      * Call this to indicate whether chunked data should be used
    175      * @param value Set true to make server respond with chunk encoded
    176      *              content data.
    177      */
    178     public void setChunked(boolean value) {
    179         chunked = value;
    180     }
    181 
    182     /**
    183      * Call this to specify the maximum number of sockets to accept
    184      * @param limit The number of sockets to accept
    185      */
    186     public void setAcceptLimit(int limit) {
    187         acceptLimit = limit;
    188     }
    189 
    190     /**
    191      * Call this to indicate redirection port requirement.
    192      * When this value is set, the server will respond to a request with
    193      * a redirect code with the Location response header set to the value
    194      * specified.
    195      * @param redirect The location to be redirected to
    196      * @param redirectCode The code to send when redirecting
    197      */
    198     public void setRedirect(String redirect, int code) {
    199         redirectHost = redirect;
    200         redirectCode = code;
    201         log("Server will redirect output to "+redirect+" code "+code);
    202     }
    203 
    204     /**
    205      * Cause the thread accepting connections on the server socket to close
    206      */
    207     public void close() {
    208         /* Stop the Accept thread */
    209         if (acceptT != null) {
    210             log("Closing AcceptThread"+acceptT);
    211             acceptT.close();
    212             acceptT = null;
    213         }
    214     }
    215     /**
    216      * The AcceptThread is responsible for initiating worker threads
    217      * to handle incoming requests from clients.
    218      */
    219     class AcceptThread extends Thread {
    220 
    221         ServerSocket ss = null;
    222         boolean running = false;
    223 
    224         public void init() {
    225             // Networking code doesn't support ServerSocket(port) yet
    226             InetSocketAddress ia = new InetSocketAddress(mPort);
    227             while (true) {
    228                 try {
    229                     ss = new ServerSocket();
    230                     // Socket timeout functionality is not available yet
    231                     //ss.setSoTimeout(5000);
    232                     ss.setReuseAddress(true);
    233                     ss.bind(ia);
    234                     break;
    235                 } catch (IOException e) {
    236                     log("IOException in AcceptThread.init()");
    237                     e.printStackTrace();
    238                     // wait and retry
    239                     try {
    240                         Thread.sleep(1000);
    241                     } catch (InterruptedException e1) {
    242                         // TODO Auto-generated catch block
    243                         e1.printStackTrace();
    244                     }
    245                 }
    246             }
    247         }
    248 
    249         /**
    250          * Main thread responding to new connections
    251          */
    252         public synchronized void run() {
    253             running = true;
    254             try {
    255                 while (running) {
    256                     // Log.d(LOGTAG, "TestWebServer run() calling accept()");
    257                     Socket s = ss.accept();
    258                     acceptedConnections++;
    259                     if (acceptedConnections >= acceptLimit) {
    260                         running = false;
    261                     }
    262 
    263                     Worker w = null;
    264                     synchronized (threads) {
    265                         if (threads.isEmpty()) {
    266                             Worker ws = new Worker();
    267                             ws.setSocket(s);
    268                             activeThreads.addElement(ws);
    269                             (new Thread(ws, "additional worker")).start();
    270                         } else {
    271                             w = (Worker) threads.elementAt(0);
    272                             threads.removeElementAt(0);
    273                             w.setSocket(s);
    274                         }
    275                     }
    276                 }
    277             } catch (SocketException e) {
    278                 log("SocketException in AcceptThread: probably closed during accept");
    279                 running = false;
    280             } catch (IOException e) {
    281                 log("IOException in AcceptThread");
    282                 e.printStackTrace();
    283                 running = false;
    284             }
    285             log("AcceptThread terminated" + this);
    286         }
    287 
    288         // Close this socket
    289         public void close() {
    290             try {
    291                 running = false;
    292                 /* Stop server socket from processing further. Currently
    293                    this does not cause the SocketException from ss.accept
    294                    therefore the acceptLimit functionality has been added
    295                    to circumvent this limitation */
    296                 ss.close();
    297 
    298                 // Stop worker threads from continuing
    299                 for (Enumeration e = activeThreads.elements(); e.hasMoreElements();) {
    300                     Worker w = (Worker)e.nextElement();
    301                     w.close();
    302                 }
    303                 activeThreads.clear();
    304 
    305             } catch (IOException e) {
    306                 /* We are shutting down the server, so we expect
    307                  * things to die. Don't propagate.
    308                  */
    309                 log("IOException caught by server socket close");
    310             }
    311         }
    312     }
    313 
    314     // Size of buffer for reading from the connection
    315     final static int BUF_SIZE = 2048;
    316 
    317     /* End of line byte sequence */
    318     static final byte[] EOL = {(byte)'\r', (byte)'\n' };
    319 
    320     /**
    321      * The worker thread handles all interactions with a current open
    322      * connection. If pipelining is turned on, this will allow this
    323      * thread to continuously operate on numerous requests before the
    324      * connection is closed.
    325      */
    326     class Worker implements HttpConstants, Runnable {
    327 
    328         /* buffer to use to hold request data */
    329         byte[] buf;
    330 
    331         /* Socket to client we're handling */
    332         private Socket s;
    333 
    334         /* Reference to current request method ID */
    335         private int requestMethod;
    336 
    337         /* Reference to current requests test file/data */
    338         private String testID;
    339 
    340         /* Reference to test number from testID */
    341         private int testNum;
    342 
    343         /* Reference to whether new request has been initiated yet */
    344         private boolean readStarted;
    345 
    346         /* Indicates whether current request has any data content */
    347         private boolean hasContent = false;
    348 
    349         boolean running = false;
    350 
    351         /* Request headers are stored here */
    352         private Hashtable<String, String> headers = new Hashtable<String, String>();
    353 
    354         /* Create a new worker thread */
    355         Worker() {
    356             buf = new byte[BUF_SIZE];
    357             s = null;
    358         }
    359 
    360         /**
    361          * Called by the AcceptThread to unblock this Worker to process
    362          * a request.
    363          * @param s The socket on which the connection has been made
    364          */
    365         synchronized void setSocket(Socket s) {
    366             this.s = s;
    367             notify();
    368         }
    369 
    370         /**
    371          * Called by the accept thread when it's closing. Potentially unblocks
    372          * the worker thread to terminate properly
    373          */
    374         synchronized void close() {
    375             running = false;
    376             notify();
    377         }
    378 
    379         /**
    380          * Main worker thread. This will wait until a request has
    381          * been identified by the accept thread upon which it will
    382          * service the thread.
    383          */
    384         public synchronized void run() {
    385             running = true;
    386             while(running) {
    387                 if (s == null) {
    388                     /* nothing to do */
    389                     try {
    390                         log(this+" Moving to wait state");
    391                         wait();
    392                     } catch (InterruptedException e) {
    393                         /* should not happen */
    394                         continue;
    395                     }
    396                     if (!running) break;
    397                 }
    398                 try {
    399                     handleClient();
    400                 } catch (Exception e) {
    401                     e.printStackTrace();
    402                 }
    403                 /* go back in wait queue if there's fewer
    404                  * than numHandler connections.
    405                  */
    406                 s = null;
    407                 Vector pool = threads;
    408                 synchronized (pool) {
    409                     if (pool.size() >= workers) {
    410                         /* too many threads, exit this one */
    411                         activeThreads.remove(this);
    412                         return;
    413                     } else {
    414                         pool.addElement(this);
    415                     }
    416                 }
    417             }
    418             log(this+" terminated");
    419         }
    420 
    421         /**
    422          * Zero out the buffer from last time
    423          */
    424         private void clearBuffer() {
    425             for (int i = 0; i < BUF_SIZE; i++) {
    426                 buf[i] = 0;
    427             }
    428         }
    429 
    430         /**
    431          * Utility method to read a line of data from the input stream
    432          * @param is Inputstream to read
    433          * @return number of bytes read
    434          */
    435         private int readOneLine(InputStream is) {
    436 
    437             int read = 0;
    438 
    439             clearBuffer();
    440             try {
    441                 log("Reading one line: started ="+readStarted+" avail="+is.available());
    442                 while ((!readStarted) || (is.available() > 0)) {
    443                     int data = is.read();
    444                     // We shouldn't get EOF but we need tdo check
    445                     if (data == -1) {
    446                         log("EOF returned");
    447                         return -1;
    448                     }
    449 
    450                     buf[read] = (byte)data;
    451 
    452                     System.out.print((char)data);
    453 
    454                     readStarted = true;
    455                     if (buf[read++]==(byte)'\n') {
    456                         System.out.println();
    457                         return read;
    458                     }
    459                 }
    460             } catch (IOException e) {
    461                 log("IOException from readOneLine");
    462                 e.printStackTrace();
    463             }
    464             return read;
    465         }
    466 
    467         /**
    468          * Read a chunk of data
    469          * @param is Stream from which to read data
    470          * @param length Amount of data to read
    471          * @return number of bytes read
    472          */
    473         private int readData(InputStream is, int length) {
    474             int read = 0;
    475             int count;
    476             // At the moment we're only expecting small data amounts
    477             byte[] buf = new byte[length];
    478 
    479             try {
    480                 while (is.available() > 0) {
    481                     count = is.read(buf, read, length-read);
    482                     read += count;
    483                 }
    484             } catch (IOException e) {
    485                 log("IOException from readData");
    486                 e.printStackTrace();
    487             }
    488             return read;
    489         }
    490 
    491         /**
    492          * Read the status line from the input stream extracting method
    493          * information.
    494          * @param is Inputstream to read
    495          * @return number of bytes read
    496          */
    497         private int parseStatusLine(InputStream is) {
    498             int index;
    499             int nread = 0;
    500 
    501             log("Parse status line");
    502             // Check for status line first
    503             nread = readOneLine(is);
    504             // Bomb out if stream closes prematurely
    505             if (nread == -1) {
    506                 requestMethod = UNKNOWN_METHOD;
    507                 return -1;
    508             }
    509 
    510             if (buf[0] == (byte)'G' &&
    511                 buf[1] == (byte)'E' &&
    512                 buf[2] == (byte)'T' &&
    513                 buf[3] == (byte)' ') {
    514                 requestMethod = GET_METHOD;
    515                 log("GET request");
    516                 index = 4;
    517             } else if (buf[0] == (byte)'H' &&
    518                        buf[1] == (byte)'E' &&
    519                        buf[2] == (byte)'A' &&
    520                        buf[3] == (byte)'D' &&
    521                        buf[4] == (byte)' ') {
    522                 requestMethod = HEAD_METHOD;
    523                 log("HEAD request");
    524                 index = 5;
    525             } else if (buf[0] == (byte)'P' &&
    526                        buf[1] == (byte)'O' &&
    527                        buf[2] == (byte)'S' &&
    528                        buf[3] == (byte)'T' &&
    529                        buf[4] == (byte)' ') {
    530                 requestMethod = POST_METHOD;
    531                 log("POST request");
    532                 index = 5;
    533             } else {
    534                 // Unhandled request
    535                 requestMethod = UNKNOWN_METHOD;
    536                 return -1;
    537             }
    538 
    539             // A valid method we understand
    540             if (requestMethod > UNKNOWN_METHOD) {
    541                 // Read file name
    542                 int i = index;
    543                 while (buf[i] != (byte)' ') {
    544                     // There should be HTTP/1.x at the end
    545                     if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
    546                         requestMethod = UNKNOWN_METHOD;
    547                         return -1;
    548                     }
    549                     i++;
    550                 }
    551 
    552                 testID = new String(buf, 0, index, i-index);
    553                 if (testID.startsWith("/")) {
    554                     testID = testID.substring(1);
    555                 }
    556 
    557                 return nread;
    558             }
    559             return -1;
    560         }
    561 
    562         /**
    563          * Read a header from the input stream
    564          * @param is Inputstream to read
    565          * @return number of bytes read
    566          */
    567         private int parseHeader(InputStream is) {
    568             int index = 0;
    569             int nread = 0;
    570             log("Parse a header");
    571             // Check for status line first
    572             nread = readOneLine(is);
    573             // Bomb out if stream closes prematurely
    574             if (nread == -1) {
    575                 requestMethod = UNKNOWN_METHOD;
    576                 return -1;
    577             }
    578             // Read header entry 'Header: data'
    579             int i = index;
    580             while (buf[i] != (byte)':') {
    581                 // There should be an entry after the header
    582 
    583                 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
    584                     return UNKNOWN_METHOD;
    585                 }
    586                 i++;
    587             }
    588 
    589             String headerName = new String(buf, 0, i);
    590             i++; // Over ':'
    591             while (buf[i] == ' ') {
    592                 i++;
    593             }
    594             String headerValue = new String(buf, i, nread-1);
    595 
    596             headers.put(headerName, headerValue);
    597             return nread;
    598         }
    599 
    600         /**
    601          * Read all headers from the input stream
    602          * @param is Inputstream to read
    603          * @return number of bytes read
    604          */
    605         private int readHeaders(InputStream is) {
    606             int nread = 0;
    607             log("Read headers");
    608             // Headers should be terminated by empty CRLF line
    609             while (true) {
    610                 int headerLen = 0;
    611                 headerLen = parseHeader(is);
    612                 if (headerLen == -1)
    613                     return -1;
    614                 nread += headerLen;
    615                 if (headerLen <= 2) {
    616                     return nread;
    617                 }
    618             }
    619         }
    620 
    621         /**
    622          * Read content data from the input stream
    623          * @param is Inputstream to read
    624          * @return number of bytes read
    625          */
    626         private int readContent(InputStream is) {
    627             int nread = 0;
    628             log("Read content");
    629             String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]);
    630             int length = new Integer(lengthString).intValue();
    631 
    632             // Read content
    633             length = readData(is, length);
    634             return length;
    635         }
    636 
    637         /**
    638          * The main loop, reading requests.
    639          */
    640         void handleClient() throws IOException {
    641             InputStream is = new BufferedInputStream(s.getInputStream());
    642             PrintStream ps = new PrintStream(s.getOutputStream());
    643             int nread = 0;
    644 
    645             /* we will only block in read for this many milliseconds
    646              * before we fail with java.io.InterruptedIOException,
    647              * at which point we will abandon the connection.
    648              */
    649             s.setSoTimeout(mTimeout);
    650             s.setTcpNoDelay(true);
    651 
    652             do {
    653                 nread = parseStatusLine(is);
    654                 if (requestMethod != UNKNOWN_METHOD) {
    655 
    656                     // If status line found, read any headers
    657                     nread = readHeaders(is);
    658 
    659                     // Then read content (if any)
    660                     // TODO handle chunked encoding from the client
    661                     if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) {
    662                         nread = readContent(is);
    663                     }
    664                 } else {
    665                     if (nread > 0) {
    666                         /* we don't support this method */
    667                         ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD +
    668                                  " unsupported method type: ");
    669                         ps.write(buf, 0, 5);
    670                         ps.write(EOL);
    671                         ps.flush();
    672                     } else {
    673                     }
    674                     if (!keepAlive || nread <= 0) {
    675                         headers.clear();
    676                         readStarted = false;
    677 
    678                         log("SOCKET CLOSED");
    679                         s.close();
    680                         return;
    681                     }
    682                 }
    683 
    684                 // Reset test number prior to outputing data
    685                 testNum = -1;
    686 
    687                 // Write out the data
    688                 printStatus(ps);
    689                 printHeaders(ps);
    690 
    691                 // Write line between headers and body
    692                 psWriteEOL(ps);
    693 
    694                 // Write the body
    695                 if (redirectCode == -1) {
    696                     switch (requestMethod) {
    697                         case GET_METHOD:
    698                             if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
    699                                 send404(ps);
    700                             } else {
    701                                 sendFile(ps);
    702                             }
    703                             break;
    704                         case HEAD_METHOD:
    705                             // Nothing to do
    706                             break;
    707                         case POST_METHOD:
    708                             // Post method write body data
    709                             if ((testNum > 0) || (testNum < TestWebData.tests.length - 1)) {
    710                                 sendFile(ps);
    711                             }
    712 
    713                             break;
    714                         default:
    715                             break;
    716                     }
    717                 } else { // Redirecting
    718                     switch (redirectCode) {
    719                         case 301:
    720                             // Seems 301 needs a body by neon (although spec
    721                             // says SHOULD).
    722                             psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_301]);
    723                             break;
    724                         case 302:
    725                             //
    726                             psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_302]);
    727                             break;
    728                         case 303:
    729                             psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_303]);
    730                             break;
    731                         case 307:
    732                             psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_307]);
    733                             break;
    734                         default:
    735                             break;
    736                     }
    737                 }
    738 
    739                 ps.flush();
    740 
    741                 // Reset for next request
    742                 readStarted = false;
    743                 headers.clear();
    744 
    745             } while (keepAlive);
    746 
    747             log("SOCKET CLOSED");
    748             s.close();
    749         }
    750 
    751         // Print string to log and output stream
    752         void psPrint(PrintStream ps, String s) throws IOException {
    753             log(s);
    754             ps.print(s);
    755         }
    756 
    757         // Print bytes to log and output stream
    758         void psWrite(PrintStream ps, byte[] bytes, int len) throws IOException {
    759             log(new String(bytes));
    760             ps.write(bytes, 0, len);
    761         }
    762 
    763         // Print CRLF to log and output stream
    764         void psWriteEOL(PrintStream ps) throws IOException {
    765             log("CRLF");
    766             ps.write(EOL);
    767         }
    768 
    769 
    770         // Print status to log and output stream
    771         void printStatus(PrintStream ps) throws IOException {
    772             // Handle redirects first.
    773             if (redirectCode != -1) {
    774                 log("REDIRECTING TO "+redirectHost+" status "+redirectCode);
    775                 psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently");
    776                 psWriteEOL(ps);
    777                 psPrint(ps, "Location: " + redirectHost);
    778                 psWriteEOL(ps);
    779                 return;
    780             }
    781 
    782 
    783             if (testID.startsWith("test")) {
    784                 testNum = Integer.valueOf(testID.substring(4))-1;
    785             }
    786 
    787             if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
    788                 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found");
    789                 psWriteEOL(ps);
    790             }  else {
    791                 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK");
    792                 psWriteEOL(ps);
    793             }
    794 
    795             log("Status sent");
    796         }
    797         /**
    798          * Create the server response and output to the stream
    799          * @param ps The PrintStream to output response headers and data to
    800          */
    801         void printHeaders(PrintStream ps) throws IOException {
    802             psPrint(ps,"Server: TestWebServer"+mPort);
    803             psWriteEOL(ps);
    804             psPrint(ps, "Date: " + (new Date()));
    805             psWriteEOL(ps);
    806             psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close"));
    807             psWriteEOL(ps);
    808 
    809             // Yuk, if we're not redirecting, we add the file details
    810             if (redirectCode == -1) {
    811 
    812                 if (!TestWebData.testParams[testNum].testDir) {
    813                     if (chunked) {
    814                         psPrint(ps, "Transfer-Encoding: chunked");
    815                     } else {
    816                         psPrint(ps, "Content-length: "+TestWebData.testParams[testNum].testLength);
    817                     }
    818                     psWriteEOL(ps);
    819 
    820                     psPrint(ps,"Last Modified: " + (new
    821                                                     Date(TestWebData.testParams[testNum].testLastModified)));
    822                     psWriteEOL(ps);
    823 
    824                     psPrint(ps, "Content-type: " + TestWebData.testParams[testNum].testType);
    825                     psWriteEOL(ps);
    826                 } else {
    827                     psPrint(ps, "Content-type: text/html");
    828                     psWriteEOL(ps);
    829                 }
    830             } else {
    831                 // Content-length of 301, 302, 303, 307 are the same.
    832                 psPrint(ps, "Content-length: "+(TestWebData.testServerResponse[TestWebData.REDIRECT_301]).length());
    833                 psWriteEOL(ps);
    834                 psWriteEOL(ps);
    835             }
    836             log("Headers sent");
    837 
    838         }
    839 
    840         /**
    841          * Sends the 404 not found message
    842          * @param ps The PrintStream to write to
    843          */
    844         void send404(PrintStream ps) throws IOException {
    845             ps.println("Not Found\n\n"+
    846                        "The requested resource was not found.\n");
    847         }
    848 
    849         /**
    850          * Sends the data associated with the headers
    851          * @param ps The PrintStream to write to
    852          */
    853         void sendFile(PrintStream ps) throws IOException {
    854             // For now just make a chunk with the whole of the test data
    855             // It might be worth making this multiple chunks for large
    856             // test data to test multiple chunks.
    857             int dataSize = TestWebData.tests[testNum].length;
    858             if (chunked) {
    859                 psPrint(ps, Integer.toHexString(dataSize));
    860                 psWriteEOL(ps);
    861                 psWrite(ps, TestWebData.tests[testNum], dataSize);
    862                 psWriteEOL(ps);
    863                 psPrint(ps, "0");
    864                 psWriteEOL(ps);
    865                 psWriteEOL(ps);
    866             } else {
    867                 psWrite(ps, TestWebData.tests[testNum], dataSize);
    868             }
    869         }
    870     }
    871 }
    872