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