/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/
package org.exist.atom.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.atom.Atom;
import org.exist.atom.AtomModule;
import org.exist.atom.modules.AtomFeeds;
import org.exist.atom.modules.AtomProtocol;
import org.exist.atom.modules.Query;
import org.exist.http.BadRequestException;
import org.exist.http.NotFoundException;
import org.exist.http.servlets.Authenticator;
import org.exist.http.servlets.BasicAuthenticator;
import org.exist.http.webdav.WebDAV;
import org.exist.security.PermissionDeniedException;
import org.exist.security.SecurityManager;
import org.exist.security.User;
import org.exist.security.XmldbPrincipal;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.util.Configuration;
import org.exist.util.DatabaseConfigurationException;
import org.exist.validation.XmlLibraryChecker;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.XMLDBException;
/**
* Implements a rest interface for exist collections as atom feeds
*
* @author Alex Milowski
*/
public class AtomServlet extends HttpServlet {
public final static String DEFAULT_ENCODING = "UTF-8";
public final static String CONF_NS = "http://www.exist-db.org/Vocabulary/AtomConfiguration/2006/1/0";
protected final static Logger LOG = Logger.getLogger(AtomServlet.class);
/**
* A user principal object that implements XmldbPrincipal
*/
static class UserXmldbPrincipal implements XmldbPrincipal {
int authMethod;
User user;
UserXmldbPrincipal(int authMethod,User user) {
this.authMethod = authMethod;
this.user = user;
}
public String getName() {
return user.getName();
}
public String getPassword() {
return authMethod==WebDAV.BASIC_AUTH ? user.getPassword() : user.getDigestPassword();
}
public boolean hasRole(String role) {
return user.hasGroup(role);
}
}
/**
* Module contexts that default to using the servlet's config
*/
class ModuleContext implements AtomModule.Context {
ServletConfig config;
String moduleLoadPath;
ModuleContext(ServletConfig config,String subpath, String moduleLoadPath) {
this.config = config;
this.moduleLoadPath = moduleLoadPath;
}
public String getDefaultCharset() {
return formEncoding;
}
public String getParameter(String name) {
return config.getInitParameter(name);
}
public String getContextPath() {
// TODO: finish
return null;
}
public URL getContextURL() {
// TODO: finish
return null;
}
public String getModuleLoadPath() {
return moduleLoadPath;
}
}
// What I want...
//private Map<String,AtomModule> modules;
private Map modules;
private Map noAuth;
private String formEncoding = null;
private BrokerPool pool = null;
private String defaultUsername = SecurityManager.GUEST_USER;
private String defaultPassword = SecurityManager.GUEST_USER;
private Authenticator authenticator;
private User defaultUser;
/* (non-Javadoc)
* @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// Configure BrokerPool
try {
if (BrokerPool.isConfigured()) {
LOG.debug("Database already started. Skipping configuration ...");
} else {
// The database isn't started, so we'll start it
String confFile = config.getInitParameter("configuration");
String dbHome = config.getInitParameter("basedir");
String start = config.getInitParameter("start");
if (confFile == null) {
confFile = "conf.xml";
}
dbHome = (dbHome == null) ? config.getServletContext().getRealPath(".") :
config.getServletContext().getRealPath(dbHome);
LOG.debug("AtomServlet: exist.home=" + dbHome);
File f = new File(dbHome + File.separator + confFile);
LOG.debug("reading configuration from " + f.getAbsolutePath());
if (!f.canRead()) {
throw new ServletException("configuration file " + confFile
+ " not found or not readable");
}
Configuration configuration = new Configuration(confFile, dbHome);
if (start != null && start.equals("true")) {
startup(configuration);
}
}
pool = BrokerPool.getInstance();
// The default user is used when there is no authentication
String option = config.getInitParameter("use-default-user");
boolean useDefaultUser = true;
if (option != null) {
useDefaultUser = option.trim().equals("true");
}
if (useDefaultUser) {
option = config.getInitParameter("user");
if (option != null) {
defaultUsername = option;
}
option = config.getInitParameter("password");
if (option != null) {
defaultPassword = option;
}
defaultUser = getDefaultUser();
if (defaultUser!=null) {
LOG.info("Using default user "+defaultUsername+" for all unauthorized requests.");
} else {
LOG.error("Default user "+defaultUsername+" cannot be found. A BASIC AUTH challenge will be the default.");
}
} else {
LOG.info("No default user. All requires must be authorized or will result in a BASIC AUTH challenge.");
defaultUser = null;
}
// Currently, we only support basic authentication
authenticator = new BasicAuthenticator(pool);
} catch (EXistException e) {
throw new ServletException("No database instance available");
} catch (DatabaseConfigurationException e) {
throw new ServletException("Unable to configure database instance: " + e.getMessage(), e);
}
//get form and container encoding's
formEncoding = config.getInitParameter("form-encoding");
if (formEncoding == null) {
formEncoding = DEFAULT_ENCODING;
}
String containerEncoding = config.getInitParameter("container-encoding");
if (containerEncoding == null) {
containerEncoding = DEFAULT_ENCODING;
}
// Load all the modules
//modules = new HashMap<String,AtomModule>();
modules = new HashMap();
noAuth = new HashMap();
String configFileOpt = config.getInitParameter("config-file");
File dbHome = pool.getConfiguration().getExistHome();
File atomConf;
if (configFileOpt == null)
atomConf = new File(dbHome,"atom-services.xml");
else
atomConf = new File(config.getServletContext().getRealPath(configFileOpt));
config.getServletContext().log("Checking for atom configuration in "+atomConf.getAbsolutePath());
if (atomConf.exists()) {
config.getServletContext().log("Loading configuration "+atomConf.getAbsolutePath());
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware(true);
DocumentBuilder docBuilder = null;
Document confDoc = null;
InputStream is = null;
try {
is = new FileInputStream(atomConf);
InputSource src = new InputSource(new InputStreamReader(is,formEncoding));
URI docBaseURI = atomConf.toURI();
src.setSystemId(docBaseURI.toString());
docBuilder = docFactory.newDocumentBuilder();
confDoc = docBuilder.parse(src);
confDoc.getDocumentElement();
// Add all the modules
NodeList moduleConfList = confDoc.getElementsByTagNameNS(CONF_NS,"module");
for (int i=0; i<moduleConfList.getLength(); i++) {
Element moduleConf = (Element)moduleConfList.item(i);
String name = moduleConf.getAttribute("name");
if (modules.get(name)!=null) {
throw new ServletException("Module '"+name+"' is configured more than once ( child # "+(i+1));
}
if ("false".equals(moduleConf.getAttribute("authenticate"))) {
noAuth.put(name,Boolean.TRUE);
}
String className = moduleConf.getAttribute("class");
if (className!=null && className.length()>0) {
try {
Class moduleClass = Class.forName(className);
AtomModule amodule = (AtomModule)moduleClass.newInstance();
modules.put(name,amodule);
amodule.init(new ModuleContext(config,name, atomConf.getParent()));
} catch (Exception ex) {
throw new ServletException("Cannot instantiate class "+className+" for module '"+name+"' due to exception: "+ex.getMessage(),ex);
}
} else {
// no class means query
Query query = new Query();
modules.put(name,query);
String allowQueryPost = moduleConf.getAttribute("query-by-post");
if ("true".equals(allowQueryPost)) {
query.setQueryByPost(true);
}
NodeList methodList = moduleConf.getElementsByTagNameNS(CONF_NS,"method");
for (int m=0; m<methodList.getLength(); m++) {
Element methodConf = (Element)methodList.item(m);
String type = methodConf.getAttribute("type");
if (type==null) {
LOG.warn("No type specified for method in module "+name);
continue;
}
// What I want but can't have because of JDK 1.4
//URI baseURI = URI.create(methodConf.getBaseURI());
URI baseURI = docBaseURI;
String queryRef = methodConf.getAttribute("query");
if (queryRef==null) {
LOG.warn("No query specified for method "+type+" in module "+name);
continue;
}
boolean fromClasspath = "true".equals(methodConf.getAttribute("from-classpath"));
Query.MethodConfiguration mconf = query.getMethodConfiguration(type);
if (mconf==null) {
LOG.warn("Unknown method "+type+" in module "+name);
continue;
}
String responseContentType = methodConf.getAttribute("content-type");
if (responseContentType!=null && responseContentType.trim().length()!=0) {
mconf.setContentType(responseContentType);
}
URL queryURI = null;
if (fromClasspath) {
LOG.debug("Nope. Attempting to get resource "+queryRef+" from "+Atom.class.getName());
queryURI = Atom.class.getResource(queryRef);
} else {
queryURI = baseURI.resolve(queryRef).toURL();
}
LOG.debug("Loading from module "+name+" method "+type+" from resource "+queryURI+" via classpath("+fromClasspath+") and ref ("+queryRef+")");
if (queryURI==null) {
throw new ServletException("Cannot find resource "+queryRef+" for module "+name);
}
mconf.setQuerySource(queryURI);
}
query.init(new ModuleContext(config,name, atomConf.getParent()));
}
}
} catch (IOException e) {
LOG.warn(e);
throw new ServletException(e.getMessage());
} catch (SAXException e) {
LOG.warn(e);
throw new ServletException(e.getMessage());
} catch (ParserConfigurationException e) {
LOG.warn(e);
throw new ServletException(e.getMessage());
} catch (EXistException e) {
LOG.warn(e);
throw new ServletException(e.getMessage());
} finally {
if (is!=null) {
try {
is.close();
} catch (IOException ex) {
}
}
}
} else {
try {
AtomProtocol protocol = new AtomProtocol();
modules.put("edit",protocol);
protocol.init(new ModuleContext(config,"edit", dbHome.getAbsolutePath()));
AtomFeeds feeds = new AtomFeeds();
modules.put("content",feeds);
feeds.init(new ModuleContext(config,"content", dbHome.getAbsolutePath()));
Query query = new Query();
query.setQueryByPost(true);
modules.put("query",query);
query.init(new ModuleContext(config,"query", dbHome.getAbsolutePath()));
Query topics = new Query();
modules.put("topic",topics);
topics.getMethodConfiguration("GET").setQuerySource(topics.getClass().getResource("topic.xq"));
topics.init(new ModuleContext(config,"topic", dbHome.getAbsolutePath()));
Query introspect = new Query();
modules.put("introspect",introspect);
introspect.getMethodConfiguration("GET").setQuerySource(introspect.getClass().getResource("introspect.xq"));
introspect.init(new ModuleContext(config,"introspect", dbHome.getAbsolutePath()));
} catch (EXistException ex) {
throw new ServletException("Exception during module init(): "+ex.getMessage(),ex);
}
}
// XML lib checks....
XmlLibraryChecker.check();
}
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException {
try {
// Get the path
String path = request.getPathInfo();
if(path==null){
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
"URL has no extra path information specified.");
return;
}
int firstSlash = path.indexOf('/',1);
if (firstSlash<0 && path.length()==1) {
response.sendError(400,"Module not specified.");
return;
}
String moduleName = firstSlash<0 ? path.substring(1) : path.substring(1,firstSlash);
path = firstSlash<0 ? "" : path.substring(firstSlash);
AtomModule module = (AtomModule)modules.get(moduleName);
if (module==null) {
response.sendError(400,"Module "+moduleName+" not found.");
return;
}
User user = null;
if (noAuth.get(moduleName)==null) {
// Authenticate
user = authenticate(request,response);
if (user == null) {
// You now get a challenge if there is no user
return;
}
}
final Principal principal = new UserXmldbPrincipal(WebDAV.BASIC_AUTH,user);
HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
public Principal getUserPrincipal() {
return principal;
}
};
// Handle the resource
DBBroker broker = null;
try {
broker = pool.get(user);
module.process(broker,new HttpRequestMessage(request,path,'/'+moduleName),new HttpResponseMessage(response));
} catch (NotFoundException ex) {
LOG.info("Resource "+path+" not found by "+moduleName,ex);
response.sendError(HttpServletResponse.SC_NOT_FOUND,ex.getMessage());
} catch (PermissionDeniedException ex) {
LOG.info("Permission denied to "+path+" by "+moduleName+" for "+user.getName(),ex);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,ex.getMessage());
} catch (BadRequestException ex) {
LOG.info("Bad request throw from module "+moduleName,ex);
response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage());
} catch (EXistException ex) {
LOG.fatal("Exception getting broker from pool for user "+user.getName(),ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"Service is not available.");
} catch (Throwable e){
LOG.error(e.getMessage(), e);
throw new ServletException("An error occurred: " + e.getMessage(), e);
} finally {
pool.release(broker);
}
} catch (IOException ex) {
LOG.fatal("I/O exception on request.",ex);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"Service is not available.");
} catch (IOException finalEx) {
LOG.fatal("Cannot return 500 on exception.",ex);
}
}
}
/* (non-Javadoc)
* @see javax.servlet.GenericServlet#destroy()
*/
public void destroy() {
super.destroy();
BrokerPool.stopAll(false);
}
private User authenticate(HttpServletRequest request,HttpServletResponse response)
throws java.io.IOException {
// First try to validate the principial if passed from the servlet engine
Principal principal = request.getUserPrincipal();
if (principal instanceof XmldbPrincipal){
String username = ((XmldbPrincipal)principal).getName();
String password = ((XmldbPrincipal)principal).getPassword();
LOG.info("Validating Principle: " + principal.getName());
User user = pool.getSecurityManager().getUser(username);
if (user != null){
if (password.equalsIgnoreCase(user.getPassword())){
LOG.info("Valid User: " + user.getName());
return user;
} else {
LOG.info( "Password invalid for user: " + username );
}
LOG.info("User not found: " + principal.getName());
}
}
String auth = request.getHeader("Authorization");
if (auth == null && defaultUser!=null) {
return defaultUser;
}
return authenticator.authenticate(request,response);
}
private User getDefaultUser() {
if (defaultUsername != null) {
User user = pool.getSecurityManager().getUser(defaultUsername);
if (user != null) {
if (!user.validate(defaultPassword)) {
return null;
}
}
return user;
}
return null;
}
private void startup(Configuration configuration)
throws ServletException {
if ( configuration == null ) {
throw new ServletException( "database has not been configured" );
}
LOG.info("configuring eXist instance");
try {
if ( !BrokerPool.isConfigured() ) {
BrokerPool.configure( 1, 5, configuration );
}
} catch ( EXistException e ) {
throw new ServletException( e.getMessage() );
} catch (DatabaseConfigurationException e) {
throw new ServletException( e.getMessage() );
}
try {
LOG.info("registering XMLDB driver");
Class clazz = Class.forName("org.exist.xmldb.DatabaseImpl");
Database database = (Database)clazz.newInstance();
DatabaseManager.registerDatabase(database);
} catch (ClassNotFoundException e) {
LOG.info("ERROR", e);
} catch (InstantiationException e) {
LOG.info("ERROR", e);
} catch (IllegalAccessException e) {
LOG.info("ERROR", e);
} catch (XMLDBException e) {
LOG.info("ERROR", e);
}
}
}