OSDN Git Service

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