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