/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.web; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import javax.net.ssl.TrustManager; import w3.html.A; import w3.html.Article; import w3.html.Aside; import w3.html.B; import w3.html.BlockQuote; import w3.html.Body; import w3.html.Br; import w3.html.BrLf; import w3.html.Button; import w3.html.Code; import w3.html.Div; import w3.html.Em; import w3.html.FieldSet; import w3.html.Footer; import w3.html.Form; import w3.html.H1; import w3.html.H2; import w3.html.H3; import w3.html.H4; import w3.html.H5; import w3.html.H6; import w3.html.Head; import w3.html.Header; import w3.html.Hr; import w3.html.Html; import w3.html.I; import w3.html.IFrame; import w3.html.Img; import w3.html.Input; import w3.html.Label; import w3.html.Legend; import w3.html.Li; import w3.html.Link; import w3.html.Meta; import w3.html.Nav; import w3.html.NbSp; import w3.html.Ol; import w3.html.OptGroup; import w3.html.Option; import w3.html.P; import w3.html.Pre; import w3.html.Q; import w3.html.S; import w3.html.Script; import w3.html.Section; import w3.html.Select; import w3.html.Span; import w3.html.Style; import w3.html.Sub; import w3.html.Sup; import w3.html.TBody; import w3.html.THead; import w3.html.Table; import w3.html.Td; import w3.html.TextArea; import w3.html.Th; import w3.html.Title; import w3.html.Tr; import w3.html.U; import w3.html.Ul; import w3.html.Wbr; import divconq.filestore.CommonPath; import divconq.hub.DomainInfo; import divconq.hub.DomainNameMapping; import divconq.hub.Hub; import divconq.hub.HubPackage; import divconq.io.LocalFileStore; import divconq.lang.op.OperationContext; import divconq.log.Logger; import divconq.net.ssl.SslHandler; import divconq.struct.Struct; import divconq.util.StringUtil; import divconq.web.dcui.AdvElement; import divconq.web.dcui.AdvForm; import divconq.web.dcui.AdvText; import divconq.web.dcui.CaptionedImage; import divconq.web.dcui.ButtonLink; import divconq.web.dcui.Document; import divconq.web.dcui.FormButton; import divconq.web.dcui.FormInstruction; import divconq.web.dcui.Html5AppHead; import divconq.web.dcui.HyperLink; import divconq.web.dcui.ICodeTag; import divconq.web.dcui.IncludeParam; import divconq.web.dcui.IncludePart; import divconq.web.dcui.LiteralText; import divconq.web.dcui.Nodes; import divconq.web.dcui.PagePart; import divconq.web.dcui.TextPart; import divconq.web.dcui.TitledSection; import divconq.web.http.SslContextFactory; import divconq.web.http.WebTrustManager; import divconq.xml.XElement; import divconq.xml.XNode; import divconq.xml.XText; public class WebDomain { protected String id = null; protected String alias = null; protected CommonPath homepath = null; protected WebSiteManager man = null; protected List<HubPackage> packagelist = null; protected XElement webconfig = null; protected TrustManager[] trustManagers = new TrustManager[1]; protected DomainNameMapping<SslContextFactory> certs = null; protected Map<String, Class<? extends ICodeTag>> codetags = new HashMap<String, Class<? extends ICodeTag>>(); protected String[] specialExtensions = new String[] { ".dcui.xml", ".html", ".gas" }; protected boolean appFramework = false; protected HtmlMode htmlmode = HtmlMode.Static; protected WebSite rootsite = null; protected Map<String, WebSite> sites = new HashMap<>(); protected Map<String, WebSite> domainsites = new HashMap<>(); public XElement getWebConfig() { return this.webconfig; } public String getId() { return this.id; } public String getAlias() { return this.alias; } public CommonPath getHomePath() { return this.homepath; } public boolean isAppFramework() { return this.appFramework; } public HtmlMode getHtmlMode() { return this.htmlmode; } public WebSite getRootSite() { return this.rootsite; } public List<HubPackage> getPackagelist() { return this.packagelist; } public String[] getSpecialExtensions() { return this.specialExtensions; } public void init(DomainInfo domain, WebSiteManager man) { this.id = domain.getId(); this.alias = domain.getAlias(); this.man = man; this.rootsite = new WebSite("root", this); this.initialTags(); this.reloadSettings(); } public void reloadSettings() { this.homepath = new CommonPath("/index.html"); this.appFramework = false; this.sites.clear(); this.domainsites.clear(); XElement settings = this.man.getModule().getLoader().getSettings(); if (Logger.isDebug()) Logger.debug("Reloading web domain settings: " + this.alias + " - " + settings); if (settings != null) { // UI = app or customer uses builder // UI = basic is just 'index.html' approach this.appFramework = (settings.hasAttribute("UI") && ("custom".equals(settings.getAttribute("UI").toLowerCase()))); if (settings.hasAttribute("HomePath")) this.homepath = new CommonPath(settings.getAttribute("HomePath")); else if (this.appFramework) this.homepath = new CommonPath("/Home"); if (settings.hasAttribute("HtmlMode")) try { this.htmlmode = HtmlMode.valueOf(settings.getAttribute("HtmlMode")); } catch(Exception x) { OperationContext.get().error("Unknown HTML Mode: " + settings.getAttribute("HtmlMode")); } for (XElement pel : settings.selectAll("Site")) { String sname = pel.getAttribute("Name"); if (StringUtil.isEmpty(sname)) continue; if ("root".equals(sname)) { this.rootsite.init(pel); } else { WebSite site = new WebSite(sname, this); site.init(pel); this.sites.put(sname, site); for (XElement del : pel.selectAll("Domain")) { String dname = del.getAttribute("Name"); this.domainsites.put(dname, site); } } } } DomainInfo domain = Hub.instance.getDomainInfo(this.id); if (domain == null) return; XElement config = domain.getSettings(); if (Logger.isDebug()) Logger.debug("Checking web domain settings: " + this.alias + " - " + config); if (config == null) { // TODO improve so this works with domain settings - with or without Web // collect a list of the packages names enabled for this domain HashSet<String> packagenames = new HashSet<>(); XElement webextconfig = this.man.getWebExtension().getLoader().getConfig(); // add to the package name list all the packages turned on for entire web service if (webextconfig != null) for (XElement pel : webextconfig.selectAll("Package")) packagenames.add(pel.hasAttribute("Id") ? pel.getAttribute("Id") :pel.getAttribute("Name")); this.packagelist = Hub.instance.getResources().getPackages().buildLookupList(packagenames); if (Logger.isDebug()) Logger.debug("Package list: " + this.alias + " - " + this.packagelist.size()); return; } this.webconfig = config.selectFirst("Web"); if (Logger.isDebug()) Logger.debug("Checking web domain web settings: " + this.alias + " - " + this.webconfig); if (this.webconfig == null) return; // UI = app or customer uses builder // UI = basic is just 'index.html' approach this.appFramework = (this.webconfig.hasAttribute("UI") && ("custom".equals(this.webconfig.getAttribute("UI").toLowerCase()))); if (this.webconfig.hasAttribute("HomePath")) this.homepath = new CommonPath(this.webconfig.getAttribute("HomePath")); else if (this.appFramework) this.homepath = new CommonPath("/Home"); if (this.webconfig.hasAttribute("HtmlMode")) try { this.htmlmode = HtmlMode.valueOf(this.webconfig.getAttribute("HtmlMode")); } catch(Exception x) { OperationContext.get().error("Unknown HTML Mode: " + this.webconfig.getAttribute("HtmlMode")); } for (XElement pel : this.webconfig.selectAll("Site")) { String sname = pel.getAttribute("Name"); if (StringUtil.isEmpty(sname)) continue; if ("root".equals(sname)) { this.rootsite.init(pel); } else { WebSite site = new WebSite(sname, this); site.init(pel); this.sites.put(sname, site); for (XElement del : pel.selectAll("Domain")) { String dname = del.getAttribute("Name"); this.domainsites.put(dname, site); } } } // ------ // collect a list of the packages names enabled for this domain HashSet<String> packagenames = new HashSet<>(); // if not in the domain, then go look in the packages for (XElement pel : this.webconfig.selectAll("Package")) packagenames.add(pel.getAttribute("Name")); XElement webextconfig = this.man.getWebExtension().getLoader().getConfig(); // add to the package name list all the packages turned on for entire web service if (webextconfig != null) for (XElement pel : webextconfig.selectAll("Package")) packagenames.add(pel.hasAttribute("Id") ? pel.getAttribute("Id") :pel.getAttribute("Name")); this.packagelist = Hub.instance.getResources().getPackages().buildLookupList(packagenames); // ------ WebTrustManager trustman = new WebTrustManager(); trustman.init(this.webconfig); this.trustManagers[0] = trustman; LocalFileStore fs = Hub.instance.getPublicFileStore(); if (fs == null) return; Path cpath = fs.getFilePath().resolve("dcw/" + this.getAlias() + "/config/certs"); if (Files.notExists(cpath)) return; this.certs = new DomainNameMapping<>(); for (XElement cel : this.webconfig.selectAll("Certificate")) { SslContextFactory ssl = new SslContextFactory(); ssl.init(cel, cpath.toString() + "/", trustManagers); this.certs.add(cel.getAttribute("Name"), ssl); } } public WebSite site(Request req) { String domain = req.getHeader("Host"); if (domain.indexOf(':') > -1) domain = domain.substring(0, domain.indexOf(':')); WebSite site = this.domainsites.get(domain); return (site != null) ? site : this.rootsite; } // matchname might be a wildcard match public SslContextFactory getSecureContextFactory(String matchname) { if (this.certs != null) return this.certs.get(matchname); return null; } public void addCodeTag(String tag, Class<? extends ICodeTag> classdef) { this.codetags.put(tag, classdef); } // something changed in the config folder public void settingsNotify() { this.reloadSettings(); this.dynNotify(); } // something changed in the www folder // force compiled content to reload from file system public void dynNotify() { this.rootsite.dynNotify(); if (this.sites != null) for (WebSite site : this.sites.values()) site.dynNotify(); } public void translatePath(WebContext ctx) { CommonPath path = ctx.getRequest().getPath(); if (path.isRoot()) ctx.getRequest().setPath(this.homepath); } public CommonPath getNotFound() { if (this.homepath != null) return this.homepath; return new CommonPath("/dcw/notfound.html"); } public void execute(WebContext ctx) { if (Logger.isDebug()) Logger.debug("Translating path: " + ctx.getRequest().getPath()); this.translatePath(ctx); CommonPath path = ctx.getRequest().getPath(); if (Logger.isDebug()) Logger.debug("Process path: " + path); // translate above should take us home for root if (path.isRoot()) { OperationContext.get().errorTr(150001); return; } WebSite site = ctx.getSite(); if (Logger.isDebug()) Logger.debug("Site: " + (site != null ? site.getAlias() : "[missing]")); IOutputAdapter output = site.findFile(ctx); if (OperationContext.get().hasErrors() || (output == null)) { OperationContext.get().errorTr(150001); return; } if (Logger.isDebug()) Logger.debug("Executing adapter: " + output.getClass().getName()); try { output.execute(ctx); } catch (Exception x) { Logger.error("Unable to process web file: " + x); x.printStackTrace(); } } public DomainInfo getDomainInfo() { return Hub.instance.getDomainInfo(this.id); } public String route(Request req, SslHandler ssl) { DomainInfo domain = Hub.instance.getDomainInfo(this.id); if (domain == null) return null; XElement config = domain.getSettings(); if (config == null) return null; String host = req.getHeader("Host"); String port = ""; if (host.contains(":")) { int pos = host.indexOf(':'); port = host.substring(pos); host = host.substring(0, pos); } XElement web = config.selectFirst("Web"); if (web == null) return null; String defPort = this.man.getDefaultTlsPort(); String orguri = req.getOriginalPathUri(); for (XElement route : web.selectAll("Route")) { if (host.equals(route.getAttribute("Name"))) { if (route.hasAttribute("RedirectPath")) return route.getAttribute("RedirectPath"); if (!route.hasAttribute("ForceTls") && !route.hasAttribute("RedirectName")) continue; boolean tlsForce = Struct.objectToBoolean(route.getAttribute("ForceTls", "False")); String rname = route.getAttribute("RedirectName"); boolean changeTls = ((ssl == null) && tlsForce); if (StringUtil.isNotEmpty(rname) || changeTls) { String path = ((ssl != null) || tlsForce) ? "https://" : "http://"; path += StringUtil.isNotEmpty(rname) ? rname : host; // if forcing a switch, use another port path += changeTls ? ":" + route.getAttribute("TlsPort", defPort) : port; return path + req.getOriginalPath(); } } if (orguri.equals(route.getAttribute("Path"))) { if (route.hasAttribute("RedirectPath")) return route.getAttribute("RedirectPath"); } } if ((ssl == null) && Struct.objectToBoolean(web.getAttribute("ForceTls", "False"))) return "https://" + host + ":" + web.getAttribute("TlsPort", defPort) + req.getOriginalPath(); return null; } // Html, Qx, Xml parsing public Nodes parseXml(WebContext ctx, XElement container) { Nodes nodes = new Nodes(); for (XNode xnode : container.getChildren()) { if (xnode instanceof XElement) { this.parseElement(ctx, nodes, (XElement)xnode); } else if (xnode instanceof XText) { String content = ((XText)xnode).getRawValue(); if (!StringUtil.isEmpty(content)) nodes.add(new LiteralText(content)); } } return nodes; } // parses the children of container public Nodes parseElement(WebContext ctx, XElement xel) { Nodes nodes = new Nodes(); this.parseElement(ctx, nodes, xel); return nodes; } // parses the children of container public void parseElement(WebContext ctx, Nodes nodes, XElement xel) { if (xel == null) return; Class<? extends ICodeTag> tag = this.codetags.get(xel.getName()); if (tag != null) try { tag.newInstance().parseElement(ctx, nodes, xel); } catch (Exception x) { // TODO Auto-generated catch block System.out.println("Site parse error: " + x); } } protected void initialTags() { this.codetags.put("a", A.class); this.codetags.put("article", Article.class); this.codetags.put("aside", Aside.class); this.codetags.put("b", B.class); this.codetags.put("blockquote", BlockQuote.class); this.codetags.put("body", Body.class); this.codetags.put("button", Button.class); this.codetags.put("br", Br.class); this.codetags.put("brlf", BrLf.class); this.codetags.put("canvas", AdvElement.class); this.codetags.put("code", Code.class); this.codetags.put("div", Div.class); this.codetags.put("em", Em.class); this.codetags.put("fieldset", FieldSet.class); this.codetags.put("footer", Footer.class); this.codetags.put("form", Form.class); this.codetags.put("h1", H1.class); this.codetags.put("h2", H2.class); this.codetags.put("h3", H3.class); this.codetags.put("h4", H4.class); this.codetags.put("h5", H5.class); this.codetags.put("h6", H6.class); this.codetags.put("head", Head.class); this.codetags.put("header", Header.class); this.codetags.put("html", Html.class); this.codetags.put("hr", Hr.class); this.codetags.put("i", I.class); this.codetags.put("iframe", IFrame.class); this.codetags.put("img", Img.class); this.codetags.put("input", Input.class); this.codetags.put("label", Label.class); this.codetags.put("legend", Legend.class); this.codetags.put("li", Li.class); this.codetags.put("link", Link.class); this.codetags.put("meta", Meta.class); this.codetags.put("nbsp", NbSp.class); this.codetags.put("nav", Nav.class); this.codetags.put("ol", Ol.class); this.codetags.put("optgroup", OptGroup.class); this.codetags.put("option", Option.class); this.codetags.put("p", P.class); this.codetags.put("pre", Pre.class); this.codetags.put("q", Q.class); this.codetags.put("section", Section.class); this.codetags.put("select", Select.class); this.codetags.put("script", Script.class); this.codetags.put("s", S.class); this.codetags.put("strong", B.class); this.codetags.put("style", Style.class); this.codetags.put("sub", Sub.class); this.codetags.put("sup", Sup.class); this.codetags.put("span", Span.class); this.codetags.put("table", Table.class); this.codetags.put("tbody", TBody.class); this.codetags.put("td", Td.class); this.codetags.put("th", Th.class); this.codetags.put("thead", THead.class); this.codetags.put("tr", Tr.class); this.codetags.put("textarea", TextArea.class); this.codetags.put("title", Title.class); this.codetags.put("u", U.class); this.codetags.put("ul", Ul.class); this.codetags.put("wbr", Wbr.class); // ============================================================== // above this point are std HTML tags, below are our enhanced tags // ============================================================== this.codetags.put("dcui", Document.class); this.codetags.put("dcem", divconq.mail.Document.class); this.codetags.put("Image", CaptionedImage.class); this.codetags.put("CaptionedImage", CaptionedImage.class); this.codetags.put("AdvText", AdvText.class); this.codetags.put("Button", ButtonLink.class); this.codetags.put("WideButton", ButtonLink.class); this.codetags.put("SubmitButton", FormButton.class); this.codetags.put("Form", AdvForm.class); this.codetags.put("Html5Head", Html5AppHead.class); this.codetags.put("IncludePart", IncludePart.class); //this.codetags.put("IncludeHolder", IncludeHolder.class); this.codetags.put("IncludeParam", IncludeParam.class); this.codetags.put("Link", HyperLink.class); this.codetags.put("LiteralText", LiteralText.class); this.codetags.put("MD", AdvText.class); this.codetags.put("Style", Style.class); this.codetags.put("Script", Script.class); this.codetags.put("PagePart", PagePart.class); this.codetags.put("TextPart", TextPart.class); //this.codetags.put("ServerScript", ServerScript.class); this.codetags.put("TitledSection", TitledSection.class); // TODO these should eventually be migrated so they can be shown in html mode too // though they wouldn't work correctly, it would just be for show (unless we do a lot more) this.codetags.put("FieldContainer", AdvElement.class); this.codetags.put("TextInput", AdvElement.class); this.codetags.put("PasswordInput", AdvElement.class); this.codetags.put("YesNo", AdvElement.class); this.codetags.put("HorizRadioGroup", AdvElement.class); this.codetags.put("RadioGroup", AdvElement.class); this.codetags.put("CheckGroup", AdvElement.class); this.codetags.put("RadioButton", AdvElement.class); this.codetags.put("RadioCheck", AdvElement.class); this.codetags.put("RadioSelect", AdvElement.class); this.codetags.put("Range", AdvElement.class); this.codetags.put("Select", AdvElement.class); this.codetags.put("TextArea", AdvElement.class); this.codetags.put("HiddenInput", AdvElement.class); this.codetags.put("FormInstruction", FormInstruction.class); } }