content: package com.taobao.arthas.core.shell.term.impl.http;
  
  import com.alibaba.arthas.deps.org.slf4j.Logger;
  import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
  import com.taobao.arthas.common.ArthasConstants;
  import com.taobao.arthas.core.security.AuthUtils;
  import com.taobao.arthas.core.security.BasicPrincipal;
  import com.taobao.arthas.core.security.BearerPrincipal;
  import com.taobao.arthas.core.security.SecurityAuthenticator;
  import com.taobao.arthas.core.server.ArthasBootstrap;
  import com.taobao.arthas.core.shell.term.impl.http.session.HttpSession;
  import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
  import com.taobao.arthas.core.util.StringUtils;
  import io.netty.buffer.ByteBuf;
  import io.netty.buffer.Unpooled;
  import io.netty.channel.ChannelDuplexHandler;
  import io.netty.channel.ChannelHandlerContext;
  import io.netty.channel.ChannelPromise;
  import io.netty.handler.codec.base64.Base64;
  import io.netty.handler.codec.http.*;
  import io.netty.util.Attribute;
  
  import javax.security.auth.Subject;
  import java.nio.charset.Charset;
  import java.security.Principal;
  import java.util.List;
  import java.util.Map;
  
  import static com.taobao.arthas.mcp.server.util.McpAuthExtractor.SUBJECT_ATTRIBUTE_KEY;
  
  
  /**
   * 
   * @author hengyunabc 2021-03-03
   *
   */
  public final class BasicHttpAuthenticatorHandler extends ChannelDuplexHandler {
      private static final Logger logger = LoggerFactory.getLogger(BasicHttpAuthenticatorHandler.class);
  
      private HttpSessionManager httpSessionManager;
  
      private SecurityAuthenticator securityAuthenticator = ArthasBootstrap.getInstance().getSecurityAuthenticator();
  
      public BasicHttpAuthenticatorHandler(HttpSessionManager httpSessionManager) {
          this.httpSessionManager = httpSessionManager;
      }
  
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
          if (!securityAuthenticator.needLogin()) {
              ctx.fireChannelRead(msg);
              return;
          }
  
          boolean authed = false;
          if (msg instanceof HttpRequest) {
              HttpRequest httpRequest = (HttpRequest) msg;
  
              // 判断session里是否有已登陆信息
              HttpSession session = httpSessionManager.getOrCreateHttpSession(ctx, httpRequest);
              if (session != null && session.getAttribute(ArthasConstants.SUBJECT_KEY) != null) {
                  authed = true;
              }
  
              boolean isMcpRequest = isMcpRequest(httpRequest);
              Principal principal = null;
              if (!authed) {
                  if (isMcpRequest) {
                      principal = extractMcpAuthSubject(httpRequest);
                  } else {
                      principal = extractBasicAuthSubject(httpRequest);
                      if (principal == null) {
                          principal = extractBasicAuthSubjectFromUrl(httpRequest);
                      }
                  }
              }
              if (!authed && principal == null) {
                  // 判断是否本地连接
                  principal = AuthUtils.localPrincipal(ctx);
              }
              Subject subject = securityAuthenticator.login(principal);
              if (subject != null) {
                  authed = true;
                  if (session != null) {
                      session.setAttribute(ArthasConstants.SUBJECT_KEY, subject);
                  }
                  ctx.channel().attr(SUBJECT_ATTRIBUTE_KEY).set(subject);
              }
  
              if (!authed) {
                  // restricted resource, so send back 401 to require valid username/password
                  HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
  
                  if (isMcpRequest) {
                      response.headers().add(HttpHeaderNames.WWW_AUTHENTICATE, "Bearer realm=\"arthas mcp\"")
                                         .add(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"arthas mcp\"");
                  } else {
                      response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"arthas webconsole\"");
                  }
                  
                  response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
                  response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
  
                  ctx.writeAndFlush(response);
                  // close the channel
                  ctx.channel().close();
                  return;
              }
  
          }
  
          ctx.fireChannelRead(msg);
      }
  
      @Override
      public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
          if (msg instanceof HttpResponse) {
              // write cookie
              HttpResponse response = (HttpResponse) msg;
              Attribute<HttpSession> attribute = ctx.channel().attr(HttpSessionManager.SESSION_KEY);
              HttpSession session = attribute.get();
              if (session != null) {
                  HttpSessionManager.setSessionCookie(response, session);
              }
          }
          super.write(ctx, msg, promise);
      }
  
      /**
       * 从url参数里提取 ?username=hello&password=world
       * 
       * @param request
       * @return
       */
      protected static BasicPrincipal extractBasicAuthSubjectFromUrl(HttpRequest request) {
          QueryStringDecoder queryDecoder = new QueryStringDecoder(request.uri());
          Map<String, List<String>> parameters = queryDecoder.parameters();
  
          List<String> passwords = parameters.get(ArthasConstants.PASSWORD_KEY);
          if (passwords == null || passwords.size() == 0) {
              return null;
          }
          String password = passwords.get(0);
  
          String username = ArthasConstants.DEFAULT_USERNAME;
          List<String> usernames = parameters.get(ArthasConstants.USERNAME_KEY);
          if (usernames != null && !usernames.isEmpty()) {
              username = usernames.get(0);
          }
          BasicPrincipal principal = new BasicPrincipal(username, password);
          logger.debug("Extracted Basic Auth principal from url: {}", principal);
          return principal;
      }
  
      /**
       * Extracts the username and password details from the HTTP basic header
       * Authorization.
       * <p/>
       * This requires that the <tt>Authorization</tt> HTTP header is provided, and
       * its using Basic. Currently Digest is <b>not</b> supported.
       *
       * @return {@link HttpPrincipal} with username and password details, or
       *         <tt>null</tt> if not possible to extract
       */
      protected static BasicPrincipal extractBasicAuthSubject(HttpRequest request) {
          String auth = request.headers().get(HttpHeaderNames.AUTHORIZATION);
          if (auth != null) {
              String constraint = StringUtils.before(auth, " ");
              if (constraint != null) {
                  if ("Basic".equalsIgnoreCase(constraint.trim())) {
                      String decoded = StringUtils.after(auth, " ");
                      if (decoded == null) {
                          logger.error("Extracted Basic Auth principal failed, bad auth String: {}", auth);
                          return null;
                      }
                      // the decoded part is base64 encoded, so we need to decode that
                      ByteBuf buf = Unpooled.wrappedBuffer(decoded.getBytes());
                      ByteBuf out = Base64.decode(buf);
                      String userAndPw = out.toString(Charset.defaultCharset());
                      String username = StringUtils.before(userAndPw, ":");
                      String password = StringUtils.after(userAndPw, ":");
                      BasicPrincipal principal = new BasicPrincipal(username, password);
                      logger.debug("Extracted Basic Auth principal from HTTP header: {}", principal);
                      return principal;
                  }
              }
          }
          return null;
      }
  
      /**
       * 判断是否为MCP请求
       * 
       * @param request
       */
      protected static boolean isMcpRequest(HttpRequest request) {
          try {
              String path = new java.net.URI(request.uri()).getPath();
              if (path == null) {
                  return false;
              }
  
              String mcpEndpoint = ArthasBootstrap.getInstance().getConfigure().getMcpEndpoint();
              if (mcpEndpoint == null || mcpEndpoint.trim().isEmpty()) {
                  // MCP 服务器未配置，不处理 MCP 请求
                  return false;
              }
              
              return mcpEndpoint.equals(path);
          } catch (Exception e) {
              logger.debug("Failed to parse request URI: {}", request.uri(), e);
              return false;
          }
      }
  
      /**
       * 为MCP请求提取认证主体，支持Bearer Token和Basic Auth两种方式
       * 
       * @param request
       */
      protected static Principal extractMcpAuthSubject(HttpRequest request) {
          // 首先尝试Bearer Token认证
          BearerPrincipal tokenPrincipal = extractBearerTokenSubject(request);
          if (tokenPrincipal != null) {
              return tokenPrincipal;
          }
  
          // 然后尝试Basic Auth认证
          BasicPrincipal basicPrincipal = extractBasicAuthSubject(request);
          if (basicPrincipal != null) {
              return basicPrincipal;
          }
  
          // 最后尝试从URL参数提取
          return extractBasicAuthSubjectFromUrl(request);
      }
  
      /**
       * 从Authorization header中提取Bearer Token
       * 
       * @param request
       */
      protected static BearerPrincipal extractBearerTokenSubject(HttpRequest request) {
          String auth = request.headers().get(HttpHeaderNames.AUTHORIZATION);
          if (auth != null) {
              String constraint = StringUtils.before(auth, " ");
              if (constraint != null) {
                  if ("Bearer".equalsIgnoreCase(constraint.trim())) {
                      String token = StringUtils.after(auth, " ");
                      if (token == null || token.trim().isEmpty()) {
                          logger.error("Extracted Bearer Token failed, bad auth String: {}", auth);
                          return null;
                      }
                      BearerPrincipal principal = new BearerPrincipal(token.trim());
                      logger.debug("Extracted Bearer Token principal: {}", principal);
                      return principal;
                  }
              }
          }
          return null;
      }
  
  }
  