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

prompto.server.JettyServer Maven / Gradle / Ivy

There is a newer version: 0.1.57
Show newest version
package prompto.server;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.security.auth.login.LoginContext;
import javax.servlet.DispatcherType;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

import org.eclipse.jetty.jaas.JAASLoginService;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.NCSARequestLog;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.SecuredRedirectHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;

import prompto.config.IDebugConfiguration;
import prompto.config.IDebugEventAdapterConfiguration;
import prompto.config.IDebugRequestListenerConfiguration;
import prompto.config.IHttpConfiguration;
import prompto.config.IKeyStoreConfiguration;
import prompto.config.IKeyStoreFactoryConfiguration;
import prompto.config.ISecretKeyConfiguration;
import prompto.config.IServerConfiguration;
import prompto.config.auth.IAuthenticationConfiguration;
import prompto.config.auth.source.IAuthenticationSourceConfiguration;
import prompto.debug.DebugEventServlet;
import prompto.debug.DebugRequestServlet;
import prompto.debug.HttpServletDebugRequestListenerFactory;
import prompto.debug.WebSocketDebugEventAdapterFactory;
import prompto.graphql.GraphQLServlet;
import prompto.runtime.Mode;
import prompto.security.IKeyStoreFactory;
import prompto.security.ISecretKeyFactory;
import prompto.security.auth.method.IAuthenticationMethodFactory;
import prompto.security.auth.source.IAuthenticationSource;
import prompto.store.DataStore;
import prompto.utils.Logger;

class JettyServer extends Server {

	static final Logger logger = new Logger();
	static final String USER_AGENT = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 RuxitSynthetic/1.0 v5282078223 t55095";
	
	IServerConfiguration config;
	Supplier auth = null;
	ServerConnector mainConnector;
	ServerConnector redirectConnector;
	ConstraintSecurityHandler securityHandler;
	HandlerList contentHandler;
	DebugRequestServlet debugRequestServlet;
	DebugEventServlet debugEventServlet;
	boolean startComplete = false;
	Thread serverThread = null;
	Throwable serverThrowable = null;
	
	
	public JettyServer(IServerConfiguration config) {
		this.config = config;
		this.setStopAtShutdown(true);
	}

	void jettyStart(Runnable onServerStopped) throws Throwable  {
		serverThrowable = null;
		startComplete = false;
		Object startFlag = new Object();
		serverThread = new Thread(new Runnable() {
			@Override
			public void run() {
				logger.info(()->"Web server about to start...");
				try {
					try {
						start();
						logger.info(()->"Web server started.");
					} finally {
						logger.info(()->"Signaling start completion...");
						synchronized (startFlag) {
							startComplete = true;
							startFlag.notify();
						}
					}
					logger.info(()->"Web server waiting ready...");
					join();
					logger.info(()->"Web server stop complete.");
					if(onServerStopped!=null)
						onServerStopped.run();
				} catch(Throwable t) {
					t.printStackTrace();
					serverThrowable = t;
				} finally {
					serverThread = null;
				}
			}
		}, "HTTP Server");
		serverThread.start();
		logger.info(()->"Waiting for start completion signal...");
		synchronized (startFlag) {
			while(!startComplete)
				startFlag.wait();
		}
		logger.info(()->"Start completion signalled.");
		forceLoginModuleInitialization();
		if(serverThrowable!=null) {
			Throwable t = serverThrowable;
			serverThrowable = null;
			throw t;
		}
	}
	
	

	private void forceLoginModuleInitialization() {
		IHttpConfiguration http = config.getHttpConfiguration();
		IAuthenticationConfiguration auth = http.getAuthenticationConfiguration();
		if(auth==null) {
			logger.info(()->"No auth required, no need to force LoginModule initialization");
			return; // no need or not possible
		} else {
			doForceLoginModuleInitialization(auth);
			if(IAuthenticationSource.instance.get()==null)
				logger.warn(()->"No authentication source configured!");
			else
				logger.info(()->"Authentication source successfully initialized");
		}
	}

	private void doForceLoginModuleInitialization(IAuthenticationConfiguration auth) {
		try {
			String serviceName = auth.getAuthenticationSourceConfiguration().getAuthenticationSourceFactory().getJettyLoginModuleName();
			LoginContext lc = new LoginContext(serviceName); 
			lc.login();
		} catch(Throwable t) {
			logger.debug(()->"During force LoginModule initialization", t);
		} 
	}
	
	void jettyStop() throws Exception {
		logger.info(()->"Stopping web server...");
		stop();
		// don't join here since it would create a deadlock
	}

	public void prepare(BiConsumer handler) throws Exception {
		prepareConnectors();
		prepareHandlers(handler);
	}
	
	private void prepareHandlers(BiConsumer handler) throws IOException {
		prepareSecurityHandler();
		prepareContentHandler(handler);
		setHandler(contentHandler);
	}

	private void prepareConnectors() throws Exception {
		mainConnector = prepareMainConnector();
		redirectConnector = prepareRedirectConnector();
		if(redirectConnector==null)
			setConnectors(new Connector[] { mainConnector });
		else
			setConnectors(new Connector[] { mainConnector, redirectConnector });
	}

	@SuppressWarnings("resource")
	private ServerConnector prepareMainConnector() throws Exception {
		ServerConnector sc = "http".equalsIgnoreCase(config.getHttpConfiguration().getProtocol()) ?
				prepareHttpConnector() :
				prepareHttpsConnector();
		if(config.getHttpConfiguration().getPort()!=-1)
			sc.setPort(config.getHttpConfiguration().getPort());
		return sc;
	}
	
	private ServerConnector prepareRedirectConnector() {
		if(config.getHttpConfiguration().getRedirectFrom()==null)
			return null;
		else {
			logger.info(()->"Preparing redirection from port " + config.getHttpConfiguration().getRedirectFrom() + " to port " + config.getHttpConfiguration().getPort());
			HttpConfiguration http = new HttpConfiguration();
			http.setSecurePort(config.getHttpConfiguration().getPort());
			http.setSecureScheme("https");
			http.addCustomizer(new SecureRequestCustomizer());
		    ServerConnector sc = new ServerConnector(this, new HttpConnectionFactory(http));
			sc.setPort(config.getHttpConfiguration().getRedirectFrom());
			return sc;
		}
	}

	private ServerConnector prepareHttpConnector() {
		return new ServerConnector(this);
	}


	private ServerConnector prepareHttpsConnector() throws Exception {
		SslConnectionFactory ssl = createSSLFactory();
		HttpConnectionFactory https = createHttpsFactory();
		return new ServerConnector(this, ssl, https);
	}
	
	private SslConnectionFactory createSSLFactory() throws Exception {
		SslContextFactory context = new SslContextFactory();
		context.setSslSessionTimeout(180000);
		context.setIncludeProtocols("TLSv1.2");
		IKeyStoreConfiguration ksc = config.getHttpConfiguration().getKeyStoreConfiguration();
		IKeyStoreFactoryConfiguration ksfc = ksc.getKeyStoreFactoryConfiguration();
		IKeyStoreFactory factory = ksfc.getKeyStoreFactory();
		context.setKeyStore(factory.newInstance(ksfc));
		ISecretKeyConfiguration secret = ksc.getSecretKeyConfiguration();
		if(secret!=null)
			context.setKeyStorePassword(ISecretKeyFactory.plainPasswordFromConfig(secret));
		ksc = config.getHttpConfiguration().getTrustStoreConfiguration();
		// trust store is optional
		if(ksc!=null) {
			ksfc = ksc.getKeyStoreFactoryConfiguration();
			factory = ksfc.getKeyStoreFactory();
			context.setTrustStore(factory.newInstance(ksfc));
			secret = ksc.getSecretKeyConfiguration();
			if(secret!=null)
				context.setTrustStorePassword(ISecretKeyFactory.plainPasswordFromConfig(secret));
		}
		return new SslConnectionFactory(context, "http/1.1");
	}

	private HttpConnectionFactory createHttpsFactory() {
		HttpConfiguration https = new HttpConfiguration();
		https.addCustomizer(new SecureRequestCustomizer());
		return new HttpConnectionFactory(https);
	}

	public int getHttpPort() {
		return mainConnector!=null ? mainConnector.getLocalPort() : -1;
	}

	private void prepareSecurityHandler() {
		logger.info(()->"Preparing security handler...");
		securityHandler = config.getHttpConfiguration().getAllowsXAuthorization() && config.getHttpConfiguration().getAllowedOrigins()!=null ?
			new ConstraintSecurityHandlerWithXAuthorization() :
			new ConstraintSecurityHandler();	
		if(getAuthenticationConfiguration()!=null)
			configureSecurityHandler();
		else
			logger.info(()->"Not using authentication!");
		logger.info(()->"Security handler successfully prepared.");
	}
	
	private IAuthenticationConfiguration getAuthenticationConfiguration() {
		if(auth==null) {
			IAuthenticationConfiguration instance = config.getHttpConfiguration().getAuthenticationConfiguration();
			auth = ()->instance;
		}
		return auth.get();
	}

	private void configureSecurityHandler() {
		try {
			securityHandler.setLoginService(prepareJettyLoginService()); // where to check credentials
			securityHandler.setAuthenticator(prepareAuthenticator()); // how to request credentials
			securityHandler.setConstraintMappings(prepareAuthConstraintMappings()); // when to require security
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private void prepareContentHandler(BiConsumer handler) throws IOException {
		logger.info(()->"Preparing web handlers...");
		HandlerList list = new HandlerList();
		list.addHandler(prepareHttpRequestLogger());
		if(config.getHttpConfiguration().getRedirectFrom()!=null)
			list.addHandler(new SecuredRedirectHandler());
		handler.accept(this, list);
		contentHandler = list;
		logger.info(()->"Web handlers successfully prepared.");
	}

	private Handler prepareHttpRequestLogger() throws IOException {
		File dir = new File("/logs");
		if(!dir.exists())
			dir = Files.createTempDirectory("prompto_jetty_").toFile();
		File file = new File(dir, "jetty-http-yyyy_mm_dd.log");
		String path = file.getAbsolutePath();
		logger.info(()->"Logging http requests to " + path + ".");
		NCSARequestLog logger = new NCSARequestLog(path);
		logger.setRetainDays(365);
		logger.setAppend(true);
		logger.setExtended(false);
		logger.setLogTimeZone("GMT");
		logger.setLogLatency(true);
		logger.setLogLocale(Locale.ENGLISH);
		// logger.setLogDateFormat("yyyy-MM-ddTHH:mm:ss"); // ISO-8601
		RequestLogHandler handler = new RequestLogHandler();
		handler.setRequestLog(logger);
		return handler;
	}

	private Authenticator prepareAuthenticator() {
		boolean xauth = config.getHttpConfiguration().getAllowsXAuthorization();
		IAuthenticationMethodFactory factory = getAuthenticationConfiguration().getAuthenticationMethodConfiguration().getAuthenticationMethodFactory();
		return factory.newAuthenticator(xauth);
	}

	private LoginService prepareJettyLoginService() throws Exception {
		IAuthenticationSourceConfiguration login = getAuthenticationConfiguration().getAuthenticationSourceConfiguration();
		String loginModuleName = login.getAuthenticationSourceFactory().installJettyLoginModule();
		JAASLoginService loginService = new JAASLoginService("prompto.login.service");
		loginService.setIdentityService(prepareIdentityService());
		loginService.setLoginModuleName(loginModuleName);
		addBean(loginService);
		return loginService;
	}

	private IdentityService prepareIdentityService() {
		return new DefaultIdentityService();
	}
	
	private List prepareAuthConstraintMappings() {
		Stream allowed = prepareAllowedConstraintMappings();
		ConstraintMapping protect = new ConstraintMapping();
		protect.setPathSpec("/"); // protect all paths
		protect.setConstraint(prepareAuthenticationConstraint());
		return Stream.concat(allowed, Stream.of(protect))
				.collect(Collectors.toList());
	}
	
	private Stream prepareAllowedConstraintMappings() {
		Constraint allow = prepareNoAuthenticationConstraint();
		Stream whiteList = getAuthenticationConfiguration().getWhiteList().stream();
		if(config.getRuntimeMode()==Mode.DEVELOPMENT)
			whiteList = Stream.concat(whiteList, Collections.singletonList("/ws/control/*").stream());
		return whiteList.map(path->{
					logger.info(()->"Allowing free access to '" + path + "'");
					ConstraintMapping cm = new ConstraintMapping();
					cm.setPathSpec(path);
					cm.setConstraint(allow);
					return cm;
				});
	}

	private Constraint prepareNoAuthenticationConstraint() {
		Constraint constraint = new Constraint();
	    constraint.setName("no-authentication");
	    constraint.setAuthenticate(false);
	    constraint.setRoles(new String[] { "*" }); // all roles  
		return constraint;
	}

	private Constraint prepareAuthenticationConstraint() {
		Constraint constraint = new Constraint();
	    constraint.setName("authentication");
	    constraint.setAuthenticate(true);
	    constraint.setRoles(new String[] { "**" }); // all authenticated roles 
		return constraint;
	}

	static abstract class WebAppContextBase extends org.eclipse.jetty.webapp.WebAppContext {
		
		public ServletHolder addServlet(CleverServlet servlet, String pathSpec) {
			ServletHolder holder = new ServletHolder(servlet);
			this.addServlet(holder, pathSpec);
			servlet.setHolder(holder);
			return holder;
		};

	}
	
	public static class WebSiteContext extends WebAppContextBase {
		
	}

	public static class WebApiContext extends WebAppContextBase {
		
	}
	
	public static class ThreadLocalCleaner implements ServletRequestListener {

		@Override
		public void requestInitialized(ServletRequestEvent sre) {
			DataStore.useGlobal();
		}

		@Override
		public void requestDestroyed(ServletRequestEvent sre) {
			DataStore.useGlobal();
		}

	}
	

	public WebApiContext newWebApiHandler() throws Exception {
		WebApiContext handler = new WebApiContext();
		handler.setContextPath("/api");
		handler.setResourceBase(getResourceBase());
		handler.addEventListener(new ThreadLocalCleaner());
		// TODO handler.setSecurityHandler(securityHandler);
		if(GraphQLServlet.isEnabled()) {
			logger.info(()->"Starting GraphQL server...");
			handler.addServlet(new GraphQLServlet(), "/graphql");
		} else 
			logger.info(()->"No GraphQL method");
		return handler;		
	}
	
	public WebSiteContext newWebSiteHandler() throws Exception {
		WebSiteContext handler = new WebSiteContext();
		handler.setContextPath("/");
		handler.setResourceBase(getResourceBase());
		handler.addEventListener(new ThreadLocalCleaner());
		handler.setSecurityHandler(securityHandler);
		String welcomePage = config.getHttpConfiguration().getWelcomePage();
		String siteMap = config.getHttpConfiguration().getSiteMap();
		if(config.getWebSiteRoot()!=null)
			handler.addServlet(new WebSiteServlet(config.getWebSiteRoot(), welcomePage), "/");
		else
			handler.addServlet(new CodeStoreServlet(welcomePage, siteMap), "/");
		handler.addServlet(new TranspilerServlet(), "*.page");   
		handler.addServlet(new ControlServlet(), "/ws/control/*");
		handler.addServlet(new BinaryServlet(), "/ws/bin/*");
		handler.addServlet(new DataServlet(), "/ws/data/*");   
		handler.addServlet(new StoreServlet(), "/ws/store/*"); 
		newDebuggerServlets(handler);
		boolean sendsXAutorization = config.getHttpConfiguration().getSendsXAuthorization();
        handler.addServlet(new PromptoServlet(sendsXAutorization), "/ws/run/*");  
        FilterHolder filterHolder = newCrossOriginHandler();
        if(filterHolder!=null)
        	handler.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
        SessionManager manager = new SessionManager(config.getHttpConfiguration());
        handler.getSessionHandler().setSessionManager(manager);
    	return handler;
	}
	
   private void newDebuggerServlets(WebSiteContext handler) throws Exception {
		IDebugConfiguration debug = config.getDebugConfiguration();
		if(debug==null)
			return;
		// if there is a config, create the servlets and map them
		// they will be wired with the actual debug component instances later
		IDebugRequestListenerConfiguration listener = debug.getRequestListenerConfiguration();
		if(listener!=null) {
			String factoryName = listener.getFactory();
			if(HttpServletDebugRequestListenerFactory.class.getName().equals(factoryName)) {
				debugRequestServlet = new DebugRequestServlet();
				handler.addServlet(debugRequestServlet, "/ws/debug-request/*");  
			}
		}
		IDebugEventAdapterConfiguration adapter = debug.getEventAdapterConfiguration();
		if(adapter!=null) {
			String factoryName = adapter.getFactory();
			if(WebSocketDebugEventAdapterFactory.class.getName().equals(factoryName)) {
				debugEventServlet = new DebugEventServlet();
				ServletHolder servletHolder = new ServletHolder(debugEventServlet);
				handler.addServlet(servletHolder, "/ws/debug-event/*");  
			}
		}
	}


   private FilterHolder newCrossOriginHandler() {
    	final String allowedOrigins = config.getHttpConfiguration().getAllowedOrigins();
    	if(allowedOrigins==null)
			return null;
		logger.info(()->"Setting allowed origins to: "  + allowedOrigins);
		FilterHolder holder = new FilterHolder();
		holder.setInitParameter(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, "true");
		holder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, allowedOrigins);
		holder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,HEAD,OPTIONS");
		holder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,X-Authorization,Content-Type,Accept,Origin,Access-Control-Allow-Origin");
		// holder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
		holder.setFilter(new LoggingCrossOriginFilter());
		return holder;
	 }

    private String getResourceBase() throws IOException {
    	return getRootURL().toExternalForm();
    }

	private URL getRootURL() throws IOException {
		URL url = AppServer.class.getResource("/js/lib/require.js");
		url = new URL(url.toExternalForm().replace("/js/lib/require.js", "/"));
		// ugly work around for unit tests
		if(url.toExternalForm().contains("/test-classes/"))
			url = new URL(url.toExternalForm().replace("/test-classes/", "/classes/"));
		return url;
	}

	public DefaultHandler newDefaultHandler() {
		return new DefaultHandler();
	}


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy