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