1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.web;
19
20 import java.io.IOException;
21 import java.security.NoSuchAlgorithmException;
22 import java.util.ArrayList;
23 import java.util.Date;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.concurrent.atomic.AtomicInteger;
27 import java.util.regex.Pattern;
28
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletResponse;
31
32 import net.bull.javamelody.Parameter;
33 import net.bull.javamelody.internal.common.LOG;
34 import net.bull.javamelody.internal.common.MessageDigestPasswordEncoder;
35 import net.bull.javamelody.internal.model.Base64Coder;
36
37 /**
38  * Authentification http des rapports.
39  * @author Emeric Vernat
40  */

41 public class HttpAuth {
42     private static final long AUTH_FAILURES_MAX = 10;
43
44     private static final long LOCK_DURATION = 60L * 60 * 1000;
45
46     private final Pattern allowedAddrPattern;
47     /**
48      * List of authorized people, when using the "authorized-users" parameter.
49      */

50     private final List<String> authorizedUsers;
51
52     private final AtomicInteger authFailuresCount = new AtomicInteger();
53
54     private Date firstFailureDate;
55
56     public HttpAuth() {
57         super();
58         this.allowedAddrPattern = getAllowedAddrPattern();
59         this.authorizedUsers = getAuthorizedUsers();
60     }
61
62     private static Pattern getAllowedAddrPattern() {
63         if (Parameter.ALLOWED_ADDR_PATTERN.getValue() != null) {
64             return Pattern.compile(Parameter.ALLOWED_ADDR_PATTERN.getValue());
65         }
66         return null;
67     }
68
69     private static List<String> getAuthorizedUsers() {
70         // security based on user / password (BASIC auth)
71         final String authUsersInParam = Parameter.AUTHORIZED_USERS.getValue();
72         if (authUsersInParam != null && !authUsersInParam.trim().isEmpty()) {
73             final List<String> authorizedUsers = new ArrayList<>();
74             // we split on new line or on comma
75             for (final String authUser : authUsersInParam.split("[\n,]")) {
76                 final String authUserTrim = authUser.trim();
77                 if (!authUserTrim.isEmpty()) {
78                     authorizedUsers.add(authUserTrim);
79                     LOG.debug("Authorized user: " + authUserTrim.split(":", 2)[0]);
80                 }
81             }
82             return authorizedUsers;
83         }
84         return null;
85     }
86
87     public boolean isAllowed(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
88             throws IOException {
89         if (!isRequestAllowed(httpRequest)) {
90             LOG.debug("Forbidden access to monitoring from " + httpRequest.getRemoteAddr());
91             httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden access");
92             return false;
93         }
94         if (!isUserAuthorized(httpRequest)) {
95             // Not allowed, so report he's unauthorized
96             httpResponse.setHeader("WWW-Authenticate""BASIC realm=\"JavaMelody\"");
97             if (isLocked()) {
98                 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
99                         "Unauthorized (locked)");
100             } else {
101                 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
102             }
103             return false;
104         }
105         return true;
106     }
107
108     private boolean isRequestAllowed(HttpServletRequest httpRequest) {
109         return allowedAddrPattern == null
110                 || allowedAddrPattern.matcher(httpRequest.getRemoteAddr()).matches();
111     }
112
113     /**
114      * Check if the user is authorized, when using the "authorized-users" parameter
115      * @param httpRequest HttpServletRequest
116      * @return true if the user is authorized
117      * @throws IOException e
118      */

119     private boolean isUserAuthorized(HttpServletRequest httpRequest) throws IOException {
120         if (authorizedUsers == null) {
121             return true;
122         }
123         // Get Authorization header
124         final String auth = httpRequest.getHeader("Authorization");
125         if (auth == null) {
126             return false// no auth
127         }
128         if (!auth.toUpperCase(Locale.ENGLISH).startsWith("BASIC ")) {
129             return false// we only do BASIC
130         }
131         // Get base64 encoded "user:password", comes after "BASIC "
132         final String userpassBase64 = auth.substring("BASIC ".length());
133         // Decode it
134         final String userpass = Base64Coder.decodeString(userpassBase64);
135
136         boolean authOk = false;
137         for (final String authorizedUser : authorizedUsers) {
138             // Hash password in userpass, if password is hashed in authorizedUser
139             final String userpassEncoded = getUserPasswordEncoded(userpass, authorizedUser);
140             if (userpassEncoded != null) {
141                 // case of hashed password like authorized-users=user:{SHA-256}c33d66fe65ffcca1f2260e6982dbf0c614b6ea3ddfdb37d6142fbec0feca5245
142                 if (authorizedUser.equals(userpassEncoded)) {
143                     authOk = true;
144                     break;
145                 }
146                 continue;
147             }
148             // case of clear test password like authorized-users=user:password
149             if (authorizedUser.equals(userpass)) {
150                 authOk = true;
151                 break;
152             }
153         }
154         return checkLockAgainstBruteForceAttack(authOk);
155     }
156
157     private String getUserPasswordEncoded(String userpassDecoded, String authorizedUser)
158             throws IOException {
159         final int indexOfStart = authorizedUser.indexOf(":{");
160         if (indexOfStart != -1) {
161             final int indexOfEnd = authorizedUser.indexOf('}', indexOfStart);
162             if (indexOfEnd != -1) {
163                 final String algorithm = authorizedUser.substring(indexOfStart + 2, indexOfEnd);
164                 final int indexOfColon = userpassDecoded.indexOf(':');
165                 if (indexOfColon != -1) {
166                     // case of hashed password like authorized-users=user:{SHA-256}c33d66fe65ffcca1f2260e6982dbf0c614b6ea3ddfdb37d6142fbec0feca5245
167                     final String pass = userpassDecoded.substring(indexOfColon + 1);
168                     return userpassDecoded.substring(0, indexOfColon + 1)
169                             + encodePassword(algorithm, pass);
170                 }
171             }
172         }
173         return null;
174     }
175
176     private String encodePassword(String algorithm, String password) throws IOException {
177         try {
178             return new MessageDigestPasswordEncoder(algorithm).encodePassword(password);
179         } catch (final NoSuchAlgorithmException e) {
180             // if algorithm in the authorized-users parameter is not available, such as SHA3-256 in Java 8,
181             // throw an exception to say that the algorithm is invalid here,
182             // (and that another such as SHA-256 should be used instead)
183             throw new IOException(e);
184         }
185     }
186
187     private boolean checkLockAgainstBruteForceAttack(boolean authOk) {
188         if (firstFailureDate == null) {
189             if (!authOk) {
190                 // auth failed for the first time, insert coin to try again
191                 firstFailureDate = new Date();
192                 authFailuresCount.set(1);
193             }
194         } else {
195             if (isLocked()) {
196                 // if too many failures, lock auth attemps for some time
197                 if (System.currentTimeMillis() - firstFailureDate.getTime() < LOCK_DURATION) {
198                     return false;
199                 }
200                 // lock is expired, reset
201                 firstFailureDate = null;
202                 authFailuresCount.set(0);
203                 return checkLockAgainstBruteForceAttack(authOk);
204             }
205             if (authOk) {
206                 // no more failure, reset
207                 firstFailureDate = null;
208                 authFailuresCount.set(0);
209             } else {
210                 // one more failure, insert coin to try again
211                 authFailuresCount.incrementAndGet();
212             }
213         }
214         return authOk;
215     }
216
217     private boolean isLocked() {
218         return authFailuresCount.get() > AUTH_FAILURES_MAX;
219     }
220 }
221