1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.catalina.realm;
18
19 import java.beans.PropertyChangeListener;
20 import java.beans.PropertyChangeSupport;
21 import java.io.IOException;
22 import java.io.UnsupportedEncodingException;
23 import java.nio.charset.Charset;
24 import java.nio.charset.StandardCharsets;
25 import java.security.NoSuchAlgorithmException;
26 import java.security.Principal;
27 import java.security.cert.X509Certificate;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31
32 import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
33 import javax.servlet.http.HttpServletResponse;
34
35 import org.apache.catalina.Container;
36 import org.apache.catalina.Context;
37 import org.apache.catalina.CredentialHandler;
38 import org.apache.catalina.Engine;
39 import org.apache.catalina.Host;
40 import org.apache.catalina.LifecycleException;
41 import org.apache.catalina.LifecycleState;
42 import org.apache.catalina.Realm;
43 import org.apache.catalina.Server;
44 import org.apache.catalina.Service;
45 import org.apache.catalina.Wrapper;
46 import org.apache.catalina.connector.Request;
47 import org.apache.catalina.connector.Response;
48 import org.apache.catalina.util.LifecycleMBeanBase;
49 import org.apache.catalina.util.SessionConfig;
50 import org.apache.catalina.util.ToStringUtil;
51 import org.apache.juli.logging.Log;
52 import org.apache.juli.logging.LogFactory;
53 import org.apache.tomcat.util.IntrospectionUtils;
54 import org.apache.tomcat.util.buf.B2CConverter;
55 import org.apache.tomcat.util.descriptor.web.SecurityCollection;
56 import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
57 import org.apache.tomcat.util.res.StringManager;
58 import org.apache.tomcat.util.security.ConcurrentMessageDigest;
59 import org.apache.tomcat.util.security.MD5Encoder;
60 import org.ietf.jgss.GSSContext;
61 import org.ietf.jgss.GSSCredential;
62 import org.ietf.jgss.GSSException;
63 import org.ietf.jgss.GSSName;
64
65 /**
66  * Simple implementation of <b>Realm</b> that reads an XML file to configure
67  * the valid users, passwords, and roles.  The file format (and default file
68  * location) are identical to those currently supported by Tomcat 3.X.
69  *
70  * @author Craig R. McClanahan
71  */

72 public abstract class RealmBase extends LifecycleMBeanBase implements Realm {
73
74     private static final Log log = LogFactory.getLog(RealmBase.class);
75
76     private static final List<Class<? extends DigestCredentialHandlerBase>> credentialHandlerClasses =
77             new ArrayList<>();
78
79     static {
80         // Order is important since it determines the search order for a
81         // matching handler if only an algorithm is specified when calling
82         // main()
83         credentialHandlerClasses.add(MessageDigestCredentialHandler.class);
84         credentialHandlerClasses.add(SecretKeyCredentialHandler.class);
85     }
86
87     // ----------------------------------------------------- Instance Variables
88
89
90     /**
91      * The Container with which this Realm is associated.
92      */

93     protected Container container = null;
94
95
96     /**
97      * Container log
98      */

99     protected Log containerLog = null;
100
101
102     private CredentialHandler credentialHandler;
103
104
105     /**
106      * The string manager for this package.
107      */

108     protected static final StringManager sm = StringManager.getManager(RealmBase.class);
109
110
111     /**
112      * The property change support for this component.
113      */

114     protected final PropertyChangeSupport support = new PropertyChangeSupport(this);
115
116
117     /**
118      * Should we validate client certificate chains when they are presented?
119      */

120     protected boolean validate = true;
121
122     /**
123      * The name of the class to use for retrieving user names from X509
124      * certificates.
125      */

126     protected String x509UsernameRetrieverClassName;
127
128     /**
129      * The object that will extract user names from X509 client certificates.
130      */

131     protected X509UsernameRetriever x509UsernameRetriever;
132
133     /**
134      * The all role mode.
135      */

136     protected AllRolesMode allRolesMode = AllRolesMode.STRICT_MODE;
137
138
139     /**
140      * When processing users authenticated via the GSS-API, should any
141      * &quot;@...&quot; be stripped from the end of the user name?
142      */

143     protected boolean stripRealmForGss = true;
144
145
146     private int transportGuaranteeRedirectStatus = HttpServletResponse.SC_FOUND;
147
148
149     // ------------------------------------------------------------- Properties
150
151     /**
152      * @return The HTTP status code used when the container needs to issue an
153      *         HTTP redirect to meet the requirements of a configured transport
154      *         guarantee.
155      */

156     public int getTransportGuaranteeRedirectStatus() {
157         return transportGuaranteeRedirectStatus;
158     }
159
160
161     /**
162      * Set the HTTP status code used when the container needs to issue an HTTP
163      * redirect to meet the requirements of a configured transport guarantee.
164      *
165      * @param transportGuaranteeRedirectStatus The status to use. This value is
166      *                                         not validated
167      */

168     public void setTransportGuaranteeRedirectStatus(int transportGuaranteeRedirectStatus) {
169         this.transportGuaranteeRedirectStatus = transportGuaranteeRedirectStatus;
170     }
171
172
173     @Override
174     public CredentialHandler getCredentialHandler() {
175         return credentialHandler;
176     }
177
178
179     @Override
180     public void setCredentialHandler(CredentialHandler credentialHandler) {
181         this.credentialHandler = credentialHandler;
182     }
183
184
185     /**
186      * Return the Container with which this Realm has been associated.
187      */

188     @Override
189     public Container getContainer() {
190         return container;
191     }
192
193
194     /**
195      * Set the Container with which this Realm has been associated.
196      *
197      * @param container The associated Container
198      */

199     @Override
200     public void setContainer(Container container) {
201
202         Container oldContainer = this.container;
203         this.container = container;
204         support.firePropertyChange("container", oldContainer, this.container);
205
206     }
207
208     /**
209      * Return the all roles mode.
210      * @return A string representation of the current all roles mode
211      */

212     public String getAllRolesMode() {
213         return allRolesMode.toString();
214     }
215
216
217     /**
218      * Set the all roles mode.
219      * @param allRolesMode A string representation of the new all roles mode
220      */

221     public void setAllRolesMode(String allRolesMode) {
222         this.allRolesMode = AllRolesMode.toMode(allRolesMode);
223     }
224
225
226     /**
227      * Return the "validate certificate chains" flag.
228      * @return The value of the validate certificate chains flag
229      */

230     public boolean getValidate() {
231         return validate;
232     }
233
234
235     /**
236      * Set the "validate certificate chains" flag.
237      *
238      * @param validate The new validate certificate chains flag
239      */

240     public void setValidate(boolean validate) {
241
242         this.validate = validate;
243
244     }
245
246     /**
247      * Gets the name of the class that will be used to extract user names
248      * from X509 client certificates.
249      * @return The name of the class that will be used to extract user names
250      *         from X509 client certificates.
251      */

252     public String getX509UsernameRetrieverClassName() {
253         return x509UsernameRetrieverClassName;
254     }
255
256     /**
257      * Sets the name of the class that will be used to extract user names
258      * from X509 client certificates. The class must implement
259      * X509UsernameRetriever.
260      *
261      * @param className The name of the class that will be used to extract user names
262      *                  from X509 client certificates.
263      * @see X509UsernameRetriever
264      */

265     public void setX509UsernameRetrieverClassName(String className) {
266         this.x509UsernameRetrieverClassName = className;
267     }
268
269     public boolean isStripRealmForGss() {
270         return stripRealmForGss;
271     }
272
273
274     public void setStripRealmForGss(boolean stripRealmForGss) {
275         this.stripRealmForGss = stripRealmForGss;
276     }
277
278
279     // --------------------------------------------------------- Public Methods
280
281
282     /**
283      * Add a property change listener to this component.
284      *
285      * @param listener The listener to add
286      */

287     @Override
288     public void addPropertyChangeListener(PropertyChangeListener listener) {
289
290         support.addPropertyChangeListener(listener);
291
292     }
293
294
295     /**
296      * Return the Principal associated with the specified username, if there
297      * is one; otherwise return <code>null</code>.
298      *
299      * @param username Username of the Principal to look up
300      */

301     @Override
302     public Principal authenticate(String username) {
303
304         if (username == null) {
305             return null;
306         }
307
308         if (containerLog.isTraceEnabled()) {
309             containerLog.trace(sm.getString("realmBase.authenticateSuccess", username));
310         }
311
312         return getPrincipal(username);
313     }
314
315
316     /**
317      * Return the Principal associated with the specified username and
318      * credentials, if there is one; otherwise return <code>null</code>.
319      *
320      * @param username Username of the Principal to look up
321      * @param credentials Password or other credentials to use in
322      *  authenticating this username
323      * @return the associated principal, or <code>null</code> if there is none.
324      */

325     @Override
326     public Principal authenticate(String username, String credentials) {
327         // No user or no credentials
328         // Can't possibly authenticate, don't bother doing anything.
329         if(username == null || credentials == null) {
330             if (containerLog.isTraceEnabled()) {
331                 containerLog.trace(sm.getString("realmBase.authenticateFailure",
332                                                 username));
333             }
334             return null;
335         }
336
337         // Look up the user's credentials
338         String serverCredentials = getPassword(username);
339
340         if (serverCredentials == null) {
341             // User was not found
342             // Waste a bit of time as not to reveal that the user does not exist.
343             getCredentialHandler().mutate(credentials);
344
345             if (containerLog.isTraceEnabled()) {
346                 containerLog.trace(sm.getString("realmBase.authenticateFailure",
347                                                 username));
348             }
349             return null;
350         }
351
352         boolean validated = getCredentialHandler().matches(credentials, serverCredentials);
353
354         if (validated) {
355             if (containerLog.isTraceEnabled()) {
356                 containerLog.trace(sm.getString("realmBase.authenticateSuccess",
357                                                 username));
358             }
359             return getPrincipal(username);
360         } else {
361             if (containerLog.isTraceEnabled()) {
362                 containerLog.trace(sm.getString("realmBase.authenticateFailure",
363                                                 username));
364             }
365             return null;
366         }
367     }
368
369
370     /**
371      * Try to authenticate with the specified username, which
372      * matches the digest calculated using the given parameters using the
373      * method described in RFC 2617 (which is a superset of RFC 2069).
374      *
375      * @param username Username of the Principal to look up
376      * @param clientDigest Digest which has been submitted by the client
377      * @param nonce Unique (or supposedly unique) token which has been used
378      * for this request
379      * @param nc the nonce counter
380      * @param cnonce the client chosen nonce
381      * @param qop the "quality of protection" (<code>nc</code> and <code>cnonce</code>
382      *        will only be used, if <code>qop</code> is not <code>null</code>).
383      * @param realm Realm name
384      * @param md5a2 Second MD5 digest used to calculate the digest :
385      * MD5(Method + ":" + uri)
386      * @return the associated principal, or <code>null</code> if there is none.
387      */

388     @Override
389     public Principal authenticate(String username, String clientDigest,
390                                   String nonce, String nc, String cnonce,
391                                   String qop, String realm,
392                                   String md5a2) {
393
394         // In digest auth, digests are always lower case
395         String md5a1 = getDigest(username, realm);
396         if (md5a1 == null)
397             return null;
398         md5a1 = md5a1.toLowerCase(Locale.ENGLISH);
399         String serverDigestValue;
400         if (qop == null) {
401             serverDigestValue = md5a1 + ":" + nonce + ":" + md5a2;
402         } else {
403             serverDigestValue = md5a1 + ":" + nonce + ":" + nc + ":" +
404                     cnonce + ":" + qop + ":" + md5a2;
405         }
406
407         byte[] valueBytes = null;
408         try {
409             valueBytes = serverDigestValue.getBytes(getDigestCharset());
410         } catch (UnsupportedEncodingException uee) {
411             throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()), uee);
412         }
413
414         String serverDigest = MD5Encoder.encode(ConcurrentMessageDigest.digestMD5(valueBytes));
415
416         if (log.isDebugEnabled()) {
417             log.debug("Digest : " + clientDigest + " Username:" + username
418                     + " ClientDigest:" + clientDigest + " nonce:" + nonce
419                     + " nc:" + nc + " cnonce:" + cnonce + " qop:" + qop
420                     + " realm:" + realm + "md5a2:" + md5a2
421                     + " Server digest:" + serverDigest);
422         }
423
424         if (serverDigest.equals(clientDigest)) {
425             return getPrincipal(username);
426         }
427
428         return null;
429     }
430
431
432     /**
433      * Return the Principal associated with the specified chain of X509
434      * client certificates.  If there is none, return <code>null</code>.
435      *
436      * @param certs Array of client certificates, with the first one in
437      *  the array being the certificate of the client itself.
438      */

439     @Override
440     public Principal authenticate(X509Certificate certs[]) {
441
442         if ((certs == null) || (certs.length < 1))
443             return null;
444
445         // Check the validity of each certificate in the chain
446         if (log.isDebugEnabled())
447             log.debug("Authenticating client certificate chain");
448         if (validate) {
449             for (int i = 0; i < certs.length; i++) {
450                 if (log.isDebugEnabled())
451                     log.debug(" Checking validity for '" +
452                         certs[i].getSubjectDN().getName() + "'");
453                 try {
454                     certs[i].checkValidity();
455                 } catch (Exception e) {
456                     if (log.isDebugEnabled())
457                         log.debug("  Validity exception", e);
458                     return null;
459                 }
460             }
461         }
462
463         // Check the existence of the client Principal in our database
464         return getPrincipal(certs[0]);
465     }
466
467
468     /**
469      * {@inheritDoc}
470      */

471     @Override
472     public Principal authenticate(GSSContext gssContext, boolean storeCred) {
473         if (gssContext.isEstablished()) {
474             GSSName gssName = null;
475             try {
476                 gssName = gssContext.getSrcName();
477             } catch (GSSException e) {
478                 log.warn(sm.getString("realmBase.gssNameFail"), e);
479             }
480
481             if (gssName!= null) {
482                 GSSCredential gssCredential = null;
483                 if (storeCred) {
484                     if (gssContext.getCredDelegState()) {
485                         try {
486                             gssCredential = gssContext.getDelegCred();
487                         } catch (GSSException e) {
488                             log.warn(sm.getString(
489                                     "realmBase.delegatedCredentialFail", gssName), e);
490                         }
491                     } else {
492                         if (log.isDebugEnabled()) {
493                             log.debug(sm.getString(
494                                     "realmBase.credentialNotDelegated", gssName));
495                         }
496                     }
497                 }
498
499                 return getPrincipal(gssName, gssCredential);
500             }
501         } else {
502             log.error(sm.getString("realmBase.gssContextNotEstablished"));
503         }
504
505         // Fail in all other cases
506         return null;
507     }
508
509
510     /**
511      * {@inheritDoc}
512      */

513     @Override
514     public Principal authenticate(GSSName gssName, GSSCredential gssCredential) {
515         if (gssName == null) {
516             return null;
517         }
518
519         return getPrincipal(gssName, gssCredential);
520     }
521
522
523     /**
524      * Execute a periodic task, such as reloading, etc. This method will be
525      * invoked inside the classloading context of this container. Unexpected
526      * throwables will be caught and logged.
527      */

528     @Override
529     public void backgroundProcess() {
530         // NOOP in base class
531     }
532
533
534     /**
535      * Return the SecurityConstraints configured to guard the request URI for
536      * this request, or <code>null</code> if there is no such constraint.
537      *
538      * @param request Request we are processing
539      * @param context Context the Request is mapped to
540      */

541     @Override
542     public SecurityConstraint [] findSecurityConstraints(Request request,
543                                                          Context context) {
544
545         ArrayList<SecurityConstraint> results = null;
546         // Are there any defined security constraints?
547         SecurityConstraint constraints[] = context.findConstraints();
548         if ((constraints == null) || (constraints.length == 0)) {
549             if (log.isDebugEnabled())
550                 log.debug("  No applicable constraints defined");
551             return null;
552         }
553
554         // Check each defined security constraint
555         String uri = request.getRequestPathMB().toString();
556         // Bug47080 - in rare cases this may be null or ""
557         // Mapper treats as '/' do the same to prevent NPE
558         if (uri == null || uri.length() == 0) {
559             uri = "/";
560         }
561
562         String method = request.getMethod();
563         int i;
564         boolean found = false;
565         for (i = 0; i < constraints.length; i++) {
566             SecurityCollection [] collection = constraints[i].findCollections();
567
568             // If collection is nullcontinue to avoid an NPE
569             // See Bugzilla 30624
570             if ( collection == null) {
571                 continue;
572             }
573
574             if (log.isDebugEnabled()) {
575                 log.debug("  Checking constraint '" + constraints[i] +
576                     "' against " + method + " " + uri + " --> " +
577                     constraints[i].included(uri, method));
578             }
579
580             for(int j=0; j < collection.length; j++){
581                 String [] patterns = collection[j].findPatterns();
582
583                 // If patterns is nullcontinue to avoid an NPE
584                 // See Bugzilla 30624
585                 if ( patterns == null) {
586                     continue;
587                 }
588
589                 for(int k=0; k < patterns.length; k++) {
590                     // Exact match including special case for the context root.
591                     if(uri.equals(patterns[k]) || patterns[k].length() == 0 && uri.equals("/")) {
592                         found = true;
593                         if(collection[j].findMethod(method)) {
594                             if(results == null) {
595                                 results = new ArrayList<>();
596                             }
597                             results.add(constraints[i]);
598                         }
599                     }
600                 }
601             }
602         }
603
604         if(found) {
605             return resultsToArray(results);
606         }
607
608         int longest = -1;
609
610         for (i = 0; i < constraints.length; i++) {
611             SecurityCollection [] collection = constraints[i].findCollections();
612
613             // If collection is nullcontinue to avoid an NPE
614             // See Bugzilla 30624
615             if ( collection == null) {
616                 continue;
617             }
618
619             if (log.isDebugEnabled()) {
620                 log.debug("  Checking constraint '" + constraints[i] +
621                     "' against " + method + " " + uri + " --> " +
622                     constraints[i].included(uri, method));
623             }
624
625             for(int j=0; j < collection.length; j++){
626                 String [] patterns = collection[j].findPatterns();
627
628                 // If patterns is nullcontinue to avoid an NPE
629                 // See Bugzilla 30624
630                 if ( patterns == null) {
631                     continue;
632                 }
633
634                 boolean matched = false;
635                 int length = -1;
636                 for(int k=0; k < patterns.length; k++) {
637                     String pattern = patterns[k];
638                     if(pattern.startsWith("/") && pattern.endsWith("/*") &&
639                        pattern.length() >= longest) {
640
641                         if(pattern.length() == 2) {
642                             matched = true;
643                             length = pattern.length();
644                         } else if(pattern.regionMatches(0,uri,0,
645                                                         pattern.length()-1) ||
646                                   (pattern.length()-2 == uri.length() &&
647                                    pattern.regionMatches(0,uri,0,
648                                                         pattern.length()-2))) {
649                             matched = true;
650                             length = pattern.length();
651                         }
652                     }
653                 }
654                 if(matched) {
655                     if(length > longest) {
656                         found = false;
657                         if(results != null) {
658                             results.clear();
659                         }
660                         longest = length;
661                     }
662                     if(collection[j].findMethod(method)) {
663                         found = true;
664                         if(results == null) {
665                             results = new ArrayList<>();
666                         }
667                         results.add(constraints[i]);
668                     }
669                 }
670             }
671         }
672
673         if(found) {
674             return  resultsToArray(results);
675         }
676
677         for (i = 0; i < constraints.length; i++) {
678             SecurityCollection [] collection = constraints[i].findCollections();
679
680             // If collection is nullcontinue to avoid an NPE
681             // See Bugzilla 30624
682             if ( collection == null) {
683                 continue;
684             }
685
686             if (log.isDebugEnabled()) {
687                 log.debug("  Checking constraint '" + constraints[i] +
688                     "' against " + method + " " + uri + " --> " +
689                     constraints[i].included(uri, method));
690             }
691
692             boolean matched = false;
693             int pos = -1;
694             for(int j=0; j < collection.length; j++){
695                 String [] patterns = collection[j].findPatterns();
696
697                 // If patterns is nullcontinue to avoid an NPE
698                 // See Bugzilla 30624
699                 if ( patterns == null) {
700                     continue;
701                 }
702
703                 for(int k=0; k < patterns.length && !matched; k++) {
704                     String pattern = patterns[k];
705                     if(pattern.startsWith("*.")){
706                         int slash = uri.lastIndexOf('/');
707                         int dot = uri.lastIndexOf('.');
708                         if(slash >= 0 && dot > slash &&
709                            dot != uri.length()-1 &&
710                            uri.length()-dot == pattern.length()-1) {
711                             if(pattern.regionMatches(1,uri,dot,uri.length()-dot)) {
712                                 matched = true;
713                                 pos = j;
714                             }
715                         }
716                     }
717                 }
718             }
719             if(matched) {
720                 found = true;
721                 if(collection[pos].findMethod(method)) {
722                     if(results == null) {
723                         results = new ArrayList<>();
724                     }
725                     results.add(constraints[i]);
726                 }
727             }
728         }
729
730         if(found) {
731             return resultsToArray(results);
732         }
733
734         for (i = 0; i < constraints.length; i++) {
735             SecurityCollection [] collection = constraints[i].findCollections();
736
737             // If collection is nullcontinue to avoid an NPE
738             // See Bugzilla 30624
739             if ( collection == null) {
740                 continue;
741             }
742
743             if (log.isDebugEnabled()) {
744                 log.debug("  Checking constraint '" + constraints[i] +
745                     "' against " + method + " " + uri + " --> " +
746                     constraints[i].included(uri, method));
747             }
748
749             for(int j=0; j < collection.length; j++){
750                 String [] patterns = collection[j].findPatterns();
751
752                 // If patterns is nullcontinue to avoid an NPE
753                 // See Bugzilla 30624
754                 if ( patterns == null) {
755                     continue;
756                 }
757
758                 boolean matched = false;
759                 for(int k=0; k < patterns.length && !matched; k++) {
760                     String pattern = patterns[k];
761                     if(pattern.equals("/")){
762                         matched = true;
763                     }
764                 }
765                 if(matched) {
766                     if(results == null) {
767                         results = new ArrayList<>();
768                     }
769                     results.add(constraints[i]);
770                 }
771             }
772         }
773
774         if(results == null) {
775             // No applicable security constraint was found
776             if (log.isDebugEnabled())
777                 log.debug("  No applicable constraint located");
778         }
779         return resultsToArray(results);
780     }
781
782     /**
783      * Convert an ArrayList to a SecurityConstraint [].
784      */

785     private SecurityConstraint [] resultsToArray(
786             ArrayList<SecurityConstraint> results) {
787         if(results == null || results.size() == 0) {
788             return null;
789         }
790         SecurityConstraint [] array = new SecurityConstraint[results.size()];
791         results.toArray(array);
792         return array;
793     }
794
795
796     /**
797      * Perform access control based on the specified authorization constraint.
798      * Return <code>true</code> if this constraint is satisfied and processing
799      * should continue, or <code>false</code> otherwise.
800      *
801      * @param request Request we are processing
802      * @param response Response we are creating
803      * @param constraints Security constraint we are enforcing
804      * @param context The Context to which client of this class is attached.
805      *
806      * @exception IOException if an input/output error occurs
807      */

808     @Override
809     public boolean hasResourcePermission(Request request,
810                                          Response response,
811                                          SecurityConstraint []constraints,
812                                          Context context)
813         throws IOException {
814
815         if (constraints == null || constraints.length == 0)
816             return true;
817
818         // Which user principal have we already authenticated?
819         Principal principal = request.getPrincipal();
820         boolean status = false;
821         boolean denyfromall = false;
822         for(int i=0; i < constraints.length; i++) {
823             SecurityConstraint constraint = constraints[i];
824
825             String roles[];
826             if (constraint.getAllRoles()) {
827                 // * means all roles defined in web.xml
828                 roles = request.getContext().findSecurityRoles();
829             } else {
830                 roles = constraint.findAuthRoles();
831             }
832
833             if (roles == null)
834                 roles = new String[0];
835
836             if (log.isDebugEnabled())
837                 log.debug("  Checking roles " + principal);
838
839             if (constraint.getAuthenticatedUsers() && principal != null) {
840                 if (log.isDebugEnabled()) {
841                     log.debug("Passing all authenticated users");
842                 }
843                 status = true;
844             } else if (roles.length == 0 && !constraint.getAllRoles() &&
845                     !constraint.getAuthenticatedUsers()) {
846                 if(constraint.getAuthConstraint()) {
847                     if( log.isDebugEnabled() )
848                         log.debug("No roles");
849                     status = false// No listed roles means no access at all
850                     denyfromall = true;
851                     break;
852                 }
853
854                 if(log.isDebugEnabled())
855                     log.debug("Passing all access");
856                 status = true;
857             } else if (principal == null) {
858                 if (log.isDebugEnabled())
859                     log.debug("  No user authenticated, cannot grant access");
860             } else {
861                 for (int j = 0; j < roles.length; j++) {
862                     if (hasRole(request.getWrapper(), principal, roles[j])) {
863                         status = true;
864                         if( log.isDebugEnabled() )
865                             log.debug( "Role found:  " + roles[j]);
866                     }
867                     else if( log.isDebugEnabled() )
868                         log.debug( "No role found:  " + roles[j]);
869                 }
870             }
871         }
872
873         if (!denyfromall && allRolesMode != AllRolesMode.STRICT_MODE &&
874                 !status && principal != null) {
875             if (log.isDebugEnabled()) {
876                 log.debug("Checking for all roles mode: " + allRolesMode);
877             }
878             // Check for an all roles(role-name="*")
879             for (int i = 0; i < constraints.length; i++) {
880                 SecurityConstraint constraint = constraints[i];
881                 String roles[];
882                 // If the all roles mode exists, sets
883                 if (constraint.getAllRoles()) {
884                     if (allRolesMode == AllRolesMode.AUTH_ONLY_MODE) {
885                         if (log.isDebugEnabled()) {
886                             log.debug("Granting access for role-name=*, auth-only");
887                         }
888                         status = true;
889                         break;
890                     }
891
892                     // For AllRolesMode.STRICT_AUTH_ONLY_MODE there must be zero roles
893                     roles = request.getContext().findSecurityRoles();
894                     if (roles.length == 0 && allRolesMode == AllRolesMode.STRICT_AUTH_ONLY_MODE) {
895                         if (log.isDebugEnabled()) {
896                             log.debug("Granting access for role-name=*, strict auth-only");
897                         }
898                         status = true;
899                         break;
900                     }
901                 }
902             }
903         }
904
905         // Return a "Forbidden" message denying access to this resource
906         if(!status) {
907             response.sendError
908                 (HttpServletResponse.SC_FORBIDDEN,
909                  sm.getString("realmBase.forbidden"));
910         }
911         return status;
912
913     }
914
915
916     /**
917      * {@inheritDoc}
918      *
919      * This method or {@link #hasRoleInternal(Principal,
920      * String)} can be overridden by Realm implementations, but the default is
921      * adequate when an instance of <code>GenericPrincipal</code> is used to
922      * represent authenticated Principals from this Realm.
923      */

924     @Override
925     public boolean hasRole(Wrapper wrapper, Principal principal, String role) {
926         // Check for a role alias
927         if (wrapper != null) {
928             String realRole = wrapper.findSecurityReference(role);
929             if (realRole != null) {
930                 role = realRole;
931             }
932         }
933
934         // Should be overridden in JAASRealm - to avoid pretty inefficient conversions
935         if (principal == null || role == null) {
936             return false;
937         }
938
939         boolean result = hasRoleInternal(principal, role);
940
941         if (log.isDebugEnabled()) {
942             String name = principal.getName();
943             if (result)
944                 log.debug(sm.getString("realmBase.hasRoleSuccess", name, role));
945             else
946                 log.debug(sm.getString("realmBase.hasRoleFailure", name, role));
947         }
948
949         return result;
950     }
951
952
953     /**
954      * Check if the specified Principal has the specified
955      * security role, within the context of this Realm.
956      *
957      * This method or {@link #hasRoleInternal(Principal,
958      * String)} can be overridden by Realm implementations, but the default is
959      * adequate when an instance of <code>GenericPrincipal</code> is used to
960      * represent authenticated Principals from this Realm.
961      *
962      * @param principal Principal for whom the role is to be checked
963      * @param role Security role to be checked
964      *
965      * @return <code>true</code> if the specified Principal has the specified
966      *         security role, within the context of this Realm; otherwise return
967      *         <code>false</code>.
968      */

969     protected boolean hasRoleInternal(Principal principal, String role) {
970         // Should be overridden in JAASRealm - to avoid pretty inefficient conversions
971         if (!(principal instanceof GenericPrincipal)) {
972             return false;
973         }
974
975         GenericPrincipal gp = (GenericPrincipal) principal;
976         return gp.hasRole(role);
977     }
978
979
980     /**
981      * Enforce any user data constraint required by the security constraint
982      * guarding this request URI.  Return <code>true</code> if this constraint
983      * was not violated and processing should continue, or <code>false</code>
984      * if we have created a response already.
985      *
986      * @param request Request we are processing
987      * @param response Response we are creating
988      * @param constraints Security constraint being checked
989      *
990      * @exception IOException if an input/output error occurs
991      */

992     @Override
993     public boolean hasUserDataPermission(Request request,
994                                          Response response,
995                                          SecurityConstraint []constraints)
996         throws IOException {
997
998         // Is there a relevant user data constraint?
999         if (constraints == null || constraints.length == 0) {
1000             if (log.isDebugEnabled())
1001                 log.debug("  No applicable security constraint defined");
1002             return true;
1003         }
1004         for(int i=0; i < constraints.length; i++) {
1005             SecurityConstraint constraint = constraints[i];
1006             String userConstraint = constraint.getUserConstraint();
1007             if (userConstraint == null) {
1008                 if (log.isDebugEnabled())
1009                     log.debug("  No applicable user data constraint defined");
1010                 return true;
1011             }
1012             if (userConstraint.equals(TransportGuarantee.NONE.name())) {
1013                 if (log.isDebugEnabled())
1014                     log.debug("  User data constraint has no restrictions");
1015                 return true;
1016             }
1017
1018         }
1019         // Validate the request against the user data constraint
1020         if (request.getRequest().isSecure()) {
1021             if (log.isDebugEnabled())
1022                 log.debug("  User data constraint already satisfied");
1023             return true;
1024         }
1025         // Initialize variables we need to determine the appropriate action
1026         int redirectPort = request.getConnector().getRedirectPortWithOffset();
1027
1028         // Is redirecting disabled?
1029         if (redirectPort <= 0) {
1030             if (log.isDebugEnabled())
1031                 log.debug("  SSL redirect is disabled");
1032             response.sendError
1033                 (HttpServletResponse.SC_FORBIDDEN,
1034                  request.getRequestURI());
1035             return false;
1036         }
1037
1038         // Redirect to the corresponding SSL port
1039         StringBuilder file = new StringBuilder();
1040         String protocol = "https";
1041         String host = request.getServerName();
1042         // Protocol
1043         file.append(protocol).append("://").append(host);
1044         // Host with port
1045         if(redirectPort != 443) {
1046             file.append(":").append(redirectPort);
1047         }
1048         // URI
1049         file.append(request.getRequestURI());
1050         String requestedSessionId = request.getRequestedSessionId();
1051         if ((requestedSessionId != null) &&
1052             request.isRequestedSessionIdFromURL()) {
1053             file.append(";");
1054             file.append(SessionConfig.getSessionUriParamName(
1055                     request.getContext()));
1056             file.append("=");
1057             file.append(requestedSessionId);
1058         }
1059         String queryString = request.getQueryString();
1060         if (queryString != null) {
1061             file.append('?');
1062             file.append(queryString);
1063         }
1064         if (log.isDebugEnabled())
1065             log.debug("  Redirecting to " + file.toString());
1066         response.sendRedirect(file.toString(), transportGuaranteeRedirectStatus);
1067         return false;
1068
1069     }
1070
1071
1072     /**
1073      * Remove a property change listener from this component.
1074      *
1075      * @param listener The listener to remove
1076      */

1077     @Override
1078     public void removePropertyChangeListener(PropertyChangeListener listener) {
1079
1080         support.removePropertyChangeListener(listener);
1081
1082     }
1083
1084
1085     @Override
1086     protected void initInternal() throws LifecycleException {
1087
1088         super.initInternal();
1089
1090         // We want logger as soon as possible
1091         if (container != null) {
1092             this.containerLog = container.getLogger();
1093         }
1094
1095         x509UsernameRetriever = createUsernameRetriever(x509UsernameRetrieverClassName);
1096     }
1097
1098     /**
1099      * Prepare for the beginning of active use of the public methods of this
1100      * component and implement the requirements of
1101      * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
1102      *
1103      * @exception LifecycleException if this component detects a fatal error
1104      *  that prevents this component from being used
1105      */

1106     @Override
1107     protected void startInternal() throws LifecycleException {
1108         if (credentialHandler == null) {
1109             credentialHandler = new MessageDigestCredentialHandler();
1110         }
1111
1112         setState(LifecycleState.STARTING);
1113     }
1114
1115
1116     /**
1117      * Gracefully terminate the active use of the public methods of this
1118      * component and implement the requirements of
1119      * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
1120      *
1121      * @exception LifecycleException if this component detects a fatal error
1122      *  that needs to be reported
1123      */

1124     @Override
1125     protected void stopInternal() throws LifecycleException {
1126         setState(LifecycleState.STOPPING);
1127     }
1128
1129
1130     /**
1131      * Return a String representation of this component.
1132      */

1133     @Override
1134     public String toString() {
1135         return ToStringUtil.toString(this);
1136     }
1137
1138
1139     // ------------------------------------------------------ Protected Methods
1140
1141     protected boolean hasMessageDigest() {
1142         CredentialHandler ch = credentialHandler;
1143         if (ch instanceof MessageDigestCredentialHandler) {
1144             return ((MessageDigestCredentialHandler) ch).getAlgorithm() != null;
1145         }
1146         return false;
1147     }
1148
1149
1150     /**
1151      * Return the digest associated with given principal's user name.
1152      * @param username the user name
1153      * @param realmName the realm name
1154      * @return the digest for the specified user
1155      */

1156     protected String getDigest(String username, String realmName) {
1157         if (hasMessageDigest()) {
1158             // Use pre-generated digest
1159             return getPassword(username);
1160         }
1161
1162         String digestValue = username + ":" + realmName + ":"
1163             + getPassword(username);
1164
1165         byte[] valueBytes = null;
1166         try {
1167             valueBytes = digestValue.getBytes(getDigestCharset());
1168         } catch (UnsupportedEncodingException uee) {
1169             throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()), uee);
1170         }
1171
1172         return MD5Encoder.encode(ConcurrentMessageDigest.digestMD5(valueBytes));
1173     }
1174
1175
1176     private String getDigestEncoding() {
1177         CredentialHandler ch = credentialHandler;
1178         if (ch instanceof MessageDigestCredentialHandler) {
1179             return ((MessageDigestCredentialHandler) ch).getEncoding();
1180         }
1181         return null;
1182     }
1183
1184
1185     private Charset getDigestCharset() throws UnsupportedEncodingException {
1186         String charset = getDigestEncoding();
1187         if (charset == null) {
1188             return StandardCharsets.ISO_8859_1;
1189         } else {
1190             return B2CConverter.getCharset(charset);
1191         }
1192     }
1193
1194
1195     /**
1196      * Get the password for the specified user.
1197      * @param username The user name
1198      * @return the password associated with the given principal's user name.
1199      */

1200     protected abstract String getPassword(String username);
1201
1202
1203     /**
1204      * Get the principal associated with the specified certificate.
1205      * @param usercert The user certificate
1206      * @return the Principal associated with the given certificate.
1207      */

1208     protected Principal getPrincipal(X509Certificate usercert) {
1209         String username = x509UsernameRetriever.getUsername(usercert);
1210
1211         if(log.isDebugEnabled())
1212             log.debug(sm.getString("realmBase.gotX509Username", username));
1213
1214         return getPrincipal(username);
1215     }
1216
1217
1218     /**
1219      * Get the principal associated with the specified user.
1220      * @param username The user name
1221      * @return the Principal associated with the given user name.
1222      */

1223     protected abstract Principal getPrincipal(String username);
1224
1225
1226     /**
1227      * Get the principal associated with the specified user name.
1228      *
1229      * @param username The user name
1230      * @param gssCredential the GSS credential of the principal
1231      * @return the principal associated with the given user name.
1232      * @deprecated This will be removed in Tomcat 10 onwards. Use
1233      *             {@link #getPrincipal(GSSName, GSSCredential)} instead.
1234      */

1235     @Deprecated
1236     protected Principal getPrincipal(String username,
1237             GSSCredential gssCredential) {
1238         Principal p = getPrincipal(username);
1239
1240         if (p instanceof GenericPrincipal) {
1241             ((GenericPrincipal) p).setGssCredential(gssCredential);
1242         }
1243
1244         return p;
1245     }
1246
1247
1248     /**
1249      * Get the principal associated with the specified {@link GSSName}.
1250      *
1251      * @param gssName The GSS name
1252      * @param gssCredential the GSS credential of the principal
1253      * @return the principal associated with the given user name.
1254      */

1255     protected Principal getPrincipal(GSSName gssName,
1256             GSSCredential gssCredential) {
1257         String name = gssName.toString();
1258
1259         if (isStripRealmForGss()) {
1260             int i = name.indexOf('@');
1261             if (i > 0) {
1262                 // Zero so we don't leave a zero length name
1263                 name = name.substring(0, i);
1264             }
1265         }
1266
1267         Principal p = getPrincipal(name);
1268
1269         if (p instanceof GenericPrincipal) {
1270             ((GenericPrincipal) p).setGssCredential(gssCredential);
1271         }
1272
1273         return p;
1274     }
1275
1276
1277     /**
1278      * Return the Server object that is the ultimate parent for the container
1279      * with which this Realm is associated. If the server cannot be found (eg
1280      * because the container hierarchy is not complete), <code>null</code> is
1281      * returned.
1282      * @return the Server associated with the realm
1283      */

1284     protected Server getServer() {
1285         Container c = container;
1286         if (c instanceof Context) {
1287             c = c.getParent();
1288         }
1289         if (c instanceof Host) {
1290             c = c.getParent();
1291         }
1292         if (c instanceof Engine) {
1293             Service s = ((Engine)c).getService();
1294             if (s != null) {
1295                 return s.getServer();
1296             }
1297         }
1298         return null;
1299     }
1300
1301
1302     // --------------------------------------------------------- Static Methods
1303
1304     /**
1305      * Generate a stored credential string for the given password and associated
1306      * parameters.
1307      * <p>The following parameters are supported:</p>
1308      * <ul>
1309      * <li><b>-a</b> - The algorithm to use to generate the stored
1310      *                 credential. If not specified a default of SHA-512 will be
1311      *                 used.</li>
1312      * <li><b>-e</b> - The encoding to use for any byte to/from character
1313      *                 conversion that may be necessary. If not specified, the
1314      *                 system encoding ({@link Charset#defaultCharset()}) will
1315      *                 be used.</li>
1316      * <li><b>-i</b> - The number of iterations to use when generating the
1317      *                 stored credential. If not specified, the default for the
1318      *                 CredentialHandler will be used.</li>
1319      * <li><b>-s</b> - The length (in bytes) of salt to generate and store as
1320      *                 part of the credential. If not specified, the default for
1321      *                 the CredentialHandler will be used.</li>
1322      * <li><b>-k</b> - The length (in bits) of the key(s), if any, created while
1323      *                 generating the credential. If not specified, the default
1324      *                 for the CredentialHandler will be used.</li>
1325      * <li><b>-h</b> - The fully qualified class name of the CredentialHandler
1326      *                 to use. If not specified, the built-in handlers will be
1327      *                 tested in turn and the first one to accept the specified
1328      *                 algorithm will be used.</li>
1329      * </ul>
1330      * <p>This generation process currently supports the following
1331      * CredentialHandlers, the correct one being selected based on the algorithm
1332      * specified:</p>
1333      * <ul>
1334      * <li>{@link MessageDigestCredentialHandler}</li>
1335      * <li>{@link SecretKeyCredentialHandler}</li>
1336      * </ul>
1337      * @param args The parameters passed on the command line
1338      */

1339     public static void main(String args[]) {
1340
1341         // Use negative values since null is not an option to indicate 'not set'
1342         int saltLength = -1;
1343         int iterations = -1;
1344         int keyLength = -1;
1345         // Default
1346         String encoding = Charset.defaultCharset().name();
1347         // Default values for these depend on whether either of them are set on
1348         // the command line
1349         String algorithm = null;
1350         String handlerClassName = null;
1351
1352         if (args.length == 0) {
1353             usage();
1354             return;
1355         }
1356
1357         int argIndex = 0;
1358
1359         while (args.length > argIndex + 2 && args[argIndex].length() == 2 &&
1360                 args[argIndex].charAt(0) == '-' ) {
1361             switch (args[argIndex].charAt(1)) {
1362             case 'a': {
1363                 algorithm = args[argIndex + 1];
1364                 break;
1365             }
1366             case 'e': {
1367                 encoding = args[argIndex + 1];
1368                 break;
1369             }
1370             case 'i': {
1371                 iterations = Integer.parseInt(args[argIndex + 1]);
1372                 break;
1373             }
1374             case 's': {
1375                 saltLength = Integer.parseInt(args[argIndex + 1]);
1376                 break;
1377             }
1378             case 'k': {
1379                 keyLength = Integer.parseInt(args[argIndex + 1]);
1380                 break;
1381             }
1382             case 'h': {
1383                 handlerClassName = args[argIndex + 1];
1384                 break;
1385             }
1386             default: {
1387                 usage();
1388                 return;
1389             }
1390             }
1391             argIndex += 2;
1392         }
1393
1394         // Determine defaults for -a and -h. The rules are more complex to
1395         // express than the implementation:
1396         // - if neither -a nor -h is set, use SHA-512 and
1397         //   MessageDigestCredentialHandler
1398         // - if only -a is set the built-in handlers will be searched in order
1399         //   (MessageDigestCredentialHandler, SecretKeyCredentialHandler) and
1400         //   the first handler that supports the algorithm will be used
1401         // - if only -h is set no default will be used for -a. The handler may
1402         //   or may nor support -a and may or may not supply a sensible default
1403         if (algorithm == null && handlerClassName == null) {
1404             algorithm = "SHA-512";
1405         }
1406
1407         CredentialHandler handler = null;
1408
1409         if (handlerClassName == null) {
1410             for (Class<? extends DigestCredentialHandlerBase> clazz : credentialHandlerClasses) {
1411                 try {
1412                     handler = clazz.getConstructor().newInstance();
1413                     if (IntrospectionUtils.setProperty(handler, "algorithm", algorithm)) {
1414                         break;
1415                     }
1416                 } catch (ReflectiveOperationException e) {
1417                     // This isn't good.
1418                     throw new RuntimeException(e);
1419                 }
1420             }
1421         } else {
1422             try {
1423                 Class<?> clazz = Class.forName(handlerClassName);
1424                 handler = (DigestCredentialHandlerBase) clazz.getConstructor().newInstance();
1425                 IntrospectionUtils.setProperty(handler, "algorithm", algorithm);
1426             } catch (ReflectiveOperationException e) {
1427                 throw new RuntimeException(e);
1428             }
1429         }
1430
1431         if (handler == null) {
1432             throw new RuntimeException(new NoSuchAlgorithmException(algorithm));
1433         }
1434
1435         IntrospectionUtils.setProperty(handler, "encoding", encoding);
1436         if (iterations > 0) {
1437             IntrospectionUtils.setProperty(handler, "iterations", Integer.toString(iterations));
1438         }
1439         if (saltLength > -1) {
1440             IntrospectionUtils.setProperty(handler, "saltLength", Integer.toString(saltLength));
1441         }
1442         if (keyLength > 0) {
1443             IntrospectionUtils.setProperty(handler, "keyLength", Integer.toString(keyLength));
1444         }
1445
1446         for (; argIndex < args.length; argIndex++) {
1447             String credential = args[argIndex];
1448             System.out.print(credential + ":");
1449             System.out.println(handler.mutate(credential));
1450         }
1451     }
1452
1453
1454     private static void usage() {
1455         System.out.println("Usage: RealmBase [-a <algorithm>] [-e <encoding>] " +
1456                 "[-i <iterations>] [-s <salt-length>] [-k <key-length>] " +
1457                 "[-h <handler-class-name>] <credentials>");
1458     }
1459
1460
1461     // -------------------- JMX and Registration  --------------------
1462
1463     @Override
1464     public String getObjectNameKeyProperties() {
1465
1466         StringBuilder keyProperties = new StringBuilder("type=Realm");
1467         keyProperties.append(getRealmSuffix());
1468         keyProperties.append(container.getMBeanKeyProperties());
1469
1470         return keyProperties.toString();
1471     }
1472
1473     @Override
1474     public String getDomainInternal() {
1475         return container.getDomain();
1476     }
1477
1478     protected String realmPath = "/realm0";
1479
1480     public String getRealmPath() {
1481         return realmPath;
1482     }
1483
1484     public void setRealmPath(String theRealmPath) {
1485         realmPath = theRealmPath;
1486     }
1487
1488     protected String getRealmSuffix() {
1489         return ",realmPath=" + getRealmPath();
1490     }
1491
1492
1493     protected static class AllRolesMode {
1494
1495         private final String name;
1496         /** Use the strict servlet spec interpretation which requires that the user
1497          * have one of the web-app/security-role/role-name
1498          */

1499         public static final AllRolesMode STRICT_MODE = new AllRolesMode("strict");
1500         /** Allow any authenticated user
1501          */

1502         public static final AllRolesMode AUTH_ONLY_MODE = new AllRolesMode("authOnly");
1503         /** Allow any authenticated user only if there are no web-app/security-roles
1504          */

1505         public static final AllRolesMode STRICT_AUTH_ONLY_MODE = new AllRolesMode("strictAuthOnly");
1506
1507         static AllRolesMode toMode(String name) {
1508             AllRolesMode mode;
1509             if (name.equalsIgnoreCase(STRICT_MODE.name)) {
1510                 mode = STRICT_MODE;
1511             } else if (name.equalsIgnoreCase(AUTH_ONLY_MODE.name)) {
1512                 mode = AUTH_ONLY_MODE;
1513             } else if (name.equalsIgnoreCase(STRICT_AUTH_ONLY_MODE.name)) {
1514                 mode = STRICT_AUTH_ONLY_MODE;
1515             } else {
1516                 throw new IllegalStateException(
1517                         sm.getString("realmBase.unknownAllRolesMode", name));
1518             }
1519             return mode;
1520         }
1521
1522         private AllRolesMode(String name) {
1523             this.name = name;
1524         }
1525
1526         @Override
1527         public boolean equals(Object o) {
1528             boolean equals = false;
1529             if (o instanceof AllRolesMode) {
1530                 AllRolesMode mode = (AllRolesMode) o;
1531                 equals = name.equals(mode.name);
1532             }
1533             return equals;
1534         }
1535
1536         @Override
1537         public int hashCode() {
1538             return name.hashCode();
1539         }
1540
1541         @Override
1542         public String toString() {
1543             return name;
1544         }
1545     }
1546
1547     private static X509UsernameRetriever createUsernameRetriever(String className)
1548         throws LifecycleException {
1549         if(null == className || "".equals(className.trim()))
1550             return new X509SubjectDnRetriever();
1551
1552         try {
1553             @SuppressWarnings("unchecked")
1554             Class<? extends X509UsernameRetriever> clazz = (Class<? extends X509UsernameRetriever>)Class.forName(className);
1555             return clazz.getConstructor().newInstance();
1556         } catch (ReflectiveOperationException e) {
1557             throw new LifecycleException(sm.getString("realmBase.createUsernameRetriever.newInstance", className), e);
1558         } catch (ClassCastException e) {
1559             throw new LifecycleException(sm.getString("realmBase.createUsernameRetriever.ClassCastException", className), e);
1560         }
1561     }
1562
1563
1564     @Override
1565     public String[] getRoles(Principal principal) {
1566         if (principal instanceof GenericPrincipal) {
1567             return ((GenericPrincipal) principal).getRoles();
1568         }
1569
1570         String className = principal.getClass().getSimpleName();
1571         throw new IllegalStateException(sm.getString("realmBase.cannotGetRoles", className));
1572     }
1573 }
1574