001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2007-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.admin.client.cli;
018
019import static com.forgerock.opendj.cli.CommonArguments.*;
020import static com.forgerock.opendj.cli.ReturnCode.*;
021import static com.forgerock.opendj.cli.Utils.*;
022
023import static org.opends.messages.AdminToolMessages.*;
024import static org.opends.messages.ToolMessages.*;
025
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.IOException;
029import java.net.InetAddress;
030import java.security.KeyStore;
031import java.security.KeyStoreException;
032import java.security.NoSuchAlgorithmException;
033import java.security.cert.CertificateException;
034import java.util.ArrayList;
035import java.util.LinkedHashSet;
036import java.util.List;
037import java.util.Set;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizableMessageBuilder;
041import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
042import org.forgerock.i18n.slf4j.LocalizedLogger;
043import org.forgerock.opendj.config.server.ConfigException;
044import org.forgerock.opendj.server.config.server.AdministrationConnectorCfg;
045import org.forgerock.opendj.server.config.server.FileBasedTrustManagerProviderCfg;
046import org.forgerock.opendj.server.config.server.RootCfg;
047import org.forgerock.opendj.server.config.server.TrustManagerProviderCfg;
048import org.opends.admin.ads.util.ApplicationTrustManager;
049import org.opends.server.config.AdministrationConnector;
050import org.opends.server.core.DirectoryServer;
051
052import com.forgerock.opendj.cli.Argument;
053import com.forgerock.opendj.cli.ArgumentException;
054import com.forgerock.opendj.cli.ArgumentParser;
055import com.forgerock.opendj.cli.BooleanArgument;
056import com.forgerock.opendj.cli.CliConstants;
057import com.forgerock.opendj.cli.FileBasedArgument;
058import com.forgerock.opendj.cli.IntegerArgument;
059import com.forgerock.opendj.cli.StringArgument;
060
061/**
062 * This is a commodity class that can be used to check the arguments required to
063 * establish a secure connection in the command line. It can be used to generate
064 * an ApplicationTrustManager object based on the options provided by the user
065 * in the command line.
066 */
067public final class SecureConnectionCliArgs
068{
069  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
070
071  private StringArgument hostNameArg;
072  private IntegerArgument portArg;
073  private StringArgument bindDnArg;
074  private StringArgument adminUidArg;
075  private FileBasedArgument bindPasswordFileArg;
076  private StringArgument bindPasswordArg;
077  private BooleanArgument trustAllArg;
078  private StringArgument trustStorePathArg;
079  private StringArgument trustStorePasswordArg;
080  private FileBasedArgument trustStorePasswordFileArg;
081  private StringArgument keyStorePathArg;
082  private StringArgument keyStorePasswordArg;
083  private FileBasedArgument keyStorePasswordFileArg;
084  private StringArgument certNicknameArg;
085  private BooleanArgument useSSLArg;
086  private BooleanArgument useStartTLSArg;
087  private StringArgument saslOptionArg;
088  private IntegerArgument connectTimeoutArg;
089
090  /** Private container for global arguments. */
091  private Set<Argument> argList;
092
093  /** The trust manager. */
094  private ApplicationTrustManager trustManager;
095
096  private boolean configurationInitialized;
097
098  /** Defines if the CLI always use the SSL connection type. */
099  private final boolean alwaysSSL;
100
101  /**
102   * Creates a new instance of secure arguments.
103   *
104   * @param alwaysSSL
105   *          If true, always use the SSL connection type. In this case, the
106   *          arguments useSSL and startTLS are not present.
107   */
108  public SecureConnectionCliArgs(boolean alwaysSSL)
109  {
110    this.alwaysSSL = alwaysSSL;
111  }
112
113  /**
114   * Indicates whether any of the arguments are present.
115   *
116   * @return boolean where true indicates that at least one of the arguments is
117   *         present
118   */
119  public boolean argumentsPresent()
120  {
121    if (argList != null)
122    {
123      for (Argument arg : argList)
124      {
125        if (arg.isPresent())
126        {
127          return true;
128        }
129      }
130    }
131    return false;
132  }
133
134  /**
135   * Get the admin UID which has to be used for the command.
136   *
137   * @return The admin UID specified by the command line argument, or the
138   *         default value, if not specified.
139   */
140  public String getAdministratorUID()
141  {
142    if (adminUidArg.isPresent())
143    {
144      return adminUidArg.getValue();
145    }
146    return adminUidArg.getDefaultValue();
147  }
148
149  /**
150   * Get the bindDN which has to be used for the command.
151   *
152   * @return The bindDN specified by the command line argument, or the default
153   *         value, if not specified.
154   */
155  public String getBindDN()
156  {
157    if (bindDnArg.isPresent())
158    {
159      return bindDnArg.getValue();
160    }
161    return bindDnArg.getDefaultValue();
162  }
163
164  /**
165   * Initialize Global option.
166   *
167   * @throws ArgumentException
168   *           If there is a problem with any of the parameters used to create
169   *           this argument.
170   * @return a ArrayList with the options created.
171   */
172  public Set<Argument> createGlobalArguments() throws ArgumentException
173  {
174    argList = new LinkedHashSet<>();
175
176    useSSLArg = useSSLArgument();
177    if (!alwaysSSL)
178    {
179      argList.add(useSSLArg);
180    }
181    else
182    {
183      // simulate that the useSSL arg has been given in the CLI
184      useSSLArg.setPresent(true);
185    }
186
187    useStartTLSArg = startTLSArgument();
188    if (!alwaysSSL)
189    {
190      argList.add(useStartTLSArg);
191    }
192
193    hostNameArg = hostNameArgument(getDefaultHostName());
194    argList.add(hostNameArg);
195
196    portArg = createPortArgument(AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT);
197    argList.add(portArg);
198
199    bindDnArg = bindDNArgument(CliConstants.DEFAULT_ROOT_USER_DN);
200    argList.add(bindDnArg);
201
202    // Classes that required admin UID to be not hidden must call createVisibleAdminUidArgument(localizedDescription)
203    adminUidArg = adminUidHiddenArgument(INFO_DESCRIPTION_ADMIN_UID.get());
204
205    bindPasswordArg = bindPasswordArgument();
206    argList.add(bindPasswordArg);
207
208    bindPasswordFileArg = bindPasswordFileArgument();
209    argList.add(bindPasswordFileArg);
210
211    saslOptionArg = saslArgument();
212    argList.add(saslOptionArg);
213
214    trustAllArg = trustAllArgument();
215    argList.add(trustAllArg);
216
217    trustStorePathArg = trustStorePathArgument();
218    argList.add(trustStorePathArg);
219
220    trustStorePasswordArg = trustStorePasswordArgument();
221    argList.add(trustStorePasswordArg);
222
223    trustStorePasswordFileArg = trustStorePasswordFileArgument();
224    argList.add(trustStorePasswordFileArg);
225
226    keyStorePathArg = keyStorePathArgument();
227    argList.add(keyStorePathArg);
228
229    keyStorePasswordArg = keyStorePasswordArgument();
230    argList.add(keyStorePasswordArg);
231
232    keyStorePasswordFileArg = keyStorePasswordFileArgument();
233    argList.add(keyStorePasswordFileArg);
234
235    certNicknameArg = certNickNameArgument();
236    argList.add(certNicknameArg);
237
238    connectTimeoutArg = connectTimeOutArgument();
239    argList.add(connectTimeoutArg);
240
241    return argList;
242  }
243
244  /**
245   * Get the host name which has to be used for the command.
246   *
247   * @return The host name specified by the command line argument, or the
248   *         default value, if not specified.
249   */
250  public String getHostName()
251  {
252    if (hostNameArg.isPresent())
253    {
254      return hostNameArg.getValue();
255    }
256    return hostNameArg.getDefaultValue();
257  }
258
259  /**
260   * Returns the current hostname.
261   *
262   * If the hostname resolution fails, this method returns {@literal "localhost"}.
263   * @return the current hostname
264     */
265  public String getDefaultHostName() {
266    try
267    {
268      return InetAddress.getLocalHost().getHostName();
269    }
270    catch (Exception e)
271    {
272      return "localhost";
273    }
274  }
275
276  /**
277   * Get the port which has to be used for the command.
278   *
279   * @return The port specified by the command line argument, or the default
280   *         value, if not specified.
281   */
282  public String getPort()
283  {
284    if (portArg.isPresent())
285    {
286      return portArg.getValue();
287    }
288    return portArg.getDefaultValue();
289  }
290
291  /**
292   * Indication if provided global options are validate.
293   *
294   * @param buf
295   *          the LocalizableMessageBuilder to write the error messages.
296   * @return return code.
297   */
298  int validateGlobalOptions(LocalizableMessageBuilder buf)
299  {
300    final List<LocalizableMessage> errors = new ArrayList<>();
301    addErrorMessageIfArgumentsConflict(errors, bindPasswordArg, bindPasswordFileArg);
302    addErrorMessageIfArgumentsConflict(errors, trustAllArg, trustStorePathArg);
303    addErrorMessageIfArgumentsConflict(errors, trustAllArg, trustStorePasswordArg);
304    addErrorMessageIfArgumentsConflict(errors, trustAllArg, trustStorePasswordFileArg);
305    addErrorMessageIfArgumentsConflict(errors, trustStorePasswordArg, trustStorePasswordFileArg);
306    addErrorMessageIfArgumentsConflict(errors, useStartTLSArg, useSSLArg);
307
308    checkIfPathArgumentIsReadable(errors, trustStorePathArg, ERR_CANNOT_READ_TRUSTSTORE);
309    checkIfPathArgumentIsReadable(errors, keyStorePathArg, ERR_CANNOT_READ_KEYSTORE);
310
311    if (!errors.isEmpty())
312    {
313      for (LocalizableMessage error : errors)
314      {
315        if (buf.length() > 0)
316        {
317          buf.append(LINE_SEPARATOR);
318        }
319        buf.append(error);
320      }
321      return CONFLICTING_ARGS.get();
322    }
323
324    return SUCCESS.get();
325  }
326
327  private void checkIfPathArgumentIsReadable(List<LocalizableMessage> errors, StringArgument pathArg, Arg1<Object> msg)
328  {
329    if (pathArg.isPresent() && !canRead(pathArg.getValue()))
330    {
331      errors.add(msg.get(pathArg.getValue()));
332    }
333  }
334
335  /**
336   * Indicate if the SSL mode is always used.
337   *
338   * @return True if SSL mode is always used.
339   */
340  public boolean alwaysSSL()
341  {
342    return alwaysSSL;
343  }
344
345  /**
346   * Handle TrustStore.
347   *
348   * @return The trustStore manager to be used for the command.
349   */
350  public ApplicationTrustManager getTrustManager()
351  {
352    if (trustManager == null)
353    {
354      KeyStore truststore = null;
355      if (trustAllArg.isPresent())
356      {
357        // Running a null TrustManager  will force createLdapsContext and
358        // createStartTLSContext to use a bindTrustManager.
359        return null;
360      }
361      else if (trustStorePathArg.isPresent())
362      {
363        try (final FileInputStream fos = new FileInputStream(trustStorePathArg.getValue()))
364        {
365          String trustStorePasswordStringValue = null;
366          if (trustStorePasswordArg.isPresent())
367          {
368            trustStorePasswordStringValue = trustStorePasswordArg.getValue();
369          }
370          else if (trustStorePasswordFileArg.isPresent())
371          {
372            trustStorePasswordStringValue = trustStorePasswordFileArg.getValue();
373          }
374
375          if (trustStorePasswordStringValue != null)
376          {
377            trustStorePasswordStringValue = System.getProperty("javax.net.ssl.trustStorePassword");
378          }
379
380          char[] trustStorePasswordValue = null;
381          if (trustStorePasswordStringValue != null)
382          {
383            trustStorePasswordValue = trustStorePasswordStringValue.toCharArray();
384          }
385
386          truststore = KeyStore.getInstance(KeyStore.getDefaultType());
387          truststore.load(fos, trustStorePasswordValue);
388        }
389        catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e)
390        {
391          // Nothing to do: if this occurs we will systematically refuse the
392          // certificates.  Maybe we should avoid this and be strict, but we
393          // are in a best effort mode.
394          logger.warn(LocalizableMessage.raw("Error with the truststore"), e);
395        }
396      }
397      trustManager = new ApplicationTrustManager(truststore);
398    }
399    return trustManager;
400  }
401
402  /**
403   * Returns {@code true} if we can read on the provided path and {@code false}
404   * otherwise.
405   *
406   * @param path
407   *          the path.
408   * @return {@code true} if we can read on the provided path and {@code false}
409   *         otherwise.
410   */
411  private boolean canRead(String path)
412  {
413    final File file = new File(path);
414    return file.exists() && file.canRead();
415  }
416
417  /**
418   * Returns the absolute path of the trust store file that appears on the
419   * config. Returns {@code null} if the trust store is not defined or it does
420   * not exist.
421   *
422   * @return the absolute path of the trust store file that appears on the
423   *         config.
424   * @throws ConfigException
425   *           if there is an error reading the configuration.
426   */
427  public String getTruststoreFileFromConfig() throws ConfigException
428  {
429    String truststoreFileAbsolute = null;
430    TrustManagerProviderCfg trustManagerCfg = null;
431    AdministrationConnectorCfg administrationConnectorCfg = null;
432
433    boolean couldInitializeConfig = configurationInitialized;
434    // Initialization for admin framework
435    if (!configurationInitialized)
436    {
437      couldInitializeConfig = initializeConfiguration();
438    }
439    if (couldInitializeConfig)
440    {
441      RootCfg root = DirectoryServer.getInstance().getServerContext().getRootConfig();
442      administrationConnectorCfg = root.getAdministrationConnector();
443
444      String trustManagerStr = administrationConnectorCfg.getTrustManagerProvider();
445      trustManagerCfg = root.getTrustManagerProvider(trustManagerStr);
446      if (trustManagerCfg instanceof FileBasedTrustManagerProviderCfg)
447      {
448        FileBasedTrustManagerProviderCfg fileBasedTrustManagerCfg = (FileBasedTrustManagerProviderCfg) trustManagerCfg;
449        String truststoreFile = fileBasedTrustManagerCfg.getTrustStoreFile();
450        // Check the file
451        if (truststoreFile.startsWith(File.separator))
452        {
453          truststoreFileAbsolute = truststoreFile;
454        }
455        else
456        {
457          truststoreFileAbsolute = DirectoryServer.getInstanceRoot() + File.separator + truststoreFile;
458        }
459        File f = new File(truststoreFileAbsolute);
460        if (!f.exists() || !f.canRead() || f.isDirectory())
461        {
462          truststoreFileAbsolute = null;
463        }
464        else
465        {
466          // Try to get the canonical path.
467          try
468          {
469            truststoreFileAbsolute = f.getCanonicalPath();
470          }
471          catch (Throwable t)
472          {
473            // We can ignore this error.
474          }
475        }
476      }
477    }
478    return truststoreFileAbsolute;
479  }
480
481  /**
482   * Returns the admin port from the configuration.
483   *
484   * @return the admin port from the configuration.
485   * @throws ConfigException
486   *           if an error occurs reading the configuration.
487   */
488  public int getAdminPortFromConfig() throws ConfigException
489  {
490    // Initialization for admin framework
491    boolean couldInitializeConfiguration = configurationInitialized;
492    if (!configurationInitialized)
493    {
494      couldInitializeConfiguration = initializeConfiguration();
495    }
496    if (couldInitializeConfiguration)
497    {
498      RootCfg root = DirectoryServer.getInstance().getServerContext().getRootConfig();
499      return root.getAdministrationConnector().getListenPort();
500    }
501    else
502    {
503      return AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT;
504    }
505  }
506
507  private boolean initializeConfiguration()
508  {
509    // check if the initialization is required
510    try
511    {
512      DirectoryServer.getInstance().getServerContext().getRootConfig().getAdministrationConnector();
513    }
514    catch (Throwable th)
515    {
516      try
517      {
518        DirectoryServer.bootstrapClient();
519        DirectoryServer.initializeJMX();
520        DirectoryServer.getInstance().initializeConfiguration();
521      }
522      catch (Exception ex)
523      {
524        // do nothing
525        return false;
526      }
527    }
528    configurationInitialized = true;
529    return true;
530  }
531
532  /**
533   * Returns the port to be used according to the configuration and the
534   * arguments provided by the user. This method should be called after the
535   * arguments have been parsed.
536   *
537   * @return the port to be used according to the configuration and the
538   *         arguments provided by the user.
539   */
540  public int getPortFromConfig()
541  {
542    int portNumber;
543    if (alwaysSSL())
544    {
545      portNumber = AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT;
546      // Try to get the port from the config file
547      try
548      {
549        portNumber = getAdminPortFromConfig();
550      }
551      catch (ConfigException ex)
552      {
553        // Nothing to do
554      }
555    }
556    else
557    {
558      portNumber = CliConstants.DEFAULT_SSL_PORT;
559    }
560    return portNumber;
561  }
562
563  /**
564   * Updates the default values of the port and the trust store with what is
565   * read in the configuration.
566   *
567   * @param parser
568   *        The argument parser where the secure connection arguments were added.
569   */
570  public void initArgumentsWithConfiguration(final ArgumentParser parser) {
571    try
572    {
573      portArg = createPortArgument(getPortFromConfig());
574      trustStorePathArg = trustStorePathArgument(getTruststoreFileFromConfig());
575      parser.replaceArgument(portArg);
576      parser.replaceArgument(trustStorePathArg);
577    }
578    catch (ConfigException | ArgumentException e)
579    {
580      logger.error(LocalizableMessage.raw(
581              "Internal error while reading arguments of this program from configuration"), e);
582    }
583  }
584
585  /**
586   * Replace the admin UID argument by a non hidden one.
587   *
588   * @param description
589   *         The localized description for the non hidden admin UID argument.
590   */
591  public void createVisibleAdminUidArgument(final LocalizableMessage description)
592  {
593    try
594    {
595      this.adminUidArg = adminUid(description);
596    }
597    catch (final ArgumentException unexpected)
598    {
599      throw new RuntimeException("Unexpected");
600    }
601  }
602
603  private IntegerArgument createPortArgument(final int defaultValue) throws ArgumentException
604  {
605    return portArgument(
606            defaultValue, alwaysSSL ? INFO_DESCRIPTION_ADMIN_PORT.get() : INFO_DESCRIPTION_PORT.get());
607  }
608
609  /**
610   * Return the 'keyStore' global argument.
611   *
612   * @return The 'keyStore' global argument.
613   */
614  public StringArgument getKeyStorePathArg() {
615    return keyStorePathArg;
616  }
617
618  /**
619   * Return the 'hostName' global argument.
620   *
621   * @return The 'hostName' global argument.
622   */
623  public StringArgument getHostNameArg() {
624    return hostNameArg;
625  }
626
627  /**
628   * Return the 'port' global argument.
629   *
630   * @return The 'port' global argument.
631   */
632  public IntegerArgument getPortArg() {
633    return portArg;
634  }
635
636  /**
637   * Return the 'bindDN' global argument.
638   *
639   * @return The 'bindDN' global argument.
640   */
641  public StringArgument getBindDnArg() {
642    return bindDnArg;
643  }
644
645  /**
646   * Return the 'adminUID' global argument.
647   *
648   * @return The 'adminUID' global argument.
649   */
650  public StringArgument getAdminUidArg() {
651    return adminUidArg;
652  }
653
654  /**
655   * Return the 'bindPasswordFile' global argument.
656   *
657   * @return The 'bindPasswordFile' global argument.
658   */
659  public FileBasedArgument getBindPasswordFileArg() {
660    return bindPasswordFileArg;
661  }
662
663  /**
664   * Return the 'bindPassword' global argument.
665   *
666   * @return The 'bindPassword' global argument.
667   */
668  public StringArgument getBindPasswordArg() {
669    return bindPasswordArg;
670  }
671
672  /**
673   * Return the 'trustAllArg' global argument.
674   *
675   * @return The 'trustAllArg' global argument.
676   */
677  public BooleanArgument getTrustAllArg() {
678    return trustAllArg;
679  }
680
681  /**
682   * Return the 'trustStore' global argument.
683   *
684   * @return The 'trustStore' global argument.
685   */
686  public StringArgument getTrustStorePathArg() {
687    return trustStorePathArg;
688  }
689
690  /**
691   * Return the 'trustStorePassword' global argument.
692   *
693   * @return The 'trustStorePassword' global argument.
694   */
695  public StringArgument getTrustStorePasswordArg() {
696    return trustStorePasswordArg;
697  }
698
699  /**
700   * Return the 'trustStorePasswordFile' global argument.
701   *
702   * @return The 'trustStorePasswordFile' global argument.
703   */
704  public FileBasedArgument getTrustStorePasswordFileArg() {
705    return trustStorePasswordFileArg;
706  }
707
708  /**
709   * Return the 'keyStorePassword' global argument.
710   *
711   * @return The 'keyStorePassword' global argument.
712   */
713  public StringArgument getKeyStorePasswordArg() {
714    return keyStorePasswordArg;
715  }
716
717  /**
718   * Return the 'keyStorePasswordFile' global argument.
719   *
720   * @return The 'keyStorePasswordFile' global argument.
721   */
722  public FileBasedArgument getKeyStorePasswordFileArg() {
723    return keyStorePasswordFileArg;
724  }
725
726  /**
727   * Return the 'certNicknameArg' global argument.
728   *
729   * @return The 'certNicknameArg' global argument.
730   */
731  public StringArgument getCertNicknameArg() {
732    return certNicknameArg;
733  }
734
735  /**
736   * Return the 'useSSLArg' global argument.
737   *
738   * @return The 'useSSLArg' global argument.
739   */
740  public BooleanArgument getUseSSLArg() {
741    return useSSLArg;
742  }
743
744  /**
745   * Return the 'useStartTLSArg' global argument.
746   *
747   * @return The 'useStartTLSArg' global argument.
748   */
749  public BooleanArgument getUseStartTLSArg() {
750    return useStartTLSArg;
751  }
752
753  /**
754   * Return the 'saslOption' argument.
755   *
756   * @return the 'saslOption' argument.
757   */
758  public StringArgument getSaslOptionArg() {
759    return saslOptionArg;
760  }
761
762  /**
763   * Return the 'connectTimeout' argument.
764   *
765   * @return the 'connectTimeout' argument.
766   */
767  public IntegerArgument getConnectTimeoutArg() {
768    return connectTimeoutArg;
769  }
770
771  /**
772   * Set the bind DN argument with the provided description.
773   * Note that this method will create a new {@link Argument} instance replacing the current one.
774   *
775   * @param description
776   *         The localized description which will be used in help messages.
777   */
778  public void setBindDnArgDescription(final LocalizableMessage description)
779  {
780    try
781    {
782      this.bindDnArg = bindDNArgument(CliConstants.DEFAULT_ROOT_USER_DN, description);
783    }
784    catch (final ArgumentException unexpected)
785    {
786      throw new RuntimeException("unexpected");
787    }
788  }
789
790  /**
791   * Set the bind password argument.
792   *
793   * @param bindPasswordArg
794   *         The argument which will replace the current one.
795   */
796  public void setBindPasswordArgument(final StringArgument bindPasswordArg)
797  {
798    this.bindPasswordArg = bindPasswordArg;
799  }
800
801  /**
802   * Set the bind password file argument.
803   *
804   * @param bindPasswordFileArg
805   *         The argument which will replace the current one.
806   */
807  public void setBindPasswordFileArgument(final FileBasedArgument bindPasswordFileArg)
808  {
809    this.bindPasswordFileArg = bindPasswordFileArg;
810  }
811}