/*
 * Copyright (C) 2004-2009 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.webdav;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.util.Base64;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;

/*
 * This is a work in progress and is checked in for future use.
 */

/**
 * Implements a very light WebDAV-ish servlet for specific purposes.  It does not support WebDAV extensions
 * to the HTTP protocol.  Instead, it supports the set of commands: GET, PUT, and DELETE.  This serves
 * as a WebDAV like storage interface for MUC shared files.  It handles part of XEP-0129: WebDAV File Transfers,
 * but not all of it.  We don't handle the PROPPATCH command, for example, as the user has no rights to set
 * the permissions on MUC shared files.
 *
 * TODO: How to handle a remote account?  As posed in the WebDAV XEP, we should send a message and wait for response.
 * TODO: How to handle SparkWeb?  Reasking for a username and password would suck.  Need some form of SSO.
 *    Maybe the SSO could be some special token provided during sign-on that the client could store and use
 *    for auth.
 *
 * @author Daniel Henninger
 */
public class WebDAVLiteServlet extends HttpServlet {

    private static final Logger Log = LoggerFactory.getLogger(WebDAVLiteServlet.class);

    // Storage directory under the Openfire install root
    private static String WEBDAV_SUBDIR = "mucFiles";

    /**
     * Retrieves a File object referring to a file in a service and room.  Leaving file as null
     * will get you the directory that would contain all of the files for a particular service and room.
     *
     * @param service Subdomain of the conference service we are forming a file path for.
     * @param room Conference room we are forming a file path for.
     * @param file Optional (can be null) filename for a path to a specific file (otherwise, directory).
     * @return The File reference constructed for the service, room, and file combination.
     */
    private File getFileReference(String service, String room, String file) {
        return new File(JiveGlobals.getHomeDirectory(), WEBDAV_SUBDIR+File.separator+service+File.separator+room+(file != null ? File.separator+file : ""));
    }
    
    /**
     * Verifies that the user is authenticated via some mechanism such as Basic Auth.  If the
     * authentication fails, this method will alter the HTTP response to include a request for
     * auth and send the unauthorized response back to the client.
     *
     * TODO: Handle some form of special token auth, perhaps provided a room connection?
     * TODO: If it's not a local account, we should try message auth access?  XEP-0070?
     * TODO: Should we support digest auth as well?
     *
     * @param request Object representing the HTTP request.
     * @param response Object representing the HTTP response.
     * @return True or false if the user is authenticated.
     * @throws ServletException If there was a servlet related exception.
     * @throws IOException If there was an IO error while setting the error.
     */
    private Boolean isAuthenticated(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String auth = request.getHeader("Authorization");
        JID jid;
        try {
            if (auth == null || !request.getAuthType().equals(HttpServletRequest.BASIC_AUTH)) {
                throw new Exception("No authorization or improper authorization provided.");
            }
            auth = auth.substring(auth.indexOf(" "));
            String decoded = new String(Base64.decode(auth));
            int i = decoded.indexOf(":");
            String username = decoded.substring(0,i);
            if (!username.contains("@")) {
                throw new Exception("Not a valid JID.");
            }
            jid = new JID(username);
            if (XMPPServer.getInstance().isLocal(jid)) {
                String password = decoded.substring(i+1, decoded.length());
                if (AuthFactory.authenticate(username, password) == null) {
                    throw new Exception("Authentication failed.");
                }
            }
            else {
                // TODO: Authenticate a remote user, probably via message auth.
                throw new Exception("Not a local account.");
            }
            return true;
        }
        catch (Exception e) {
            /*
             * This covers all possible authentication issues.  Eg:
             * - not enough of auth info passed in
             * - failed auth
             */
            response.setHeader("WWW-Authenticate", "Basic realm=\"Openfire WebDAV\"");
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
    }

    /**
     * Verifies that the authenticated user is a member of a conference service and room, or else
     * they are not entitled to view any of the files in the room.
     *
     * @param request Object representing the HTTP request.
     * @param response Object representing the HTTP response.
     * @param service Subdomain of the conference service they are trying to access files for.
     * @param room Room in the conference service they are trying to access files for.
     * @return True or false if the user is authenticated.
     * @throws ServletException If there was a servlet related exception.
     * @throws IOException If there was an IO error while setting the error.
     */
    private Boolean isAuthorized(HttpServletRequest request, HttpServletResponse response,
                                 String service, String room) throws ServletException, IOException {
        String auth = request.getHeader("Authorization");
        try {
            if (auth == null || !request.getAuthType().equals(HttpServletRequest.BASIC_AUTH)) {
                throw new Exception("No authorization or improper authorization provided.");
            }
            auth = auth.substring(auth.indexOf(" "));
            String decoded = new String(Base64.decode(auth));
            int i = decoded.indexOf(":");
            String username = decoded.substring(0,i);
            if (!username.contains("@")) {
                throw new Exception("Not a valid JID.");
            }
            final JID bareJID = new JID(username).asBareJID();
            XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(service).getChatRoom(room).getOccupantsByBareJID(bareJID);
            return true;
        }
        catch (Exception e) {
            /*
             * This covers all possible authorization issues.  Eg:
             * - accessing a room that doesn't exist
             * - accessing a room that user isn't a member of
             */
            response.setHeader("WWW-Authenticate", "Basic realm=\"Openfire WebDAV\"");
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return false;
        }
    }

    /**
     * Initialize the WebDAV servlet, auto-creating it's file root if it doesn't exist.
     *
     * @param servletConfig Configuration settings of the servlet from web.xml.
     * @throws ServletException If there was an exception setting up the servlet.
     */
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        super.init(servletConfig);
        File webdavDir = new File(JiveGlobals.getHomeDirectory(), WEBDAV_SUBDIR);
        if (!webdavDir.exists()) {
            webdavDir.mkdirs();
        }
    }

    /**
     * Handles a GET request for files or for a file listing.
     *
     * @param request Object representing the HTTP request.
     * @param response Object representing the HTTP response.
     * @throws ServletException If there was a servlet related exception.
     * @throws IOException If there was an IO error while setting the error.
     */
    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws
                ServletException, IOException {
        // Verify authentication
        if (!isAuthenticated(request, response)) return;

        String path = request.getPathInfo();
        Log.debug("WebDAVLiteServlet: GET with path = "+path);
        if (path == null || !path.startsWith("/rooms/")) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        String[] pathPcs = path.split("/");
        if (pathPcs.length < 4 || pathPcs.length > 5) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        String service = pathPcs[2];
        String room = pathPcs[3];

        // Verify authorization
        if (!isAuthorized(request, response, service, room)) return;
        
        if (pathPcs.length == 5) {
            // File retrieval
            String filename = pathPcs[4];
            File file = getFileReference(service, room, filename);
            Log.debug("WebDAVListServlet: File path = "+file.getAbsolutePath());
            Log.debug("WebDAVListServlet: Service = "+service+", room = "+room+", file = "+filename);
            if (file.exists()) {
                response.setStatus(HttpServletResponse.SC_OK);
                response.setContentType("application/octet-stream");
                response.setContentLength((int)file.length());
                FileInputStream fileStream = new FileInputStream(file);
                byte[] byteArray = new byte[(int)file.length()];
                new DataInputStream(fileStream).readFully(byteArray);
                fileStream.close();
                response.getOutputStream().write(byteArray);
            }
            else {
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }
        else {
            // File listing
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("text/plain");
            response.setCharacterEncoding("UTF-8");
            String content = "Files available for "+room+"@"+service+":\n";
            File fileDir = getFileReference(service, room, null);
            Log.debug("WebDAVListServlet: File path = "+fileDir.getAbsolutePath());
            if (fileDir.exists()) {
                File[] files = fileDir.listFiles();
                for (File file : files) {
                    content += file.getName()+"\n";
                }
            }
            response.getOutputStream().write(content.getBytes());
            Log.debug("WebDAVListServlet: Service = "+service+", room = "+room);
        }
    }

    /**
     * Handles a PUT request for uploading files.
     *
     * @param request Object representing the HTTP request.
     * @param response Object representing the HTTP response.
     * @throws ServletException If there was a servlet related exception.
     * @throws IOException If there was an IO error while setting the error.
     */
    @Override
    protected void doPut(HttpServletRequest request,
                         HttpServletResponse response) throws
                ServletException, IOException {
        // Verify authentication
        if (!isAuthenticated(request, response)) return;

        String path = request.getPathInfo();
        Log.debug("WebDAVLiteServlet: PUT with path = "+path);
        if (request.getContentLength() <= 0) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        if (path == null || !path.startsWith("/rooms/")) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        String[] pathPcs = path.split("/");
        if (pathPcs.length != 5) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        String service = pathPcs[2];
        String room = pathPcs[3];
        String filename = pathPcs[4];

        // Verify authorization
        if (!isAuthorized(request, response, service, room)) return;

        Log.debug("WebDAVListServlet: Service = "+service+", room = "+room+", file = "+filename);
        File file = getFileReference(service, room, filename);
        Boolean overwriteFile = file.exists();
        FileOutputStream fileStream = new FileOutputStream(file, false);
        ServletInputStream inputStream = request.getInputStream();
        byte[] byteArray = new byte[request.getContentLength()];
        int bytesRead = 0;
        while (bytesRead != -1) {
            bytesRead = inputStream.read(byteArray, bytesRead, request.getContentLength());   
        }
        fileStream.write(byteArray);
        fileStream.close();
        inputStream.close();
        if (overwriteFile) {
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
            response.setHeader("Location", request.getRequestURI());
        }
        else {
            response.setStatus(HttpServletResponse.SC_CREATED);
            response.setHeader("Location", request.getRequestURI());
        }
    }

    /**
     * Handles a DELETE request for deleting files.
     *
     * @param request Object representing the HTTP request.
     * @param response Object representing the HTTP response.
     * @throws ServletException If there was a servlet related exception.
     * @throws IOException If there was an IO error while setting the error.
     */
    @Override
    protected void doDelete(HttpServletRequest request,
                         HttpServletResponse response) throws
                ServletException, IOException {
        // Verify authentication
        if (!isAuthenticated(request, response)) return;
        
        String path = request.getPathInfo();
        Log.debug("WebDAVLiteServlet: DELETE with path = "+path);
        if (path == null || !path.startsWith("/rooms/")) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        String[] pathPcs = path.split("/");
        if (pathPcs.length != 5) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        String service = pathPcs[2];
        String room = pathPcs[3];
        String filename = pathPcs[4];

        // Verify authorization
        if (!isAuthorized(request, response, service, room)) return;

        Log.debug("WebDAVListServlet: Service = "+service+", room = "+room+", file = "+filename);
        File file = getFileReference(service, room, filename);
        if (file.exists()) {
            file.delete();
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
        }
        else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
    }

}
