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}