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