All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.main.java.org.kawanfw.sql.servlet.ServerSqlManager Maven / Gradle / Ivy

Go to download

AceQL HTTP is a framework of REST like http APIs that allow to access to remote SQL databases over http from any device that supports http. AceQL HTTP is provided with three client SDK: - The AceQL C# Client SDK allows to wrap the HTTP APIs using Microsoft SQL Server like calls in their code, just like they would for a local database. - The AceQL Java Client JDBC Driver allows to wrap the HTTP APIs using JDBC calls in their code, just like they would for a local database. - The AceQL Python Client SDK allows SQL calls to be encoded with standard unmodified DB-API 2.0 syntax

There is a newer version: 12.2
Show newest version
/*
 * This file is part of AceQL HTTP.
 * AceQL HTTP: SQL Over HTTP
 * Copyright (C) 2020,  KawanSoft SAS
 * (http://www.kawansoft.com). All rights reserved.
 *
 * AceQL HTTP 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.1 of the License, or (at your option) any later version.
 *
 * AceQL HTTP 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 *
 * Any modifications to this file must keep this entire header
 * intact.
 */
package org.kawanfw.sql.servlet;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.tomcat.util.http.fileupload.FileUploadException;
import org.kawanfw.sql.api.server.DatabaseConfigurator;
import org.kawanfw.sql.api.server.auth.UserAuthenticator;
import org.kawanfw.sql.api.server.auth.headers.RequestHeadersAuthenticator;
import org.kawanfw.sql.api.server.blob.BlobDownloadConfigurator;
import org.kawanfw.sql.api.server.blob.BlobUploadConfigurator;
import org.kawanfw.sql.api.server.firewall.SqlFirewallManager;
import org.kawanfw.sql.api.server.session.SessionConfigurator;
import org.kawanfw.sql.servlet.sql.json_return.ExceptionReturner;
import org.kawanfw.sql.servlet.sql.json_return.JsonErrorReturn;
import org.kawanfw.sql.servlet.sql.json_return.JsonOkReturn;
import org.kawanfw.sql.tomcat.ServletParametersStore;
import org.kawanfw.sql.util.FrameworkDebug;

/**
 * Http JDBC Server
 *
 * @author Nicolas de Pomereu
 */

@SuppressWarnings("serial")
public class ServerSqlManager extends HttpServlet {

    private static boolean DEBUG = FrameworkDebug.isSet(ServerSqlManager.class);

    public static String CR_LF = System.getProperty("line.separator");

    /** The properties file */
    private static File aceqlServerProperties = null;

    public static final String DATABASE_CONFIGURATOR_CLASS_NAME = "databaseConfiguratorClassName";
    public static final String USER_AUTHENTICATOR_CLASS_NAME = "userAuthenticatorClassName";
    public static final String REQUEST_HEADERS_AUTHENTICATOR_CLASS_NAME = "requestHeadersAuthenticatorClassName";
    public static final String SQL_FIREWALL_MANAGER_CLASS_NAMES = "sqlFirewallManagerClassNames";
    public static final String BLOB_DOWNLOAD_CONFIGURATOR_CLASS_NAME = "blobDownloadConfiguratorClassName";
    public static final String BLOB_UPLOAD_CONFIGURATOR_CLASS_NAME = "blobUploadConfiguratorClassName";
    public static final String SESSION_CONFIGURATOR_CLASS_NAME = "sessionConfiguratorClassName";
    public static final String JWT_SESSION_CONFIGURATOR_SECRET = "jwtSessionConfiguratorSecret";
    
    /** The map of (database, DatabaseConfigurator) */
    private static Map databaseConfigurators = new ConcurrentHashMap<>();

    /** The map of (database, List) */
    private static Map> sqlFirewallMap = new ConcurrentHashMap<>();

    /** The UserAuthenticator instance */
    private static UserAuthenticator userAuthenticator = null;

    /** The RequestHeadersAuthenticator instance */
    private static RequestHeadersAuthenticator requestHeadersAuthenticator = null;

    /** The BlobUploadConfigurator instance */
    private static BlobUploadConfigurator blobUploadConfigurator = null;

    /** The BlobUploadConfigurator instance */
    private static BlobDownloadConfigurator blobDownloadConfigurator = null;

    /** The SessionConfigurator instance */
    private static SessionConfigurator sessionConfigurator = null;

    /** The Exception thrown at init */
    private Exception exception = null;

    /** The init error message trapped */
    private String initErrrorMesage = null;

    /** The executor to use */
    private ThreadPoolExecutor threadPoolExecutor = null;

    /**
     * @return userAuthenticator
     */
    public static UserAuthenticator getUserAuthenticator() {
	return userAuthenticator;
    }

    /**
     * @return the requestHeadersAuthenticator
     */
    public static RequestHeadersAuthenticator getRequestHeadersAuthenticator() {
	return requestHeadersAuthenticator;
    }

    /**
     * @return the blobUploadConfigurator
     */
    public static BlobUploadConfigurator getBlobUploadConfigurator() {
	return blobUploadConfigurator;
    }

    /**
     * @return the blobDownloadConfigurator
     */
    public static BlobDownloadConfigurator getBlobDownloadConfigurator() {
	return blobDownloadConfigurator;
    }

    /**
     * Getter to used in all classes to get the DatabaseConfigurator for the
     * database name
     *
     * @param database the database to load the DatabaseConfigurator for the
     *                 database name
     * @return
     */
    public static DatabaseConfigurator getDatabaseConfigurator(String database) {
	return databaseConfigurators.get(database);
    }

    /**
     * Getter to used in all classes to get the SessionConfigurator
     *
     * @return the sessionConfigurator
     */
    public static SessionConfigurator getSessionManagerConfigurator() {
	return sessionConfigurator;
    }

    /**
     * Returns the list of SqlFirewallManager
     *
     * @return the list of SqlFirewallManager
     */
    public static Map> getSqlFirewallMap() {
	return sqlFirewallMap;
    }

    public static File getAceqlServerProperties() {
	return aceqlServerProperties;
    }

    public static void setAceqlServerProperties(File aceqlServerProperties) {
	ServerSqlManager.aceqlServerProperties = aceqlServerProperties;
    }

    /**
     * Init
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
	super.init(config);
	ServerSqlManagerInit serverSqlManagerInit = new ServerSqlManagerInit(config);
	userAuthenticator = serverSqlManagerInit.getUserAuthenticator();
	requestHeadersAuthenticator = serverSqlManagerInit.getRequestHeadersAuthenticator();
	databaseConfigurators = serverSqlManagerInit.getDatabaseConfigurators();
	sqlFirewallMap = serverSqlManagerInit.getSqlFirewallMap();
	blobUploadConfigurator = serverSqlManagerInit.getBlobUploadConfigurator();
	blobDownloadConfigurator = serverSqlManagerInit.getBlobDownloadConfigurator();
	sessionConfigurator = serverSqlManagerInit.getSessionConfigurator();
	exception = serverSqlManagerInit.getException();
	initErrrorMesage = serverSqlManagerInit.getInitErrrorMesage();
	threadPoolExecutor = serverSqlManagerInit.getThreadPoolExecutor();
    }

    @Override
    public void destroy() {
	super.destroy();
	if (threadPoolExecutor != null) {
	    try {
		threadPoolExecutor.shutdown();
	    } catch (Exception e) {
		e.printStackTrace(); // Should never happen
	    }
	}

    }

    /**
     * Entry point. All is Async in AceQL.
     */
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
	    throws ServletException, IOException {

	/* Call in async mode */
	final AsyncContext asyncContext = request.startAsync();
	asyncContext.setTimeout(0);
	asyncContext.addListener(new ServerAsyncListener());

	// Just in case
	Objects.requireNonNull(threadPoolExecutor, "threadPoolExecutor cannot be null!");

	threadPoolExecutor.execute(new Runnable() {
	    @Override
	    public void run() {

		HttpServletRequest request = (HttpServletRequest) asyncContext.getRequest();
		HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();

		try {
		    handleRequestWrapper(request, response);
		} finally {
		    asyncContext.complete();
		}
	    }
	});
    }

    /**
     * POST & GET. Handles all servlet calls. Allows to log Exceptions including
     * runtime Exceptions
     *
     * @param request
     * @param response
     * @throws UnsupportedEncodingException
     */
    private void handleRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
	OutputStream out = null;
	try {
	    out = response.getOutputStream();
	    handleRequest(request, response, out);
	} catch (Throwable e) {
	    try {
		// Always use our own tmp file for logging or exception
		PrivateTmpLogger privateTmpLogger = new PrivateTmpLogger(e);
		privateTmpLogger.log();

		ExceptionReturner.logAndReturnException(request, response, out, e);
	    } catch (IOException ioe) {
		ioe.printStackTrace(System.out);
	    }
	}
    }

    /**
     * Don't catch Exception in this method. All Throwable are catch in caller
     * handleRequestWrapper
     *
     * @param request
     * @param response
     * @param out
     * @throws UnsupportedEncodingException
     * @throws IOException
     * @throws FileUploadException
     * @throws SQLException
     */
    private void handleRequest(HttpServletRequest request, HttpServletResponse response, OutputStream out)
	    throws UnsupportedEncodingException, IOException, SQLException, FileUploadException {
	request.setCharacterEncoding("UTF-8");

	if (isExceptionSet(response, out)) {
	    return;
	}

	debug("after RequestInfoStore.init(request);");
	debug(request.getRemoteAddr());

	// Wrap the HttpServletRequest in roder to allow to set new parameters
	HttpServletRequestHolder requestHolder = new HttpServletRequestHolder(request);

	ServerSqlDispatch dispatch = new ServerSqlDispatch();
	debug("before dispatch.executeRequest()");

	// Allows to emulate a request parameter from the Servlet Path:
	// domain/aceql/database/[database]/username/[username]/login
	// domain/aceql/session/[session]/[action_name]/[action_value]

	String database = null;
	String username = null;
	String sessionId = null;
	String connectionId = null;
	String action = null;
	String actionValue = null;

	// Minimalist URL analyzer
	debug("servlet Path : " + request.getServletPath());
	debug("getRequestURI: " + request.getRequestURI());

	String servletPath = request.getServletPath();
	String requestUri = request.getRequestURI();

	String servletName = ServletParametersStore.getServletName();
	if (!checkRequestStartsWithAceqlServlet(response, out, servletPath, requestUri, servletName)) {
	    return;
	}

	if (getVersion(out, requestUri, servletName)) {
	    return;
	}

	try {
	    ServletPathAnalyzer servletPathAnalyzer = new ServletPathAnalyzer(requestUri, servletName);
	    action = servletPathAnalyzer.getAction();
	    actionValue = servletPathAnalyzer.getActionValue();
	    database = servletPathAnalyzer.getDatabase();
	    username = servletPathAnalyzer.getUsername();
	    sessionId = servletPathAnalyzer.getSession();
	    connectionId = servletPathAnalyzer.getConnection();

	} catch (Exception e) {
	    // Happens if bad request ==> 400
	    String errorMessage = e.getMessage();
	    JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_BAD_REQUEST,
		    JsonErrorReturn.ERROR_ACEQL_ERROR, errorMessage);
	    // out.println(errorReturn.build());
	    writeLine(out, errorReturn.build());
	    return;
	}

	// Check that the request headers are accepted
	if (!validateHeaders(request, response, out)) {
	    return;
	}

	// In other cases than connect, username & database are null
	if (username == null && database == null) {

	    if (!checkSessionIsVerified(response, out, sessionId)) {
		return;
	    }

	    username = sessionConfigurator.getUsername(sessionId);
	    database = sessionConfigurator.getDatabase(sessionId);

	    if (!checkUsernameAndDatabase(response, out, database, username)) {
		return;
	    }
	}

	debugValues(database, username, sessionId, connectionId, action, actionValue);

	requestHolder.setParameter(HttpParameter.ACTION, action);
	requestHolder.setParameter(HttpParameter.ACTION_VALUE, actionValue);
	requestHolder.setParameter(HttpParameter.SESSION_ID, sessionId);
	requestHolder.setParameter(HttpParameter.CONNECTION_ID, connectionId);
	requestHolder.setParameter(HttpParameter.USERNAME, username);
	requestHolder.setParameter(HttpParameter.DATABASE, database);

	// Tests exceptions
	ServerSqlManager.testThrowException();
	dispatch.executeRequestInTryCatch(requestHolder, response, out);
    }

    /**
     * Checks that headers are authenticated/validated.
     * @param request
     * @param response
     * @param out
     * @return
     * @throws IOException
     */
    private boolean validateHeaders(HttpServletRequest request, HttpServletResponse response, OutputStream out)
	    throws IOException {

	// Request Headers;
	Map headers = new HashMap<>();
	Enumeration e = request.getHeaderNames();
	while (e.hasMoreElements()) {
	    String key = (String) e.nextElement();
	    String value = request.getHeader(key);
	    headers.put(key, value);
	}

	boolean checked = requestHeadersAuthenticator.validate(headers);

	if (!checked) {
	    JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_UNAUTHORIZED,
		    JsonErrorReturn.ERROR_ACEQL_ERROR, JsonErrorReturn.INVALID_SESSION_ID);
	    // out.println(errorReturn.build());
	    writeLine(out, errorReturn.build());
	    return false;
	}

	return checked;
    }

    /**
     * @param database
     * @param username
     * @param sessionId
     * @param connectionId
     * @param action
     * @param actionValue
     */
    private void debugValues(String database, String username, String sessionId, String connectionId, String action,
	    String actionValue) {
	debug("");
	debug("action      : " + action);
	debug("actionValue : " + actionValue);
	debug("username    : " + username);
	debug("sessionId   : " + sessionId);
	debug("connectionId: " + connectionId);
	debug("database    : " + database);
    }

    /**
     * @param response
     * @param out
     * @param database
     * @param username
     * @throws IOException
     */
    private boolean checkUsernameAndDatabase(HttpServletResponse response, OutputStream out, String database,
	    String username) throws IOException {
	if (username == null || database == null) {
	    JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_UNAUTHORIZED,
		    JsonErrorReturn.ERROR_ACEQL_ERROR, JsonErrorReturn.INVALID_SESSION_ID);
	    // out.println(errorReturn.build());
	    writeLine(out, errorReturn.build());
	    return false;
	}
	return true;
    }

    /**
     * @param response
     * @param out
     * @param sessionId
     * @throws IOException
     */
    private boolean checkSessionIsVerified(HttpServletResponse response, OutputStream out, String sessionId)
	    throws IOException {
	boolean isVerified = sessionConfigurator.verifySessionId(sessionId);

	if (!isVerified) {
	    JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_UNAUTHORIZED,
		    JsonErrorReturn.ERROR_ACEQL_ERROR, JsonErrorReturn.INVALID_SESSION_ID);
	    // out.println(errorReturn.build());
	    writeLine(out, errorReturn.build());
	}
	return isVerified;
    }

    /**
     * @param response
     * @param out
     * @throws IOException
     */
    private boolean isExceptionSet(HttpServletResponse response, OutputStream out) throws IOException {
	// If Init fail, say it cleanly to client, instead of bad 500 Servlet
	if (exception != null) {
	    JsonErrorReturn jsonErrorReturn = new JsonErrorReturn(response,
		    HttpServletResponse.SC_INTERNAL_SERVER_ERROR, JsonErrorReturn.ERROR_ACEQL_ERROR,
		    initErrrorMesage + " Reason: " + exception.getMessage(), ExceptionUtils.getStackTrace(exception));

	    writeLine(out, jsonErrorReturn.build());
	    return true;
	}
	return false;
    }

    /**
     * @param out
     * @param requestUri
     * @param servletName
     * @throws IOException
     */
    private boolean getVersion(OutputStream out, String requestUri, String servletName) throws IOException {
	// Display version if we just call the servlet
	if (requestUri.endsWith("/" + servletName) || requestUri.endsWith("/" + servletName + "/")) {
	    String version = org.kawanfw.sql.version.Version.getVersion();
	    writeLine(out, JsonOkReturn.build("version", version));
	    return true;
	}
	return false;
    }

    /**
     * @param response
     * @param out
     * @param servletPath
     * @param requestUri
     * @param servletName
     * @throws IOException
     */
    private boolean checkRequestStartsWithAceqlServlet(HttpServletResponse response, OutputStream out,
	    String servletPath, String requestUri, String servletName) throws IOException {
	if (!requestUri.startsWith("/" + servletName) && !servletPath.startsWith("/" + servletName)) {

	    // System.out.println("servletPath:" + servletPath);
	    // System.out.println("urlContent :" + urlContent);

	    if (requestUri.equals("/")) {
		JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_BAD_REQUEST,
			JsonErrorReturn.ERROR_ACEQL_ERROR,
			JsonErrorReturn.ACEQL_SERVLET_NOT_FOUND_IN_PATH + servletName);
		// out.println(errorReturn.build());
		writeLine(out, errorReturn.build());
		return false;
	    } else {
		String servlet = requestUri.substring(1);
		JsonErrorReturn errorReturn = new JsonErrorReturn(response, HttpServletResponse.SC_BAD_REQUEST,
			JsonErrorReturn.ERROR_ACEQL_ERROR, JsonErrorReturn.UNKNOWN_SERVLET + servlet);
		// out.println(errorReturn.build());
		writeLine(out, errorReturn.build());
		return false;
	    }
	} else {
	    return true;
	}
    }

    /**
     * Throws an Exception for tests purposes if
     * user.home/.kawansoft/throw_exception.txt exists
     */
    public static void testThrowException() {
	File file = new File(
		SystemUtils.USER_HOME + File.separator + ".kawansoft" + File.separator + "throw_exception.txt");
	if (file.exists()) {
	    throw new IllegalArgumentException(
		    "Exception thrown because user.home/.kawansoft/throw_exception.txt exists!");
	}
    }

    /**
     * Write a line of string on the servlet output stream. Will add the necessary
     * CR_LF
     *
     * @param out the servlet output stream
     * @param s   the string to write
     * @throws IOException
     */
    public static void write(OutputStream out, String s) throws IOException {
	out.write((s + CR_LF).getBytes("UTF-8"));
    }

    /**
     * Write a CR/LF on the servlet output stream. Designed to add a end line to
     * ResultSetWriter action
     *
     * @param out the servlet output stream
     * @throws IOException
     */
    public static void writeLine(OutputStream out) throws IOException {
	out.write((CR_LF).getBytes("UTF-8"));
    }

    /**
     * Write a line of string on the servlet output stream. Will add the necessary
     * CR_LF
     *
     * @param out the servlet output stream
     * @param s   the string to write
     * @throws IOException
     */
    public static void writeLine(OutputStream out, String s) throws IOException {
	out.write((s + CR_LF).getBytes("UTF-8"));
    }

    /**
     * Method called by children Servlet for debug purpose Println is done only if
     * class name name is in kawansoft-debug.ini
     */
    public static void debug(String s) {
	if (DEBUG) {
	    System.out.println(new Date() + " " + s);
	}
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy