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