package WEBPIECESxPACKAGE;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.ServerSocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.webpieces.httpcommon.api.RequestListener;
import org.webpieces.nio.api.channels.TCPServerChannel;
import org.webpieces.router.api.PortConfig;
import org.webpieces.router.api.RouterConfig;
import org.webpieces.templating.api.TemplateConfig;
import org.webpieces.util.file.VirtualFile;
import org.webpieces.util.file.VirtualFileClasspath;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.util.security.SecretKeyInfo;
import org.webpieces.webserver.api.WebServer;
import org.webpieces.webserver.api.WebServerConfig;
import org.webpieces.webserver.api.WebServerFactory;
import com.google.inject.Module;
import com.google.inject.util.Modules;
import WEBPIECESxPACKAGE.base.tags.TagLookupOverride;
/**
* Changes to any class in this package (or any classes these classes reference) WILL require a
* restart when you are running the DevelopmentServer. This class should try to remain pretty
* thin and you should avoid linking any classes in this package to classes outside this
* package(This is only true if you want to keep using the development server). In production,
* we do not play any classloader games at all(unlike play framework) avoiding any prod issues.
*/
public class Server {
/*******************************************************************************
* When running the dev server, changes to this file AND to any files in this package
* require a server restart(you can try not to but it won't work)
*******************************************************************************/
private static final Logger log = LoggerFactory.getLogger(Server.class);
public static final Charset ALL_FILE_ENCODINGS = StandardCharsets.UTF_8;
//Welcome to YOUR main method as webpieces webserver is just a library you use that you can
//swap literally any piece of
public static void main(String[] args) throws InterruptedException {
try {
Server server = new Server(null, null, new ServerConfig("production"));
server.start();
synchronized (Server.class) {
//wait forever so server doesn't shut down..
Server.class.wait();
}
} catch(Throwable e) {
log.error("Failed to startup. exiting jvm", e);
System.exit(1); // should not be needed BUT some 3rd party libraries start non-daemon threads :(
}
}
private WebServer webServer;
public Server(
Module platformOverrides,
Module appOverrides,
ServerConfig svrConfig) {
String filePath = System.getProperty("user.dir");
log.info("original user.dir before modification="+filePath);
modifyUserDirForManyEnvironments(filePath);
VirtualFile metaFile = svrConfig.getMetaFile();
//Dev server has to override this
if(metaFile == null)
metaFile = new VirtualFileClasspath("appmeta.txt", Server.class.getClassLoader());
if(!metaFile.exists())
throw new RuntimeException("file not found="+metaFile);
//This override is only needed if you want to add your own Html Tags to re-use
//you can delete this code if you are not adding your own html tags
Module allOverrides = new TagLookupOverride();
if(platformOverrides != null) {
allOverrides = Modules.combine(platformOverrides, allOverrides);
}
SecretKeyInfo signingKey = new SecretKeyInfo(fetchKey(), "HmacSHA1");
//Different pieces of the server have different configuration objects where settings are set
//You could move these to property files but definitely put some thought if you want people
//randomly changing those properties and restarting the server without going through some testing
//by a QA team. We leave most of these properties right here.
RouterConfig routerConfig = new RouterConfig()
.setMetaFile(metaFile)
.setWebappOverrides(appOverrides)
.setWebAppMetaProperties(svrConfig.getWebAppMetaProperties())
.setSecretKey(signingKey)
.setPortConfigCallback(() -> fetchPortsForRedirects());
WebServerConfig config = new WebServerConfig()
.setPlatformOverrides(allOverrides)
.setHttpListenAddress(new InetSocketAddress(svrConfig.getHttpPort()))
.setHttpsListenAddress(new InetSocketAddress(svrConfig.getHttpsPort()))
.setSslEngineFactory(new WebSSLFactory())
.setFunctionToConfigureServerSocket(s -> configure(s))
.setValidateRouteIdsOnStartup(svrConfig.isValidateRouteIdsOnStartup())
.setStaticFileCacheTimeSeconds(svrConfig.getStaticFileCacheTimeSeconds());
TemplateConfig templateConfig = new TemplateConfig();
webServer = WebServerFactory.create(config, routerConfig, templateConfig);
}
PortConfig fetchPortsForRedirects() {
//NOTE: You will need to modify this so it detects when you are behind a firewall that has ports exposed to
//customers different than the ports your server exposes
boolean useFirewallPorts = false;
//NOTE: for running locally and for tests, you must set useFirewallPorts=false
int httpPort = 80; //good security teams generally have the firewall on port 80 and your server on something like 8080
int httpsPort = 443; //good security teams generally have the firewall on port 443 and your server on something like 8443
if(!useFirewallPorts) {
//otherwise use the same port the webserver is bound to
//this is for running locally AND for local tests
httpPort = getUnderlyingHttpChannel().getLocalAddress().getPort();
httpsPort = getUnderlyingHttpsChannel().getLocalAddress().getPort();
}
return new PortConfig(httpPort, httpsPort);
}
private byte[] fetchKey() {
//This is purely so it works before template creation
//NOTE: our build runs all template tests that are generated to make sure we don't break template
//generation but for that to work pre-generation, we need this code but you are free to delete it...
String base64Key = "__SECRETKEYHERE__"; //This gets replaced with a unique key each generated project which you need to keep or replace with your own!!!
if(base64Key.startsWith("__SECRETKEY")) //This does not get replaced (user can remove it from template)
return base64Key.getBytes();
return Base64.getDecoder().decode(base64Key);
}
private void modifyUserDirForManyEnvironments(String filePath) {
String finalUserDir = modifyUserDirForManyEnvironmentsImpl(filePath);
System.setProperty("user.dir", finalUserDir);
log.info("RECONFIGURED user.dir="+finalUserDir);
}
/**
* I like things to work seamlessly but user.dir is a huge issue in multiple environments I am working in.
* main server has to work in N configurations that should be tested and intellij is a PITA since
* it is inconsistent. PP means runs in the project the file is in and eclipse is consistent with
* gradle while intellij is only half the time....
*
* app dev - The environment when you generate a project and import it into an IDE
* webpieces - The environment where you test the template directly when bringing in webpieces into an IDE
*
*
* * app dev / eclipse -
* * PP - running myapp/src/tests - user.dir=myapp-all/myapp
* * PP - DevServer - user.dir=myapp-all/myapp-dev
* * PP - SemiProductionServer - user.dir=myapp-all/myapp-dev
* * PP - ProdServer - user.dir=myapp-all/myapp
* * app dev / intellij (it's different paths than eclipse :( ). user.dir starts as myapp directory
* * PP - running myapp/src/tests - myapp-all/myapp
* * NO - DevServer - user.dir=myapp-all :( what the hell! different from running tests
* * NO - SemiProductionServer - user.dir=myapp-all
* * NO - ProdServer - user.dir=myapp-all
* * webpieces / eclipse - same as app dev because eclipse is nice in this aspect
* * webpieces / intellij - ANNOYING and completely different. Runs out of webpieces a few levels down from actual subproject
* * PP - tests in webpieces gradle - myapp-all/myapp
* * PP - tests in myapp's gradle run - myapp-all/myapp
* * NO - production - user.dir=from distribution myapp directory which has subdirs bin, lib, config, public
* * Future? - run DevSvr,SemiProdSvr,ProdSvr from gradle?....screw that for now..it's easy to run from IDE so why bother(it may just work though too)
*
* - so in production, the relative paths work from myapp so 'public/' is a valid location for html files resolving to myapp/public
* - in testing, IF we want myapp-all/myapp/src/dist/public involved, it would be best to run from myapp-all/myapp/src/dist so 'public/' is still a valid location
* - in devserver, semiprodserver, and prod server, the same idea follows where myapp-all/myapp/src/dist should be the user.dir!!!
*
* - sooooo, algorithm is this
* - if user.dir=myapp-all, modify user.dir to myapp-all/myapp/src/dist (you are in intellij)
* - else if user.dir=myapp-dev, modify to ../myapp/src/dist
* - else if myapp has directories bin, lib, config, public then do nothing
* - else modify user.dir=myapp to myapp/src/dist
*/
private String modifyUserDirForManyEnvironmentsImpl(String filePath) {
File f = new File(filePath);
String name = f.getName();
if("WEBPIECESxAPPNAME-all".equals(name)) {
return new File(filePath, "WEBPIECESxAPPNAME/src/dist").getAbsolutePath();
} else if("WEBPIECESxAPPNAME-dev".equals(name)) {
File parent = f.getParentFile();
return new File(parent, "WEBPIECESxAPPNAME/src/dist").getAbsolutePath();
} else if(!"WEBPIECESxAPPNAME".equals(name)) {
if(filePath.endsWith("WEBPIECESxAPPNAME/src/dist"))
return filePath; //This occurs when a previous test ran already and set user.dir
else if(filePath.endsWith("webpieces")) //
return filePath+"/webserver/webpiecesServerBuilder/templateProject/WEBPIECESxAPPNAME/src/dist";
throw new IllegalStateException("bug, we must have missed an environment="+name);
}
File bin = new File(f, "bin");
File lib = new File(f, "lib");
File config = new File(f, "config");
File publicFile = new File(f, "public");
if(bin.exists() && lib.exists() && config.exists() && publicFile.exists()) {
return filePath;
}
return new File(f, "src/dist").getAbsolutePath();
}
/**
* This is a bit clunky BUT if jdk authors add methods that you can configure, we do not have
* to change our platform every time so you can easily set the new properties rather than waiting for
* us to release a new version
*/
public void configure(ServerSocketChannel channel) throws SocketException {
channel.socket().setReuseAddress(true);
//channel.socket().setSoTimeout(timeout);
//channel.socket().setReceiveBufferSize(size);
}
public RequestListener start() {
return webServer.start();
}
public void stop() {
webServer.stop();
}
public TCPServerChannel getUnderlyingHttpChannel() {
return webServer.getUnderlyingHttpChannel();
}
public TCPServerChannel getUnderlyingHttpsChannel() {
return webServer.getUnderlyingHttpsChannel();
}
}