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 
TestWebServer()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      */
initServer(boolean log)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      */
initServer(int port, boolean log)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      */
initServer(int port, int timeout, boolean log)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      */
log(String s)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      */
setHttpVersion11(boolean set)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      */
setKeepAlive(boolean value)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      */
setChunked(boolean value)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      */
setAcceptLimit(int limit)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      */
setRedirect(String redirect, int code)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      */
close()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 
init()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          */
run()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
close()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 */
Worker()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          */
setSocket(Socket s)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          */
close()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          */
run()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          */
clearBuffer()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          */
readOneLine(InputStream is)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          */
readData(InputStream is, int length)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          */
parseStatusLine(InputStream is)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          */
parseHeader(InputStream is)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          */
readHeaders(InputStream is)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          */
readContent(InputStream is)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          */
handleClient()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
psPrint(PrintStream ps, String s)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
psWrite(PrintStream ps, byte[] bytes, int len)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
psWriteEOL(PrintStream ps)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
printStatus(PrintStream ps)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.parseInt(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          */
printHeaders(PrintStream ps)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          */
send404(PrintStream ps)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          */
sendFile(PrintStream ps)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