/*
*
* Copyright 2014 McEvoy Software Ltd.
*
* 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 io.milton.http.http11.auth;
import io.milton.http.Request.Method;
import io.milton.http.*;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.exceptions.NotFoundException;
import io.milton.http.webdav.WebDavResponseHandler;
import io.milton.resource.GetableResource;
import io.milton.resource.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.List;
import net.sf.json.JSONObject;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This decorates a wrapped response handler, and gives it the ability to
* generate login pages. When activated, this will suppress the http
* authorisation status code and instead render a login page.
*
* Note that the conditions under which a login page is produced in place of a
* http challenge are quite specific and should not interfere with non web
* browser user agents.
*
* This will usually be used together with FormAuthenticationHandler and
* CookieAuthenticationHandler to provide a complete authentication mechanism
* integrated into the normal milton life cycle
*
* @author brad
*/
public class LoginResponseHandler extends AbstractWrappingResponseHandler {
private static final Logger log = LoggerFactory.getLogger(LoginResponseHandler.class);
public static final String ATT_DISABLE = "auth.disable.html";
/**
* Called when authentication has detected a user type which is not
* compatible with form authentication. This will prevent a html form being
* presented, so the user will be forced to login via Basic or Diget
*
* @param r
*/
public static void setDisableHtmlResponse(Request r) {
log.trace("html login response disabled for this request");
r.getAttributes().put(ATT_DISABLE, Boolean.TRUE);
}
private String loginPage = "/login.html";
private final ResourceFactory resourceFactory;
private final LoginPageTypeHandler loginPageTypeHandler;
private List<String> excludePaths;
private boolean enabled = true;
public LoginResponseHandler(WebDavResponseHandler wrapped, ResourceFactory resourceFactory, LoginPageTypeHandler loginPageTypeHandler) {
super(wrapped);
this.resourceFactory = resourceFactory;
this.loginPageTypeHandler = loginPageTypeHandler;
}
/**
* If responding with a login page, the request attribute "authReason" is
* set to either "required", indicating that the user must login; or
* "notPermitted" indicating that the user is currently logged in but does
* not have permission
*
* @param resource
* @param response
* @param request
*/
@Override
public void respondUnauthorised(Resource resource, Response response, Request request) {
log.info("respondUnauthorised");
//String acceptHeader = request.getAcceptHeader();
Boolean disabled = (Boolean) request.getAttributes().get(ATT_DISABLE);
if (disabled == null || !disabled) {
if (isEnabled() && !excluded(request) && isGetOrPost(request)) {
if (loginPageTypeHandler.canLogin(resource, request)) {
attemptRespondLoginPage(request, resource, response);
return;
} else if (loginPageTypeHandler.isAjax(resource, request)) {
respondJson(request, response, resource);
return;
}
}
} else {
log.trace("html login form has been disabled for this request");
}
log.trace("respond with normal 401");
wrapped.respondUnauthorised(resource, response, request);
}
private void attemptRespondLoginPage(Request request, Resource resource, Response response) throws RuntimeException {
log.trace("attemptRespondLoginPage");
Resource rLogin;
try {
rLogin = resourceFactory.getResource(request.getHostHeader(), loginPage);
} catch (NotAuthorizedException e) {
throw new RuntimeException(e);
} catch (BadRequestException ex) {
throw new RuntimeException(ex);
}
if (rLogin == null || !(rLogin instanceof GetableResource)) {
log.info("Couldnt find login resource: " + request.getHostHeader() + loginPage + " with resource factory: " + resourceFactory.getClass());
wrapped.respondUnauthorised(resource, response, request);
} else {
log.trace("respond with 200 to suppress login prompt, using resource: " + rLogin.getName() + " - " + rLogin.getClass());
try {
// set request attribute so rendering knows it authorisation failed, or authentication is required
Auth auth = request.getAuthorization();
if (auth != null && auth.getTag() != null) {
// no authentication was attempted,
request.getAttributes().put("authReason", "notPermitted");
} else {
request.getAttributes().put("authReason", "required");
}
response.setStatus(Response.Status.SC_BAD_REQUEST); // error code to avoid caching
GetableResource gr = (GetableResource) rLogin;
gr.sendContent(response.getOutputStream(), null, null, gr.getContentType(null));
response.getOutputStream().flush();
//wrapped.respondContent(gr, response, request, null);
} catch (NotAuthorizedException ex) {
response.setStatus(Response.Status.SC_INTERNAL_SERVER_ERROR);
response.close();
log.error("Exception generating login page", ex);
} catch (BadRequestException ex) {
response.setStatus(Response.Status.SC_INTERNAL_SERVER_ERROR);
response.close();
log.error("Exception generating login page", ex);
} catch (NotFoundException ex) {
response.setStatus(Response.Status.SC_INTERNAL_SERVER_ERROR);
response.close();
log.error("Exception generating login page", ex);
} catch(IOException ex) {
response.setStatus(Response.Status.SC_INTERNAL_SERVER_ERROR);
response.close();
log.error("Exception generating login page", ex);
}
}
}
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public ResourceFactory getResourceFactory() {
return resourceFactory;
}
public List<String> getExcludePaths() {
return excludePaths;
}
public void setExcludePaths(List<String> excludePaths) {
this.excludePaths = excludePaths;
}
private boolean excluded(Request request) {
if (CollectionUtils.isEmpty(excludePaths)) {
return false;
}
for (String s : excludePaths) {
if (request.getAbsolutePath().startsWith(s)) {
return true;
}
}
return false;
}
private boolean isGetOrPost(Request request) {
return request.getMethod().equals(Method.GET) || request.getMethod().equals(Method.POST);
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
private void respondJson(Request request, Response response, Resource resource) {
JSONObject json = new JSONObject();
Boolean loginResult = (Boolean) request.getAttributes().get("loginResult");
json.accumulate("loginResult", loginResult);
Auth auth = request.getAuthorization();
if (auth != null && auth.getTag() != null) {
json.accumulate("authReason", "notPermitted");
} else {
json.accumulate("authReason", "required");
}
String userUrl = (String) request.getAttributes().get("userUrl");
if (userUrl != null) {
json.accumulate("userUrl", userUrl);
}
response.setStatus(Response.Status.SC_BAD_REQUEST);
response.setCacheControlNoCacheHeader();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
Writer pw = new OutputStreamWriter(bout, "UTF-8");
json.write(pw);
pw.flush();
byte[] arr = bout.toByteArray();
response.setContentLengthHeader((long) arr.length);
response.getOutputStream().write(arr);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
public interface LoginPageTypeHandler {
/**
* Return true if the given resource and request is suitable for
* presenting a web browser login page
*
* @param r
* @param request
* @return
*/
boolean canLogin(Resource r, Request request);
/**
* Return true if the request indicates that the login response should
* be given as json data (ie response to an ajax login)
*
* @param acceptHeader
* @return
*/
boolean isAjax(Resource r, Request request);
}
/**
* Default implementation which uses some sensible rules about content types
* etc
*/
public static class ContentTypeLoginPageTypeHandler implements LoginPageTypeHandler {
@Override
public boolean canLogin(Resource resource, Request request) {
if (resource instanceof GetableResource) {
String ctHeader = request.getAcceptHeader();
GetableResource gr = (GetableResource) resource;
String ctResource = gr.getContentType("text/html");
if (ctResource == null) {
if (ctHeader != null) {
boolean b = ctHeader.contains("html");
log.trace("isPage: resource has no content type, depends on requested content type: " + b);
return b;
} else {
log.trace("isPage: resource has no content type, and no requeted content type, so assume false");
return false;
}
} else {
boolean b = ctResource.contains("html");
log.trace("isPage: resource has content type. is html? " + b);
return b;
}
} else {
log.trace("isPage: resource is not getable");
return false;
}
}
@Override
public boolean isAjax(Resource r, Request request) {
String acceptHeader = request.getAcceptHeader();
return acceptHeader != null && (acceptHeader.contains("application/json") || acceptHeader.contains("text/javascript"));
}
}
}