diff --git a/src/CmsSiteConfigToLetsEncryptConfigConverter2.java b/src/CmsSiteConfigToLetsEncryptConfigConverter2.java new file mode 100644 index 0000000000..d881eaddbf --- /dev/null +++ b/src/CmsSiteConfigToLetsEncryptConfigConverter2.java @@ -0,0 +1,521 @@ +/* +* This library is part of OpenCms - + * the Open Source Content Management System + * + * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) + * + * This library 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. + * + * This library 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. + * + * For further information about Alkacon Software, please see the + * company website: http://www.alkacon.com + * + * For further information about OpenCms, please see the + * project website: http://www.opencms.org + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.opencms.letsencrypt; + +import org.opencms.json.JSONArray; +import org.opencms.json.JSONObject; +import org.opencms.letsencrypt.CmsLetsEncryptConfiguration.Mode; +import org.opencms.main.CmsLog; +import org.opencms.report.I_CmsReport; +import org.opencms.site.CmsSSLMode; +import org.opencms.site.CmsSite; +import org.opencms.site.CmsSiteManagerImpl; +import org.opencms.site.CmsSiteMatcher; +import org.opencms.util.CmsStringUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.logging.Log; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList; +import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixListFactory; + +/** + * Class which converts the OpenCms site configuration to a certificate configuration for the LetsEncrypt docker instance. + */ +public class CmsSiteConfigToLetsEncryptConfigConverter { + + /** + * Represents a grouping of domains into certificates. + */ + public static class DomainGrouping { + + /** The list of domain sets. */ + private List> m_domainGroups = Lists.newArrayList(); + + /** + * Adds a domain group.

+ * + * @param domains the domain group + */ + public void addDomainSet(Set domains) { + + if (!domains.isEmpty()) { + m_domainGroups.add(domains); + } + } + + /** + * Generates the JSON configuration corresponding to the domain grouping.

+ * + * @return the JSON configuration corresponding to the domain grouping + */ + public String generateCertJson() { + + try { + JSONObject result = new JSONObject(); + for (Set domainGroup : m_domainGroups) { + String key = computeName(domainGroup); + if (key != null) { + result.put(key, new JSONArray(domainGroup)); + } + } + return result.toString(); + } catch (Exception e) { + LOG.error(e.getLocalizedMessage(), e); + return null; + } + } + + /** + * Checks all domains for resolvability and return the unresolvable ones. + * + * @return the set of unresolvable domains + */ + public Set getUnresolvableDomains() { + + Set result = Sets.newHashSet(); + for (Set domainGroup : m_domainGroups) { + for (String domain : domainGroup) { + try { + InetAddress.getByName(domain); + } catch (UnknownHostException e) { + result.add(domain); + } catch (SecurityException e) { + LOG.error(e.getLocalizedMessage(), e); + } + } + } + return result; + } + + /** + * Checks if the domain grouping does not contain any domain groups. + * + * @return true if there are no domain groups + */ + public boolean isEmpty() { + + return m_domainGroups.isEmpty(); + } + + /** + * Deterministically generates a certificate name for a set of domains.

+ * + * @param domains the domains + * @return the certificate name + */ + private String computeName(Set domains) { + + try { + List domainList = Lists.newArrayList(domains); + Collections.sort(domainList); + String prefix = domainList.get(0); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + for (String domain : domainList) { + md5.update(domain.getBytes("UTF-8")); + md5.update((byte)10); + } + + return prefix + "-" + new String(Hex.encodeHex(md5.digest())); + } catch (Exception e) { + LOG.error(e.getLocalizedMessage(), e); + return null; + } + + } + } + + /** + * Represents the domain information for a single site.

+ */ + public static class SiteDomainInfo { + + /** The common root domain, or null if there is no common root domain. */ + private String m_commonRootDomain; + + /** The set of domains for the site. */ + private Set m_domains = Sets.newHashSet(); + + /** True if an invalid port was used. */ + private boolean m_invalidPort; + + /** + * Creates a new instance.

+ * + * @param domains the set of domains + * @param commonRootDomain the common root domain + * @param invalidPort true if an invalid port was used + */ + public SiteDomainInfo(Set domains, String commonRootDomain, boolean invalidPort) { + super(); + m_domains = domains; + m_commonRootDomain = commonRootDomain; + m_invalidPort = invalidPort; + } + + /** + * Gets the common root domain.

+ * + * @return the common root domain + */ + public String getCommonRootDomain() { + + return m_commonRootDomain; + } + + /** + * Gets the set of domains.

+ * + * @return the set of domains + */ + public Set getDomains() { + + return m_domains; + } + + /** + * True if an invalid port was used.

+ * + * @return true if an invalid port was used + */ + public boolean hasInvalidPort() { + + return m_invalidPort; + } + } + + /** + * Timed cache for the public suffix list.

+ */ + static class SuffixListCache { + + /** The public suffix list. */ + private PublicSuffixList m_suffixList; + + /** The time the list was last cached. */ + private long m_timestamp = -1; + + /** + * Gets the public suffix list, loading it if hasn't been loaded before or the time since it was loaded was too long ago.

+ * + * @return the public suffix list + */ + public synchronized PublicSuffixList getPublicSuffixList() { + + long now = System.currentTimeMillis(); + if ((m_suffixList == null) || ((now - m_timestamp) > (1000 * 3600))) { + PublicSuffixListFactory factory = new PublicSuffixListFactory(); + try (InputStream stream = CmsSiteConfigToLetsEncryptConfigConverter.class.getResourceAsStream( + "public_suffix_list.dat")) { + m_suffixList = factory.build(stream); + m_timestamp = now; + } catch (IOException e) { + LOG.error(e.getLocalizedMessage(), e); + } + } + return m_suffixList; + + } + } + + /** The logger used for this class. */ + static final Log LOG = CmsLog.getLog(CmsSiteConfigToLetsEncryptConfigConverter.class); + + /** Disables grouping. */ + public static final boolean GROUPING_DISABLED = true; + + /** Lock to prevent two converters from running simultaneously. */ + private static Object LOCK = new Object(); + + /** The cache for the public suffix list. */ + private static SuffixListCache SUFFIX_LIST_CACHE = new SuffixListCache(); + + /** The configuration. */ + private CmsLetsEncryptConfiguration m_config; + + /** The object to which the configuration is sent after it is generated. */ + private I_CmsLetsEncryptUpdater m_configUpdater; + + /** + * Creates a new instance.

+ * + * @param config the LetsEncrypt configuration + */ + public CmsSiteConfigToLetsEncryptConfigConverter(CmsLetsEncryptConfiguration config) { + m_config = config; + m_configUpdater = new CmsLetsEncryptUpdater(config); + } + + /** + * Computes the domain information for a single site.

+ * + * @param site the site + * @return the domain information for a site + */ + private static SiteDomainInfo getDomainInfo(CmsSite site) { + + List urls = Lists.newArrayList(); + for (CmsSiteMatcher matcher : site.getAllMatchers()) { + urls.add(matcher.getUrl()); + + } + return getDomainInfo(urls); + } + + /** + * Computes the SiteDomainInfo bean for a collection of URIs.

+ * + * @param uris a collection of URIs + * @return the SiteDomainInfo bean for the URIs + */ + private static SiteDomainInfo getDomainInfo(Collection uris) { + + Set rootDomains = Sets.newHashSet(); + Set domains = Sets.newHashSet(); + boolean invalidPort = false; + + for (String uriStr : uris) { + + try { + URI uri = new URI(uriStr); + int port = uri.getPort(); + if (!((port == 80) || (port == 443) || (port == -1))) { + invalidPort = true; + } + String rootDomain = getDomainRoot(uri); + if (rootDomain == null) { + LOG.warn("Host is not under public suffix, skipping it: " + uri); + continue; + } + domains.add(uri.getHost()); + rootDomains.add(rootDomain); + } catch (URISyntaxException e) { + LOG.warn("getDomainInfo: invalid URI " + uriStr, e); + continue; + } + } + String rootDomain = (rootDomains.size() == 1 ? rootDomains.iterator().next() : null); + return new SiteDomainInfo(domains, rootDomain, invalidPort); + } + + /** + * Calculates the domain root for a given uri.

+ * + * @param uri an URI + * @return the domain root for the uri + */ + private static String getDomainRoot(URI uri) { + + String host = uri.getHost(); + return SUFFIX_LIST_CACHE.getPublicSuffixList().getRegistrableDomain(host); + } + + /** + * Gets the domains for a collection of SiteDomainInfo beans.

+ * + * @param infos a collection of SiteDomainInfo beans + * @return the domains for the beans + */ + private static Set getDomains(Collection infos) { + + Set domains = Sets.newHashSet(); + for (SiteDomainInfo info : infos) { + for (String domain : info.getDomains()) { + domains.add(domain); + } + } + return domains; + } + + /** + * Runs the certificate configuration update for the sites configured in a site manager.

+ * + * @param report the report to write to + * @param siteManager the site manager instance + * + * @return true if the Letsencrypt update was successful + */ + public boolean run(I_CmsReport report, CmsSiteManagerImpl siteManager) { + + synchronized (LOCK) { + // *not* using getAvailable sites here, because the result does not include sites with unpublished site folders if called with a CmsObject in the Online project + // Instead we use getSites() and avoid duplicates using an IdentityHashMap + IdentityHashMap siteIdMap = new IdentityHashMap(); + for (CmsSite site : siteManager.getSites().values()) { + if (site.getSSLMode() == CmsSSLMode.LETS_ENCRYPT) { + siteIdMap.put(site, site); + } + } + List sites = Lists.newArrayList(siteIdMap.values()); + List workplaceServers = siteManager.getWorkplaceServers(CmsSSLMode.LETS_ENCRYPT); + return run(report, sites, workplaceServers); + } + } + + /** + * Computes the domain grouping for a set of sites and workplace URLs.

+ * + * @param sites the sites + * @param workplaceUrls the workplace URLS + * @return the domain grouping + */ + private DomainGrouping computeDomainGrouping(Collection sites, Collection workplaceUrls) { + + DomainGrouping result = new DomainGrouping(); + if (LOG.isInfoEnabled()) { + LOG.info("Computing domain grouping for sites..."); + List servers = Lists.newArrayList(); + for (CmsSite site : sites) { + servers.add(site.getUrl()); + } + LOG.info("SITES = " + CmsStringUtil.listAsString(servers, ", ")); + } + + Mode mode = m_config.getMode(); + boolean addWp = false; + boolean addSites = false; + if ((mode == Mode.all) || (mode == Mode.sites)) { + addSites = true; + } + if ((mode == Mode.all) || (mode == Mode.workplace)) { + addWp = true; + } + + if (addWp) { + Set workplaceDomains = Sets.newHashSet(); + for (String wpServer : workplaceUrls) { + try { + URI uri = new URI(wpServer); + workplaceDomains.add(uri.getHost()); + } catch (Exception e) { + LOG.error(e.getLocalizedMessage(), e); + } + } + result.addDomainSet(workplaceDomains); + } + + if (addSites) { + Multimap infosByRootDomain = ArrayListMultimap.create(); + + List ungroupedSites = Lists.newArrayList(); + for (CmsSite site : sites) { + SiteDomainInfo info = getDomainInfo(site); + if (info.hasInvalidPort()) { + LOG.warn("Invalid port occuring in site definition: " + site); + continue; + } + String root = info.getCommonRootDomain(); + if ((root == null) || GROUPING_DISABLED) { + ungroupedSites.add(info); + } else { + infosByRootDomain.put(root, info); + } + } + List keysToRemove = Lists.newArrayList(); + + for (String key : infosByRootDomain.keySet()) { + Collection siteInfos = infosByRootDomain.get(key); + Set domains = getDomains(siteInfos); + if (domains.size() > 100) { + LOG.info("Too many domains for root domain " + key + ", splitting them up by site instead."); + keysToRemove.add(key); + for (SiteDomainInfo info : siteInfos) { + ungroupedSites.add(info); + } + } + } + for (String key : keysToRemove) { + infosByRootDomain.removeAll(key); + } + for (SiteDomainInfo ungroupedSite : ungroupedSites) { + Set domains = getDomains(Collections.singletonList(ungroupedSite)); + result.addDomainSet(domains); + LOG.info("DOMAINS (site config): " + domains); + } + for (String key : infosByRootDomain.keySet()) { + Set domains = getDomains(infosByRootDomain.get(key)); + result.addDomainSet(domains); + LOG.info("DOMAINS (" + key + ")" + domains); + } + } + return result; + } + + /** + * Runs the certificate configuration update for a given set of sites and workplace URLS.

+ * + * @param report the report to write to + * @param sites the sites + * @param workplaceUrls the workplace URLS + * + * @return true if the Letsencrypt update was successful + */ + private boolean run(I_CmsReport report, Collection sites, Collection workplaceUrls) { + + try { + DomainGrouping domainGrouping = computeDomainGrouping(sites, workplaceUrls); + if (domainGrouping.isEmpty()) { + report.println( + org.opencms.ui.apps.Messages.get().container( + org.opencms.ui.apps.Messages.RPT_LETSENCRYPT_NO_DOMAINS_0)); + return false; + } + String certConfig = domainGrouping.generateCertJson(); + if (!m_configUpdater.update(certConfig)) { + report.println( + org.opencms.ui.apps.Messages.get().container( + org.opencms.ui.apps.Messages.RPT_LETSENCRYPT_UPDATE_FAILED_0), + I_CmsReport.FORMAT_WARNING); + return false; + } + return true; + } catch (Exception e) { + report.println(e); + return false; + } + } + +} diff --git a/src/CmsUsersCsvDownloadDialog.java b/src/CmsUsersCsvDownloadDialog.java new file mode 100644 index 0000000000..d421318257 --- /dev/null +++ b/src/CmsUsersCsvDownloadDialog.java @@ -0,0 +1,297 @@ +/* + * This library is part of OpenCms - + * the Open Source Content Management System + * + * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) + * + * This library 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. + * + * This library 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. + * + * For further information about Alkacon Software GmbH & Co. KG, please see the + * company website: http://www.alkacon.com + * + * For further information about OpenCms, please see the + * project website: http://www.opencms.org + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.opencms.workplace.tools.accounts; + +import org.opencms.db.CmsUserExportSettings; +import org.opencms.file.CmsUser; +import org.opencms.flex.CmsFlexController; +import org.opencms.jsp.CmsJspActionElement; +import org.opencms.main.CmsException; +import org.opencms.main.CmsRuntimeException; +import org.opencms.main.OpenCms; +import org.opencms.security.CmsRole; +import org.opencms.security.I_CmsPrincipal; +import org.opencms.ui.apps.user.CmsImportExportUserDialog; +import org.opencms.util.CmsRequestUtil; +import org.opencms.util.CmsStringUtil; +import org.opencms.util.CmsUUID; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.PageContext; + +/** + * Generates a CSV file for a given list.

+ * + * @since 6.0.0 + */ +public class CmsUsersCsvDownloadDialog extends A_CmsUserDataImexportDialog { + + /** + * Public constructor.

+ * + * @param jsp an initialized JSP action element + */ + public CmsUsersCsvDownloadDialog(CmsJspActionElement jsp) { + + super(jsp); + } + + /** + * Public constructor with JSP variables.

+ * + * @param context the JSP page context + * @param req the JSP request + * @param res the JSP response + */ + public CmsUsersCsvDownloadDialog(PageContext context, HttpServletRequest req, HttpServletResponse res) { + + this(new CmsJspActionElement(context, req, res)); + } + + /** + * @see org.opencms.workplace.tools.accounts.A_CmsUserDataImexportDialog#actionCommit() + */ + @Override + public void actionCommit() { + + // empty + } + + /** + * @see org.opencms.workplace.CmsWidgetDialog#dialogButtonsCustom() + */ + @Override + public String dialogButtonsCustom() { + + return dialogButtons(new int[] {BUTTON_CLOSE}, new String[1]); + } + + /** + * Generates the CSV file for the given users.

+ * + * @return CSV file + */ + public String generateCsv() { + + Map> objects = getData(); + + // get the data object from session + List groups = objects.get("groups"); + List roles = objects.get("roles"); + + Map exportUsers = new HashMap(); + try { + if (((groups == null) || (groups.size() < 1)) && ((roles == null) || (roles.size() < 1))) { + exportUsers = CmsImportExportUserDialog.addExportAllUsers(getCms(), getParamOufqn(), exportUsers); + } else { + exportUsers = CmsImportExportUserDialog.addExportUsersFromGroups(getCms(), groups, exportUsers); + exportUsers = CmsImportExportUserDialog.addExportUsersFromRoles( + getCms(), + getParamOufqn(), + roles, + exportUsers); + } + } catch (CmsException e) { + throw new CmsRuntimeException(Messages.get().container(Messages.ERR_GET_EXPORT_USERS_0), e); + } + + StringBuffer buffer = new StringBuffer(); + CmsUserExportSettings settings = OpenCms.getImportExportManager().getUserExportSettings(); + + String separator = CmsStringUtil.substitute(settings.getSeparator(), "\\t", "\t"); + List values = settings.getColumns(); + + buffer.append("name"); + Iterator itValues = values.iterator(); + while (itValues.hasNext()) { + buffer.append(separator); + buffer.append(itValues.next()); + } + buffer.append("\n"); + + Object[] users = exportUsers.values().toArray(); + + for (int i = 0; i < users.length; i++) { + CmsUser exportUser = (CmsUser)users[i]; + if (!exportUser.getOuFqn().equals(getParamOufqn())) { + // skip users of others ous + continue; + } + if (!isExportable(exportUser)) { + continue; + } + buffer.append(exportUser.getSimpleName()); + itValues = values.iterator(); + while (itValues.hasNext()) { + buffer.append(separator); + String curValue = itValues.next(); + try { + Method method = CmsUser.class.getMethod( + "get" + curValue.substring(0, 1).toUpperCase() + curValue.substring(1)); + String curOutput = (String)method.invoke(exportUser); + if (CmsStringUtil.isEmptyOrWhitespaceOnly(curOutput) || curOutput.equals("null")) { + curOutput = (String)exportUser.getAdditionalInfo(curValue); + } + + if (curValue.equals("password")) { + curOutput = OpenCms.getPasswordHandler().getDigestType() + "_" + curOutput; + } + + if (!CmsStringUtil.isEmptyOrWhitespaceOnly(curOutput) && !curOutput.equals("null")) { + buffer.append(curOutput); + } + } catch (NoSuchMethodException e) { + Object obj = exportUser.getAdditionalInfo(curValue); + if (obj != null) { + String curOutput = String.valueOf(obj); + if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(curOutput)) { + buffer.append(curOutput); + } + } + } catch (IllegalAccessException e) { + throw new CmsRuntimeException(Messages.get().container(Messages.ERR_ILLEGAL_ACCESS_0), e); + } catch (InvocationTargetException e) { + throw new CmsRuntimeException(Messages.get().container(Messages.ERR_INVOCATION_TARGET_0), e); + } + } + buffer.append("\n"); + } + HttpServletResponse res = CmsFlexController.getController(getJsp().getRequest()).getTopResponse(); + res.setContentType("text/comma-separated-values"); + String filename = "export_users" + new Random().nextInt(1024) + ".csv"; + res.setHeader( + "Content-Disposition", + new StringBuffer("attachment; filename=\"").append(filename).append("\"").toString()); + return buffer.toString(); + } + + /** + * Creates the dialog HTML for all defined widgets of the named dialog (page).

+ * + * This overwrites the method from the super class to create a layout variation for the widgets.

+ * + * @param dialog the dialog (page) to get the HTML for + * @return the dialog HTML for all defined widgets of the named dialog (page) + */ + @Override + protected String createDialogHtml(String dialog) { + + StringBuffer result = new StringBuffer(1024); + + result.append(createWidgetTableStart()); + // show error header once if there were validation errors + result.append(createWidgetErrorHeader()); + + if (dialog.equals(PAGES[0])) { + // create the widgets for the first dialog page + result.append("\n"); + result.append(dialogBlockStart(key(Messages.GUI_USERDATA_EXPORT_LABEL_HINT_BLOCK_0))); + result.append(key(Messages.GUI_USERDATA_DOWNLOAD_LABEL_HINT_TEXT_0)); + result.append(" "); + result.append(key(Messages.GUI_USERDATA_DOWNLOAD_LABEL_HINT_CLICK_0)); + result.append("."); + result.append(dialogBlockEnd()); + } + + result.append(createWidgetTableEnd()); + return result.toString(); + } + + /** + * @see org.opencms.workplace.tools.accounts.A_CmsUserDataImexportDialog#defineWidgets() + */ + @Override + protected void defineWidgets() { + + // empty + } + + /** + * Returns the export options data.

+ * + * @return the export options data + */ + protected Map> getData() { + + return (Map>)((Map)getSettings().getDialogObject()).get( + CmsUserDataExportDialog.class.getName()); + } + + /** + * Returns the download path.

+ * + * @return the download path + */ + protected String getDownloadPath() { + + return "/system/workplace/admin/accounts/imexport_user_data/csvdownload.jsp"; + } + + /** + * Checks if the user can be exported.

+ * + * @param exportUser the suer to check + * + * @return true if the user can be exported + */ + protected boolean isExportable(CmsUser exportUser) { + + return exportUser.getFlags() < I_CmsPrincipal.FLAG_CORE_LIMIT; + } + + /** + * @see org.opencms.workplace.CmsWidgetDialog#validateParamaters() + */ + @Override + protected void validateParamaters() throws Exception { + + if (getParamOufqn() == null) { + setParamOufqn(""); + } + OpenCms.getRoleManager().checkRole(getCms(), CmsRole.ACCOUNT_MANAGER.forOrgUnit(getParamOufqn())); + } +}