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