001/** 002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net> 003 * 004 * This library is free software; you can redistribute it and/or 005 * modify it under the terms of the GNU Lesser General Public 006 * License as published by the Free Software Foundation; either 007 * version 2.1 of the License, or (at your option) any later version. 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this library; if not, write to the Free Software 016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 017 */ 018 019package net.sourceforge.spnego; 020 021import java.io.ByteArrayOutputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.net.HttpURLConnection; 026import java.net.URL; 027import java.security.PrivilegedActionException; 028import java.util.ArrayList; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033import java.util.concurrent.locks.Lock; 034import java.util.concurrent.locks.ReentrantLock; 035import java.util.logging.Level; 036import java.util.logging.Logger; 037 038import javax.security.auth.callback.CallbackHandler; 039import javax.security.auth.login.LoginContext; 040import javax.security.auth.login.LoginException; 041 042import net.sourceforge.spnego.SpnegoHttpFilter.Constants; 043 044import org.ietf.jgss.GSSContext; 045import org.ietf.jgss.GSSCredential; 046import org.ietf.jgss.GSSException; 047 048/** 049 * This Class may be used by custom clients as a convenience when connecting 050 * to a protected HTTP server. 051 * 052 * <p> 053 * This mechanism is an alternative to HTTP Basic Authentication where the 054 * HTTP server does not support Basic Auth but instead has SPNEGO support 055 * (take a look at {@link SpnegoHttpFilter}). 056 * </p> 057 * 058 * <p> 059 * A krb5.conf and a login.conf is required when using this class. Take a 060 * look at the <a href="http://spnego.sourceforge.net" target="_blank">spnego.sourceforge.net</a> 061 * documentation for an example krb5.conf and login.conf file. 062 * Also, you must provide a keytab file, or a username and password, or allowtgtsessionkey. 063 * </p> 064 * 065 * <p> 066 * Example usage (username/password): 067 * <pre> 068 * public static void main(final String[] args) throws Exception { 069 * System.setProperty("java.security.krb5.conf", "krb5.conf"); 070 * System.setProperty("sun.security.krb5.debug", "true"); 071 * System.setProperty("java.security.auth.login.config", "login.conf"); 072 * 073 * SpnegoHttpURLConnection spnego = null; 074 * 075 * try { 076 * spnego = new SpnegoHttpURLConnection("spnego-client", "dfelix", "myp@s5"); 077 * spnego.connect(new URL("http://medusa:8080/index.jsp")); 078 * 079 * System.out.println(spnego.getResponseCode()); 080 * 081 * } finally { 082 * if (null != spnego) { 083 * spnego.disconnect(); 084 * } 085 * } 086 * } 087 * </pre> 088 * </p> 089 * 090 * <p> 091 * Alternatively, if the server supports HTTP Basic Authentication, this Class 092 * is NOT needed and instead you can do something like the following: 093 * <pre> 094 * public static void main(final String[] args) throws Exception { 095 * final String creds = "dfelix:myp@s5"; 096 * 097 * final String token = Base64.encode(creds.getBytes()); 098 * 099 * URL url = new URL("http://medusa:8080/index.jsp"); 100 * 101 * HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 102 * 103 * conn.setRequestProperty(Constants.AUTHZ_HEADER 104 * , Constants.BASIC_HEADER + " " + token); 105 * 106 * conn.connect(); 107 * 108 * System.out.println("Response Code:" + conn.getResponseCode()); 109 * } 110 * </pre> 111 * </p> 112 * 113 * <p> 114 * To see a working example and instructions on how to use a keytab, take 115 * a look at the <a href="http://spnego.sourceforge.net/client_keytab.html" 116 * target="_blank">creating a client keytab</a> example. 117 * </p> 118 * 119 * <p> 120 * Finally, the {@link SpnegoSOAPConnection} class is another example of a class 121 * that uses this class. 122 * <p> 123 * 124 * @author Darwin V. Felix 125 * 126 */ 127public final class SpnegoHttpURLConnection { 128 129 private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME); 130 131 /** GSSContext is not thread-safe. */ 132 private static final Lock LOCK = new ReentrantLock(); 133 134 private static final byte[] EMPTY_BYTE = new byte[0]; 135 136 /** 137 * If false, this connection object has not created a communications link to 138 * the specified URL. If true, the communications link has been established. 139 */ 140 private transient boolean connected = false; 141 142 /** 143 * Default is set to false instead of true. 144 * 145 * @see java.net.HttpURLConnection#getInstanceFollowRedirects() 146 */ 147 private boolean instanceFollowRedirects = true; 148 149 /** 150 * Default is GET. 151 * 152 * @see java.net.HttpURLConnection#getRequestMethod() 153 */ 154 private transient String requestMethod = "GET"; 155 156 /** 157 * @see java.net.URLConnection#getRequestProperties() 158 */ 159 private final transient Map<String, List<String>> requestProperties = 160 new LinkedHashMap<String, List<String>>(); 161 162 /** 163 * Login Context for authenticating client. If username/password 164 * or GSSCredential is provided (in constructor) then this 165 * field will always be null. 166 */ 167 private final transient LoginContext loginContext; 168 169 /** 170 * Client's credentials. If username/password or LoginContext is provided 171 * (in constructor) then this field will always be null. 172 */ 173 private transient GSSCredential credential; 174 175 /** 176 * Flag to determine if GSSContext has been established. Users of this 177 * class should always check that this field is true before using/trusting 178 * the contents of the response. 179 */ 180 private transient boolean cntxtEstablished = false; 181 182 /** 183 * Ref to HTTP URL Connection object after calling connect method. 184 * Always call spnego.disconnect() when done using this class. 185 */ 186 private transient HttpURLConnection conn = null; 187 188 /** 189 * Request credential to be delegated. 190 * Default is false. 191 */ 192 private transient boolean reqCredDeleg = false; 193 194 /** 195 * Determines if the GSSCredentials (if any) used during the 196 * connection request should be automatically disposed by 197 * this class when finished. 198 * Default is true. 199 */ 200 private transient boolean autoDisposeCreds = true; 201 202 /** 203 * Number of times request was redirected. 204 */ 205 private transient int redirectCount = 0; 206 207 /** 208 * GSSContext request Mutual Authentication. 209 */ 210 private transient boolean mutualAuth = true; 211 212 /** 213 * GSSContext request Message Confidentiality. 214 * Default is true. 215 */ 216 private transient boolean confidentiality = true; 217 218 /** 219 * GSSContext request Message Integrity. 220 * Default is true. 221 */ 222 private transient boolean messageIntegrity = true; 223 224 /** 225 * GSSContext request Replay Detection. 226 * Default is true. 227 */ 228 private transient boolean replayDetection = true; 229 230 /** 231 * GSSContext request Sequence Detection. 232 * Default is true. 233 */ 234 private transient boolean sequenceDetection = true; 235 236 /** 237 * Number of times redirects will be allowed. 238 */ 239 private static final int MAX_REDIRECTS = 20; 240 241 /** 242 * Creates an instance where the LoginContext relies on a keytab 243 * file being specified by "java.security.auth.login.config" or 244 * where LoginContext relies on tgtsessionkey. 245 * 246 * @param loginModuleName 247 * @throws LoginException 248 */ 249 public SpnegoHttpURLConnection(final String loginModuleName) 250 throws LoginException { 251 252 this.loginContext = new LoginContext(loginModuleName); 253 this.loginContext.login(); 254 this.credential = null; 255 } 256 257 /** 258 * Create an instance where the GSSCredential is specified by the parameter 259 * and where the GSSCredential is automatically disposed after use. 260 * 261 * @param creds credentials to use 262 */ 263 public SpnegoHttpURLConnection(final GSSCredential creds) { 264 this(creds, true); 265 } 266 267 /** 268 * Create an instance where the GSSCredential is specified by the parameter 269 * and whether the GSSCredential should be disposed after use. 270 * 271 * @param creds credentials to use 272 * @param dispose true if GSSCredential should be diposed after use 273 */ 274 public SpnegoHttpURLConnection(final GSSCredential creds, final boolean dispose) { 275 this.loginContext = null; 276 this.credential = creds; 277 this.autoDisposeCreds = dispose; 278 } 279 280 /** 281 * Creates an instance where the LoginContext does not require a keytab 282 * file. However, the "java.security.auth.login.config" property must still 283 * be set prior to instantiating this object. 284 * 285 * @param loginModuleName 286 * @param username 287 * @param password 288 * @throws LoginException 289 */ 290 public SpnegoHttpURLConnection(final String loginModuleName, 291 final String username, final String password) throws LoginException { 292 293 final CallbackHandler handler = SpnegoProvider.getUsernamePasswordHandler( 294 username, password); 295 296 this.loginContext = new LoginContext(loginModuleName, handler); 297 this.loginContext.login(); 298 this.credential = null; 299 } 300 301 /** 302 * Throws IllegalStateException if this connection object has not yet created 303 * a communications link to the specified URL. 304 */ 305 private void assertConnected() { 306 if (!this.connected) { 307 throw new IllegalStateException("Not connected."); 308 } 309 } 310 311 /** 312 * Throws IllegalStateException if this connection object has already created 313 * a communications link to the specified URL. 314 */ 315 private void assertNotConnected() { 316 if (this.connected) { 317 throw new IllegalStateException("Already connected."); 318 } 319 } 320 321 /** 322 * Opens a communications link to the resource referenced by 323 * this URL, if such a connection has not already been established. 324 * 325 * <p> 326 * This implementation simply calls this objects 327 * connect(URL, ByteArrayOutputStream) method but passing in a null 328 * for the second argument. 329 * </p> 330 * 331 * @param url 332 * @return an HttpURLConnection object 333 * @throws GSSException 334 * @throws PrivilegedActionException 335 * @throws IOException 336 * @throws LoginException 337 * 338 * @see java.net.URLConnection#connect() 339 */ 340 public HttpURLConnection connect(final URL url) 341 throws GSSException, PrivilegedActionException, IOException { 342 343 return this.connect(url, null); 344 } 345 346 /** 347 * Opens a communications link to the resource referenced by 348 * this URL, if such a connection has not already been established. 349 * 350 * @param url 351 * @param dooutput optional message/payload to send to server 352 * @return an HttpURLConnection object 353 * @throws GSSException 354 * @throws PrivilegedActionException 355 * @throws IOException 356 * @throws LoginException 357 * 358 * @see java.net.URLConnection#connect() 359 */ 360 public HttpURLConnection connect(final URL url, final ByteArrayOutputStream dooutput) 361 throws GSSException, PrivilegedActionException, IOException { 362 363 // assert 364 if (!this.messageIntegrity && this.confidentiality) { 365 throw new IllegalStateException("Message Integrity was set " 366 + "to false but Confidentiality set to true."); 367 } 368 369 assertNotConnected(); 370 371 GSSContext context = null; 372 373 try { 374 byte[] data = null; 375 376 SpnegoHttpURLConnection.LOCK.lock(); 377 try { 378 // work-around to GSSContext/AD timestamp vs sequence field replay bug 379 try { Thread.sleep(31); } catch (InterruptedException e) { assert true; } 380 381 context = this.getGSSContext(url); 382 context.requestMutualAuth(this.mutualAuth); 383 context.requestConf(this.confidentiality); 384 context.requestInteg(this.messageIntegrity); 385 context.requestReplayDet(this.replayDetection); 386 context.requestSequenceDet(this.sequenceDetection); 387 context.requestCredDeleg(this.reqCredDeleg); 388 389 data = context.initSecContext(EMPTY_BYTE, 0, 0); 390 } finally { 391 SpnegoHttpURLConnection.LOCK.unlock(); 392 } 393 394 this.conn = (HttpURLConnection) url.openConnection(); 395 this.connected = true; 396 397 final Set<String> keys = this.requestProperties.keySet(); 398 for (final String key : keys) { 399 for (String value : this.requestProperties.get(key)) { 400 this.conn.addRequestProperty(key, value); 401 } 402 } 403 404 this.conn.setInstanceFollowRedirects(false); 405 this.conn.setRequestMethod(this.requestMethod); 406 407 this.conn.setRequestProperty(Constants.AUTHZ_HEADER 408 , Constants.NEGOTIATE_HEADER + ' ' + Base64.encode(data)); 409 410 if (null != dooutput && dooutput.size() > 0) { 411 this.conn.setDoOutput(true); 412 dooutput.writeTo(this.conn.getOutputStream()); 413 } 414 415 this.conn.connect(); 416 417 // redirect if 302 418 if (this.conn.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP 419 && this.redirectCount < SpnegoHttpURLConnection.MAX_REDIRECTS) { 420 421 return this.redirect(url, dooutput); 422 } 423 424 final SpnegoAuthScheme scheme = SpnegoProvider.getAuthScheme( 425 this.conn.getHeaderField(Constants.AUTHN_HEADER)); 426 427 // app servers will not return a WWW-Authenticate on 302, (and 30x...?) 428 if (null == scheme) { 429 LOGGER.fine("SpnegoProvider.getAuthScheme(...) returned null."); 430 431 // client requesting to skip context loop if 200 and mutualAuth=false 432 } else if (this.conn.getResponseCode() == HttpURLConnection.HTTP_OK 433 && !this.mutualAuth) { 434 LOGGER.fine("SpnegoProvider.getAuthScheme(...) returned null."); 435 436 } else { 437 data = scheme.getToken(); 438 439 if (Constants.NEGOTIATE_HEADER.equalsIgnoreCase(scheme.getScheme())) { 440 SpnegoHttpURLConnection.LOCK.lock(); 441 try { 442 data = context.initSecContext(data, 0, data.length); 443 } finally { 444 SpnegoHttpURLConnection.LOCK.unlock(); 445 } 446 447 // TODO : support context loops where i>1 448 if (null != data) { 449 LOGGER.warning("Server requested context loop: " + data.length); 450 } 451 452 } else { 453 throw new UnsupportedOperationException("Scheme NOT Supported: " 454 + scheme.getScheme()); 455 } 456 457 this.cntxtEstablished = context.isEstablished(); 458 } 459 } finally { 460 this.dispose(context); 461 } 462 463 return this.conn; 464 } 465 466 /** 467 * Logout the LoginContext instance, and call dispose() on GSSCredential 468 * if autoDisposeCreds is set to true, and call dispose on the passed-in 469 * GSSContext instance. 470 */ 471 private void dispose(final GSSContext context) { 472 if (null != context) { 473 try { 474 SpnegoHttpURLConnection.LOCK.lock(); 475 try { 476 context.dispose(); 477 } finally { 478 SpnegoHttpURLConnection.LOCK.unlock(); 479 } 480 } catch (GSSException gsse) { 481 LOGGER.log(Level.WARNING, "call to dispose context failed.", gsse); 482 } 483 } 484 485 if (null != this.credential && this.autoDisposeCreds) { 486 try { 487 this.credential.dispose(); 488 } catch (final GSSException gsse) { 489 LOGGER.log(Level.WARNING, "call to dispose credential failed.", gsse); 490 } 491 } 492 493 if (null != this.loginContext) { 494 try { 495 this.loginContext.logout(); 496 } catch (final LoginException lex) { 497 LOGGER.log(Level.WARNING, "call to logout context failed.", lex); 498 } 499 } 500 } 501 502 /** 503 * Logout and clear request properties. 504 * 505 * @see java.net.HttpURLConnection#disconnect() 506 */ 507 public void disconnect() { 508 this.dispose(null); 509 this.requestProperties.clear(); 510 this.connected = false; 511 if (null != this.conn) { 512 this.conn.disconnect(); 513 } 514 } 515 516 /** 517 * Returns true if GSSContext has been established. 518 * 519 * @return true if GSSContext has been established, false otherwise. 520 */ 521 public boolean isContextEstablished() { 522 return this.cntxtEstablished; 523 } 524 525 /** 526 * Internal sanity check to validate not null key/value pairs. 527 */ 528 private void assertKeyValue(final String key, final String value) { 529 if (null == key || key.isEmpty()) { 530 throw new IllegalArgumentException("key parameter is null or empty"); 531 } 532 if (null == value) { 533 throw new IllegalArgumentException("value parameter is null"); 534 } 535 } 536 537 /** 538 * Adds an HTTP Request property. 539 * 540 * @param key request property name 541 * @param value request propery value 542 * @see java.net.URLConnection#addRequestProperty(String, String) 543 */ 544 public void addRequestProperty(final String key, final String value) { 545 assertNotConnected(); 546 assertKeyValue(key, value); 547 548 if (this.requestProperties.containsKey(key)) { 549 this.requestProperties.get(key).add(value); 550 } else { 551 setRequestProperty(key, value); 552 } 553 } 554 555 /** 556 * Sets an HTTP Request property. 557 * 558 * @param key request property name 559 * @param value request property value 560 * @see java.net.URLConnection#setRequestProperty(String, String) 561 */ 562 public void setRequestProperty(final String key, final String value) { 563 assertNotConnected(); 564 assertKeyValue(key, value); 565 566 final List<String> val = new ArrayList<String>(); 567 val.add(value); 568 569 this.requestProperties.put(key, val); 570 } 571 572 /** 573 * Returns a GSSContextt for the given url with a default lifetime. 574 * 575 * @param url http address 576 * @return GSSContext for the given url 577 * @throws GSSException 578 * @throws PrivilegedActionException 579 */ 580 private GSSContext getGSSContext(final URL url) throws GSSException 581 , PrivilegedActionException { 582 583 if (null == this.credential) { 584 if (null == this.loginContext) { 585 throw new IllegalStateException( 586 "GSSCredential AND LoginContext NOT initialized"); 587 588 } else { 589 this.credential = SpnegoProvider.getClientCredential( 590 this.loginContext.getSubject()); 591 } 592 } 593 594 return SpnegoProvider.getGSSContext(this.credential, url); 595 } 596 597 /** 598 * Returns an error stream that reads from this open connection. 599 * 600 * @return error stream that reads from this open connection 601 * @throws IOException 602 * 603 * @see java.net.HttpURLConnection#getErrorStream() 604 */ 605 public InputStream getErrorStream() throws IOException { 606 assertConnected(); 607 608 return this.conn.getInputStream(); 609 } 610 611 /** 612 * Get header value at specified index. 613 * 614 * @param index 615 * @return header value at specified index 616 */ 617 public String getHeaderField(final int index) { 618 assertConnected(); 619 620 return this.conn.getHeaderField(index); 621 } 622 623 /** 624 * Get header value by header name. 625 * 626 * @param name name header 627 * @return header value 628 * @see java.net.HttpURLConnection#getHeaderField(String) 629 */ 630 public String getHeaderField(final String name) { 631 assertConnected(); 632 633 return this.conn.getHeaderField(name); 634 } 635 636 /** 637 * Get header field key at specified index. 638 * 639 * @param index 640 * @return header field key at specified index 641 */ 642 public String getHeaderFieldKey(final int index) { 643 assertConnected(); 644 645 return this.conn.getHeaderFieldKey(index); 646 } 647 648 /** 649 * @return true if it should follow redirects 650 * @see java.net.HttpURLConnection#getInstanceFollowRedirects() 651 */ 652 public boolean getInstanceFollowRedirects() { // NOPMD 653 return this.instanceFollowRedirects; 654 } 655 656 /** 657 * @param followRedirects 658 * @see java.net.HttpURLConnection#setInstanceFollowRedirects(boolean) 659 */ 660 public void setInstanceFollowRedirects(final boolean followRedirects) { 661 assertNotConnected(); 662 663 this.instanceFollowRedirects = followRedirects; 664 } 665 666 /** 667 * Returns an input stream that reads from this open connection. 668 * 669 * @return input stream that reads from this open connection 670 * @throws IOException 671 * 672 * @see java.net.HttpURLConnection#getInputStream() 673 */ 674 public InputStream getInputStream() throws IOException { 675 assertConnected(); 676 677 return this.conn.getInputStream(); 678 } 679 680 /** 681 * Returns an output stream that writes to this open connection. 682 * 683 * @return output stream that writes to this connections 684 * @throws IOException 685 * 686 * @see java.net.HttpURLConnection#getOutputStream() 687 */ 688 public OutputStream getOutputStream() throws IOException { 689 assertConnected(); 690 691 return this.conn.getOutputStream(); 692 } 693 694 /** 695 * Returns HTTP Status code. 696 * 697 * @return HTTP Status Code 698 * @throws IOException 699 * 700 * @see java.net.HttpURLConnection#getResponseCode() 701 */ 702 public int getResponseCode() throws IOException { 703 assertConnected(); 704 705 return this.conn.getResponseCode(); 706 } 707 708 /** 709 * Returns HTTP Status message. 710 * 711 * @return HTTP Status Message 712 * @throws IOException 713 * 714 * @see java.net.HttpURLConnection#getResponseMessage() 715 */ 716 public String getResponseMessage() throws IOException { 717 assertConnected(); 718 719 return this.conn.getResponseMessage(); 720 } 721 722 private HttpURLConnection redirect(final URL url, final ByteArrayOutputStream dooutput) 723 throws GSSException, PrivilegedActionException, IOException { 724 725 this.redirectCount++; 726 final String location = this.getHeaderField("location"); 727 assert !location.isEmpty(); 728 729 if (!instanceFollowRedirects && '/' != location.charAt(0)) { 730 final URL erl = new URL(location); 731 if (!url.getHost().equalsIgnoreCase(erl.getHost()) 732 || url.getPort() != erl.getPort()) { 733 734 this.redirectCount = SpnegoHttpURLConnection.MAX_REDIRECTS; 735 return this.conn; 736 } 737 } 738 739 final List<String> cookies = 740 this.conn.getHeaderFields().get("Set-Cookie"); 741 742 this.conn.disconnect(); 743 this.connected = false; 744 745 if (null != cookies) { 746 this.requestProperties.remove("Cookie"); 747 for (String cookie : cookies) { 748 this.addRequestProperty("Cookie", cookie); 749 } 750 } 751 752 if ('/' == location.charAt(0)) { 753 final String[] str = url.toString().split("/"); 754 final String newLocation = str[0] + "//" + str[2] + location; 755 return this.connect(new URL(newLocation), dooutput); 756 } else { 757 return this.connect(new URL(location), dooutput); 758 } 759 } 760 761 /** 762 * Request that this GSSCredential be allowed for delegation. 763 * 764 * @param requestDelegation true to allow/request delegation 765 */ 766 public void requestCredDeleg(final boolean requestDelegation) { 767 this.assertNotConnected(); 768 769 this.reqCredDeleg = requestDelegation; 770 } 771 772 /** 773 * Specify if GSSContext should request Confidentiality. 774 * Default is true. 775 * 776 * @param confidential pass true for confidentiality 777 */ 778 public void setConfidentiality(final boolean confidential) { 779 this.confidentiality = confidential; 780 } 781 782 /** 783 * Specify if GSSContext should request Message Integrity. 784 * Default is true. 785 * 786 * @param integrity pass true for message integrity 787 */ 788 public void setMessageIntegrity(final boolean integrity) { 789 this.messageIntegrity = integrity; 790 } 791 792 /** 793 * Specify if GSSContext should request Mutual Auth. 794 * Default is true. 795 * 796 * @param mutual pass true for mutual authentication 797 */ 798 public void setMutualAuth(final boolean mutual) { 799 this.mutualAuth = mutual; 800 } 801 802 /** 803 * Specify if if GSSContext should request should request Replay Detection. 804 * Default is true. 805 * 806 * @param replay pass true for replay detection 807 */ 808 public void setReplayDetection(final boolean replay) { 809 this.replayDetection = replay; 810 } 811 812 /** 813 * May override the default GET method. 814 * 815 * @param method 816 * 817 * @see java.net.HttpURLConnection#setRequestMethod(String) 818 */ 819 public void setRequestMethod(final String method) { 820 assertNotConnected(); 821 822 this.requestMethod = method; 823 } 824 825 /** 826 * Specify if if GSSContext should request Sequence Detection. 827 * Default is true. 828 * 829 * @param sequence pass true for sequence detection 830 */ 831 public void setSequenceDetection(final boolean sequence) { 832 this.sequenceDetection = sequence; 833 } 834}