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}