1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.waarp.openr66.protocol.http.restv2.resthandlers;
22
23 import io.cdap.http.HandlerHook;
24 import io.cdap.http.HttpResponder;
25 import io.cdap.http.internal.HandlerInfo;
26 import io.netty.handler.codec.http.DefaultHttpHeaders;
27 import io.netty.handler.codec.http.HttpRequest;
28 import io.netty.handler.codec.http.HttpResponseStatus;
29 import org.joda.time.DateTime;
30 import org.waarp.common.crypto.HmacSha256;
31 import org.waarp.common.database.exception.WaarpDatabaseException;
32 import org.waarp.common.logging.WaarpLogger;
33 import org.waarp.common.logging.WaarpLoggerFactory;
34 import org.waarp.common.role.RoleDefault;
35 import org.waarp.common.role.RoleDefault.ROLE;
36 import org.waarp.common.utility.BaseXx;
37 import org.waarp.common.utility.ParametersChecker;
38 import org.waarp.common.utility.WaarpStringUtils;
39 import org.waarp.openr66.dao.DAOFactory;
40 import org.waarp.openr66.dao.HostDAO;
41 import org.waarp.openr66.dao.exception.DAOConnectionException;
42 import org.waarp.openr66.dao.exception.DAONoDataException;
43 import org.waarp.openr66.database.data.DbHostAuth;
44 import org.waarp.openr66.pojo.Host;
45 import org.waarp.openr66.protocol.http.restv2.RestServiceInitializer;
46 import org.waarp.openr66.protocol.http.restv2.converters.HostConfigConverter;
47 import org.waarp.openr66.protocol.http.restv2.dbhandlers.AbstractRestDbHandler;
48 import org.waarp.openr66.protocol.http.restv2.dbhandlers.RequiredRole;
49
50 import javax.ws.rs.Consumes;
51 import javax.ws.rs.InternalServerErrorException;
52 import javax.ws.rs.NotAllowedException;
53 import javax.ws.rs.core.MediaType;
54 import java.lang.reflect.Method;
55 import java.text.ParseException;
56 import java.util.Arrays;
57 import java.util.List;
58 import java.util.regex.Matcher;
59 import java.util.regex.Pattern;
60
61 import static io.netty.handler.codec.http.HttpMethod.*;
62 import static io.netty.handler.codec.http.HttpResponseStatus.*;
63 import static javax.ws.rs.core.HttpHeaders.*;
64 import static javax.ws.rs.core.MediaType.*;
65 import static org.glassfish.jersey.message.internal.HttpHeaderReader.*;
66 import static org.glassfish.jersey.message.internal.MediaTypes.*;
67 import static org.waarp.common.role.RoleDefault.ROLE.*;
68 import static org.waarp.openr66.protocol.configuration.Configuration.*;
69 import static org.waarp.openr66.protocol.http.restv2.RestConstants.*;
70
71
72
73
74
75
76
77
78 public class RestHandlerHook implements HandlerHook {
79
80
81
82
83 private final boolean authenticated;
84
85
86
87
88 private final HmacSha256 hmac;
89
90
91
92
93 private final long delay;
94
95
96
97
98 private static final WaarpLogger logger =
99 WaarpLoggerFactory.getLogger(RestHandlerHook.class);
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 @Override
118 public final boolean preCall(final HttpRequest request,
119 final HttpResponder responder,
120 final HandlerInfo handlerInfo) {
121
122 try {
123 final AbstractRestDbHandler handler = getHandler(handlerInfo);
124 if (!handler.checkCRUD(request)) {
125 responder.sendStatus(METHOD_NOT_ALLOWED);
126 return false;
127 }
128
129 final Method handleMethod = getMethod(handler, handlerInfo);
130 if (authenticated && !request.method().equals(OPTIONS)) {
131 final String user = checkCredentials(request);
132 if (!checkAuthorization(user, handleMethod)) {
133 responder.sendStatus(FORBIDDEN);
134 return false;
135 }
136 }
137
138 final List<MediaType> expectedTypes = getExpectedMediaTypes(handleMethod);
139 if (!checkContentType(request, expectedTypes)) {
140 final DefaultHttpHeaders headers = new DefaultHttpHeaders();
141 headers.add(ACCEPT, convertToString(expectedTypes));
142 responder.sendStatus(UNSUPPORTED_MEDIA_TYPE, headers);
143 return false;
144 }
145
146 return true;
147 } catch (final NotAllowedException e) {
148 logger.info(e.getMessage());
149 final DefaultHttpHeaders headers = new DefaultHttpHeaders();
150 headers.add(WWW_AUTHENTICATE, "Basic, HMAC");
151 responder.sendStatus(UNAUTHORIZED, headers);
152 } catch (final InternalServerErrorException e) {
153 logger.error(e);
154 responder.sendStatus(INTERNAL_SERVER_ERROR);
155 } catch (final Throwable t) {
156 logger.error("RESTv2 Unexpected exception caught ->", t);
157 responder.sendStatus(INTERNAL_SERVER_ERROR);
158 }
159 return false;
160 }
161
162
163
164
165
166
167
168
169
170
171
172
173 private AbstractRestDbHandler getHandler(final HandlerInfo handlerInfo) {
174 for (final AbstractRestDbHandler h : RestServiceInitializer.handlers) {
175 if (h.getClass().getName().equals(handlerInfo.getHandlerName())) {
176 return h;
177 }
178 }
179 throw new IllegalArgumentException(
180 "The handler " + handlerInfo.getHandlerName() + " does not exist.");
181 }
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196 private Method getMethod(final AbstractRestDbHandler handler,
197 final HandlerInfo handlerInfo) {
198 Method method = null;
199 for (final Method m : handler.getClass().getMethods()) {
200 if (m.getName().equals(handlerInfo.getMethodName()) &&
201 m.getParameterTypes()[0] == HttpRequest.class &&
202 m.getParameterTypes()[1] == HttpResponder.class) {
203 method = m;
204 break;
205 }
206 }
207 if (method == null) {
208 throw new IllegalArgumentException(
209 "The handler " + handlerInfo.getHandlerName() +
210 " does not have a method " + handlerInfo.getMethodName());
211 }
212 return method;
213 }
214
215
216
217
218
219
220
221
222
223
224
225
226 private List<MediaType> getExpectedMediaTypes(final Method method) {
227 List<MediaType> consumedTypes = WILDCARD_TYPE_SINGLETON_LIST;
228
229 if (method.isAnnotationPresent(Consumes.class)) {
230 consumedTypes = createFrom(method.getAnnotation(Consumes.class));
231 } else {
232 logger.warn(String.format(
233 "[RESTv2] The method %s of handler %s is missing " +
234 "a '%s' annotation for the expected request content type, " +
235 "the default value '%s' was given instead.", method.getName(),
236 method.getDeclaringClass().getSimpleName(),
237 Consumes.class.getSimpleName(), WILDCARD));
238 }
239
240 return consumedTypes;
241 }
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256 private boolean checkContentType(final HttpRequest request,
257 final List<MediaType> consumedTypes) {
258
259 final String contentTypeHeader = request.headers().get(CONTENT_TYPE);
260 if (ParametersChecker.isEmpty(contentTypeHeader)) {
261 return true;
262 }
263
264 final MediaType requestType;
265 try {
266 requestType = readAcceptMediaType(contentTypeHeader).get(0);
267 } catch (final ParseException e) {
268 return false;
269 }
270 for (final MediaType consumedType : consumedTypes) {
271 if (requestType.isCompatible(consumedType)) {
272 return true;
273 }
274 }
275 return false;
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292 protected final String checkCredentials(final HttpRequest request) {
293
294 final String authorization = request.headers().get(AUTHORIZATION);
295
296 if (authorization == null) {
297 throw new NotAllowedException("Missing header for authentication.");
298 }
299
300 final Pattern basicPattern = Pattern.compile("(Basic) (\\w+=*)");
301 final Matcher basicMatcher = basicPattern.matcher(authorization);
302
303 if (basicMatcher.find()) {
304
305 final String[] credentials;
306 credentials = new String(BaseXx.getFromBase64(basicMatcher.group(2)),
307 WaarpStringUtils.UTF8).split(":", 2);
308 if (credentials.length != 2) {
309 throw new NotAllowedException(
310 "Invalid header for Basic authentication.");
311 }
312 final String user = credentials[0];
313 final String pswd = credentials[1];
314
315 HostDAO hostDAO = null;
316 Host host;
317 try {
318 hostDAO = DAO_FACTORY.getHostDAO(true);
319 if (!hostDAO.exist(user)) {
320 throw new NotAllowedException("User does not exist.");
321 }
322 host = hostDAO.select(user);
323 } catch (final DAOConnectionException e) {
324 throw new InternalServerErrorException(e);
325 } catch (final DAONoDataException e) {
326 throw new InternalServerErrorException(e);
327 } finally {
328 DAOFactory.closeDAO(hostDAO);
329 }
330
331 final String key;
332 try {
333 key = configuration.getCryptoKey().cryptToHex(pswd);
334 } catch (final Exception e) {
335 throw new InternalServerErrorException(
336 "An error occurred when encrypting the password", e);
337 }
338 if (!Arrays.equals(host.getHostkey(),
339 key.getBytes(WaarpStringUtils.UTF8))) {
340 throw new NotAllowedException("Invalid password.");
341 }
342
343 return user;
344 }
345
346 final String authUser = request.headers().get(AUTH_USER);
347 final String authDate = request.headers().get(AUTH_TIMESTAMP);
348
349 final Pattern hmacPattern = Pattern.compile("(HMAC) (\\w+)");
350 final Matcher hmacMatcher = hmacPattern.matcher(authorization);
351
352 if (hmacMatcher.find() && authUser != null && authDate != null) {
353
354 final String authKey = hmacMatcher.group(2);
355 final DateTime requestDate;
356 try {
357 requestDate = DateTime.parse(authDate);
358 } catch (final IllegalArgumentException e) {
359 throw new NotAllowedException("Invalid authentication timestamp.");
360 }
361 final DateTime limitTime = requestDate.plus(delay);
362 if (DateTime.now().isAfter(limitTime)) {
363 throw new NotAllowedException("Authentication expired.");
364 }
365
366 HostDAO hostDAO = null;
367 Host host;
368 try {
369 hostDAO = DAO_FACTORY.getHostDAO(true);
370 if (!hostDAO.exist(authUser)) {
371 throw new NotAllowedException("User does not exist.");
372 }
373 host = hostDAO.select(authUser);
374 } catch (final DAOConnectionException e) {
375 throw new InternalServerErrorException(e);
376 } catch (final DAONoDataException e) {
377 throw new InternalServerErrorException(e);
378 } finally {
379 DAOFactory.closeDAO(hostDAO);
380 }
381
382 validateHMACCredentials(host, authDate, authUser, authKey);
383
384 return authUser;
385 }
386
387 throw new NotAllowedException("Missing credentials.");
388 }
389
390 protected final void validateHMACCredentials(final Host host,
391 final String authDate,
392 final String authUser,
393 final String authKey)
394 throws InternalServerErrorException {
395 final String pswd;
396 try {
397 pswd = configuration.getCryptoKey().decryptHexInString(host.getHostkey());
398 } catch (final Exception e) {
399 throw new InternalServerErrorException(
400 "An error occurred when decrypting the password", e);
401 }
402
403 final String key;
404 try {
405 key = hmac.cryptToHex(authDate + authUser + pswd);
406 } catch (final Exception e) {
407 throw new InternalServerErrorException(
408 "An error occurred when hashing the key", e);
409 }
410
411 if (!key.equals(authKey)) {
412 throw new NotAllowedException("Invalid password.");
413 }
414 }
415
416
417
418
419
420
421
422
423
424
425
426 protected final boolean checkAuthorization(final String user,
427 final Method method) {
428 try {
429 final DbHostAuth hostAuth = new DbHostAuth(user);
430 if (hostAuth.isAdminrole()) {
431 return true;
432 }
433 } catch (final WaarpDatabaseException e) {
434
435 }
436
437 ROLE requiredRole = NOACCESS;
438 if (method.isAnnotationPresent(RequiredRole.class)) {
439 requiredRole = method.getAnnotation(RequiredRole.class).value();
440 } else {
441 logger.warn(String.format("[RESTv2] The method %s of handler %s is " +
442 "missing a '%s' annotation for the minimum required role, " +
443 "the default value '%s' was given instead.",
444 method.getName(),
445 method.getDeclaringClass().getSimpleName(),
446 RequiredRole.class.getSimpleName(), NOACCESS));
447 }
448 if (requiredRole == NOACCESS) {
449 return true;
450 }
451
452 final List<ROLE> roles = HostConfigConverter.getRoles(user);
453 if (roles != null) {
454 final RoleDefault roleDefault = new RoleDefault();
455 for (final ROLE roleType : roles) {
456 roleDefault.addRole(roleType);
457 }
458 return roleDefault.isContaining(requiredRole);
459 }
460 return false;
461 }
462
463
464
465
466
467
468
469
470
471
472 @Override
473 public final void postCall(final HttpRequest httpRequest,
474 final HttpResponseStatus httpResponseStatus,
475 final HandlerInfo handlerInfo) {
476
477 }
478
479
480
481
482
483
484
485
486
487
488
489 public RestHandlerHook(final boolean authenticated, final HmacSha256 hmac,
490 final long delay) {
491 this.authenticated = authenticated;
492 this.hmac = hmac;
493 this.delay = delay;
494 }
495 }