1 /* Request.java -- 2 Copyright (C) 2004, 2005, 2006, 2007 Free Software Foundation, Inc. 3 4 This file is part of GNU Classpath. 5 6 GNU Classpath is free software; you can redistribute it and/or modify 7 it under the terms of the GNU General Public License as published by 8 the Free Software Foundation; either version 2, or (at your option) 9 any later version. 10 11 GNU Classpath is distributed in the hope that it will be useful, but 12 WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 General Public License for more details. 15 16 You should have received a copy of the GNU General Public License 17 along with GNU Classpath; see the file COPYING. If not, write to the 18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19 02110-1301 USA. 20 21 Linking this library statically or dynamically with other modules is 22 making a combined work based on this library. Thus, the terms and 23 conditions of the GNU General Public License cover the whole 24 combination. 25 26 As a special exception, the copyright holders of this library give you 27 permission to link this library with independent modules to produce an 28 executable, regardless of the license terms of these independent 29 modules, and to copy and distribute the resulting executable under 30 terms of your choice, provided that you also meet, for each linked 31 independent module, the terms and conditions of the license of that 32 module. An independent module is a module which is not derived from 33 or based on this library. If you modify this library, you may extend 34 this exception to your version of the library, but you are not 35 obligated to do so. If you do not wish to do so, delete this 36 exception statement from your version. */ 37 38 39 package gnu.java.net.protocol.http; 40 41 import gnu.java.lang.CPStringBuilder; 42 import gnu.java.net.LineInputStream; 43 import gnu.java.util.Base64; 44 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.net.ProtocolException; 49 import java.security.MessageDigest; 50 import java.security.NoSuchAlgorithmException; 51 import java.text.DateFormat; 52 import java.text.ParseException; 53 import java.util.Calendar; 54 import java.util.Date; 55 import java.util.HashMap; 56 import java.util.Map; 57 import java.util.Properties; 58 import java.util.zip.GZIPInputStream; 59 import java.util.zip.InflaterInputStream; 60 61 /** 62 * A single HTTP request. 63 * 64 * @author Chris Burdess (dog@gnu.org) 65 */ 66 public class Request 67 { 68 69 /** 70 * The connection context in which this request is invoked. 71 */ 72 protected final HTTPConnection connection; 73 74 /** 75 * The HTTP method to invoke. 76 */ 77 protected final String method; 78 79 /** 80 * The path identifying the resource. 81 * This string must conform to the abs_path definition given in RFC2396, 82 * with an optional "?query" part, and must be URI-escaped by the caller. 83 */ 84 protected final String path; 85 86 /** 87 * The headers in this request. 88 */ 89 protected final Headers requestHeaders; 90 91 /** 92 * The request body provider. 93 */ 94 protected RequestBodyWriter requestBodyWriter; 95 96 /** 97 * Map of response header handlers. 98 */ 99 protected Map<String, ResponseHeaderHandler> responseHeaderHandlers; 100 101 /** 102 * The authenticator. 103 */ 104 protected Authenticator authenticator; 105 106 /** 107 * Whether this request has been dispatched yet. 108 */ 109 private boolean dispatched; 110 111 /** 112 * Constructor for a new request. 113 * @param connection the connection context 114 * @param method the HTTP method 115 * @param path the resource path including query part 116 */ Request(HTTPConnection connection, String method, String path)117 protected Request(HTTPConnection connection, String method, 118 String path) 119 { 120 this.connection = connection; 121 this.method = method; 122 this.path = path; 123 requestHeaders = new Headers(); 124 responseHeaderHandlers = new HashMap<String, ResponseHeaderHandler>(); 125 } 126 127 /** 128 * Returns the connection associated with this request. 129 * @see #connection 130 */ getConnection()131 public HTTPConnection getConnection() 132 { 133 return connection; 134 } 135 136 /** 137 * Returns the HTTP method to invoke. 138 * @see #method 139 */ getMethod()140 public String getMethod() 141 { 142 return method; 143 } 144 145 /** 146 * Returns the resource path. 147 * @see #path 148 */ getPath()149 public String getPath() 150 { 151 return path; 152 } 153 154 /** 155 * Returns the full request-URI represented by this request, as specified 156 * by HTTP/1.1. 157 */ getRequestURI()158 public String getRequestURI() 159 { 160 return connection.getURI() + path; 161 } 162 163 /** 164 * Returns the headers in this request. 165 */ getHeaders()166 public Headers getHeaders() 167 { 168 return requestHeaders; 169 } 170 171 /** 172 * Returns the value of the specified header in this request. 173 * @param name the header name 174 */ getHeader(String name)175 public String getHeader(String name) 176 { 177 return requestHeaders.getValue(name); 178 } 179 180 /** 181 * Returns the value of the specified header in this request as an integer. 182 * @param name the header name 183 */ getIntHeader(String name)184 public int getIntHeader(String name) 185 { 186 return requestHeaders.getIntValue(name); 187 } 188 189 /** 190 * Returns the value of the specified header in this request as a date. 191 * @param name the header name 192 */ getDateHeader(String name)193 public Date getDateHeader(String name) 194 { 195 return requestHeaders.getDateValue(name); 196 } 197 198 /** 199 * Sets the specified header in this request. 200 * @param name the header name 201 * @param value the header value 202 */ setHeader(String name, String value)203 public void setHeader(String name, String value) 204 { 205 requestHeaders.put(name, value); 206 } 207 208 /** 209 * Convenience method to set the entire request body. 210 * @param requestBody the request body content 211 */ setRequestBody(byte[] requestBody)212 public void setRequestBody(byte[] requestBody) 213 { 214 setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody)); 215 } 216 217 /** 218 * Sets the request body provider. 219 * @param requestBodyWriter the handler used to obtain the request body 220 */ setRequestBodyWriter(RequestBodyWriter requestBodyWriter)221 public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter) 222 { 223 this.requestBodyWriter = requestBodyWriter; 224 } 225 226 /** 227 * Sets a callback handler to be invoked for the specified header name. 228 * @param name the header name 229 * @param handler the handler to receive the value for the header 230 */ setResponseHeaderHandler(String name, ResponseHeaderHandler handler)231 public void setResponseHeaderHandler(String name, 232 ResponseHeaderHandler handler) 233 { 234 responseHeaderHandlers.put(name, handler); 235 } 236 237 /** 238 * Sets an authenticator that can be used to handle authentication 239 * automatically. 240 * @param authenticator the authenticator 241 */ setAuthenticator(Authenticator authenticator)242 public void setAuthenticator(Authenticator authenticator) 243 { 244 this.authenticator = authenticator; 245 } 246 247 /** 248 * Dispatches this request. 249 * A request can only be dispatched once; calling this method a second 250 * time results in a protocol exception. 251 * @exception IOException if an I/O error occurred 252 * @return an HTTP response object representing the result of the operation 253 */ dispatch()254 public Response dispatch() 255 throws IOException 256 { 257 if (dispatched) 258 { 259 throw new ProtocolException("request already dispatched"); 260 } 261 final String CRLF = "\r\n"; 262 final String HEADER_SEP = ": "; 263 final String US_ASCII = "US-ASCII"; 264 final String version = connection.getVersion(); 265 Response response; 266 int contentLength = -1; 267 boolean retry = false; 268 int attempts = 0; 269 boolean expectingContinue = false; 270 if (requestBodyWriter != null) 271 { 272 contentLength = requestBodyWriter.getContentLength(); 273 String expect = getHeader("Expect"); 274 if (expect != null && expect.equals("100-continue")) 275 { 276 expectingContinue = true; 277 } 278 else 279 { 280 setHeader("Content-Length", Integer.toString(contentLength)); 281 } 282 } 283 284 try 285 { 286 // Loop while authentication fails or continue 287 do 288 { 289 retry = false; 290 291 // Get socket output and input streams 292 OutputStream out = connection.getOutputStream(); 293 294 // Request line 295 String requestUri = path; 296 if (connection.isUsingProxy() && 297 !"*".equals(requestUri) && 298 !"CONNECT".equals(method)) 299 { 300 requestUri = getRequestURI(); 301 } 302 String line = method + ' ' + requestUri + ' ' + version + CRLF; 303 out.write(line.getBytes(US_ASCII)); 304 // Request headers 305 for (Headers.HeaderElement elt : requestHeaders) 306 { 307 line = elt.name + HEADER_SEP + elt.value + CRLF; 308 out.write(line.getBytes(US_ASCII)); 309 } 310 out.write(CRLF.getBytes(US_ASCII)); 311 // Request body 312 if (requestBodyWriter != null && !expectingContinue) 313 { 314 byte[] buffer = new byte[4096]; 315 int len; 316 int count = 0; 317 318 requestBodyWriter.reset(); 319 do 320 { 321 len = requestBodyWriter.write(buffer); 322 if (len > 0) 323 { 324 out.write(buffer, 0, len); 325 } 326 count += len; 327 } 328 while (len > -1 && count < contentLength); 329 } 330 out.flush(); 331 // Get response 332 while(true) 333 { 334 response = readResponse(connection.getInputStream()); 335 int sc = response.getCode(); 336 if (sc == 401 && authenticator != null) 337 { 338 if (authenticate(response, attempts++)) 339 { 340 retry = true; 341 } 342 } 343 else if (sc == 100) 344 { 345 if (expectingContinue) 346 { 347 requestHeaders.remove("Expect"); 348 setHeader("Content-Length", 349 Integer.toString(contentLength)); 350 expectingContinue = false; 351 retry = true; 352 } 353 else 354 { 355 // A conforming server can send an unsoliceted 356 // Continue response but *should* not (RFC 2616 357 // sec 8.2.3). Ignore the bogus Continue 358 // response and get the real response that 359 // should follow 360 continue; 361 } 362 } 363 break; 364 } 365 } 366 while (retry); 367 } 368 catch (IOException e) 369 { 370 connection.close(); 371 throw e; 372 } 373 return response; 374 } 375 readResponse(InputStream in)376 Response readResponse(InputStream in) 377 throws IOException 378 { 379 String line; 380 int len; 381 382 // Read response status line 383 LineInputStream lis = new LineInputStream(in); 384 385 line = lis.readLine(); 386 if (line == null) 387 { 388 throw new ProtocolException("Peer closed connection"); 389 } 390 if (!line.startsWith("HTTP/")) 391 { 392 throw new ProtocolException(line); 393 } 394 len = line.length(); 395 int start = 5, end = 6; 396 while (line.charAt(end) != '.') 397 { 398 end++; 399 } 400 int majorVersion = Integer.parseInt(line.substring(start, end)); 401 start = end + 1; 402 end = start + 1; 403 while (line.charAt(end) != ' ') 404 { 405 end++; 406 } 407 int minorVersion = Integer.parseInt(line.substring(start, end)); 408 start = end + 1; 409 end = start + 3; 410 int code = Integer.parseInt(line.substring(start, end)); 411 String message = line.substring(end + 1, len - 1); 412 // Read response headers 413 Headers responseHeaders = new Headers(); 414 responseHeaders.parse(lis); 415 notifyHeaderHandlers(responseHeaders); 416 InputStream body = null; 417 418 switch (code) 419 { 420 case 100: 421 break; 422 case 204: 423 case 205: 424 case 304: 425 body = createResponseBodyStream(responseHeaders, majorVersion, 426 minorVersion, in, false); 427 break; 428 default: 429 body = createResponseBodyStream(responseHeaders, majorVersion, 430 minorVersion, in, true); 431 } 432 433 // Construct response 434 Response ret = new Response(majorVersion, minorVersion, code, 435 message, responseHeaders, body); 436 return ret; 437 } 438 notifyHeaderHandlers(Headers headers)439 void notifyHeaderHandlers(Headers headers) 440 { 441 for (Headers.HeaderElement entry : headers) 442 { 443 // Handle Set-Cookie 444 if ("Set-Cookie".equalsIgnoreCase(entry.name)) 445 handleSetCookie(entry.value); 446 447 ResponseHeaderHandler handler = 448 (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name); 449 if (handler != null) 450 handler.setValue(entry.value); 451 } 452 } 453 createResponseBodyStream(Headers responseHeaders, int majorVersion, int minorVersion, InputStream in, boolean mayHaveBody)454 private InputStream createResponseBodyStream(Headers responseHeaders, 455 int majorVersion, 456 int minorVersion, 457 InputStream in, 458 boolean mayHaveBody) 459 throws IOException 460 { 461 long contentLength = -1; 462 463 // Persistent connections are the default in HTTP/1.1 464 boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) || 465 "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) || 466 (connection.majorVersion == 1 && connection.minorVersion == 0) || 467 (majorVersion == 1 && minorVersion == 0); 468 469 String transferCoding = responseHeaders.getValue("Transfer-Encoding"); 470 if ("HEAD".equals(method) || !mayHaveBody) 471 { 472 // Special case no body. 473 in = new LimitedLengthInputStream(in, 0, true, connection, doClose); 474 } 475 else if ("chunked".equalsIgnoreCase(transferCoding)) 476 { 477 in = new LimitedLengthInputStream(in, -1, false, connection, doClose); 478 479 in = new ChunkedInputStream(in, responseHeaders); 480 } 481 else 482 { 483 contentLength = responseHeaders.getLongValue("Content-Length"); 484 485 if (contentLength < 0) 486 doClose = true; // No Content-Length, must close. 487 488 in = new LimitedLengthInputStream(in, contentLength, 489 contentLength >= 0, 490 connection, doClose); 491 } 492 String contentCoding = responseHeaders.getValue("Content-Encoding"); 493 if (contentCoding != null && !"identity".equals(contentCoding)) 494 { 495 if ("gzip".equals(contentCoding)) 496 { 497 in = new GZIPInputStream(in); 498 } 499 else if ("deflate".equals(contentCoding)) 500 { 501 in = new InflaterInputStream(in); 502 } 503 else 504 { 505 throw new ProtocolException("Unsupported Content-Encoding: " + 506 contentCoding); 507 } 508 // Remove the Content-Encoding header because the content is 509 // no longer compressed. 510 responseHeaders.remove("Content-Encoding"); 511 } 512 return in; 513 } 514 authenticate(Response response, int attempts)515 boolean authenticate(Response response, int attempts) 516 throws IOException 517 { 518 String challenge = response.getHeader("WWW-Authenticate"); 519 if (challenge == null) 520 { 521 challenge = response.getHeader("Proxy-Authenticate"); 522 } 523 int si = challenge.indexOf(' '); 524 String scheme = (si == -1) ? challenge : challenge.substring(0, si); 525 if ("Basic".equalsIgnoreCase(scheme)) 526 { 527 Properties params = parseAuthParams(challenge.substring(si + 1)); 528 String realm = params.getProperty("realm"); 529 Credentials creds = authenticator.getCredentials(realm, attempts); 530 String userPass = creds.getUsername() + ':' + creds.getPassword(); 531 byte[] b_userPass = userPass.getBytes("US-ASCII"); 532 byte[] b_encoded = Base64.encode(b_userPass).getBytes("US-ASCII"); 533 String authorization = 534 scheme + " " + new String(b_encoded, "US-ASCII"); 535 setHeader("Authorization", authorization); 536 return true; 537 } 538 else if ("Digest".equalsIgnoreCase(scheme)) 539 { 540 Properties params = parseAuthParams(challenge.substring(si + 1)); 541 String realm = params.getProperty("realm"); 542 String nonce = params.getProperty("nonce"); 543 String qop = params.getProperty("qop"); 544 String algorithm = params.getProperty("algorithm"); 545 String digestUri = getRequestURI(); 546 Credentials creds = authenticator.getCredentials(realm, attempts); 547 String username = creds.getUsername(); 548 String password = creds.getPassword(); 549 connection.incrementNonce(nonce); 550 try 551 { 552 MessageDigest md5 = MessageDigest.getInstance("MD5"); 553 final byte[] COLON = { 0x3a }; 554 555 // Calculate H(A1) 556 md5.reset(); 557 md5.update(username.getBytes("US-ASCII")); 558 md5.update(COLON); 559 md5.update(realm.getBytes("US-ASCII")); 560 md5.update(COLON); 561 md5.update(password.getBytes("US-ASCII")); 562 byte[] ha1 = md5.digest(); 563 if ("md5-sess".equals(algorithm)) 564 { 565 byte[] cnonce = generateNonce(); 566 md5.reset(); 567 md5.update(ha1); 568 md5.update(COLON); 569 md5.update(nonce.getBytes("US-ASCII")); 570 md5.update(COLON); 571 md5.update(cnonce); 572 ha1 = md5.digest(); 573 } 574 String ha1Hex = toHexString(ha1); 575 576 // Calculate H(A2) 577 md5.reset(); 578 md5.update(method.getBytes("US-ASCII")); 579 md5.update(COLON); 580 md5.update(digestUri.getBytes("US-ASCII")); 581 if ("auth-int".equals(qop)) 582 { 583 byte[] hEntity = null; // TODO hash of entity body 584 md5.update(COLON); 585 md5.update(hEntity); 586 } 587 byte[] ha2 = md5.digest(); 588 String ha2Hex = toHexString(ha2); 589 590 // Calculate response 591 md5.reset(); 592 md5.update(ha1Hex.getBytes("US-ASCII")); 593 md5.update(COLON); 594 md5.update(nonce.getBytes("US-ASCII")); 595 if ("auth".equals(qop) || "auth-int".equals(qop)) 596 { 597 String nc = getNonceCount(nonce); 598 byte[] cnonce = generateNonce(); 599 md5.update(COLON); 600 md5.update(nc.getBytes("US-ASCII")); 601 md5.update(COLON); 602 md5.update(cnonce); 603 md5.update(COLON); 604 md5.update(qop.getBytes("US-ASCII")); 605 } 606 md5.update(COLON); 607 md5.update(ha2Hex.getBytes("US-ASCII")); 608 String digestResponse = toHexString(md5.digest()); 609 610 String authorization = scheme + 611 " username=\"" + username + "\"" + 612 " realm=\"" + realm + "\"" + 613 " nonce=\"" + nonce + "\"" + 614 " uri=\"" + digestUri + "\"" + 615 " response=\"" + digestResponse + "\""; 616 setHeader("Authorization", authorization); 617 return true; 618 } 619 catch (NoSuchAlgorithmException e) 620 { 621 return false; 622 } 623 } 624 // Scheme not recognised 625 return false; 626 } 627 parseAuthParams(String text)628 Properties parseAuthParams(String text) 629 { 630 int len = text.length(); 631 String key = null; 632 CPStringBuilder buf = new CPStringBuilder(); 633 Properties ret = new Properties(); 634 boolean inQuote = false; 635 for (int i = 0; i < len; i++) 636 { 637 char c = text.charAt(i); 638 if (c == '"') 639 { 640 inQuote = !inQuote; 641 } 642 else if (c == '=' && key == null) 643 { 644 key = buf.toString().trim(); 645 buf.setLength(0); 646 } 647 else if (c == ' ' && !inQuote) 648 { 649 String value = unquote(buf.toString().trim()); 650 ret.put(key, value); 651 key = null; 652 buf.setLength(0); 653 } 654 else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' ')) 655 { 656 buf.append(c); 657 } 658 } 659 if (key != null) 660 { 661 String value = unquote(buf.toString().trim()); 662 ret.put(key, value); 663 } 664 return ret; 665 } 666 unquote(String text)667 String unquote(String text) 668 { 669 int len = text.length(); 670 if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') 671 { 672 return text.substring(1, len - 1); 673 } 674 return text; 675 } 676 677 /** 678 * Returns the number of times the specified nonce value has been seen. 679 * This always returns an 8-byte 0-padded hexadecimal string. 680 */ getNonceCount(String nonce)681 String getNonceCount(String nonce) 682 { 683 int nc = connection.getNonceCount(nonce); 684 String hex = Integer.toHexString(nc); 685 CPStringBuilder buf = new CPStringBuilder(); 686 for (int i = 8 - hex.length(); i > 0; i--) 687 { 688 buf.append('0'); 689 } 690 buf.append(hex); 691 return buf.toString(); 692 } 693 694 /** 695 * Client nonce value. 696 */ 697 byte[] nonce; 698 699 /** 700 * Generates a new client nonce value. 701 */ generateNonce()702 byte[] generateNonce() 703 throws IOException, NoSuchAlgorithmException 704 { 705 if (nonce == null) 706 { 707 long time = System.currentTimeMillis(); 708 MessageDigest md5 = MessageDigest.getInstance("MD5"); 709 md5.update(Long.toString(time).getBytes("US-ASCII")); 710 nonce = md5.digest(); 711 } 712 return nonce; 713 } 714 toHexString(byte[] bytes)715 String toHexString(byte[] bytes) 716 { 717 char[] ret = new char[bytes.length * 2]; 718 for (int i = 0, j = 0; i < bytes.length; i++) 719 { 720 int c =(int) bytes[i]; 721 if (c < 0) 722 { 723 c += 0x100; 724 } 725 ret[j++] = Character.forDigit(c / 0x10, 0x10); 726 ret[j++] = Character.forDigit(c % 0x10, 0x10); 727 } 728 return new String(ret); 729 } 730 731 /** 732 * Parse the specified cookie list and notify the cookie manager. 733 */ handleSetCookie(String text)734 void handleSetCookie(String text) 735 { 736 CookieManager cookieManager = connection.getCookieManager(); 737 if (cookieManager == null) 738 { 739 return; 740 } 741 String name = null; 742 String value = null; 743 String comment = null; 744 String domain = connection.getHostName(); 745 String path = this.path; 746 int lsi = path.lastIndexOf('/'); 747 if (lsi != -1) 748 { 749 path = path.substring(0, lsi); 750 } 751 boolean secure = false; 752 Date expires = null; 753 754 int len = text.length(); 755 String attr = null; 756 CPStringBuilder buf = new CPStringBuilder(); 757 boolean inQuote = false; 758 for (int i = 0; i <= len; i++) 759 { 760 char c =(i == len) ? '\u0000' : text.charAt(i); 761 if (c == '"') 762 { 763 inQuote = !inQuote; 764 } 765 else if (!inQuote) 766 { 767 if (c == '=' && attr == null) 768 { 769 attr = buf.toString().trim(); 770 buf.setLength(0); 771 } 772 else if (c == ';' || i == len || c == ',') 773 { 774 String val = unquote(buf.toString().trim()); 775 if (name == null) 776 { 777 name = attr; 778 value = val; 779 } 780 else if ("Comment".equalsIgnoreCase(attr)) 781 { 782 comment = val; 783 } 784 else if ("Domain".equalsIgnoreCase(attr)) 785 { 786 domain = val; 787 } 788 else if ("Path".equalsIgnoreCase(attr)) 789 { 790 path = val; 791 } 792 else if ("Secure".equalsIgnoreCase(val)) 793 { 794 secure = true; 795 } 796 else if ("Max-Age".equalsIgnoreCase(attr)) 797 { 798 int delta = Integer.parseInt(val); 799 Calendar cal = Calendar.getInstance(); 800 cal.setTimeInMillis(System.currentTimeMillis()); 801 cal.add(Calendar.SECOND, delta); 802 expires = cal.getTime(); 803 } 804 else if ("Expires".equalsIgnoreCase(attr)) 805 { 806 DateFormat dateFormat = new HTTPDateFormat(); 807 try 808 { 809 expires = dateFormat.parse(val); 810 } 811 catch (ParseException e) 812 { 813 // if this isn't a valid date, it may be that 814 // the value was returned unquoted; in that case, we 815 // want to continue buffering the value 816 buf.append(c); 817 continue; 818 } 819 } 820 attr = null; 821 buf.setLength(0); 822 // case EOL 823 if (i == len || c == ',') 824 { 825 Cookie cookie = new Cookie(name, value, comment, domain, 826 path, secure, expires); 827 cookieManager.setCookie(cookie); 828 } 829 if (c == ',') 830 { 831 // Reset cookie fields 832 name = null; 833 value = null; 834 comment = null; 835 domain = connection.getHostName(); 836 path = this.path; 837 if (lsi != -1) 838 { 839 path = path.substring(0, lsi); 840 } 841 secure = false; 842 expires = null; 843 } 844 } 845 else 846 { 847 buf.append(c); 848 } 849 } 850 else 851 { 852 buf.append(c); 853 } 854 } 855 } 856 857 } 858