001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.servlet; 020 021import org.apache.oozie.client.OozieClient.SYSTEM_MODE; 022import org.apache.oozie.client.rest.JsonBean; 023import org.apache.oozie.client.rest.RestConstants; 024import org.apache.oozie.service.DagXLogInfoService; 025import org.apache.oozie.service.InstrumentationService; 026import org.apache.oozie.service.ProxyUserService; 027import org.apache.oozie.service.Services; 028import org.apache.oozie.service.XLogService; 029import org.apache.oozie.util.Instrumentation; 030import org.apache.oozie.util.LogUtils; 031import org.apache.oozie.util.ParamChecker; 032import org.apache.oozie.util.XLog; 033import org.apache.oozie.ErrorCode; 034import org.json.simple.JSONObject; 035import org.json.simple.JSONStreamAware; 036 037import javax.servlet.ServletConfig; 038import javax.servlet.ServletException; 039import javax.servlet.http.HttpServlet; 040import javax.servlet.http.HttpServletRequest; 041import javax.servlet.http.HttpServletResponse; 042import java.io.IOException; 043import java.security.AccessControlException; 044import java.util.*; 045import java.util.concurrent.atomic.AtomicLong; 046 047/** 048 * Base class for Oozie web service API Servlets. <p/> This class provides common instrumentation, error logging and 049 * other common functionality. 050 */ 051public abstract class JsonRestServlet extends HttpServlet { 052 053 static final String JSON_UTF8 = RestConstants.JSON_CONTENT_TYPE + "; charset=\"UTF-8\""; 054 055 protected static final String XML_UTF8 = RestConstants.XML_CONTENT_TYPE + "; charset=\"UTF-8\""; 056 057 protected static final String TEXT_UTF8 = RestConstants.TEXT_CONTENT_TYPE + "; charset=\"UTF-8\""; 058 059 protected static final String AUDIT_OPERATION = "audit.operation"; 060 protected static final String AUDIT_PARAM = "audit.param"; 061 protected static final String AUDIT_ERROR_CODE = "audit.error.code"; 062 protected static final String AUDIT_ERROR_MESSAGE = "audit.error.message"; 063 protected static final String AUDIT_HTTP_STATUS_CODE = "audit.http.status.code"; 064 065 private XLog auditLog; 066 XLog.Info logInfo; 067 068 /** 069 * This bean defines a query string parameter. 070 */ 071 public static class ParameterInfo { 072 private String name; 073 private Class type; 074 private List<String> methods; 075 private boolean required; 076 077 /** 078 * Creates a ParameterInfo with querystring parameter definition. 079 * 080 * @param name querystring parameter name. 081 * @param type type for the parameter value, valid types are: <code>Integer, Boolean and String</code> 082 * @param required indicates if the parameter is required. 083 * @param methods HTTP methods the parameter is used by. 084 */ 085 public ParameterInfo(String name, Class type, boolean required, List<String> methods) { 086 this.name = ParamChecker.notEmpty(name, "name"); 087 if (type != Integer.class && type != Boolean.class && type != String.class) { 088 throw new IllegalArgumentException("Type must be integer, boolean or string"); 089 } 090 this.type = ParamChecker.notNull(type, "type"); 091 this.required = required; 092 this.methods = ParamChecker.notNull(methods, "methods"); 093 } 094 095 } 096 097 /** 098 * This bean defines a REST resource. 099 */ 100 public static class ResourceInfo { 101 private String name; 102 private boolean wildcard; 103 private List<String> methods; 104 private Map<String, ParameterInfo> parameters = new HashMap<String, ParameterInfo>(); 105 106 /** 107 * Creates a ResourceInfo with a REST resource definition. 108 * 109 * @param name name of the REST resource, it can be an fixed resource name, empty or a wildcard ('*'). 110 * @param methods HTTP methods supported by the resource. 111 * @param parameters parameters supported by the resource. 112 */ 113 public ResourceInfo(String name, List<String> methods, List<ParameterInfo> parameters) { 114 this.name = name; 115 wildcard = name.equals("*"); 116 for (ParameterInfo parameter : parameters) { 117 this.parameters.put(parameter.name, parameter); 118 } 119 this.methods = ParamChecker.notNull(methods, "methods"); 120 } 121 } 122 123 /** 124 * Name of the instrumentation group for the WS layer, value is 'webservices'. 125 */ 126 protected static final String INSTRUMENTATION_GROUP = "webservices"; 127 128 private static final String INSTR_TOTAL_REQUESTS_SAMPLER = "requests"; 129 private static final String INSTR_TOTAL_REQUESTS_COUNTER = "requests"; 130 private static final String INSTR_TOTAL_FAILED_REQUESTS_COUNTER = "failed"; 131 private static AtomicLong TOTAL_REQUESTS_SAMPLER_COUNTER; 132 133 private Instrumentation instrumentation; 134 private String instrumentationName; 135 private AtomicLong samplerCounter = new AtomicLong(); 136 private ThreadLocal<Instrumentation.Cron> requestCron = new ThreadLocal<Instrumentation.Cron>(); 137 private List<ResourceInfo> resourcesInfo = new ArrayList<ResourceInfo>(); 138 private boolean allowSafeModeChanges; 139 140 /** 141 * Creates a servlet with a specified instrumentation sampler name for its requests. 142 * 143 * @param instrumentationName instrumentation name for timer and samplers for the servlet. 144 * @param resourcesInfo list of resource definitions supported by the servlet, empty and wildcard resources must be 145 * the last ones, in that order, first empty and the wildcard. 146 */ 147 public JsonRestServlet(String instrumentationName, ResourceInfo... resourcesInfo) { 148 this.instrumentationName = ParamChecker.notEmpty(instrumentationName, "instrumentationName"); 149 if (resourcesInfo.length == 0) { 150 throw new IllegalArgumentException("There must be at least one ResourceInfo"); 151 } 152 this.resourcesInfo = Arrays.asList(resourcesInfo); 153 auditLog = XLog.getLog("oozieaudit"); 154 auditLog.setMsgPrefix(""); 155 logInfo = new XLog.Info(XLog.Info.get()); 156 } 157 158 /** 159 * Enable HTTP POST/PUT/DELETE methods while in safe mode. 160 * 161 * @param allow <code>true</code> enabled safe mode changes, <code>false</code> disable safe mode changes 162 * (default). 163 */ 164 protected void setAllowSafeModeChanges(boolean allow) { 165 allowSafeModeChanges = allow; 166 } 167 168 /** 169 * Define an instrumentation sampler. <p/> Sampling period is 60 seconds, the sampling frequency is 1 second. <p/> 170 * The instrumentation group used is {@link #INSTRUMENTATION_GROUP}. 171 * 172 * @param samplerName sampler name. 173 * @param samplerCounter sampler counter. 174 */ 175 private void defineSampler(String samplerName, final AtomicLong samplerCounter) { 176 instrumentation.addSampler(INSTRUMENTATION_GROUP, samplerName, 60, 1, new Instrumentation.Variable<Long>() { 177 public Long getValue() { 178 return samplerCounter.get(); 179 } 180 }); 181 } 182 183 /** 184 * Add an instrumentation cron. 185 * 186 * @param name name of the timer for the cron. 187 * @param cron cron to add to a instrumentation timer. 188 */ 189 private void addCron(String name, Instrumentation.Cron cron) { 190 instrumentation.addCron(INSTRUMENTATION_GROUP, name, cron); 191 } 192 193 /** 194 * Start the request instrumentation cron. 195 */ 196 protected void startCron() { 197 requestCron.get().start(); 198 } 199 200 /** 201 * Stop the request instrumentation cron. 202 */ 203 protected void stopCron() { 204 requestCron.get().stop(); 205 } 206 207 /** 208 * Initializes total request and servlet request samplers. 209 */ 210 public void init(ServletConfig servletConfig) throws ServletException { 211 super.init(servletConfig); 212 instrumentation = Services.get().get(InstrumentationService.class).get(); 213 synchronized (JsonRestServlet.class) { 214 if (TOTAL_REQUESTS_SAMPLER_COUNTER == null) { 215 TOTAL_REQUESTS_SAMPLER_COUNTER = new AtomicLong(); 216 defineSampler(INSTR_TOTAL_REQUESTS_SAMPLER, TOTAL_REQUESTS_SAMPLER_COUNTER); 217 } 218 } 219 defineSampler(instrumentationName, samplerCounter); 220 } 221 222 /** 223 * Convenience method for instrumentation counters. 224 * 225 * @param name counter name. 226 * @param count count to increment the counter. 227 */ 228 private void incrCounter(String name, int count) { 229 if (instrumentation != null) { 230 instrumentation.incr(INSTRUMENTATION_GROUP, name, count); 231 } 232 } 233 234 /** 235 * Logs audit information for write requests to the audit log. 236 * 237 * @param request the http request. 238 */ 239 private void logAuditInfo(HttpServletRequest request) { 240 if (request.getAttribute(AUDIT_OPERATION) != null) { 241 Integer httpStatusCode = (Integer) request.getAttribute(AUDIT_HTTP_STATUS_CODE); 242 httpStatusCode = (httpStatusCode != null) ? httpStatusCode : HttpServletResponse.SC_OK; 243 String status = (httpStatusCode == HttpServletResponse.SC_OK) ? "SUCCESS" : "FAILED"; 244 String operation = (String) request.getAttribute(AUDIT_OPERATION); 245 String param = (String) request.getAttribute(AUDIT_PARAM); 246 String user = XLog.Info.get().getParameter(XLogService.USER); 247 String group = XLog.Info.get().getParameter(XLogService.GROUP); 248 String jobId = XLog.Info.get().getParameter(DagXLogInfoService.JOB); 249 String app = XLog.Info.get().getParameter(DagXLogInfoService.APP); 250 251 String errorCode = (String) request.getAttribute(AUDIT_ERROR_CODE); 252 String errorMessage = (String) request.getAttribute(AUDIT_ERROR_MESSAGE); 253 String hostDetail = request.getRemoteAddr(); 254 255 auditLog.info( 256 "IP [{0}], USER [{1}], GROUP [{2}], APP [{3}], JOBID [{4}], OPERATION [{5}], PARAMETER [{6}], STATUS [{7}]," 257 + " HTTPCODE [{8}], ERRORCODE [{9}], ERRORMESSAGE [{10}]", hostDetail, user, group, app, 258 jobId, operation, param, status, httpStatusCode, errorCode, errorMessage); 259 } 260 } 261 262 /** 263 * Dispatches to super after loginfo and intrumentation handling. In case of errors dispatches error response codes 264 * and does error logging. 265 */ 266 @SuppressWarnings("unchecked") 267 protected final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, 268 IOException { 269 //if (Services.get().isSafeMode() && !request.getMethod().equals("GET") && !allowSafeModeChanges) { 270 if (Services.get().getSystemMode() != SYSTEM_MODE.NORMAL && !request.getMethod().equals("GET") && !allowSafeModeChanges) { 271 sendErrorResponse(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.E0002.toString(), 272 ErrorCode.E0002.getTemplate()); 273 return; 274 } 275 Instrumentation.Cron cron = new Instrumentation.Cron(); 276 requestCron.set(cron); 277 try { 278 cron.start(); 279 validateRestUrl(request.getMethod(), getResourceName(request), request.getParameterMap()); 280 XLog.Info.get().clear(); 281 String user = getUser(request); 282 TOTAL_REQUESTS_SAMPLER_COUNTER.incrementAndGet(); 283 samplerCounter.incrementAndGet(); 284 //If trace is enabled then display the request headers 285 XLog log = XLog.getLog(getClass()); 286 if (log.isTraceEnabled()){ 287 logHeaderInfo(request); 288 } 289 super.service(request, response); 290 } 291 catch (XServletException ex) { 292 XLog log = XLog.getLog(getClass()); 293 log.warn("URL[{0} {1}] error[{2}], {3}", request.getMethod(), getRequestUrl(request), ex.getErrorCode(), ex 294 .getMessage(), ex); 295 request.setAttribute(AUDIT_ERROR_MESSAGE, ex.getMessage()); 296 request.setAttribute(AUDIT_ERROR_CODE, ex.getErrorCode().toString()); 297 request.setAttribute(AUDIT_HTTP_STATUS_CODE, ex.getHttpStatusCode()); 298 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 299 sendErrorResponse(response, ex.getHttpStatusCode(), ex.getErrorCode().toString(), ex.getMessage()); 300 } 301 catch (AccessControlException ex) { 302 XLog log = XLog.getLog(getClass()); 303 log.error("URL[{0} {1}] error, {2}", request.getMethod(), getRequestUrl(request), ex.getMessage(), ex); 304 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 305 sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, ErrorCode.E1400.toString(), 306 ex.getMessage()); 307 } 308 catch (IllegalArgumentException ex){ 309 XLog log = XLog.getLog(getClass()); 310 log.warn("URL[{0} {1}] user error, {2}", request.getMethod(), getRequestUrl(request), ex.getMessage(), ex); 311 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 312 sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E1603.toString(), 313 ex.getMessage()); 314 } 315 catch (RuntimeException ex) { 316 XLog log = XLog.getLog(getClass()); 317 log.error("URL[{0} {1}] error, {2}", request.getMethod(), getRequestUrl(request), ex.getMessage(), ex); 318 incrCounter(INSTR_TOTAL_FAILED_REQUESTS_COUNTER, 1); 319 throw ex; 320 } 321 finally { 322 logAuditInfo(request); 323 TOTAL_REQUESTS_SAMPLER_COUNTER.decrementAndGet(); 324 incrCounter(INSTR_TOTAL_REQUESTS_COUNTER, 1); 325 samplerCounter.decrementAndGet(); 326 XLog.Info.remove(); 327 cron.stop(); 328 // TODO 329 incrCounter(instrumentationName, 1); 330 incrCounter(instrumentationName + "-" + request.getMethod(), 1); 331 addCron(instrumentationName, cron); 332 addCron(instrumentationName + "-" + request.getMethod(), cron); 333 requestCron.remove(); 334 } 335 } 336 337 private void logHeaderInfo(HttpServletRequest request){ 338 XLog log = XLog.getLog(getClass()); 339 StringBuilder traceInfo = new StringBuilder(4096); 340 //Display request URL and request.getHeaderNames(); 341 Enumeration<String> names = (Enumeration<String>) request.getHeaderNames(); 342 traceInfo.append("Request URL: ").append(getRequestUrl(request)).append("\nRequest Headers:\n"); 343 while (names.hasMoreElements()) { 344 String name = names.nextElement(); 345 String value = request.getHeader(name); 346 traceInfo.append(name).append(" : ").append(value).append("\n"); 347 } 348 log.trace(traceInfo); 349 } 350 351 private String getRequestUrl(HttpServletRequest request) { 352 StringBuffer url = request.getRequestURL(); 353 if (request.getQueryString() != null) { 354 url.append("?").append(request.getQueryString()); 355 } 356 return url.toString(); 357 } 358 359 /** 360 * Sends a JSON response. 361 * 362 * @param response servlet response. 363 * @param statusCode HTTP status code. 364 * @param bean bean to send as JSON response. 365 * @param timeZoneId time zone to use for dates in the JSON response. 366 * @throws java.io.IOException thrown if the bean could not be serialized to the response output stream. 367 */ 368 protected void sendJsonResponse(HttpServletResponse response, int statusCode, JsonBean bean, String timeZoneId) 369 throws IOException { 370 response.setStatus(statusCode); 371 JSONObject json = bean.toJSONObject(timeZoneId); 372 response.setContentType(JSON_UTF8); 373 json.writeJSONString(response.getWriter()); 374 } 375 376 /** 377 * Sends a error response. 378 * 379 * @param response servlet response. 380 * @param statusCode HTTP status code. 381 * @param error error code. 382 * @param message error message. 383 * @throws java.io.IOException thrown if the error response could not be set. 384 */ 385 protected void sendErrorResponse(HttpServletResponse response, int statusCode, String error, String message) 386 throws IOException { 387 response.setHeader(RestConstants.OOZIE_ERROR_CODE, error); 388 response.setHeader(RestConstants.OOZIE_ERROR_MESSAGE, message); 389 response.sendError(statusCode); 390 } 391 392 protected void sendJsonResponse(HttpServletResponse response, int statusCode, JSONStreamAware json) 393 throws IOException { 394 if (statusCode == HttpServletResponse.SC_OK || statusCode == HttpServletResponse.SC_CREATED) { 395 response.setStatus(statusCode); 396 } 397 else { 398 response.sendError(statusCode); 399 } 400 response.setStatus(statusCode); 401 response.setContentType(JSON_UTF8); 402 json.writeJSONString(response.getWriter()); 403 } 404 405 /** 406 * Validates REST URL using the ResourceInfos of the servlet. 407 * 408 * @param method HTTP method. 409 * @param resourceName resource name. 410 * @param queryStringParams query string parameters. 411 * @throws javax.servlet.ServletException thrown if the resource name or parameters are incorrect. 412 */ 413 @SuppressWarnings("unchecked") 414 protected void validateRestUrl(String method, String resourceName, Map<String, String[]> queryStringParams) 415 throws ServletException { 416 417 if (resourceName.contains("/")) { 418 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 419 } 420 421 boolean valid = false; 422 for (int i = 0; !valid && i < resourcesInfo.size(); i++) { 423 ResourceInfo resourceInfo = resourcesInfo.get(i); 424 if (resourceInfo.name.equals(resourceName) || resourceInfo.wildcard) { 425 if (!resourceInfo.methods.contains(method)) { 426 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 427 } 428 for (Map.Entry<String, String[]> entry : queryStringParams.entrySet()) { 429 String name = entry.getKey(); 430 ParameterInfo parameterInfo = resourceInfo.parameters.get(name); 431 if (parameterInfo != null) { 432 if (!parameterInfo.methods.contains(method)) { 433 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0302, name); 434 } 435 String value = entry.getValue()[0].trim(); 436 if (parameterInfo.type.equals(Boolean.class)) { 437 value = value.toLowerCase(); 438 if (!value.equals("true") && !value.equals("false")) { 439 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0304, name, 440 "boolean"); 441 } 442 } 443 if (parameterInfo.type.equals(Integer.class)) { 444 try { 445 Integer.parseInt(value); 446 } 447 catch (NumberFormatException ex) { 448 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0304, name, 449 "integer"); 450 } 451 } 452 } 453 } 454 for (ParameterInfo parameterInfo : resourceInfo.parameters.values()) { 455 if (parameterInfo.methods.contains(method) && parameterInfo.required 456 && queryStringParams.get(parameterInfo.name) == null) { 457 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0305, 458 parameterInfo.name); 459 } 460 } 461 valid = true; 462 } 463 } 464 if (!valid) { 465 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0301, resourceName); 466 } 467 } 468 469 /** 470 * Return the resource name of the request. <p/> The resource name is the whole extra path. If the extra path starts 471 * with '/', the first '/' is trimmed. 472 * 473 * @param request request instance 474 * @return the resource name, <code>null</code> if none. 475 */ 476 protected String getResourceName(HttpServletRequest request) { 477 String requestPath = request.getPathInfo(); 478 if (requestPath != null) { 479 while (requestPath.startsWith("/")) { 480 requestPath = requestPath.substring(1); 481 } 482 requestPath = requestPath.trim(); 483 } 484 else { 485 requestPath = ""; 486 } 487 return requestPath; 488 } 489 490 /** 491 * Return the request content type, lowercase and without attributes. 492 * 493 * @param request servlet request. 494 * @return the request content type, <code>null</code> if none. 495 */ 496 protected String getContentType(HttpServletRequest request) { 497 String contentType = request.getContentType(); 498 if (contentType != null) { 499 int index = contentType.indexOf(";"); 500 if (index > -1) { 501 contentType = contentType.substring(0, index); 502 } 503 contentType = contentType.toLowerCase(); 504 } 505 return contentType; 506 } 507 508 /** 509 * Validate and return the content type of the request. 510 * 511 * @param request servlet request. 512 * @param expected expected contentType. 513 * @return the normalized content type (lowercase and without modifiers). 514 * @throws XServletException thrown if the content type is invalid. 515 */ 516 protected String validateContentType(HttpServletRequest request, String expected) throws XServletException { 517 String contentType = getContentType(request); 518 if (contentType == null || contentType.trim().length() == 0) { 519 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0300, contentType); 520 } 521 if (!contentType.equals(expected)) { 522 throw new XServletException(HttpServletResponse.SC_BAD_REQUEST, ErrorCode.E0300, contentType); 523 } 524 return contentType; 525 } 526 527 /** 528 * Request attribute constant for the authenticatio token. 529 */ 530 public static final String AUTH_TOKEN = "oozie.auth.token"; 531 532 /** 533 * Request attribute constant for the user name. 534 */ 535 public static final String USER_NAME = "oozie.user.name"; 536 537 protected static final String UNDEF = "?"; 538 539 /** 540 * Return the user name of the request if any. 541 * 542 * @param request request. 543 * @return the user name, <code>null</code> if there is none. 544 */ 545 protected String getUser(HttpServletRequest request) { 546 String userName = (String) request.getAttribute(USER_NAME); 547 548 String doAsUserName = request.getParameter(RestConstants.DO_AS_PARAM); 549 if (doAsUserName != null && !doAsUserName.equals(userName)) { 550 ProxyUserService proxyUser = Services.get().get(ProxyUserService.class); 551 try { 552 proxyUser.validate(userName, HostnameFilter.get(), doAsUserName); 553 } 554 catch (IOException ex) { 555 throw new RuntimeException(ex); 556 } 557 auditLog.info("Proxy user [{0}] DoAs user [{1}] Request [{2}]", userName, doAsUserName, 558 getRequestUrl(request)); 559 560 XLog.Info.get().setParameter(XLogService.USER, userName + " doAs " + doAsUserName); 561 562 userName = doAsUserName; 563 } 564 else { 565 XLog.Info.get().setParameter(XLogService.USER, userName); 566 } 567 return (userName != null) ? userName : UNDEF; 568 } 569 570 /** 571 * Set the thread local log info with the given information. 572 * 573 * @param actionid action ID. 574 */ 575 protected void setLogInfo(String actionid) { 576 LogUtils.setLogInfo(actionid); 577 } 578}