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