/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.solr.security.hadoop;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
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.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationHandler;
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.protocol.HttpContext;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpListenerFactory;
import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.request.SolrRequestInfo;
import org.eclipse.jetty.client.api.Request;

/**
 * AuthenticationHandler that supports delegation tokens and simple authentication via the "user"
 * http parameter
 */
public class HttpParamDelegationTokenPlugin extends KerberosPlugin {
  public static final String USER_PARAM = "user"; // http parameter for user authentication
  public static final String REMOTE_HOST_PARAM =
      "remoteHost"; // http parameter for indicating remote host
  public static final String REMOTE_ADDRESS_PARAM =
      "remoteAddress"; // http parameter for indicating remote address
  public static final String INTERNAL_REQUEST_HEADER =
      "internalRequest"; // http header for indicating internal request

  boolean isSolrThread() {
    return ExecutorUtil.isSolrServerThread();
  }

  private final HttpRequestInterceptor interceptor =
      new HttpRequestInterceptor() {
        @Override
        public void process(HttpRequest httpRequest, HttpContext httpContext)
            throws HttpException, IOException {
          getPrincipal().ifPresent(usr -> httpRequest.setHeader(INTERNAL_REQUEST_HEADER, usr));
        }
      };

  public HttpParamDelegationTokenPlugin(CoreContainer coreContainer) {
    super(coreContainer);
  }

  @Override
  public void init(Map<String, Object> pluginConfig) {
    try {
      final FilterConfig initConf = getInitFilterConfig(pluginConfig, true);

      FilterConfig conf =
          new FilterConfig() {
            @Override
            public ServletContext getServletContext() {
              return initConf.getServletContext();
            }

            @Override
            public Enumeration<String> getInitParameterNames() {
              return initConf.getInitParameterNames();
            }

            @Override
            public String getInitParameter(String param) {
              if (AuthenticationFilter.AUTH_TYPE.equals(param)) {
                return HttpParamDelegationTokenAuthenticationHandler.class.getName();
              }
              return initConf.getInitParameter(param);
            }

            @Override
            public String getFilterName() {
              return "HttpParamFilter";
            }
          };
      Filter kerberosFilter = new HttpParamToRequestFilter();
      kerberosFilter.init(conf);
      setKerberosFilter(kerberosFilter);
    } catch (ServletException e) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR,
          "Error initializing kerberos authentication plugin: " + e);
    }
  }

  private Optional<String> getPrincipal() {
    SolrRequestInfo reqInfo = SolrRequestInfo.getRequestInfo();
    String usr;
    if (reqInfo != null) {
      Principal principal = reqInfo.getUserPrincipal();
      if (principal == null) {
        // this had a request but not authenticated
        // so we don't not need to set a principal
        return Optional.empty();
      } else {
        usr = principal.getName();
      }
    } else {
      if (!isSolrThread()) {
        // if this is not running inside a Solr threadpool (as in testcases)
        // then no need to add any header
        return Optional.empty();
      }
      // this request seems to be originated from Solr itself
      usr = "$"; // special name to denote the user is the node itself
    }
    return Optional.of(usr);
  }

  @Override
  public void setup(Http2SolrClient client) {
    final HttpListenerFactory.RequestResponseListener listener =
        new HttpListenerFactory.RequestResponseListener() {
          @Override
          public void onQueued(Request request) {
            getPrincipal().ifPresent(usr -> request.header(INTERNAL_REQUEST_HEADER, usr));
          }
        };
    client.addListenerFactory(() -> listener);
  }

  @Override
  public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) {
    HttpClientUtil.addRequestInterceptor(interceptor);
    builder = super.getHttpClientBuilder(builder);
    return builder;
  }

  @Override
  public void close() {
    HttpClientUtil.removeRequestInterceptor(interceptor);
    super.close();
  }

  private static String getHttpParam(HttpServletRequest request, String param) {
    List<NameValuePair> pairs =
        URLEncodedUtils.parse(request.getQueryString(), StandardCharsets.UTF_8);
    for (NameValuePair nvp : pairs) {
      if (param.equals(nvp.getName())) {
        return nvp.getValue();
      }
    }
    return null;
  }

  public static class HttpParamDelegationTokenAuthenticationHandler
      extends DelegationTokenAuthenticationHandler {

    public HttpParamDelegationTokenAuthenticationHandler() {
      super(new HttpParamAuthenticationHandler());
    }

    @Override
    public void init(Properties config) throws ServletException {
      Properties conf = new Properties();
      for (Map.Entry<Object, Object> entry : config.entrySet()) {
        conf.setProperty((String) entry.getKey(), (String) entry.getValue());
      }
      conf.setProperty(TOKEN_KIND, KerberosPlugin.DELEGATION_TOKEN_TYPE_DEFAULT);
      super.init(conf);
    }

    private static class HttpParamAuthenticationHandler implements AuthenticationHandler {
      @Override
      public String getType() {
        return "dummy";
      }

      @Override
      public void init(Properties config) throws ServletException {}

      @Override
      public void destroy() {}

      @Override
      public boolean managementOperation(
          AuthenticationToken token, HttpServletRequest request, HttpServletResponse response)
          throws IOException, AuthenticationException {
        return false;
      }

      @Override
      public AuthenticationToken authenticate(
          HttpServletRequest request, HttpServletResponse response)
          throws IOException, AuthenticationException {
        AuthenticationToken token = null;
        String userName = getHttpParam(request, USER_PARAM);
        if (userName == null) {
          // check if this is an internal request
          userName = request.getHeader(INTERNAL_REQUEST_HEADER);
        }
        if (userName != null) {
          return new AuthenticationToken(userName, userName, "test");
        } else {
          response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
          response.setHeader("WWW-Authenticate", "dummy");
        }
        return token;
      }
    }
  }

  /** Filter that converts http params to HttpServletRequest params */
  private static class HttpParamToRequestFilter extends DelegationTokenKerberosFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
      final HttpServletRequest httpRequest = (HttpServletRequest) request;
      final HttpServletRequestWrapper requestWrapper =
          new HttpServletRequestWrapper(httpRequest) {
            @Override
            public String getRemoteHost() {
              String param = getHttpParam(httpRequest, REMOTE_HOST_PARAM);
              return param != null ? param : httpRequest.getRemoteHost();
            }

            @Override
            public String getRemoteAddr() {
              String param = getHttpParam(httpRequest, REMOTE_ADDRESS_PARAM);
              return param != null ? param : httpRequest.getRemoteAddr();
            }
          };

      super.doFilter(requestWrapper, response, chain);
    }

    @Override
    protected void doFilter(
        FilterChain filterChain, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
      // remove the filter-specific authentication information, so it doesn't get accidentally
      // forwarded.
      List<NameValuePair> newPairs = new ArrayList<>();
      List<NameValuePair> pairs =
          URLEncodedUtils.parse(request.getQueryString(), StandardCharsets.UTF_8);
      for (NameValuePair nvp : pairs) {
        if (!USER_PARAM.equals(nvp.getName())) {
          newPairs.add(nvp);
        } else {
          request.setAttribute(USER_PARAM, nvp.getValue());
        }
      }
      final String queryStringNoUser = URLEncodedUtils.format(newPairs, StandardCharsets.UTF_8);
      HttpServletRequest requestWrapper =
          new HttpServletRequestWrapper(request) {
            @Override
            public String getQueryString() {
              return queryStringNoUser;
            }
          };
      super.doFilter(filterChain, requestWrapper, response);
    }
  }
}
