// Copyright (C) 2009 The Android Open Source Project // // 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 com.google.gerrit.httpd.auth.container; import com.google.gerrit.common.PageLinks; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import org.eclipse.jgit.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.IOException; import javax.annotation.Nullable; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Initializes the user session if HTTP authentication is enabled. * <p> * If HTTP authentication has been enabled this servlet binds to {@code /login/} * and initializes the user session based on user information contained in the * HTTP request. */ @Singleton class HttpLoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(HttpLoginServlet.class); private static final String AUTHORIZATION = "Authorization"; private final Provider<WebSession> webSession; private final Provider<String> urlProvider; private final AccountManager accountManager; private final String loginHeader; @Inject HttpLoginServlet(final AuthConfig authConfig, final Provider<WebSession> webSession, @CanonicalWebUrl @Nullable final Provider<String> urlProvider, final AccountManager accountManager) { this.webSession = webSession; this.urlProvider = urlProvider; this.accountManager = accountManager; final String hdr = authConfig.getLoginHttpHeader(); this.loginHeader = hdr != null && !hdr.equals("") ? hdr : AUTHORIZATION; } @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) throws ServletException, IOException { final String token = getToken(req); if ("logout".equals(token) || "signout".equals(token)) { req.getRequestDispatcher("/logout").forward(req, rsp); return; } rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); rsp.setHeader("Pragma", "no-cache"); rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); final String user = getRemoteUser(req); if (user == null || "".equals(user)) { log.error("Unable to authenticate user by " + loginHeader + " request header. Check container or server configuration."); final Document doc = HtmlDomUtil.parseFile( // HttpLoginServlet.class, "ConfigurationError.html"); replace(doc, "loginHeader", loginHeader); replace(doc, "ServerName", req.getServerName()); replace(doc, "ServerPort", ":" + req.getServerPort()); replace(doc, "ContextPath", req.getContextPath()); final byte[] bin = HtmlDomUtil.toUTF8(doc); rsp.setStatus(HttpServletResponse.SC_FORBIDDEN); rsp.setContentType("text/html"); rsp.setCharacterEncoding("UTF-8"); rsp.setContentLength(bin.length); final ServletOutputStream out = rsp.getOutputStream(); try { out.write(bin); } finally { out.flush(); out.close(); } return; } final AuthRequest areq = AuthRequest.forUser(user); final AuthResult arsp; try { arsp = accountManager.authenticate(areq); } catch (AccountException e) { log.error("Unable to authenticate user \"" + user + "\"", e); rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } final StringBuilder rdr = new StringBuilder(); rdr.append(urlProvider.get()); rdr.append('#'); if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + ",")) { rdr.append(PageLinks.REGISTER); rdr.append(','); } rdr.append(token); webSession.get().login(arsp, true /* persistent cookie */); rsp.sendRedirect(rdr.toString()); } private void replace(Document doc, String name, String value) { Element e = HtmlDomUtil.find(doc, name); if (e != null) { e.setTextContent(value); } else { replaceByClass(doc, name, value); } } private void replaceByClass(Node parent, String name, String value) { final NodeList list = parent.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { final Node n = list.item(i); if (n instanceof Element) { final Element e = (Element) n; if (name.equals(e.getAttribute("class"))) { e.setTextContent(value); } } replaceByClass(n, name, value); } } private String getToken(final HttpServletRequest req) { String token = req.getPathInfo(); if (token != null && token.startsWith("/")) { token = token.substring(1); } if (token == null || token.isEmpty()) { token = PageLinks.MINE; } return token; } private String getRemoteUser(final HttpServletRequest req) { if (AUTHORIZATION.equals(loginHeader)) { final String user = req.getRemoteUser(); if (user != null && !"".equals(user)) { // The container performed the authentication, and has the user // identity already decoded for us. Honor that as we have been // configured to honor HTTP authentication. // return user; } // If the container didn't do the authentication we might // have done it in the front-end web server. Try to split // the identity out of the Authorization header and honor it. // String auth = req.getHeader(AUTHORIZATION); if (auth == null || "".equals(auth)) { return null; } else if (auth.startsWith("Basic ")) { auth = auth.substring("Basic ".length()); auth = new String(Base64.decode(auth)); final int c = auth.indexOf(':'); return c > 0 ? auth.substring(0, c) : null; } else if (auth.startsWith("Digest ")) { final int u = auth.indexOf("username=\""); if (u <= 0) { return null; } auth = auth.substring(u + 10); final int e = auth.indexOf('"'); return e > 0 ? auth.substring(0, auth.indexOf('"')) : null; } else { return null; } } else { // Nonstandard HTTP header. We have been told to trust this // header blindly as-is. // final String user = req.getHeader(loginHeader); return user != null && !"".equals(user) ? user : null; } } }