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