/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Sam
 */

package com.caucho.server.admin;

import com.caucho.config.ConfigException;
import com.caucho.config.program.ContainerProgram;
import com.caucho.config.program.PropertyValueProgram;
import com.caucho.config.types.RawString;
import com.caucho.server.dispatch.ServletMapping;
import com.caucho.server.hmux.HmuxRequest;
import com.caucho.server.host.HostConfig;
import com.caucho.server.security.AbstractConstraint;
import com.caucho.server.security.SecurityConstraint;
import com.caucho.server.security.AuthorizationResult;
import com.caucho.server.webapp.WebAppConfig;
import com.caucho.util.InetNetwork;
import com.caucho.util.L10N;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.logging.Logger;

abstract public class ManagementService
{
  private static final L10N L = new L10N(ManagementService.class);
  protected final Logger log = Logger.getLogger(getClass().getName());

  private final Management _management;
  private final String _serviceName;

  // private String _password;

  private InetNetwork[]_allowedNetworks;

  protected ManagementService(Management management, String serviceName)
  {
    _management = management;
    _serviceName = serviceName;
  }

  public void start()
  {
    /*
    _password = _management.getRemoteCookie();
    
    if (_password == null) {
      log.warning(L.l("jmx-remote disabled.  jmx-remote requires at least one enabled management <user>"));
      return;
    }
    */
    
    HostConfig hostConfig = _management.getHostConfig();

    WebAppConfig webAppConfig = new WebAppConfig();
    webAppConfig.setId(_serviceName);
    webAppConfig.setRootDirectory(new RawString("memory:/admin-dummy-root"));

    hostConfig.addBuilderProgram(new PropertyValueProgram("web-app", webAppConfig));

    ServletMapping servlet = new ServletMapping();

    servlet.setServletName(_serviceName);
    servlet.addURLPattern("/*");
    servlet.addURLRegexp(".*");
    servlet.setServletClass(ManagementServlet.class.getName());

    ContainerProgram servletInit = new ContainerProgram();
    servletInit.addProgram(new PropertyValueProgram("service", this));
    servlet.setInit(servletInit);

    webAppConfig.addBuilderProgram(new PropertyValueProgram("servlet-mapping", servlet));

    SecurityConstraint constraint = new SecurityConstraint();
    constraint.addURLPattern("/*");
    constraint.addConstraint(new HmuxConstraint(this));
    constraint.init();
    webAppConfig.addBuilderProgram(new PropertyValueProgram("security-constraint", constraint));

    try {
      _allowedNetworks = new InetNetwork[] {
        new InetNetwork(InetAddress.getByName("127.0.0.1"), 24),
        new InetNetwork(InetAddress.getByName("10.0.0.0"), 24),
        new InetNetwork(InetAddress.getByName("172.16.0.0"), 20),
        new InetNetwork(InetAddress.getByName("192.168.0.0"), 16),
      };
    } catch (UnknownHostException e) {
      throw new RuntimeException(e);
    }
  }

  // first stage security - security constraint authorization
  public boolean isAuthorized(HttpServletRequest request,
                              HttpServletResponse response,
                              ServletContext application)
    throws ServletException, IOException
  {
    // The order of tests is important for the QA, see server/2e2-

    HttpServletResponse res = (HttpServletResponse) response;

    if (! (request instanceof HmuxRequest)) {
      log.warning(L.l("{0} attempt with non-hmux-request '{1}' from ip {2}",
                      _serviceName, request, request.getRemoteAddr()));

      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return false;
    }

    HmuxRequest hmuxRequest = (HmuxRequest) request;

    if (!request.getServletPath().equals("")) {
      log.warning(L.l("{0} attempt with invalid servlet-path '{1}' from address '{2}'",
                     _serviceName, request.getServletPath(), request.getRemoteAddr()));

      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return false;

    }

    if (request.getPathInfo() != null) {
      log.warning(L.l("{0} attempt with invalid path-info '{1}' from address '{2}'",
                      _serviceName, request.getPathInfo(), request.getRemoteAddr()));

      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return false;

    }

    // The remote test needs to be last to allow the other QA to work properly
    // XXX: this should be configurable, i.e. setReadConstraints and setWriteConstraints

    String remoteAddr = hmuxRequest.getRemoteAddr();

    boolean isValidAddr = false;
    for (int i = 0; i < _allowedNetworks.length; i++) {
      if (_allowedNetworks[i].isMatch(remoteAddr))
        isValidAddr = true;
    }

    if (! isValidAddr) {
      log.warning(L.l("{0} attempt from invalid address '{1}'",
                      _serviceName, remoteAddr));

      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return false;
    }

    return true;
  }

  // second stage security - read/write permission

  private boolean isRequestAllowed(ServletRequest request, ServletResponse response)
    throws IOException, ServletException
  {
    HttpServletResponse res = (HttpServletResponse) response;

    // check attributes for evidence of forward, includes, error page handling
    Enumeration<String> attributeNames = request.getAttributeNames();

    while (attributeNames.hasMoreElements()) {
      String  attributeName = attributeNames.nextElement();

      if (attributeName.equals("javax.servlet.request.X509Certificate"))
        continue;
      
      log.warning(L.l("management service request attribute '{0}' invalidates request",
                      attributeName));

      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return false;
    }

    return true;
  }

  protected boolean isReadAllowed(ServletRequest request, ServletResponse response)
    throws IOException, ServletException
  {
    if (!isRequestAllowed(request, response))
      return false;

    return true;
  }

  protected boolean isWriteAllowed(ServletRequest request, ServletResponse response)
    throws IOException, ServletException
  {
    if (!isRequestAllowed(request, response))
      return false;

    return true;
  }

  abstract public void service(ServletRequest request, ServletResponse response)
    throws IOException, ServletException;

  public String toString()
  {
    return (getClass().getSimpleName()
            + "[" + _serviceName
            + "," + _management.getServerId() + "]");
  }

  private static class HmuxConstraint
    extends AbstractConstraint
  {
    private final ManagementService _service;

    public HmuxConstraint(ManagementService service)
    {
      _service = service;
    }

    public AuthorizationResult isAuthorized(HttpServletRequest request,
                                            HttpServletResponse response,
                                            ServletContext application)
      throws ServletException, IOException
    {
      if (_service.isAuthorized(request, response, application))
        return AuthorizationResult.ALLOW;
      else
        return AuthorizationResult.DENY_SENT_RESPONSE;
    }
  }
}
