001/* 002 * Copyright 2007-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2022 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2007-2022 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk; 037 038 039 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.List; 044import java.util.logging.Level; 045import javax.security.auth.callback.Callback; 046import javax.security.auth.callback.CallbackHandler; 047import javax.security.auth.callback.NameCallback; 048import javax.security.auth.callback.PasswordCallback; 049import javax.security.sasl.RealmCallback; 050import javax.security.sasl.RealmChoiceCallback; 051import javax.security.sasl.Sasl; 052import javax.security.sasl.SaslClient; 053 054import com.unboundid.asn1.ASN1OctetString; 055import com.unboundid.util.Debug; 056import com.unboundid.util.DebugType; 057import com.unboundid.util.InternalUseOnly; 058import com.unboundid.util.NotMutable; 059import com.unboundid.util.NotNull; 060import com.unboundid.util.Nullable; 061import com.unboundid.util.StaticUtils; 062import com.unboundid.util.ThreadSafety; 063import com.unboundid.util.ThreadSafetyLevel; 064import com.unboundid.util.Validator; 065 066import static com.unboundid.ldap.sdk.LDAPMessages.*; 067 068 069 070/** 071 * This class provides a SASL DIGEST-MD5 bind request implementation as 072 * described in <A HREF="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</A>. The 073 * DIGEST-MD5 mechanism can be used to authenticate over an insecure channel 074 * without exposing the credentials (although it requires that the server have 075 * access to the clear-text password). It is similar to CRAM-MD5, but provides 076 * better security by combining random data from both the client and the server, 077 * and allows for greater security and functionality, including the ability to 078 * specify an alternate authorization identity and the ability to use data 079 * integrity or confidentiality protection. 080 * <BR><BR> 081 * Elements included in a DIGEST-MD5 bind request include: 082 * <UL> 083 * <LI>Authentication ID -- A string which identifies the user that is 084 * attempting to authenticate. It should be an "authzId" value as 085 * described in section 5.2.1.8 of 086 * <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>. That is, 087 * it should be either "dn:" followed by the distinguished name of the 088 * target user, or "u:" followed by the username. If the "u:" form is 089 * used, then the mechanism used to resolve the provided username to an 090 * entry may vary from server to server.</LI> 091 * <LI>Authorization ID -- An optional string which specifies an alternate 092 * authorization identity that should be used for subsequent operations 093 * requested on the connection. Like the authentication ID, the 094 * authorization ID should use the "authzId" syntax.</LI> 095 * <LI>Realm -- An optional string which specifies the realm into which the 096 * user should authenticate.</LI> 097 * <LI>Password -- The clear-text password for the target user.</LI> 098 * </UL> 099 * <H2>Example</H2> 100 * The following example demonstrates the process for performing a DIGEST-MD5 101 * bind against a directory server with a username of "john.doe" and a password 102 * of "password": 103 * <PRE> 104 * DIGESTMD5BindRequest bindRequest = 105 * new DIGESTMD5BindRequest("u:john.doe", "password"); 106 * BindResult bindResult; 107 * try 108 * { 109 * bindResult = connection.bind(bindRequest); 110 * // If we get here, then the bind was successful. 111 * } 112 * catch (LDAPException le) 113 * { 114 * // The bind failed for some reason. 115 * bindResult = new BindResult(le.toLDAPResult()); 116 * ResultCode resultCode = le.getResultCode(); 117 * String errorMessageFromServer = le.getDiagnosticMessage(); 118 * } 119 * </PRE> 120 */ 121@NotMutable() 122@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 123public final class DIGESTMD5BindRequest 124 extends SASLBindRequest 125 implements CallbackHandler 126{ 127 /** 128 * The name for the DIGEST-MD5 SASL mechanism. 129 */ 130 @NotNull public static final String DIGESTMD5_MECHANISM_NAME = "DIGEST-MD5"; 131 132 133 134 /** 135 * The serial version UID for this serializable class. 136 */ 137 private static final long serialVersionUID = 867592367640540593L; 138 139 140 141 // The password for this bind request. 142 @NotNull private final ASN1OctetString password; 143 144 // The message ID from the last LDAP message sent from this request. 145 private int messageID = -1; 146 147 // The SASL quality of protection value(s) allowed for the DIGEST-MD5 bind 148 // request. 149 @NotNull private final List<SASLQualityOfProtection> allowedQoP; 150 151 // A list that will be updated with messages about any unhandled callbacks 152 // encountered during processing. 153 @NotNull private final List<String> unhandledCallbackMessages; 154 155 // The authentication ID string for this bind request. 156 @NotNull private final String authenticationID; 157 158 // The authorization ID string for this bind request, if available. 159 @Nullable private final String authorizationID; 160 161 // The realm form this bind request, if available. 162 @Nullable private final String realm; 163 164 165 166 /** 167 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 168 * ID and password. It will not include an authorization ID, a realm, or any 169 * controls. 170 * 171 * @param authenticationID The authentication ID for this bind request. It 172 * must not be {@code null}. 173 * @param password The password for this bind request. It must not 174 * be {@code null}. 175 */ 176 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 177 @NotNull final String password) 178 { 179 this(authenticationID, null, new ASN1OctetString(password), null, 180 NO_CONTROLS); 181 182 Validator.ensureNotNull(password); 183 } 184 185 186 187 /** 188 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 189 * ID and password. It will not include an authorization ID, a realm, or any 190 * controls. 191 * 192 * @param authenticationID The authentication ID for this bind request. It 193 * must not be {@code null}. 194 * @param password The password for this bind request. It must not 195 * be {@code null}. 196 */ 197 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 198 @NotNull final byte[] password) 199 { 200 this(authenticationID, null, new ASN1OctetString(password), null, 201 NO_CONTROLS); 202 203 Validator.ensureNotNull(password); 204 } 205 206 207 208 /** 209 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 210 * ID and password. It will not include an authorization ID, a realm, or any 211 * controls. 212 * 213 * @param authenticationID The authentication ID for this bind request. It 214 * must not be {@code null}. 215 * @param password The password for this bind request. It must not 216 * be {@code null}. 217 */ 218 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 219 @NotNull final ASN1OctetString password) 220 { 221 this(authenticationID, null, password, null, NO_CONTROLS); 222 } 223 224 225 226 /** 227 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 228 * 229 * @param authenticationID The authentication ID for this bind request. It 230 * must not be {@code null}. 231 * @param authorizationID The authorization ID for this bind request. It 232 * may be {@code null} if there will not be an 233 * alternate authorization identity. 234 * @param password The password for this bind request. It must not 235 * be {@code null}. 236 * @param realm The realm to use for the authentication. It may 237 * be {@code null} if the server supports a default 238 * realm. 239 * @param controls The set of controls to include in the request. 240 */ 241 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 242 @Nullable final String authorizationID, 243 @NotNull final String password, 244 @Nullable final String realm, 245 @Nullable final Control... controls) 246 { 247 this(authenticationID, authorizationID, new ASN1OctetString(password), 248 realm, controls); 249 250 Validator.ensureNotNull(password); 251 } 252 253 254 255 /** 256 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 257 * 258 * @param authenticationID The authentication ID for this bind request. It 259 * must not be {@code null}. 260 * @param authorizationID The authorization ID for this bind request. It 261 * may be {@code null} if there will not be an 262 * alternate authorization identity. 263 * @param password The password for this bind request. It must not 264 * be {@code null}. 265 * @param realm The realm to use for the authentication. It may 266 * be {@code null} if the server supports a default 267 * realm. 268 * @param controls The set of controls to include in the request. 269 */ 270 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 271 @Nullable final String authorizationID, 272 @NotNull final byte[] password, 273 @Nullable final String realm, 274 @Nullable final Control... controls) 275 { 276 this(authenticationID, authorizationID, new ASN1OctetString(password), 277 realm, controls); 278 279 Validator.ensureNotNull(password); 280 } 281 282 283 284 /** 285 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 286 * 287 * @param authenticationID The authentication ID for this bind request. It 288 * must not be {@code null}. 289 * @param authorizationID The authorization ID for this bind request. It 290 * may be {@code null} if there will not be an 291 * alternate authorization identity. 292 * @param password The password for this bind request. It must not 293 * be {@code null}. 294 * @param realm The realm to use for the authentication. It may 295 * be {@code null} if the server supports a default 296 * realm. 297 * @param controls The set of controls to include in the request. 298 */ 299 public DIGESTMD5BindRequest(@NotNull final String authenticationID, 300 @Nullable final String authorizationID, 301 @NotNull final ASN1OctetString password, 302 @Nullable final String realm, final 303 @Nullable Control... controls) 304 { 305 super(controls); 306 307 Validator.ensureNotNull(authenticationID, password); 308 309 this.authenticationID = authenticationID; 310 this.authorizationID = authorizationID; 311 this.password = password; 312 this.realm = realm; 313 314 allowedQoP = Collections.singletonList(SASLQualityOfProtection.AUTH); 315 316 unhandledCallbackMessages = new ArrayList<>(5); 317 } 318 319 320 321 /** 322 * Creates a new SASL DIGEST-MD5 bind request with the provided set of 323 * properties. 324 * 325 * @param properties The properties to use for this 326 * @param controls The set of controls to include in the request. 327 */ 328 public DIGESTMD5BindRequest( 329 @NotNull final DIGESTMD5BindRequestProperties properties, 330 @Nullable final Control... controls) 331 { 332 super(controls); 333 334 Validator.ensureNotNull(properties); 335 336 authenticationID = properties.getAuthenticationID(); 337 authorizationID = properties.getAuthorizationID(); 338 password = properties.getPassword(); 339 realm = properties.getRealm(); 340 allowedQoP = properties.getAllowedQoP(); 341 342 unhandledCallbackMessages = new ArrayList<>(5); 343 } 344 345 346 347 /** 348 * {@inheritDoc} 349 */ 350 @Override() 351 @NotNull() 352 public String getSASLMechanismName() 353 { 354 return DIGESTMD5_MECHANISM_NAME; 355 } 356 357 358 359 /** 360 * Retrieves the authentication ID for this bind request. 361 * 362 * @return The authentication ID for this bind request. 363 */ 364 @NotNull() 365 public String getAuthenticationID() 366 { 367 return authenticationID; 368 } 369 370 371 372 /** 373 * Retrieves the authorization ID for this bind request, if any. 374 * 375 * @return The authorization ID for this bind request, or {@code null} if 376 * there should not be a separate authorization identity. 377 */ 378 @Nullable() 379 public String getAuthorizationID() 380 { 381 return authorizationID; 382 } 383 384 385 386 /** 387 * Retrieves the string representation of the password for this bind request. 388 * 389 * @return The string representation of the password for this bind request. 390 */ 391 @NotNull() 392 public String getPasswordString() 393 { 394 return password.stringValue(); 395 } 396 397 398 399 /** 400 * Retrieves the bytes that comprise the the password for this bind request. 401 * 402 * @return The bytes that comprise the password for this bind request. 403 */ 404 @NotNull() 405 public byte[] getPasswordBytes() 406 { 407 return password.getValue(); 408 } 409 410 411 412 /** 413 * Retrieves the realm for this bind request, if any. 414 * 415 * @return The realm for this bind request, or {@code null} if none was 416 * defined and the server should use the default realm. 417 */ 418 @Nullable() 419 public String getRealm() 420 { 421 return realm; 422 } 423 424 425 426 /** 427 * Retrieves the list of allowed qualities of protection that may be used for 428 * communication that occurs on the connection after the authentication has 429 * completed, in order from most preferred to least preferred. 430 * 431 * @return The list of allowed qualities of protection that may be used for 432 * communication that occurs on the connection after the 433 * authentication has completed, in order from most preferred to 434 * least preferred. 435 */ 436 @NotNull() 437 public List<SASLQualityOfProtection> getAllowedQoP() 438 { 439 return allowedQoP; 440 } 441 442 443 444 /** 445 * Sends this bind request to the target server over the provided connection 446 * and returns the corresponding response. 447 * 448 * @param connection The connection to use to send this bind request to the 449 * server and read the associated response. 450 * @param depth The current referral depth for this request. It should 451 * always be one for the initial request, and should only 452 * be incremented when following referrals. 453 * 454 * @return The bind response read from the server. 455 * 456 * @throws LDAPException If a problem occurs while sending the request or 457 * reading the response. 458 */ 459 @Override() 460 @NotNull() 461 protected BindResult process(@NotNull final LDAPConnection connection, 462 final int depth) 463 throws LDAPException 464 { 465 unhandledCallbackMessages.clear(); 466 467 468 final HashMap<String,Object> saslProperties = 469 new HashMap<>(StaticUtils.computeMapCapacity(20)); 470 saslProperties.put(Sasl.QOP, SASLQualityOfProtection.toString(allowedQoP)); 471 saslProperties.put(Sasl.SERVER_AUTH, "false"); 472 473 final SaslClient saslClient; 474 try 475 { 476 final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME }; 477 saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap", 478 connection.getConnectedAddress(), 479 saslProperties, this); 480 } 481 catch (final Exception e) 482 { 483 Debug.debugException(e); 484 throw new LDAPException(ResultCode.LOCAL_ERROR, 485 ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get( 486 StaticUtils.getExceptionMessage(e)), 487 e); 488 } 489 490 final SASLClientBindHandler bindHandler = new SASLClientBindHandler(this, 491 connection, DIGESTMD5_MECHANISM_NAME, saslClient, getControls(), 492 getResponseTimeoutMillis(connection), unhandledCallbackMessages); 493 494 try 495 { 496 return bindHandler.processSASLBind(); 497 } 498 finally 499 { 500 messageID = bindHandler.getMessageID(); 501 } 502 } 503 504 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override() 510 @NotNull() 511 public DIGESTMD5BindRequest getRebindRequest(@NotNull final String host, 512 final int port) 513 { 514 final DIGESTMD5BindRequestProperties properties = 515 new DIGESTMD5BindRequestProperties(authenticationID, password); 516 properties.setAuthorizationID(authorizationID); 517 properties.setRealm(realm); 518 properties.setAllowedQoP(allowedQoP); 519 520 return new DIGESTMD5BindRequest(properties, getControls()); 521 } 522 523 524 525 /** 526 * Handles any necessary callbacks required for SASL authentication. 527 * 528 * @param callbacks The set of callbacks to be handled. 529 */ 530 @InternalUseOnly() 531 @Override() 532 public void handle(@NotNull final Callback[] callbacks) 533 { 534 for (final Callback callback : callbacks) 535 { 536 if (callback instanceof NameCallback) 537 { 538 ((NameCallback) callback).setName(authenticationID); 539 } 540 else if (callback instanceof PasswordCallback) 541 { 542 ((PasswordCallback) callback).setPassword( 543 password.stringValue().toCharArray()); 544 } 545 else if (callback instanceof RealmCallback) 546 { 547 final RealmCallback rc = (RealmCallback) callback; 548 if (realm == null) 549 { 550 final String defaultRealm = rc.getDefaultText(); 551 if (defaultRealm == null) 552 { 553 unhandledCallbackMessages.add( 554 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get( 555 String.valueOf(rc.getPrompt()))); 556 } 557 else 558 { 559 rc.setText(defaultRealm); 560 } 561 } 562 else 563 { 564 rc.setText(realm); 565 } 566 } 567 else if (callback instanceof RealmChoiceCallback) 568 { 569 final RealmChoiceCallback rcc = (RealmChoiceCallback) callback; 570 if (realm == null) 571 { 572 final String choices = 573 StaticUtils.concatenateStrings("{", " '", ",", "'", " }", 574 rcc.getChoices()); 575 unhandledCallbackMessages.add( 576 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get( 577 rcc.getPrompt(), choices)); 578 } 579 else 580 { 581 final String[] choices = rcc.getChoices(); 582 for (int i=0; i < choices.length; i++) 583 { 584 if (choices[i].equals(realm)) 585 { 586 rcc.setSelectedIndex(i); 587 break; 588 } 589 } 590 } 591 } 592 else 593 { 594 // This is an unexpected callback. 595 if (Debug.debugEnabled(DebugType.LDAP)) 596 { 597 Debug.debug(Level.WARNING, DebugType.LDAP, 598 "Unexpected DIGEST-MD5 SASL callback of type " + 599 callback.getClass().getName()); 600 } 601 602 unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get( 603 callback.getClass().getName())); 604 } 605 } 606 } 607 608 609 610 /** 611 * {@inheritDoc} 612 */ 613 @Override() 614 public int getLastMessageID() 615 { 616 return messageID; 617 } 618 619 620 621 /** 622 * {@inheritDoc} 623 */ 624 @Override() 625 @NotNull() 626 public DIGESTMD5BindRequest duplicate() 627 { 628 return duplicate(getControls()); 629 } 630 631 632 633 /** 634 * {@inheritDoc} 635 */ 636 @Override() 637 @NotNull() 638 public DIGESTMD5BindRequest duplicate(@Nullable final Control[] controls) 639 { 640 final DIGESTMD5BindRequestProperties properties = 641 new DIGESTMD5BindRequestProperties(authenticationID, password); 642 properties.setAuthorizationID(authorizationID); 643 properties.setRealm(realm); 644 properties.setAllowedQoP(allowedQoP); 645 646 final DIGESTMD5BindRequest bindRequest = 647 new DIGESTMD5BindRequest(properties, controls); 648 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 649 return bindRequest; 650 } 651 652 653 654 /** 655 * {@inheritDoc} 656 */ 657 @Override() 658 public void toString(@NotNull final StringBuilder buffer) 659 { 660 buffer.append("DIGESTMD5BindRequest(authenticationID='"); 661 buffer.append(authenticationID); 662 buffer.append('\''); 663 664 if (authorizationID != null) 665 { 666 buffer.append(", authorizationID='"); 667 buffer.append(authorizationID); 668 buffer.append('\''); 669 } 670 671 if (realm != null) 672 { 673 buffer.append(", realm='"); 674 buffer.append(realm); 675 buffer.append('\''); 676 } 677 678 buffer.append(", qop='"); 679 buffer.append(SASLQualityOfProtection.toString(allowedQoP)); 680 buffer.append('\''); 681 682 final Control[] controls = getControls(); 683 if (controls.length > 0) 684 { 685 buffer.append(", controls={"); 686 for (int i=0; i < controls.length; i++) 687 { 688 if (i > 0) 689 { 690 buffer.append(", "); 691 } 692 693 buffer.append(controls[i]); 694 } 695 buffer.append('}'); 696 } 697 698 buffer.append(')'); 699 } 700 701 702 703 /** 704 * {@inheritDoc} 705 */ 706 @Override() 707 public void toCode(@NotNull final List<String> lineList, 708 @NotNull final String requestID, 709 final int indentSpaces, final boolean includeProcessing) 710 { 711 // Create and update the bind request properties object. 712 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 713 "DIGESTMD5BindRequestProperties", 714 requestID + "RequestProperties", 715 "new DIGESTMD5BindRequestProperties", 716 ToCodeArgHelper.createString(authenticationID, "Authentication ID"), 717 ToCodeArgHelper.createString("---redacted-password---", "Password")); 718 719 if (authorizationID != null) 720 { 721 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 722 requestID + "RequestProperties.setAuthorizationID", 723 ToCodeArgHelper.createString(authorizationID, null)); 724 } 725 726 if (realm != null) 727 { 728 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 729 requestID + "RequestProperties.setRealm", 730 ToCodeArgHelper.createString(realm, null)); 731 } 732 733 final ArrayList<String> qopValues = new ArrayList<>(3); 734 for (final SASLQualityOfProtection qop : allowedQoP) 735 { 736 qopValues.add("SASLQualityOfProtection." + qop.name()); 737 } 738 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 739 requestID + "RequestProperties.setAllowedQoP", 740 ToCodeArgHelper.createRaw(qopValues, null)); 741 742 743 // Create the request variable. 744 final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(2); 745 constructorArgs.add( 746 ToCodeArgHelper.createRaw(requestID + "RequestProperties", null)); 747 748 final Control[] controls = getControls(); 749 if (controls.length > 0) 750 { 751 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 752 "Bind Controls")); 753 } 754 755 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 756 "DIGESTMD5BindRequest", requestID + "Request", 757 "new DIGESTMD5BindRequest", constructorArgs); 758 759 760 // Add lines for processing the request and obtaining the result. 761 if (includeProcessing) 762 { 763 // Generate a string with the appropriate indent. 764 final StringBuilder buffer = new StringBuilder(); 765 for (int i=0; i < indentSpaces; i++) 766 { 767 buffer.append(' '); 768 } 769 final String indent = buffer.toString(); 770 771 lineList.add(""); 772 lineList.add(indent + "try"); 773 lineList.add(indent + '{'); 774 lineList.add(indent + " BindResult " + requestID + 775 "Result = connection.bind(" + requestID + "Request);"); 776 lineList.add(indent + " // The bind was processed successfully."); 777 lineList.add(indent + '}'); 778 lineList.add(indent + "catch (LDAPException e)"); 779 lineList.add(indent + '{'); 780 lineList.add(indent + " // The bind failed. Maybe the following will " + 781 "help explain why."); 782 lineList.add(indent + " // Note that the connection is now likely in " + 783 "an unauthenticated state."); 784 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 785 lineList.add(indent + " String message = e.getMessage();"); 786 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 787 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 788 lineList.add(indent + " Control[] responseControls = " + 789 "e.getResponseControls();"); 790 lineList.add(indent + '}'); 791 } 792 } 793}