/*
* Copyright 2012 LinkedIn Corp.
*
* 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 azkaban.webapp.servlet;
import azkaban.webapp.WebMetrics;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import azkaban.utils.WebUtils;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import azkaban.project.Project;
import azkaban.server.session.Session;
import azkaban.user.Permission;
import azkaban.user.Role;
import azkaban.user.User;
import azkaban.user.UserManager;
import azkaban.user.UserManagerException;
import azkaban.utils.StringUtils;
/**
* Abstract Servlet that handles auto login when the session hasn't been
* verified.
*/
public abstract class LoginAbstractAzkabanServlet extends
AbstractAzkabanServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger
.getLogger(LoginAbstractAzkabanServlet.class.getName());
private static final String SESSION_ID_NAME = "azkaban.browser.session.id";
private static final int DEFAULT_UPLOAD_DISK_SPOOL_SIZE = 20 * 1024 * 1024;
private static HashMap<String, String> contextType =
new HashMap<String, String>();
static {
contextType.put(".js", "application/javascript");
contextType.put(".css", "text/css");
contextType.put(".png", "image/png");
contextType.put(".jpeg", "image/jpeg");
contextType.put(".gif", "image/gif");
contextType.put(".jpg", "image/jpeg");
contextType.put(".eot", "application/vnd.ms-fontobject");
contextType.put(".svg", "image/svg+xml");
contextType.put(".ttf", "application/octet-stream");
contextType.put(".woff", "application/x-font-woff");
}
private File webResourceDirectory = null;
private MultipartParser multipartParser;
private boolean shouldLogRawUserAgent = false;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
multipartParser = new MultipartParser(DEFAULT_UPLOAD_DISK_SPOOL_SIZE);
shouldLogRawUserAgent =
getApplication().getServerProps().getBoolean("accesslog.raw.useragent",
false);
}
public void setResourceDirectory(File file) {
this.webResourceDirectory = file;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
WebMetrics.INSTANCE.markWebGetCall();
// Set session id
Session session = getSessionFromRequest(req);
logRequest(req, session);
if (hasParam(req, "logout")) {
resp.sendRedirect(req.getContextPath());
if (session != null) {
getApplication().getSessionCache()
.removeSession(session.getSessionId());
}
return;
}
if (session != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found session " + session.getUser());
}
if (handleFileGet(req, resp)) {
return;
}
handleGet(req, resp, session);
} else {
if (hasParam(req, "ajax")) {
HashMap<String, String> retVal = new HashMap<String, String>();
retVal.put("error", "session");
this.writeJSON(resp, retVal);
} else {
handleLogin(req, resp);
}
}
}
/**
* Log out request - the format should be close to Apache access log format
*
* @param req
* @param session
*/
private void logRequest(HttpServletRequest req, Session session) {
StringBuilder buf = new StringBuilder();
buf.append(getRealClientIpAddr(req)).append(" ");
if (session != null && session.getUser() != null) {
buf.append(session.getUser().getUserId()).append(" ");
} else {
buf.append(" - ").append(" ");
}
buf.append("\"");
buf.append(req.getMethod()).append(" ");
buf.append(req.getRequestURI()).append(" ");
if (req.getQueryString() != null) {
buf.append(req.getQueryString()).append(" ");
} else {
buf.append("-").append(" ");
}
buf.append(req.getProtocol()).append("\" ");
String userAgent = req.getHeader("User-Agent");
if (shouldLogRawUserAgent) {
buf.append(userAgent);
} else {
// simply log a short string to indicate browser or not
if (StringUtils.isFromBrowser(userAgent)) {
buf.append("browser");
} else {
buf.append("not-browser");
}
}
logger.info(buf.toString());
}
private boolean handleFileGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
if (webResourceDirectory == null) {
return false;
}
// Check if it's a resource
String prefix = req.getContextPath() + req.getServletPath();
String path = req.getRequestURI().substring(prefix.length());
int index = path.lastIndexOf('.');
if (index == -1) {
return false;
}
String extension = path.substring(index);
if (contextType.containsKey(extension)) {
File file = new File(webResourceDirectory, path);
if (!file.exists() || !file.isFile()) {
return false;
}
resp.setContentType(contextType.get(extension));
OutputStream output = resp.getOutputStream();
BufferedInputStream input = null;
try {
input = new BufferedInputStream(new FileInputStream(file));
IOUtils.copy(input, output);
} finally {
if (input != null) {
input.close();
}
}
output.flush();
return true;
}
return false;
}
private String getRealClientIpAddr(HttpServletRequest req){
// If some upstream device added an X-Forwarded-For header
// use it for the client ip
// This will support scenarios where load balancers or gateways
// front the Azkaban web server and a changing Ip address invalidates
// the session
HashMap<String, String> headers = new HashMap<>();
headers.put(WebUtils.X_FORWARDED_FOR_HEADER,
req.getHeader(WebUtils.X_FORWARDED_FOR_HEADER.toLowerCase()));
WebUtils utils = new WebUtils();
return utils.getRealClientIpAddr(headers, req.getRemoteAddr());
}
private Session getSessionFromRequest(HttpServletRequest req)
throws ServletException {
String remoteIp = getRealClientIpAddr(req);
Cookie cookie = getCookieByName(req, SESSION_ID_NAME);
String sessionId = null;
if (cookie != null) {
sessionId = cookie.getValue();
}
if (sessionId == null && hasParam(req, "session.id")) {
sessionId = getParam(req, "session.id");
}
return getSessionFromSessionId(sessionId, remoteIp);
}
private Session getSessionFromSessionId(String sessionId, String remoteIp) {
if (sessionId == null) {
return null;
}
Session session = getApplication().getSessionCache().getSession(sessionId);
// Check if the IP's are equal. If not, we invalidate the sesson.
if (session == null || !remoteIp.equals(session.getIp())) {
return null;
}
return session;
}
private void handleLogin(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
handleLogin(req, resp, null);
}
private void handleLogin(HttpServletRequest req, HttpServletResponse resp,
String errorMsg) throws ServletException, IOException {
Page page = newPage(req, resp, "azkaban/webapp/servlet/velocity/login.vm");
if (errorMsg != null) {
page.add("errorMsg", errorMsg);
}
page.render();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Session session = getSessionFromRequest(req);
WebMetrics.INSTANCE.markWebPostCall();
logRequest(req, session);
// Handle Multipart differently from other post messages
if (ServletFileUpload.isMultipartContent(req)) {
Map<String, Object> params = multipartParser.parseMultipart(req);
if (session == null) {
// See if the session id is properly set.
if (params.containsKey("session.id")) {
String sessionId = (String) params.get("session.id");
String ip = getRealClientIpAddr(req);
session = getSessionFromSessionId(sessionId, ip);
if (session != null) {
handleMultiformPost(req, resp, params, session);
return;
}
}
// if there's no valid session, see if it's a one time session.
if (!params.containsKey("username") || !params.containsKey("password")) {
writeResponse(resp, "Login error. Need username and password");
return;
}
String username = (String) params.get("username");
String password = (String) params.get("password");
String ip = getRealClientIpAddr(req);
try {
session = createSession(username, password, ip);
} catch (UserManagerException e) {
writeResponse(resp, "Login error: " + e.getMessage());
return;
}
}
handleMultiformPost(req, resp, params, session);
} else if (hasParam(req, "action")
&& getParam(req, "action").equals("login")) {
HashMap<String, Object> obj = new HashMap<String, Object>();
handleAjaxLoginAction(req, resp, obj);
this.writeJSON(resp, obj);
} else if (session == null) {
if (hasParam(req, "username") && hasParam(req, "password")) {
// If it's a post command with curl, we create a temporary session
try {
session = createSession(req);
} catch (UserManagerException e) {
writeResponse(resp, "Login error: " + e.getMessage());
}
handlePost(req, resp, session);
} else {
// There are no valid sessions and temporary logins, no we either pass
// back a message or redirect.
if (isAjaxCall(req)) {
String response =
createJsonResponse("error", "Invalid Session. Need to re-login",
"login", null);
writeResponse(resp, response);
} else {
handleLogin(req, resp, "Enter username and password");
}
}
} else {
handlePost(req, resp, session);
}
}
private Session createSession(HttpServletRequest req)
throws UserManagerException, ServletException {
String username = getParam(req, "username");
String password = getParam(req, "password");
String ip = getRealClientIpAddr(req);
return createSession(username, password, ip);
}
private Session createSession(String username, String password, String ip)
throws UserManagerException, ServletException {
UserManager manager = getApplication().getUserManager();
User user = manager.getUser(username, password);
String randomUID = UUID.randomUUID().toString();
Session session = new Session(randomUID, user, ip);
return session;
}
protected boolean hasPermission(Project project, User user,
Permission.Type type) {
UserManager userManager = getApplication().getUserManager();
if (project.hasPermission(user, type)) {
return true;
}
for (String roleName : user.getRoles()) {
Role role = userManager.getRole(roleName);
if (role.getPermission().isPermissionSet(type)
|| role.getPermission().isPermissionSet(Permission.Type.ADMIN)) {
return true;
}
}
return false;
}
protected void handleAjaxLoginAction(HttpServletRequest req,
HttpServletResponse resp, Map<String, Object> ret)
throws ServletException {
if (hasParam(req, "username") && hasParam(req, "password")) {
Session session = null;
try {
session = createSession(req);
} catch (UserManagerException e) {
ret.put("error", "Incorrect Login. " + e.getMessage());
return;
}
Cookie cookie = new Cookie(SESSION_ID_NAME, session.getSessionId());
cookie.setPath("/");
resp.addCookie(cookie);
getApplication().getSessionCache().addSession(session);
ret.put("status", "success");
ret.put("session.id", session.getSessionId());
} else {
ret.put("error", "Incorrect Login.");
}
}
protected void writeResponse(HttpServletResponse resp, String response)
throws IOException {
Writer writer = resp.getWriter();
writer.append(response);
writer.flush();
}
protected boolean isAjaxCall(HttpServletRequest req) throws ServletException {
String value = req.getHeader("X-Requested-With");
if (value != null) {
logger.info("has X-Requested-With " + value);
return value.equals("XMLHttpRequest");
}
return false;
}
/**
* The get request is handed off to the implementor after the user is logged
* in.
*
* @param req
* @param resp
* @param session
* @throws ServletException
* @throws IOException
*/
protected abstract void handleGet(HttpServletRequest req,
HttpServletResponse resp, Session session) throws ServletException,
IOException;
/**
* The post request is handed off to the implementor after the user is logged
* in.
*
* @param req
* @param resp
* @param session
* @throws ServletException
* @throws IOException
*/
protected abstract void handlePost(HttpServletRequest req,
HttpServletResponse resp, Session session) throws ServletException,
IOException;
/**
* The post request is handed off to the implementor after the user is logged
* in.
*
* @param req
* @param resp
* @param session
* @throws ServletException
* @throws IOException
*/
protected void handleMultiformPost(HttpServletRequest req,
HttpServletResponse resp, Map<String, Object> multipart, Session session)
throws ServletException, IOException {
}
}