package restservices.publish;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.httpclient.HttpStatus;
import restservices.RestServices;
import restservices.proxies.Cookie;
import restservices.util.DataWriter;
import restservices.util.Function;
import restservices.util.Utils;
import system.proxies.User;
import com.google.common.base.Preconditions;
import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.m2ee.api.IMxRuntimeResponse;
import com.mendix.systemwideinterfaces.core.IContext;
import com.mendix.systemwideinterfaces.core.IMendixObject;
import com.mendix.systemwideinterfaces.core.ISession;
import com.mendix.systemwideinterfaces.core.IUser;
import communitycommons.StringUtils;
public class RestServiceRequest {
public static enum ResponseType { JSON, XML, HTML, PLAIN, BINARY }
public static enum RequestContentType { JSON, FORMENCODED, MULTIPART, OTHER }
HttpServletRequest request;
HttpServletResponse response;
private ResponseType responseContentType;
private RequestContentType requestContentType;
private IContext context;
protected DataWriter datawriter;
private boolean autoLogout;
private ISession activeSession;
private IMxRuntimeResponse mxresponse;
private String relpath;
public RestServiceRequest(HttpServletRequest request, HttpServletResponse response, IMxRuntimeResponse mxresponse, String relpath) {
this.request = request;
this.response = response;
this.mxresponse = mxresponse;
this.relpath = relpath;
this.requestContentType = determineRequestContentType(request);
this.responseContentType = determineResponseContentType(request);
try {
this.datawriter = new DataWriter(response.getOutputStream(), responseContentType == ResponseType.HTML ? DataWriter.HTML : responseContentType == ResponseType.XML ? DataWriter.XML : DataWriter.JSON);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void setContext(IContext context) {
this.context = context;
}
public IContext getContext() {
return this.context;
}
boolean authenticate(String roleOrMicroflow, ISession existingSession) throws Exception {
if ("*".equals(roleOrMicroflow)) {
setContext(Core.createSystemContext());
return true;
}
else if (roleOrMicroflow.indexOf('.') != -1) //Modeler forbids dots in userrole names, while microflow names always are a qualified name
return authenticateWithMicroflow(roleOrMicroflow);
else
return authenticateWithCredentials(roleOrMicroflow, existingSession);
}
private boolean authenticateWithCredentials(String role,
ISession existingSession) throws Exception {
String authHeader = request.getHeader(RestServices.HEADER_AUTHORIZATION);
String username = null;
String password = null;
ISession session = null;
if (authHeader != null && authHeader.trim().startsWith(RestServices.BASIC_AUTHENTICATION)) {
String base64 = StringUtils.base64Decode(authHeader.trim().substring(RestServices.BASIC_AUTHENTICATION.length()).trim());
String[] parts = base64.split(":");
username = parts[0];
password = parts[1];
}
try {
//Check credentials provided by request
if (username != null) {
try {
session = Core.login(username, password);
}
catch (Exception e) { //can throw both authentication exceptions and core runtime exceptions, depending on whether the password or the username is wrong...
//Invalid credentials
}
if (session == null) {
RestServices.LOGPUBLISH.warn("Invalid credentials for user '" + username + "'");
setStatus(HttpStatus.SC_UNAUTHORIZED);
write("Invalid credentials");
return false;
}
//same user as the one in the current session? recylcle the session
if (existingSession != null && session.getId().equals(existingSession.getId()) && existingSession.getUser().getName().equals(session.getUser().getName())) {
Core.logout(session);
session = existingSession;
}
else
this.autoLogout = true;
}
//check session from cookies
else if (existingSession != null)
session = existingSession;
//session found?
if (session != null && session.getUser() != null && session.getUser().getUserRoleNames().contains(role)) {
setContext(session.createContext());
this.activeSession = session;
return true;
}
}
catch(Exception e) {
RestServices.LOGPUBLISH.warn("Failed to authenticate '" + username + "'" + e.getMessage(), e);
throw e;
}
return false;
}
private static final Map<String, Object> EMPTY_MAP = new HashMap<String, Object>();
private boolean authenticateWithMicroflow(final String microflowName) throws Exception {
try {
// Create a context and transaction, so that headers can be inspected during the execution of the authorization microflow.
final IContext c = Core.createSystemContext();
this.setContext(c);
IMendixObject userobject = withTransaction(new Function<IMendixObject>() {
@Override
public IMendixObject apply() throws CoreException {
return Core.execute(c, microflowName, EMPTY_MAP);
}
});
this.setContext(null); //authentication was in system context, but execution will be in user context
if (userobject == null)
return false;
String username = (String) userobject.getValue(c, User.MemberNames.Name.toString());
if (username == null || username.isEmpty())
throw new IllegalStateException("Trying to authenticate a user without a name"); //yes, this actually went wrong once during testing, due to a broken DB record...
IUser user = Core.getUser(c, username);
ISession session = Core.initializeSession(user, null);
this.autoLogout = true;
this.activeSession = session;
this.setContext(session.createContext());
return true;
} catch (Exception e) {
RestServices.LOGPUBLISH.warn("Failed to authenticate request using microflow '" + microflowName + "', microflow threw an unexpected exception: " + e.getMessage(), e);
throw e;
}
}
private RequestContentType determineRequestContentType(HttpServletRequest request) {
String ct = request.getHeader(RestServices.HEADER_CONTENTTYPE);
if (ct == null)
return RequestContentType.OTHER;
if (ct.contains("json"))
return RequestContentType.JSON;
else if (ct.contains(RestServices.CONTENTTYPE_FORMENCODED))
return RequestContentType.FORMENCODED;
else if (ct.contains(RestServices.CONTENTTYPE_MULTIPART))
return RequestContentType.MULTIPART;
else
return RequestContentType.OTHER;
}
private ResponseType determineResponseContentType(HttpServletRequest request) {
String ct = request.getParameter(RestServices.PARAM_CONTENTTYPE);
if (ct == null)
ct = request.getHeader(RestServices.HEADER_ACCEPT);
if (ct != null) {
if (ct.contains("json"))
return ResponseType.JSON;
if (ct.contains("html"))
return ResponseType.HTML;
if (ct.contains("xml"))
return ResponseType.XML;
}
return ResponseType.JSON; //Not set, fall back to default json
}
public void setResponseContentType(ResponseType responseType) {
switch (responseType) {
case HTML:
response.setContentType("text/html;charset=UTF-8");
break;
case JSON:
response.setContentType("application/json;charset=UTF-8");
break;
case PLAIN:
response.setContentType("text/plain;charset=UTF-8");
break;
case XML:
response.setContentType("text/xml;charset=UTF-8");
break;
case BINARY:
response.setContentType(RestServices.CONTENTTYPE_OCTET);
break;
default:
throw new IllegalStateException();
}
}
public ResponseType getResponseContentType() {
return this.responseContentType;
}
public RequestContentType getRequestContentType() {
return this.requestContentType;
}
public RestServiceRequest write(String data) {
try {
this.response.getOutputStream().write(data.getBytes(RestServices.UTF8));
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
public void close() {
try {
this.response.getOutputStream().close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void startHTMLDoc() {
this.write("<!DOCTYPE HTML><html><head><style>" + RestServices.STYLESHEET + "</style><head><body>");
}
private void endHTMLDoc() {
String url = Utils.getRequestUrl(request);
this.write("<p><center><small>View as: <a href='")
.write(Utils.appendParamToUrl(url, "contenttype", "xml"))
.write("'>XML</a> <a href='")
.write(Utils.appendParamToUrl(url, "contenttype", "json"))
.write("'>JSON</a></small></center></p>");
this.write("<hr /><p><center><small>Generated by the <a href='https://github.com/mendix/RestServices' target='_blank'>RestServices</a> module (v")
.write(RestServices.VERSION)
.write("). Powered by Mendix.</small></center></body></html>");
}
private void startXMLDoc() {
this.write("<?xml version=\"1.0\" encoding=\"utf-8\"?><response>");
}
private void endXMLDoc() {
this.write("</response>");
}
public void setStatus(int status) {
response.setStatus(status);
}
public String getETag() {
return request.getHeader(RestServices.HEADER_IFNONEMATCH);
}
public void dispose() {
if (autoLogout && this.activeSession != null)
Core.logout(this.activeSession);
}
public IUser getCurrentUser() {
return activeSession.getUser();
}
public void startDoc() {
setResponseContentType(responseContentType);
if (getResponseContentType() == ResponseType.HTML) {
startHTMLDoc();
}
else if (getResponseContentType() == ResponseType.XML) {
startXMLDoc();
}
}
public void endDoc() {
if (getResponseContentType() == ResponseType.HTML)
endHTMLDoc();
else if (getResponseContentType() == ResponseType.XML)
endXMLDoc();
close();
}
public String getRequestParameter(String param, String defaultValue) {
String result = request.getParameter(param);
return result == null ? defaultValue : result;
}
public String getPath() {
return relpath;
}
private static final Map<String, RestServiceRequest> currentRequests = new ConcurrentHashMap<String, RestServiceRequest>();
public <T> T withTransaction(final Function<T> worker) throws Exception {
IContext c = getContext();
Preconditions.checkNotNull(c, "RestServiceRequest has no context");
if (c.isInTransaction())
throw new IllegalStateException("Already in transaction");
c.startTransaction();
String transactionId = c.getTransactionId().toString();
currentRequests.put(transactionId, this);
boolean hasException = true;
try {
T res = Utils.withSessionCache(c, worker);
hasException = false;
return res;
}
finally {
currentRequests.remove(transactionId);
if (hasException)
c.rollbackTransAction();
else
c.endTransaction();
}
}
public static RestServiceRequest getCurrentRequest(IContext context) {
return currentRequests.get(context.getTransactionId().toString());
}
public static String getRequestHeader(IContext context, String headerName) {
RestServiceRequest current = getCurrentRequest(context);
if (current == null)
throw new IllegalStateException("Not handling a request currently");
return current.request.getHeader(headerName);
}
public static List<Cookie> getRequestCookies(IContext context) {
RestServiceRequest current = getCurrentRequest(context);
if (current == null)
throw new IllegalStateException("Not handling a request currently");
List<Cookie> cookies = new ArrayList<Cookie>();
for (javax.servlet.http.Cookie c : current.request.getCookies()) {
Cookie r = new Cookie(context);
cookies.add(r);
r.setDomain(c.getDomain());
r.setName(c.getName());
r.setValue(c.getValue());
r.setPath(c.getPath());
r.setMaxAgeSeconds(c.getMaxAge());
}
return cookies;
}
public static void setResponseHeader(IContext context, String headerName, String value) {
RestServiceRequest current = getCurrentRequest(context);
if (current == null)
throw new IllegalStateException("Not handling a request currently");
current.response.setHeader(headerName, value);
}
public static void setResponseCookie(IContext context, Cookie cookie) {
RestServiceRequest current = getCurrentRequest(context);
if (current == null)
throw new IllegalStateException("Not handling a request currently");
if (cookie == null || cookie.getName().isEmpty())
throw new IllegalArgumentException("Not a valid cookie");
current.mxresponse.addCookie(cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getDomain() == null ? "" : cookie.getDomain(), cookie.getMaxAgeSeconds(), cookie.getHttpOnly());
}
public static void setResponseStatus(IContext context, int status) {
RestServiceRequest current = getCurrentRequest(context);
if (current == null)
throw new IllegalStateException("Not handling a request currently");
if (status < 200 || status >= 600)
throw new IllegalArgumentException("Response status should be between 200 and 599");
current.setStatus(status);
}
}