1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package com.s3auth.hosts;
31
32 import com.jcabi.aspects.Cacheable;
33 import com.jcabi.aspects.Immutable;
34 import com.jcabi.aspects.LogExceptions;
35 import com.jcabi.aspects.Loggable;
36 import com.jcabi.log.Logger;
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
39 import java.net.URI;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.concurrent.ConcurrentMap;
42 import java.util.concurrent.TimeUnit;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 import javax.validation.constraints.NotNull;
46 import lombok.EqualsAndHashCode;
47 import org.apache.commons.codec.binary.Base64;
48 import org.apache.commons.codec.digest.Crypt;
49 import org.apache.commons.codec.digest.DigestUtils;
50 import org.apache.commons.codec.digest.Md5Crypt;
51 import org.apache.commons.io.Charsets;
52
53
54
55
56
57
58
59
60
61 @Immutable
62 @Loggable(Loggable.DEBUG)
63 @EqualsAndHashCode(of = "host")
64 @SuppressWarnings("PMD.UnusedPrivateField")
65 final class Htpasswd {
66
67
68
69
70 private static final int LIFETIME = 5;
71
72
73
74
75
76 private static final Htpasswd.Algorithm[] ALGORITHMS = {
77 new Htpasswd.Md5(),
78 new Htpasswd.Sha(),
79 new Htpasswd.UnixCrypt(),
80 new Htpasswd.PlainText(),
81 };
82
83
84
85
86 private final transient Host host;
87
88
89
90
91
92 Htpasswd(@NotNull final Host hst) {
93 this.host = hst;
94 }
95
96 @Override
97 public String toString() {
98 return Logger.format(
99 ".htpasswd(%d user(s), reloaded every %d min)",
100 this.fetch().size(),
101 Htpasswd.LIFETIME
102 );
103 }
104
105
106
107
108
109
110
111
112 @LogExceptions
113 public boolean authorized(@NotNull final String user,
114 @NotNull final String password) throws IOException {
115 final ConcurrentMap<String, String> users = this.fetch();
116 return users.containsKey(user)
117 && Htpasswd.matches(users.get(user), password);
118 }
119
120
121
122
123
124 @Cacheable(lifetime = Htpasswd.LIFETIME, unit = TimeUnit.MINUTES)
125 private ConcurrentMap<String, String> fetch() {
126 final ConcurrentMap<String, String> map =
127 new ConcurrentHashMap<String, String>(0);
128 final String[] lines = this.content().split("\n");
129 for (final String line : lines) {
130 if (line.isEmpty()) {
131 continue;
132 }
133 final String[] parts = line.trim().split(":", 2);
134 if (parts.length != 2) {
135 continue;
136 }
137 map.put(parts[0].trim(), parts[1].trim());
138 }
139 return map;
140 }
141
142
143
144
145
146 private String content() {
147 String content;
148 try {
149 final Resource res = this.host.fetch(
150 URI.create("/.htpasswd"),
151 Range.ENTIRE,
152 Version.LATEST
153 );
154 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
155 res.writeTo(baos);
156 content = baos.toString(Charsets.UTF_8.name()).trim();
157 } catch (final IOException ex) {
158 Logger.warn(
159 this,
160 "#content(): failed to fetch .htpasswd from %s: %s",
161 this.host, ex.getMessage()
162 );
163 content = "";
164 }
165 return content;
166 }
167
168
169
170
171
172
173
174
175 private static boolean matches(final String hash, final String password)
176 throws IOException {
177 boolean matches = false;
178 for (final Htpasswd.Algorithm algo : Htpasswd.ALGORITHMS) {
179 if (algo.matches(hash, password)) {
180 matches = true;
181 break;
182 }
183 }
184 return matches;
185 }
186
187
188
189
190 private interface Algorithm {
191
192
193
194
195
196
197
198 boolean matches(String hash, String password) throws IOException;
199 }
200
201
202
203
204 @Loggable(Loggable.DEBUG)
205 private static final class Md5 implements Htpasswd.Algorithm {
206
207
208
209 private static final Pattern PATTERN =
210 Pattern.compile("\\$apr1\\$([^\\$]+)\\$([a-zA-Z0-9/\\.]+=*)");
211 @Override
212 public boolean matches(final String hash, final String password) {
213 final Matcher matcher = Htpasswd.Md5.PATTERN.matcher(hash);
214 final boolean matches;
215 if (matcher.matches()) {
216 final String result = Md5Crypt.apr1Crypt(
217 password,
218 matcher.group(1)
219 );
220 matches = hash.equals(result);
221 } else {
222 matches = false;
223 }
224 return matches;
225 }
226 }
227
228
229
230
231 @Loggable(Loggable.DEBUG)
232 private static final class Sha implements Htpasswd.Algorithm {
233
234
235
236 private static final Pattern PATTERN =
237 Pattern.compile("\\{SHA\\}([a-zA-Z0-9/\\+]+=*)");
238 @Override
239 public boolean matches(final String hash, final String password) {
240 final Matcher matcher = Htpasswd.Sha.PATTERN.matcher(hash);
241 final boolean matches;
242 if (matcher.matches()) {
243 final String required = Base64.encodeBase64String(
244 DigestUtils.sha1(password)
245 );
246 matches = matcher.group(1).equals(required);
247 } else {
248 matches = false;
249 }
250 return matches;
251 }
252 }
253
254
255
256
257 @Loggable(Loggable.DEBUG)
258 private static final class UnixCrypt implements Htpasswd.Algorithm {
259
260
261
262 private static final Pattern PATTERN =
263 Pattern.compile("(\\$[156]\\$)?[a-zA-Z0-9./]+(\\$.*)*");
264 @Override
265 public boolean matches(final String hash, final String password) {
266 return Htpasswd.UnixCrypt.PATTERN.matcher(hash).matches()
267 && hash.equals(Crypt.crypt(password, hash));
268 }
269 }
270
271
272
273
274 @Loggable(Loggable.DEBUG)
275 private static final class PlainText implements Htpasswd.Algorithm {
276 @Override
277 public boolean matches(final String hash, final String password) {
278 return password.equals(hash);
279 }
280 }
281
282 }