<commit_before>/*
 * CollabNet Subversion Edge
 * Copyright (C) 2010, CollabNet Inc. All rights reserved.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.collabnet.svnedge.admin

import org.apache.commons.io.FileUtils
import org.codehaus.groovy.grails.commons.ConfigurationHolder


import grails.util.GrailsUtil
import groovy.io.FileType

import java.net.URL
import java.util.Calendar
import com.collabnet.svnedge.admin.LogManagementService
import com.collabnet.svnedge.domain.AdvancedConfiguration
import com.collabnet.svnedge.domain.LogConfiguration
import com.collabnet.svnedge.domain.Server 
import com.collabnet.svnedge.domain.ServerMode 
import com.collabnet.svnedge.domain.integration.CtfServer 
import com.collabnet.svnedge.domain.integration.ReplicaConfiguration 
import com.collabnet.svnedge.util.ConfigUtil
import com.collabnet.svnedge.domain.integration.ReplicatedRepository
import sun.misc.BASE64Encoder
import com.collabnet.svnedge.util.SSLUtil
import com.collabnet.svnedge.domain.NetworkConfiguration
import org.ini4j.Ini

class ServerConfService {

    boolean transactional = true
    def commandLineService
    def securityService
    def networkingService
    def csvnAuthenticationProvider
    def grailsApplication
    
    private static final String DONT_EDIT = """#
#
# DO NOT EDIT THIS FILE IT WILL BE REGENERATED AUTOMATICALLY BY SUBVERSION EDGE
#
# If you must make a change to the contents of this file then copy and paste the
# content into the httpd.conf file and comment out the Include statement for
# this file. The httpd.conf file is not modified or generated and is safe for
# you to modify.
#
#
"""

private static final String DONT_EDIT_VIEWVC = """#
#
# DO NOT EDIT THIS FILE IT WILL BE REGENERATED AUTOMATICALLY BY SUBVERSION EDGE
#
# If you must make a change to the contents of this file then the changes need
# to be made to the viewvc.conf.dist file.  Please note that though it is 
# unlikely, it is possible that future updates will overwrite your changes.
# So they may need to be applied again after an upgrade.
#
#
"""

    private static final String WIN_CTF_COMMENT = """#
#
# DO NOT EDIT THIS FILE. Provided for CollabNet TeamForge for Windows use only.
#
"""


    private static final String DEFAULT_SVN_ACCESS = """#
#
# DEFAULT ACCESS PERMISSIONS GRANTING ALL USERS FULL CONTROL OF ALL REPOS
#
[/]
* = rw
"""

    private static final String CTF_AUTHORIZER = """
[authz-teamforge]
"""
    public static final String DEFAULT_SSL_EXPLOIT_DIRECTIVES = """
# Protect against BEAST exploit
SSLProtocol ALL -SSLv2
SSLHonorCipherOrder on
SSLCipherSuite RC4-SHA:HIGH
"""
    
    def bootstrap = { server ->

        log.debug("Bootstrapping serverConfService")
        log.debug("Setting up data artifacts directory...")
        def appHome = ConfigUtil.appHome()
        bootstrapDataDirectory(appHome)
        bootstrapConfiguration(server)
        
        conditionalWriteHttpdConf()
        deployPythonBindings(appHome)

        try {
            this.writeConfigFiles()
        } catch (Exception e) {
            log.error("Unable to write initial set of configuration " +
                "files", e)
        }
    }
    
    /**
     * Service method for replica mode to sync this server configuration with the master
     */
    private String syncReplicaMasterVersion() {    
        
        Server server = Server.getServer()
        if (server.mode != ServerMode.REPLICA) {
            log.debug("Not a replica, no reason to sync configuration with master svn")
            return ''
        }
        def rrList = ReplicatedRepository.list()
        ReplicatedRepository rr = (rrList) ? rrList.get(0) : null
        if (!rr) {
            log.info("No replicated repositories, SVNMasterVersion cannot be " +
                     "determined until a repository is added.")
            return null
        }
        
        def repoName = rr.repo.name
        ReplicaConfiguration replicaConfig = ReplicaConfiguration.currentConfig
        CtfServer ctfServer = CtfServer.getServer()

        String username = ctfServer.ctfUsername
        String password = securityService.decrypt(ctfServer.ctfPassword)
        String repoUrl = replicaConfig.svnMasterUrl
        if (!repoUrl.endsWith("/")) {
            repoUrl += "/"
        }
        repoUrl += repoName

        return svnServerVersion(repoUrl, username, password)
    }
    
    /**
     * helper to evaluate an svn instance url for support of httpv2
     * @param repoUrl
     * @param uname
     * @param password
     * @return boolean true if verified, false if unknown or verified no support
     */
    def String svnServerVersion(String repoUrl, String uname, String password) {

        URL url = new URL(repoUrl);
        int port = url.port
        if (port == -1) {
            port = (url.protocol == "https") ? 443 : 80
        }
        String credentials = "${uname}:${password}"
        byte[] credentialsBytes = credentials.getBytes("UTF8");
        String encodedCredentials = new BASE64Encoder().encode(credentialsBytes)

        String body = """
OPTIONS ${url.path} HTTP/1.1
Host: ${url.host}
Content-Type: text/xml
Authorization: Basic ${encodedCredentials}
DAV: http://subversion.tigris.org/xmlns/dav/svn/depth
DAV: http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV: http://subversion.tigris.org/xmlns/dav/svn/log-revprops
Content-Length: 107

<?xml version="1.0" encoding="utf-8"?>
<D:options xmlns:D="DAV:">
<D:activity-collection-set/>
</D:options>
"""
        log.debug "Opening connection to ${url.host} ${port}"
        String returnVal = '1.6'
        try {
            Socket s
            if (url.protocol == "https" ) {
                s = SSLUtil.createTrustingSocket(url.host, port);
            }
            else {
                s = new Socket(url.host, port);
            }
            s.withStreams { input, output ->
                output << body
                def reader = input.newReader()
                def line

                while ((line = reader.readLine()) != null) {
                    if (Float.valueOf(returnVal) < 1.7 && line.contains("SVN-Me-Resource")) {
                        returnVal = '1.7'
                    } 
                    else if (Float.valueOf(returnVal) < 1.8 &&
                             line.contains('SVN-Supported-Posts: create-txn-with-props')) {
                        returnVal = '1.8'
                    }
                }
            }
        }
        catch (Exception e) {
            log.warn("Unable to connect to repo", e)
        }
        return returnVal + '.0'
    }

    private String confDirPath() {
        return ConfigUtil.confDirPath()
    }
    
    private void bootstrapConfiguration(server ) {
        // remove unnecessary scm artifact
        switch(GrailsUtil.environment) {
            case "test":
                log.debug("Deleting older httpd.conf file")
                new File(confDirPath(), "httpd.conf").delete()

                File scmProps = new File(server.repoParentDir,".scm.properties")
                if (scmProps.exists() && scmProps.delete()) {
                    log.debug("Removed previous .scm.properties file")
                }
                break
        }
    }

    /**
     * Bootstraps the temp-data director to data moving the data directory as 
     * discussed on artf62798
     * @param config is the configuration properties
     */
    def bootstrapDataDirectory(appHome) {
        def tempDataDir = new File(appHome, "temp-data")
        if (tempDataDir.exists()) {
            log.info("Bootstrapping the temporary data directory...")
            //The data exists, it's a possible update. Copy each
            //existing ones, maintaining the version .new-1, .new-2, ...
            boolean recur = true
            def allTmpFiles = FileUtils.listFiles(tempDataDir, null, recur)
            for (fileOnTemp in allTmpFiles) {
                //only returns file names, no dirs and subdirs
                def fileOnDataPath = fileOnTemp.canonicalPath.replace(
                        "temp-data", "data")
                def fileOnDataDir = new File(fileOnDataPath)
                if (!fileOnDataDir.exists()) {
                    def parentDir = fileOnDataDir.getParentFile()
                    //create all dirs and subdirs for the file.
                    parentDir.mkdirs()
                    fileOnTemp.renameTo(fileOnDataDir)
                } else {
                    //skip if it's a directory, but rename the file.
                    if (!fileOnTemp.isDirectory()) {
                        def bpkFileOnData = new File(fileOnDataDir.canonicalPath
                            + ".bkp")
                        if (!bpkFileOnData.exists()) {
                            fileOnDataDir.renameTo(bpkFileOnData)
                            log.info("Backing up \"${fileOnDataDir}\" as " + 
                                "\"${bpkFileOnData}\".")
                        } else {
                            int numberOfNewFile = 1
                            //gets the name of the next "new" artifact n
                            def nextBkpFile = null;
                            while((nextBkpFile = new File(
                                fileOnDataDir.canonicalPath + ".bkp" + 
                                (++numberOfNewFile))).exists())
                            fileOnDataDir.renameTo(nextBkpFile)
                            log.info("Backing up \"${fileOnDataDir}\" as " + 
                                "\"${nextBkpFile}\"")
                        }
                        // move the file from the temp dir and the data dir.
                        fileOnTemp.renameTo(fileOnDataDir)
                        log.info("Updating the file \"${fileOnDataDir}\"")
                    }
                }
            }
            //delete the temp-data's empty dirs and subdirs
            FileUtils.deleteDirectory(tempDataDir)
        }
    }

    private boolean isWindows() {
        return null != ConfigUtil.serviceName()
    }
    
    def getHttpdUser() {
        return commandLineService.getPathOwner(Server.getServer().repoParentDir)    
    }
    
    def getHttpdGroup() {
        return commandLineService.getPathGroup(Server.getServer().repoParentDir)
    }
    
    private def deployPythonBindings(appHome) {
        if (isWindows()) {
            return
        }

        //Removing existing 'svn-python' directory/link and recreating as symlink.
        String libDir = new File(appHome, "lib").absolutePath
        File pythonBindingDir = new File(libDir, "svn-python")
        if (pythonBindingDir.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                pythonBindingDir.absolutePath)
        }
        File actualPythonBindingDir = new File(libDir,
            "svn-python${getPythonVersion()}")
        commandLineService.executeWithOutput("ln", "-sf", 
            actualPythonBindingDir.absolutePath, pythonBindingDir.absolutePath)

        //Recreateing 'libsvn_swig_py-1' real library name as symlink.
        File swigPythonLibPath = new File(libDir, "libsvn_swig_py-1.so.0.0.0")
        if (swigPythonLibPath.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath.absolutePath)
        }
        File actualSwigPythonLibPath = new File(libDir, 
            "libsvn_swig_py-1.so.0.0.0${getPythonVersion()}")
        commandLineService.executeWithOutput("ln", "-sf",
            actualSwigPythonLibPath.absolutePath, swigPythonLibPath.absolutePath)

        //Recreating 'libsvn_swig_py-1' soname as symlink.
        File swigPythonLibPath1 = new File(libDir, "libsvn_swig_py-1.so.0")
        if (swigPythonLibPath1.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath1.absolutePath)
        }
        commandLineService.executeWithOutput("ln", "-sf", 
            swigPythonLibPath.absolutePath, swigPythonLibPath1.absolutePath)

        //Recreateing 'libsvn_swig_py-1' linker name as symlink.
        File swigPythonLibPath2 = new File(libDir, "libsvn_swig_py-1.so")
        if (swigPythonLibPath2.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath2.absolutePath)
        }
        commandLineService.executeWithOutput("ln", "-sf", 
            swigPythonLibPath.absolutePath, swigPythonLibPath2.absolutePath)
    }
    
    private def conditionalWriteHttpdConf() {
        def confDir = new File(confDirPath())
        File dest = new File(confDir, "httpd.conf")
        if (!dest.exists()) {
            writeHttpdConf(dest)
        } else {
            def conf = dest.text
            if (conf.contains('mod_authn_alias.so') || 
                conf.contains('mod_authn_default.so') || 
                conf.contains('mod_authz_default.so')) {
                backupAndOverwriteHttpdConfFor2Dot2()
            }
        }
    }
    
    private def writeHttpdConf(destConfFile) {
        def distFile = new File(ConfigUtil.distDir(), "httpd.conf.dist")
        if (!distFile.exists()) {
            distFile = new File(confDirPath(), "httpd.conf.dist")
        }
        String s = distFile.getText()
        s = s.replace("__CSVN_HOME__", ConfigUtil.appHome())
        destConfFile.write(s)
    }
    
    def writeConfigFiles() {
        def confDirPath = confDirPath()
        Server server = getServer()
        conditionalWriteHttpdConf()
        writeModulesConf(server)
        writeMainConf(server)
        writeLogConf()
        writeSvnViewvcConf(server)
        writeDefaultDirectoriesConf()
        writeMiscellaneousConf()
        writeSvnClientConf()
        File teamforgePropsTemplate =
            new File(confDirPath, "teamforge.properties.dist")
        if (teamforgePropsTemplate.exists()) {
            writeTeamforgeConf(server, teamforgePropsTemplate)
        }

        String s = new File(confDirPath, "viewvc.conf.dist").getText()
        s = s.replace("__CSVN_REPO_ROOT__", server.repoParentDir)
        s = s.replace("__CSVN_CONF__", confDirPath)
        s = s.replace("__CSVN_VIEWVC_TEMPLATES__", 
                      getViewvcTemplateDir(server))
        s = s.replace("__CSVN_SVN_CLIENT__", ConfigUtil.svnPath())
        s = s.replace("__SERVER_ADMIN__", server.adminEmail)
        s = s.replace("__CSVN_TWITTER_BOOTSTRAP_VERSION__", 
                grailsApplication.metadata['vendor.twitter-bootstrap.version'])
        s = s.replace("__CSVN_JQUERY_JS_FILENAME__", 
                'jquery-' + grailsApplication.metadata['vendor.jquery.version'] +
                '.min.js')
        s = s.replace("__CSVN_SVNEDGE_CSS_FILENAME__", 
                'svnedge-' + grailsApplication.metadata['app.svnedgeCss.version'] +
                '.css')
        s = s.replace("__CSVN_APPLICATION_JS_FILENAME__", 
                'application-' + grailsApplication.metadata['app.applicationJs.version'] +
                '.js')
        s = s.replace("__CSVN_SVN_BASE_URL__", server.svnURL())
        if (server.forceUsernameCase) {
            s = s.replace("#force_username_case = ", "force_username_case = lower")
        }

        String serverMode = server.mode.toString()
        String appServerUrl, docroot, authorizer, isRootInUrl

        // CTF default ViewVC "allowed_views"
        String allowedViews = "annotate, co, diff, markup"
        if (server.managedByCtf()) {
            appServerUrl = CtfServer.getServer().baseUrl
            docroot = server.urlPrefix() + "/viewvc-static"
            authorizer = "teamforge"
            isRootInUrl = "0"
            s += CTF_AUTHORIZER
        } else {
            appServerUrl = ''
            docroot = "/viewvc-static"
            authorizer = "svnauthz"
            isRootInUrl = "1"
            // if listing repositories, add "roots" view in ViewVC
            if (server.advancedConfig().listParentPath) {
                allowedViews += ", roots"
            }
        }

        s = s.replace("__CSVN_SERVERMODE__", serverMode)
        s = s.replace("__CSVN_APP_SERVER_ROOT_URL__", appServerUrl)
        s = s.replace("__CSVN_VIEWVC_DOCROOT__", docroot)
        s = s.replace("__CSVN_VIEWVC_AUTHORIZER__", authorizer)
        s = s.replace("__CSVN_ROOT_IN_URL__", isRootInUrl)
        s = s.replace("__CSVN_ALLOWED_VIEWS__", allowedViews)

        if (isWindows()) {
            s = s.replace("__DIFF__", "diff.exe")
        } else {
            s = s.replace("__DIFF__", "/usr/bin/diff")
        }            
        new File(confDirPath, "viewvc.conf").write(DONT_EDIT_VIEWVC + s)
        writeWindowsShebang()
    }

    def writeWindowsShebang() {
        if (isWindows()) {
            String linuxShebang = "#!/usr/bin/env python"
            String winShebang = "#!${ConfigUtil.appHome()}\\Python25\\python.exe"
            File cgiBinDir = new File(ConfigUtil.binDirPath(), 'cgi-bin')
            cgiBinDir.eachFile(FileType.FILES) { f ->
                if (f.name.endsWith('.cgi')) {
                    String s = f.text
                    String winContent = s.replace(linuxShebang, winShebang)
                    if (s != winContent) {
                        f.text = winContent
                    }
                }
            }
        }
    }

    /**
     * service method to fetch the text of the svn_acccess_file (path-based permissions)
     */
    String readSvnAccessFile() {

        File f = new File(confDirPath(), "svn_access_file")
        if (f.exists()) {
            return f.getText("UTF-8")
        }
        else {
            return DEFAULT_SVN_ACCESS
        }

    }

    /**
     * service method to write the input text to the svn_acccess_file (path-based permissions)
     */
    boolean writeSvnAccessFile(String content) {

        File f = new File(confDirPath(), "svn_access_file")
        if (!f.exists() || f.canWrite()) {
            f.setText(content.replace("\r\n", "\n").trim(), "UTF-8")
            return true
        }
        else {
            return false
        }
    }

    /**
     * service method to validate input text to the svn_acccess_file (path-based permissions)
     */
    String[] validateSvnAccessFile(String content) {

        File f = File.createTempFile("svn_access_file", ".temp")
        f.deleteOnExit()

        if (f.canWrite()) {
            f.setText(content.replace("\r\n", "\n").trim(), "UTF-8")
        }

        def output = commandLineService.execute(
                ConfigUtil.svnauthzPath(), 'validate', f.absolutePath)

        f.delete()

        return (output)
    }

    private def writeMainConf(server) {
        def hostname, port
        if (server) {
            hostname = server.hostname
            port = String.valueOf(server.port)
        } else {
            def config = ConfigurationHolder.config
            hostname = networkingService.hostname
            port = String.valueOf(config.svnedge.defaultHighPort)
        }        
        def serverName = "${hostname}:${port}"
        def serverPort = "${port}"
        
        def ldapConfSnippet = getLdapHttpdConf(server)
        def fileAuthConfSnippet = (server.managedByCtf()) ? 
            "" : getFileAuthHttpdConf(server)
        def sslSnippet = getSSLHttpdConf(server)
        def conf = """${DONT_EDIT}
ServerAdmin "${server.adminEmail}"
ServerName "${serverName}"
Listen ${serverPort}
${getAuthHelperListen(server)}
${getDefaultHttpdSnippets()}
${ldapConfSnippet}
${fileAuthConfSnippet}
${sslSnippet}
"""
        new File(confDirPath(), "csvn_main_httpd.conf").write(conf)
    }

    private static final String MODULES_CONF_FILENAME = 
            "csvn_modules_httpd.conf"
    
    private static final String DIRS_CONF_FILENAME = 
            "csvn_default_dirs_httpd.conf"
    
    private static final String MISC_CONF_FILENAME = 
            "csvn_misc_httpd.conf"
    
    private def writeModulesConf(server) {
        def conf = """${DONT_EDIT}
LoadModule dav_module lib/modules/mod_dav.so
LoadModule dav_fs_module lib/modules/mod_dav_fs.so
LoadModule dav_svn_module     lib/modules/mod_dav_svn.so
LoadModule authz_core_module lib/modules/mod_authz_core.so
LoadModule authz_host_module lib/modules/mod_authz_host.so
LoadModule authz_groupfile_module lib/modules/mod_authz_groupfile.so
LoadModule authz_user_module lib/modules/mod_authz_user.so
LoadModule authz_svn_module   lib/modules/mod_authz_svn.so
LoadModule authn_core_module lib/modules/mod_authn_core.so
LoadModule authn_file_module lib/modules/mod_authn_file.so
LoadModule auth_basic_module lib/modules/mod_auth_basic.so
LoadModule alias_module lib/modules/mod_alias.so
LoadModule env_module lib/modules/mod_env.so
LoadModule setenvif_module lib/modules/mod_setenvif.so
LoadModule log_config_module lib/modules/mod_log_config.so
LoadModule cgi_module lib/modules/mod_cgi.so
LoadModule dir_module lib/modules/mod_dir.so
LoadModule mime_module lib/modules/mod_mime.so
LoadModule deflate_module lib/modules/mod_deflate.so
"""
		if (!isWindows()) {
			conf += "LoadModule unixd_module lib/modules/mod_unixd.so\n"
		}
        
        if (server.useSsl) {
            conf += "LoadModule socache_shmcb_module lib/modules/mod_socache_shmcb.so\n"
        }

        if (server.mode == ServerMode.REPLICA) {
            conf += """LoadModule proxy_module lib/modules/mod_proxy.so
LoadModule proxy_http_module lib/modules/mod_proxy_http.so
"""
        }

        boolean ctfMode = server.managedByCtf() ||
            server.convertingToManagedByCtf()
        if (ctfMode) {
            conf += 
"""LoadModule authnz_ctf_module lib/modules/mod_authnz_ctf.so
"""
        }
        if (isLdapLoginEnabled(server) || ctfMode) {
            conf += """
# Required for SCRIPT_URI/URL in viewvc libs, not just rewrite rules
LoadModule rewrite_module lib/modules/mod_rewrite.so
"""
        }

        new File(confDirPath(), MODULES_CONF_FILENAME).write(conf)
    }
    
    private writeDefaultDirectoriesConf() {
        def conf = """${DONT_EDIT}
DocumentRoot "www"
DirectoryIndex index.html

<Directory />
  Options FollowSymLinks
  AllowOverride None
  Require all denied
</Directory>

<Directory "www">
  Options Indexes FollowSymLinks
  AllowOverride None
  Require all granted
</Directory>

#
# The following lines prevent .htaccess and .htpasswd files from being 
# viewed by Web clients. 
#
<FilesMatch "^\\.ht">
    Require all denied
</FilesMatch>
"""
        new File(confDirPath(), DIRS_CONF_FILENAME).write(conf)
    }

    private writeMiscellaneousConf() {
        def conf = """${DONT_EDIT}
LimitXMLRequestBody 0
ServerSignature  Off
ServerTokens  Prod
TraceEnable Off
TypesConfig "data/conf/mime.types"
"""
        new File(confDirPath(), MISC_CONF_FILENAME).write(conf)
    }
    
    def writeLogConf() {
        AdvancedConfiguration advConfig = AdvancedConfiguration.getConfig()
        def binDirPath = ConfigUtil.binDirPath()
        def rotatelogs = new File(binDirPath, 'rotatelogs').absolutePath
                .replace('\\', '/')
        def escapeQuote = '\\"'
        if (isWindows()) {
            rotatelogs += '.exe'
        }
        def pipeRotateLogs = "|${escapeQuote}${rotatelogs}${escapeQuote} -l"
        def rotatePeriod = '86400'
        def logsDirPath = ConfigUtil.logsDirPath().replace('\\', '/')
        def logNameSuffix = "%Y_%m_%d_%H_%M_%S"
        LogConfiguration logConfig = LogConfiguration.getConfig()
        def logLevel = logConfig?.apacheLogLevel ?: LogManagementService.ApacheLogLevel.WARN 
        int maxLogSize = logConfig.maxLogSize
        if (maxLogSize > 0) {
            rotatePeriod += ' ' + maxLogSize + LogManagementService.LOG_SIZE_SUFFIX
        }
        def conf = """${DONT_EDIT}
LogLevel ${logLevel.toString().toLowerCase()}
ErrorLog "${pipeRotateLogs} ${escapeQuote}${logsDirPath}/error_${logNameSuffix}.log${escapeQuote} ${rotatePeriod}"
"""
        if (logConfig.enableAccessLog) {
            if (logConfig.minimizeLogging) {
                conf += """# Don't log GET/PROPFIND /!svn/ requests
SetEnvIf Request_URI "^" in_repos=0
SetEnvIf Request_URI "/!svn/" in_repos=1
SetEnvIf Request_Method "GET" do_not_log
SetEnvIf Request_Method "PROPFIND" do_not_log
SetEnvIf in_repos 0 !do_not_log
"""
            }
            conf += """LogFormat "${advConfig.accessLogFormat}" common
CustomLog "${pipeRotateLogs} ${escapeQuote}${logsDirPath}/access_${logNameSuffix}.log${escapeQuote} ${rotatePeriod}" common env=!do_not_log
"""
        }
        if (logConfig.enableSubversionLog) {
            conf += "CustomLog \"${pipeRotateLogs} " + 
                    "${escapeQuote}${logsDirPath}/subversion_${logNameSuffix}.log${escapeQuote} " +
                    "${rotatePeriod}\" \"${advConfig.svnLogFormat}\" env=SVN-ACTION\n"
        }
        new File(confDirPath(), "csvn_logging.conf").write(conf)
    }

    private def createMissingCtfWinConf() {
        File ctfHttpdConf = new File(confDirPath(), "ctf_httpd.conf")
        if (!ctfHttpdConf.exists()) {
            ctfHttpdConf.text = WIN_CTF_COMMENT
        }
    }

    private def writeSvnViewvcConf(server) {
        boolean ctfMode = server.managedByCtf() ||
            server.convertingToManagedByCtf()
        createMissingCtfWinConf()
        def confDirPath = confDirPath()
        def conf = """${DONT_EDIT}
Include "${confDirPath}/ctf_httpd.conf"
"""
        AdvancedConfiguration advConfig = AdvancedConfiguration.getConfig()
        if (advConfig.compressionLevel != 5) {
            conf += "SVNCompressionLevel ${advConfig.compressionLevel}\n"
        }
        if (advConfig.useUtf8) {
            conf += 'SVNUseUTF8 On\n'
        }
        if (advConfig.inMemoryCacheSize != 16) {
            conf += 'SVNInMemoryCacheSize ' + (advConfig.inMemoryCacheSize * 1024) + '\n'
        }

        boolean isLdapLoginEnabled = isLdapLoginEnabled(server)
        if (isLdapLoginEnabled) {
            conf += "<VirtualHost *:${server.port}>\n"
        }

        conf += server.useSsl ? "SSLEngine On\n" : "# SSL is off\n"
        if (isLdapLoginEnabled || ctfMode) {
            conf += """
# Required for SCRIPT_URI/URL in viewvc libs, not just rewrite rules
RewriteEngine on
"""
            if (isLdapLoginEnabled) {
                conf += "RewriteOptions inherit\n"
            }
        }

        conf += """
# Apache will issue sub_req for PATH_INFO on ScriptAlias which
# gives a spurious error message. Setting auth at root level to avoid clogging logs.
<Location />
"""
        conf += ctfMode ? getCtfBasicAuth(server) : getViewVCHttpdConf(server)
        conf += "</Location>\n"
                    
        String contextPath = server.getSvnBasePath()
        if (server.mode == ServerMode.REPLICA) {
            def replicaConfig = ReplicaConfiguration.getCurrentConfig()
            if (server.useSsl) {
                conf += "SSLProxyEngine on\n"
            }
            if (replicaConfig.svnMasterUrl) {
                // proxy to the svn master might be needed
                def netCfg = networkingService.getNetworkConfiguration()
                if (netCfg?.httpProxyHost) {
                    conf += "ProxyRemote ${replicaConfig.svnMasterUrl}"
                    conf += " http://${netCfg.httpProxyHost}:${netCfg.httpProxyPort ?: 80}\n"
                }
                contextPath = replicaConfig.contextPath()
            }
            conf += "<Location " + contextPath + ">"
        } else {
            if (advConfig.listParentPath) {
                conf += """
# Work around authz and SVNListParentPath issue
RedirectMatch ^(${contextPath})\$ \$1/
"""
            }
            conf += "<Location ${contextPath}/>"
        }
        conf += """   
   DAV svn
   SVNParentPath "${escapePath(server.repoParentDir)}"
   SVNReposName "${advConfig.svnRealm}"
"""
        if (advConfig.compressionLevel > 0) {
            conf += '  SetOutputFilter DEFLATE\n'
        }
        if (!advConfig.allowBulkUpdates) {
            conf += '  SVNAllowBulkUpdates Off\n'
        } else if (advConfig.preferBulkUpdates) {
            conf += '  SVNAllowBulkUpdates Prefer\n'
        }
        
        if (advConfig.autoVersioning) {
            conf += """  SVNAutoversioning On
  ModMimeUsePathInfo On
"""
        }
        if (advConfig.hooksEnv) {
            conf += "  SVNHooksEnv ${advConfig.hooksEnv}\n"
            
        }
        if (advConfig.inMemoryCacheSize > 0) {
            if (advConfig.cacheFullTexts) {
                conf += '  SVNCacheFullTexts On\n'
            }
            if (advConfig.cacheTextDeltas) {
                conf += '  SVNCacheTextDeltas On\n'
            }
            if (advConfig.cacheRevProps) {
                conf += '  SVNCacheRevProps On\n'
            }
        }

        if (ctfMode) {
            conf += """   ${getAuthnzCTFDirective()}
   SVNPathAuthz short_circuit
"""
            conf += getSvnMasterDirectiveIfReplica(server)
        } else {
            conf += getSVNHttpdConf(server, advConfig)
        }  
        conf += "</Location>\n\n"

        def viewvcTemplateDirPath = getViewvcTemplateDir(server)
        conf += """
<Directory "${viewvcTemplateDirPath}/docroot">
  AllowOverride None
  Options None
</Directory>
<Location /viewvc-static>
  Require all granted
</Location>
Alias /viewvc-static "${viewvcTemplateDirPath}/docroot"
"""

        conf += ctfMode ? """
RewriteRule ^/viewcvs(.*)\$ /viewvc\$1 [R,L]
ScriptAlias /viewvc "${ConfigUtil.binDirPath()}/cgi-bin/ctf_viewvc.cgi"
""" : """
ScriptAlias /viewvc "${ConfigUtil.binDirPath()}/cgi-bin/viewvc.cgi"
"""
        conf += """
<Location /viewvc>
  AddDefaultCharset UTF-8
  SetEnv CSVN_HOME "${ConfigUtil.appHome()}"
"""
        if (ctfMode) {
            conf += getCtfViewVCHttpdConf(server)
        }
        conf += "</Location>\n"
        
        if (isLdapLoginEnabled) {
            conf += """
</VirtualHost>

#
# auth helper endpoint for use by SvnEdge
#        
<VirtualHost localhost:${csvnAuthenticationProvider.getAuthHelperPort(server, false)}>
  <Location "/">
${getAuthBasic(server)}
  Require valid-user
  </Location>
</VirtualHost>
"""
        }
        new File(confDirPath, "svn_viewvc_httpd.conf").write(conf)
    }
    
    /**
     * returns the path to the appropriate viewvc template dir based
     * on server mode
     * @param server the Server instance
     * @return absolute path to the appropriate template dir
     */
    private def getViewvcTemplateDir(server) {
        return ConfigUtil.viewvcTemplateDir() 
    }

    private def getAuthHelperListen(server) {
        def conf = ""
        if (isLdapLoginEnabled(server)) {
            conf += "Listen ${csvnAuthenticationProvider.getAuthHelperPort(server, true)}"
        }
        conf
    }
    
    private boolean isLdapLoginEnabled(Server server) {
        return server.mode == ServerMode.STANDALONE && server.ldapEnabled && server.ldapEnabledConsole
    }

    private def getDefaultHttpdSnippets() {
        def conf = ""
        if (isWindows() == false) {
            Server server = Server.getServer()
            def repoParentPath = server.repoParentDir
            conf = """User ${getHttpdUser()}
Group ${getHttpdGroup()}
"""
        }
        conf += """PidFile "${ConfigUtil.dataDirPath()}/run/httpd.pid"
MaxKeepAliveRequests 10000
"""
    }    

    private def getLdapURL(server) {
        def ldapUrl = server.ldapURL()
        if (server.ldapAuthBasedn) {
            ldapUrl += server.ldapAuthBasedn
        }
        if (server.ldapLoginAttribute) {
            ldapUrl += "?${server.ldapLoginAttribute}"
        } else {
            ldapUrl += "?uid"
        }
        if (server.ldapSearchScope) {
            ldapUrl += "?${server.ldapSearchScope}"
        } else {
            ldapUrl += "?sub"
        }
        if (server.ldapFilter) {
            ldapUrl += "?${server.ldapFilter}"
        }
        ldapUrl
    }

    private def getLdapHttpdConf(server) {
        def conf = ""
        if (server.ldapEnabled) {
            def ldapURL = getLdapURL(server)
            conf = """LoadModule ldap_module lib/modules/mod_ldap.so
LoadModule authnz_ldap_module lib/modules/mod_authnz_ldap.so
<AuthnProviderAlias ldap ldap-users>
  AuthLDAPUrl "${ldapURL}" "${server.ldapSecurityLevel}"
"""

            if (server.ldapAuthBinddn && server.ldapAuthBindPassword) {
                conf = """${conf}
  AuthLDAPBindDN "${server.ldapAuthBinddn}"
  AuthLDAPBindPassword "${server.ldapAuthBindPassword}"
"""
            }
            conf = """${conf}
</AuthnProviderAlias>
"""
            if (!server.ldapServerCertVerificationNeeded) {
            conf = """${conf}
LDAPVerifyServerCert Off
"""
            }
            
            AdvancedConfiguration advConfig = server.advancedConfig()
            if (advConfig.ldapConnectionPoolTtl > 0) {
                conf += "LDAPConnectionPoolTTL ${advConfig.ldapConnectionPoolTtl}\n"
            }
            if (advConfig.ldapTimeout > 0) {
                conf += "LDAPTimeout ${advConfig.ldapTimeout}\n"
            }
        }
        conf
    }

    private def getFileAuthHttpdConf(server) {
        def conf = ""
        if (server.fileLoginEnabled) {
            conf = """
<AuthnProviderAlias file csvn-file-users>
  AuthUserFile "${confDirPath()}/svn_auth_file"
</AuthnProviderAlias>
"""
        }
        conf
    }

    private def getPythonVersion() {
        String version;
        if (isWindows()) {
            version = ""
        } else {
            version = commandLineService
                .executeWithOutput("python", "-c", "import platform; " + 
                "print str(platform.python_version_tuple()[0]) + '.' + " +
                "str(platform.python_version_tuple()[1])")
            if (version.length()) {
                version = "." + version
                /*Remove the trailing new line */
                version = version.trim()
            }
        }
        version
    }

    def getSvnMasterDirectiveIfReplica(server) {
        def conf = ""
        if (server.mode == ServerMode.REPLICA) {
            def replicaConfig = ReplicaConfiguration.getCurrentConfig()
            def masterSVNURI = replicaConfig.svnMasterUrl
            conf += "   SVNMasterURI ${masterSVNURI}\n"
            def masterVersion = ConfigUtil.replicaSvnMasterVersion()
            if (masterVersion) {
                conf += """   # SVNMasterVersion value is defined in <conf>/overrides.properties
   SVNMasterVersion ${masterVersion}
"""
            } else {
                masterVersion = syncReplicaMasterVersion()
                if (masterVersion) {
                    conf += "   SVNMasterVersion ${masterVersion}\n"
                } else {
                    conf += "   # SVNMasterVersion unknown; Cannot check version until repository is added\n"
                }
            }
        }
        return conf
    }

    def getCtfBasicAuth(server) {
        return """  AuthType Basic
  AuthName "TeamForge Authorization Realm"
  AuthBasicAuthoritative Off
  AuthUserFile /dev/null
  Require valid-user
"""
    }

    def getAuthnzCTFDirective() {
        return """AuthnzCTFPropertiesFile "${escapePath(new File(confDirPath(), "teamforge.properties").absolutePath)}"
"""
    }
    
    private def getSSLHttpdConf(server) {
        def conf = ""
        def confDir = confDirPath()
        if (server.useSsl) {
            conf = """
LoadModule ssl_module lib/modules/mod_ssl.so
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLCertificateFile    "${confDir}/server.crt"
SSLCertificateKeyFile "${confDir}/server.key"
SSLSessionCache       "shmcb:${ConfigUtil.dataDirPath()}/run/ssl_scache(512000)"
"""
            File f = new File(confDir, "server.ca")
            def chainFilePath = f.absolutePath
            if (f.exists()) {
                conf += """SSLCertificateChainFile "${chainFilePath}"
"""
            }
            File extraConfig = new File(confDir, "ssl_httpd.conf")
            if (extraConfig.exists()) {
                if (extraConfig.length() > 0) {
                    conf += """Include "${confDir}/ssl_httpd.conf"<commit_msg>[artf7720] Enable mod_deflate in configuration<commit_after>/*
 * CollabNet Subversion Edge
 * Copyright (C) 2010, CollabNet Inc. All rights reserved.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.collabnet.svnedge.admin

import org.apache.commons.io.FileUtils
import org.codehaus.groovy.grails.commons.ConfigurationHolder


import grails.util.GrailsUtil
import groovy.io.FileType

import java.net.URL
import java.util.Calendar
import com.collabnet.svnedge.admin.LogManagementService
import com.collabnet.svnedge.domain.AdvancedConfiguration
import com.collabnet.svnedge.domain.LogConfiguration
import com.collabnet.svnedge.domain.Server 
import com.collabnet.svnedge.domain.ServerMode 
import com.collabnet.svnedge.domain.integration.CtfServer 
import com.collabnet.svnedge.domain.integration.ReplicaConfiguration 
import com.collabnet.svnedge.util.ConfigUtil
import com.collabnet.svnedge.domain.integration.ReplicatedRepository
import sun.misc.BASE64Encoder
import com.collabnet.svnedge.util.SSLUtil
import com.collabnet.svnedge.domain.NetworkConfiguration
import org.ini4j.Ini

class ServerConfService {

    boolean transactional = true
    def commandLineService
    def securityService
    def networkingService
    def csvnAuthenticationProvider
    def grailsApplication
    
    private static final String DONT_EDIT = """#
#
# DO NOT EDIT THIS FILE IT WILL BE REGENERATED AUTOMATICALLY BY SUBVERSION EDGE
#
# If you must make a change to the contents of this file then copy and paste the
# content into the httpd.conf file and comment out the Include statement for
# this file. The httpd.conf file is not modified or generated and is safe for
# you to modify.
#
#
"""

private static final String DONT_EDIT_VIEWVC = """#
#
# DO NOT EDIT THIS FILE IT WILL BE REGENERATED AUTOMATICALLY BY SUBVERSION EDGE
#
# If you must make a change to the contents of this file then the changes need
# to be made to the viewvc.conf.dist file.  Please note that though it is 
# unlikely, it is possible that future updates will overwrite your changes.
# So they may need to be applied again after an upgrade.
#
#
"""

    private static final String WIN_CTF_COMMENT = """#
#
# DO NOT EDIT THIS FILE. Provided for CollabNet TeamForge for Windows use only.
#
"""


    private static final String DEFAULT_SVN_ACCESS = """#
#
# DEFAULT ACCESS PERMISSIONS GRANTING ALL USERS FULL CONTROL OF ALL REPOS
#
[/]
* = rw
"""

    private static final String CTF_AUTHORIZER = """
[authz-teamforge]
"""
    public static final String DEFAULT_SSL_EXPLOIT_DIRECTIVES = """
# Protect against BEAST exploit
SSLProtocol ALL -SSLv2
SSLHonorCipherOrder on
SSLCipherSuite RC4-SHA:HIGH
"""
    
    def bootstrap = { server ->

        log.debug("Bootstrapping serverConfService")
        log.debug("Setting up data artifacts directory...")
        def appHome = ConfigUtil.appHome()
        bootstrapDataDirectory(appHome)
        bootstrapConfiguration(server)
        
        conditionalWriteHttpdConf()
        deployPythonBindings(appHome)

        try {
            this.writeConfigFiles()
        } catch (Exception e) {
            log.error("Unable to write initial set of configuration " +
                "files", e)
        }
    }
    
    /**
     * Service method for replica mode to sync this server configuration with the master
     */
    private String syncReplicaMasterVersion() {    
        
        Server server = Server.getServer()
        if (server.mode != ServerMode.REPLICA) {
            log.debug("Not a replica, no reason to sync configuration with master svn")
            return ''
        }
        def rrList = ReplicatedRepository.list()
        ReplicatedRepository rr = (rrList) ? rrList.get(0) : null
        if (!rr) {
            log.info("No replicated repositories, SVNMasterVersion cannot be " +
                     "determined until a repository is added.")
            return null
        }
        
        def repoName = rr.repo.name
        ReplicaConfiguration replicaConfig = ReplicaConfiguration.currentConfig
        CtfServer ctfServer = CtfServer.getServer()

        String username = ctfServer.ctfUsername
        String password = securityService.decrypt(ctfServer.ctfPassword)
        String repoUrl = replicaConfig.svnMasterUrl
        if (!repoUrl.endsWith("/")) {
            repoUrl += "/"
        }
        repoUrl += repoName

        return svnServerVersion(repoUrl, username, password)
    }
    
    /**
     * helper to evaluate an svn instance url for support of httpv2
     * @param repoUrl
     * @param uname
     * @param password
     * @return boolean true if verified, false if unknown or verified no support
     */
    def String svnServerVersion(String repoUrl, String uname, String password) {

        URL url = new URL(repoUrl);
        int port = url.port
        if (port == -1) {
            port = (url.protocol == "https") ? 443 : 80
        }
        String credentials = "${uname}:${password}"
        byte[] credentialsBytes = credentials.getBytes("UTF8");
        String encodedCredentials = new BASE64Encoder().encode(credentialsBytes)

        String body = """
OPTIONS ${url.path} HTTP/1.1
Host: ${url.host}
Content-Type: text/xml
Authorization: Basic ${encodedCredentials}
DAV: http://subversion.tigris.org/xmlns/dav/svn/depth
DAV: http://subversion.tigris.org/xmlns/dav/svn/mergeinfo
DAV: http://subversion.tigris.org/xmlns/dav/svn/log-revprops
Content-Length: 107

<?xml version="1.0" encoding="utf-8"?>
<D:options xmlns:D="DAV:">
<D:activity-collection-set/>
</D:options>
"""
        log.debug "Opening connection to ${url.host} ${port}"
        String returnVal = '1.6'
        try {
            Socket s
            if (url.protocol == "https" ) {
                s = SSLUtil.createTrustingSocket(url.host, port);
            }
            else {
                s = new Socket(url.host, port);
            }
            s.withStreams { input, output ->
                output << body
                def reader = input.newReader()
                def line

                while ((line = reader.readLine()) != null) {
                    if (Float.valueOf(returnVal) < 1.7 && line.contains("SVN-Me-Resource")) {
                        returnVal = '1.7'
                    } 
                    else if (Float.valueOf(returnVal) < 1.8 &&
                             line.contains('SVN-Supported-Posts: create-txn-with-props')) {
                        returnVal = '1.8'
                    }
                }
            }
        }
        catch (Exception e) {
            log.warn("Unable to connect to repo", e)
        }
        return returnVal + '.0'
    }

    private String confDirPath() {
        return ConfigUtil.confDirPath()
    }
    
    private void bootstrapConfiguration(server ) {
        // remove unnecessary scm artifact
        switch(GrailsUtil.environment) {
            case "test":
                log.debug("Deleting older httpd.conf file")
                new File(confDirPath(), "httpd.conf").delete()

                File scmProps = new File(server.repoParentDir,".scm.properties")
                if (scmProps.exists() && scmProps.delete()) {
                    log.debug("Removed previous .scm.properties file")
                }
                break
        }
    }

    /**
     * Bootstraps the temp-data director to data moving the data directory as 
     * discussed on artf62798
     * @param config is the configuration properties
     */
    def bootstrapDataDirectory(appHome) {
        def tempDataDir = new File(appHome, "temp-data")
        if (tempDataDir.exists()) {
            log.info("Bootstrapping the temporary data directory...")
            //The data exists, it's a possible update. Copy each
            //existing ones, maintaining the version .new-1, .new-2, ...
            boolean recur = true
            def allTmpFiles = FileUtils.listFiles(tempDataDir, null, recur)
            for (fileOnTemp in allTmpFiles) {
                //only returns file names, no dirs and subdirs
                def fileOnDataPath = fileOnTemp.canonicalPath.replace(
                        "temp-data", "data")
                def fileOnDataDir = new File(fileOnDataPath)
                if (!fileOnDataDir.exists()) {
                    def parentDir = fileOnDataDir.getParentFile()
                    //create all dirs and subdirs for the file.
                    parentDir.mkdirs()
                    fileOnTemp.renameTo(fileOnDataDir)
                } else {
                    //skip if it's a directory, but rename the file.
                    if (!fileOnTemp.isDirectory()) {
                        def bpkFileOnData = new File(fileOnDataDir.canonicalPath
                            + ".bkp")
                        if (!bpkFileOnData.exists()) {
                            fileOnDataDir.renameTo(bpkFileOnData)
                            log.info("Backing up \"${fileOnDataDir}\" as " + 
                                "\"${bpkFileOnData}\".")
                        } else {
                            int numberOfNewFile = 1
                            //gets the name of the next "new" artifact n
                            def nextBkpFile = null;
                            while((nextBkpFile = new File(
                                fileOnDataDir.canonicalPath + ".bkp" + 
                                (++numberOfNewFile))).exists())
                            fileOnDataDir.renameTo(nextBkpFile)
                            log.info("Backing up \"${fileOnDataDir}\" as " + 
                                "\"${nextBkpFile}\"")
                        }
                        // move the file from the temp dir and the data dir.
                        fileOnTemp.renameTo(fileOnDataDir)
                        log.info("Updating the file \"${fileOnDataDir}\"")
                    }
                }
            }
            //delete the temp-data's empty dirs and subdirs
            FileUtils.deleteDirectory(tempDataDir)
        }
    }

    private boolean isWindows() {
        return null != ConfigUtil.serviceName()
    }
    
    def getHttpdUser() {
        return commandLineService.getPathOwner(Server.getServer().repoParentDir)    
    }
    
    def getHttpdGroup() {
        return commandLineService.getPathGroup(Server.getServer().repoParentDir)
    }
    
    private def deployPythonBindings(appHome) {
        if (isWindows()) {
            return
        }

        //Removing existing 'svn-python' directory/link and recreating as symlink.
        String libDir = new File(appHome, "lib").absolutePath
        File pythonBindingDir = new File(libDir, "svn-python")
        if (pythonBindingDir.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                pythonBindingDir.absolutePath)
        }
        File actualPythonBindingDir = new File(libDir,
            "svn-python${getPythonVersion()}")
        commandLineService.executeWithOutput("ln", "-sf", 
            actualPythonBindingDir.absolutePath, pythonBindingDir.absolutePath)

        //Recreateing 'libsvn_swig_py-1' real library name as symlink.
        File swigPythonLibPath = new File(libDir, "libsvn_swig_py-1.so.0.0.0")
        if (swigPythonLibPath.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath.absolutePath)
        }
        File actualSwigPythonLibPath = new File(libDir, 
            "libsvn_swig_py-1.so.0.0.0${getPythonVersion()}")
        commandLineService.executeWithOutput("ln", "-sf",
            actualSwigPythonLibPath.absolutePath, swigPythonLibPath.absolutePath)

        //Recreating 'libsvn_swig_py-1' soname as symlink.
        File swigPythonLibPath1 = new File(libDir, "libsvn_swig_py-1.so.0")
        if (swigPythonLibPath1.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath1.absolutePath)
        }
        commandLineService.executeWithOutput("ln", "-sf", 
            swigPythonLibPath.absolutePath, swigPythonLibPath1.absolutePath)

        //Recreateing 'libsvn_swig_py-1' linker name as symlink.
        File swigPythonLibPath2 = new File(libDir, "libsvn_swig_py-1.so")
        if (swigPythonLibPath2.exists()) {
            commandLineService.executeWithOutput("rm", "-rf", 
                swigPythonLibPath2.absolutePath)
        }
        commandLineService.executeWithOutput("ln", "-sf", 
            swigPythonLibPath.absolutePath, swigPythonLibPath2.absolutePath)
    }
    
    private def conditionalWriteHttpdConf() {
        def confDir = new File(confDirPath())
        File dest = new File(confDir, "httpd.conf")
        if (!dest.exists()) {
            writeHttpdConf(dest)
        } else {
            def conf = dest.text
            if (conf.contains('mod_authn_alias.so') || 
                conf.contains('mod_authn_default.so') || 
                conf.contains('mod_authz_default.so')) {
                backupAndOverwriteHttpdConfFor2Dot2()
            }
        }
    }
    
    private def writeHttpdConf(destConfFile) {
        def distFile = new File(ConfigUtil.distDir(), "httpd.conf.dist")
        if (!distFile.exists()) {
            distFile = new File(confDirPath(), "httpd.conf.dist")
        }
        String s = distFile.getText()
        s = s.replace("__CSVN_HOME__", ConfigUtil.appHome())
        destConfFile.write(s)
    }
    
    def writeConfigFiles() {
        def confDirPath = confDirPath()
        Server server = getServer()
        conditionalWriteHttpdConf()
        writeModulesConf(server)
        writeMainConf(server)
        writeLogConf()
        writeSvnViewvcConf(server)
        writeDefaultDirectoriesConf()
        writeMiscellaneousConf()
        writeSvnClientConf()
        File teamforgePropsTemplate =
            new File(confDirPath, "teamforge.properties.dist")
        if (teamforgePropsTemplate.exists()) {
            writeTeamforgeConf(server, teamforgePropsTemplate)
        }

        String s = new File(confDirPath, "viewvc.conf.dist").getText()
        s = s.replace("__CSVN_REPO_ROOT__", server.repoParentDir)
        s = s.replace("__CSVN_CONF__", confDirPath)
        s = s.replace("__CSVN_VIEWVC_TEMPLATES__", 
                      getViewvcTemplateDir(server))
        s = s.replace("__CSVN_SVN_CLIENT__", ConfigUtil.svnPath())
        s = s.replace("__SERVER_ADMIN__", server.adminEmail)
        s = s.replace("__CSVN_TWITTER_BOOTSTRAP_VERSION__", 
                grailsApplication.metadata['vendor.twitter-bootstrap.version'])
        s = s.replace("__CSVN_JQUERY_JS_FILENAME__", 
                'jquery-' + grailsApplication.metadata['vendor.jquery.version'] +
                '.min.js')
        s = s.replace("__CSVN_SVNEDGE_CSS_FILENAME__", 
                'svnedge-' + grailsApplication.metadata['app.svnedgeCss.version'] +
                '.css')
        s = s.replace("__CSVN_APPLICATION_JS_FILENAME__", 
                'application-' + grailsApplication.metadata['app.applicationJs.version'] +
                '.js')
        s = s.replace("__CSVN_SVN_BASE_URL__", server.svnURL())
        if (server.forceUsernameCase) {
            s = s.replace("#force_username_case = ", "force_username_case = lower")
        }

        String serverMode = server.mode.toString()
        String appServerUrl, docroot, authorizer, isRootInUrl

        // CTF default ViewVC "allowed_views"
        String allowedViews = "annotate, co, diff, markup"
        if (server.managedByCtf()) {
            appServerUrl = CtfServer.getServer().baseUrl
            docroot = server.urlPrefix() + "/viewvc-static"
            authorizer = "teamforge"
            isRootInUrl = "0"
            s += CTF_AUTHORIZER
        } else {
            appServerUrl = ''
            docroot = "/viewvc-static"
            authorizer = "svnauthz"
            isRootInUrl = "1"
            // if listing repositories, add "roots" view in ViewVC
            if (server.advancedConfig().listParentPath) {
                allowedViews += ", roots"
            }
        }

        s = s.replace("__CSVN_SERVERMODE__", serverMode)
        s = s.replace("__CSVN_APP_SERVER_ROOT_URL__", appServerUrl)
        s = s.replace("__CSVN_VIEWVC_DOCROOT__", docroot)
        s = s.replace("__CSVN_VIEWVC_AUTHORIZER__", authorizer)
        s = s.replace("__CSVN_ROOT_IN_URL__", isRootInUrl)
        s = s.replace("__CSVN_ALLOWED_VIEWS__", allowedViews)

        if (isWindows()) {
            s = s.replace("__DIFF__", "diff.exe")
        } else {
            s = s.replace("__DIFF__", "/usr/bin/diff")
        }            
        new File(confDirPath, "viewvc.conf").write(DONT_EDIT_VIEWVC + s)
        writeWindowsShebang()
    }

    def writeWindowsShebang() {
        if (isWindows()) {
            String linuxShebang = "#!/usr/bin/env python"
            String winShebang = "#!${ConfigUtil.appHome()}\\Python25\\python.exe"
            File cgiBinDir = new File(ConfigUtil.binDirPath(), 'cgi-bin')
            cgiBinDir.eachFile(FileType.FILES) { f ->
                if (f.name.endsWith('.cgi')) {
                    String s = f.text
                    String winContent = s.replace(linuxShebang, winShebang)
                    if (s != winContent) {
                        f.text = winContent
                    }
                }
            }
        }
    }

    /**
     * service method to fetch the text of the svn_acccess_file (path-based permissions)
     */
    String readSvnAccessFile() {

        File f = new File(confDirPath(), "svn_access_file")
        if (f.exists()) {
            return f.getText("UTF-8")
        }
        else {
            return DEFAULT_SVN_ACCESS
        }

    }

    /**
     * service method to write the input text to the svn_acccess_file (path-based permissions)
     */
    boolean writeSvnAccessFile(String content) {

        File f = new File(confDirPath(), "svn_access_file")
        if (!f.exists() || f.canWrite()) {
            f.setText(content.replace("\r\n", "\n").trim(), "UTF-8")
            return true
        }
        else {
            return false
        }
    }

    /**
     * service method to validate input text to the svn_acccess_file (path-based permissions)
     */
    String[] validateSvnAccessFile(String content) {

        File f = File.createTempFile("svn_access_file", ".temp")
        f.deleteOnExit()

        if (f.canWrite()) {
            f.setText(content.replace("\r\n", "\n").trim(), "UTF-8")
        }

        def output = commandLineService.execute(
                ConfigUtil.svnauthzPath(), 'validate', f.absolutePath)

        f.delete()

        return (output)
    }

    private def writeMainConf(server) {
        def hostname, port
        if (server) {
            hostname = server.hostname
            port = String.valueOf(server.port)
        } else {
            def config = ConfigurationHolder.config
            hostname = networkingService.hostname
            port = String.valueOf(config.svnedge.defaultHighPort)
        }        
        def serverName = "${hostname}:${port}"
        def serverPort = "${port}"
        
        def ldapConfSnippet = getLdapHttpdConf(server)
        def fileAuthConfSnippet = (server.managedByCtf()) ? 
            "" : getFileAuthHttpdConf(server)
        def sslSnippet = getSSLHttpdConf(server)
        def conf = """${DONT_EDIT}
ServerAdmin "${server.adminEmail}"
ServerName "${serverName}"
Listen ${serverPort}
${getAuthHelperListen(server)}
${getDefaultHttpdSnippets()}
${ldapConfSnippet}
${fileAuthConfSnippet}
${sslSnippet}
"""
        new File(confDirPath(), "csvn_main_httpd.conf").write(conf)
    }

    private static final String MODULES_CONF_FILENAME = 
            "csvn_modules_httpd.conf"
    
    private static final String DIRS_CONF_FILENAME = 
            "csvn_default_dirs_httpd.conf"
    
    private static final String MISC_CONF_FILENAME = 
            "csvn_misc_httpd.conf"
    
    private def writeModulesConf(server) {
        def conf = """${DONT_EDIT}
LoadModule dav_module lib/modules/mod_dav.so
LoadModule dav_fs_module lib/modules/mod_dav_fs.so
LoadModule dav_svn_module     lib/modules/mod_dav_svn.so
LoadModule authz_core_module lib/modules/mod_authz_core.so
LoadModule authz_host_module lib/modules/mod_authz_host.so
LoadModule authz_groupfile_module lib/modules/mod_authz_groupfile.so
LoadModule authz_user_module lib/modules/mod_authz_user.so
LoadModule authz_svn_module   lib/modules/mod_authz_svn.so
LoadModule authn_core_module lib/modules/mod_authn_core.so
LoadModule authn_file_module lib/modules/mod_authn_file.so
LoadModule auth_basic_module lib/modules/mod_auth_basic.so
LoadModule alias_module lib/modules/mod_alias.so
LoadModule env_module lib/modules/mod_env.so
LoadModule setenvif_module lib/modules/mod_setenvif.so
LoadModule log_config_module lib/modules/mod_log_config.so
LoadModule cgi_module lib/modules/mod_cgi.so
LoadModule dir_module lib/modules/mod_dir.so
LoadModule mime_module lib/modules/mod_mime.so
"""
        File f = new File(ConfigUtil.appHome(), 'lib/modules/mod_deflate.so')
        if (f.exists()) {
            conf += "LoadModule deflate_module lib/modules/mod_deflate.so\n"
        }
		if (!isWindows()) {
			conf += "LoadModule unixd_module lib/modules/mod_unixd.so\n"
		}
        
        if (server.useSsl) {
            conf += """LoadModule ssl_module lib/modules/mod_ssl.so
LoadModule socache_shmcb_module lib/modules/mod_socache_shmcb.so
"""
        }

        if (server.mode == ServerMode.REPLICA) {
            conf += """LoadModule proxy_module lib/modules/mod_proxy.so
LoadModule proxy_http_module lib/modules/mod_proxy_http.so
"""
        }

        boolean ctfMode = server.managedByCtf() ||
            server.convertingToManagedByCtf()
        if (ctfMode) {
            conf += 
"""LoadModule authnz_ctf_module lib/modules/mod_authnz_ctf.so
"""
        }
        if (server.ldapEnabled) {
            conf += """LoadModule ldap_module lib/modules/mod_ldap.so
LoadModule authnz_ldap_module lib/modules/mod_authnz_ldap.so
"""
        }
        if (isLdapLoginEnabled(server) || ctfMode) {
            conf += """
# Required for SCRIPT_URI/URL in viewvc libs, not just rewrite rules
LoadModule rewrite_module lib/modules/mod_rewrite.so
"""
        }

        new File(confDirPath(), MODULES_CONF_FILENAME).write(conf)
    }
    
    private writeDefaultDirectoriesConf() {
        def conf = """${DONT_EDIT}
DocumentRoot "www"
DirectoryIndex index.html

<Directory />
  Options FollowSymLinks
  AllowOverride None
  Require all denied
</Directory>

<Directory "www">
  Options Indexes FollowSymLinks
  AllowOverride None
  Require all granted
</Directory>

#
# The following lines prevent .htaccess and .htpasswd files from being 
# viewed by Web clients. 
#
<FilesMatch "^\\.ht">
    Require all denied
</FilesMatch>
"""
        new File(confDirPath(), DIRS_CONF_FILENAME).write(conf)
    }

    private writeMiscellaneousConf() {
        def conf = """${DONT_EDIT}
LimitXMLRequestBody 0
ServerSignature  Off
ServerTokens  Prod
TraceEnable Off
TypesConfig "data/conf/mime.types"
"""
        new File(confDirPath(), MISC_CONF_FILENAME).write(conf)
    }
    
    def writeLogConf() {
        AdvancedConfiguration advConfig = AdvancedConfiguration.getConfig()
        def binDirPath = ConfigUtil.binDirPath()
        def rotatelogs = new File(binDirPath, 'rotatelogs').absolutePath
                .replace('\\', '/')
        def escapeQuote = '\\"'
        if (isWindows()) {
            rotatelogs += '.exe'
        }
        def pipeRotateLogs = "|${escapeQuote}${rotatelogs}${escapeQuote} -l"
        def rotatePeriod = '86400'
        def logsDirPath = ConfigUtil.logsDirPath().replace('\\', '/')
        def logNameSuffix = "%Y_%m_%d_%H_%M_%S"
        LogConfiguration logConfig = LogConfiguration.getConfig()
        def logLevel = logConfig?.apacheLogLevel ?: LogManagementService.ApacheLogLevel.WARN 
        int maxLogSize = logConfig.maxLogSize
        if (maxLogSize > 0) {
            rotatePeriod += ' ' + maxLogSize + LogManagementService.LOG_SIZE_SUFFIX
        }
        def conf = """${DONT_EDIT}
LogLevel ${logLevel.toString().toLowerCase()}
ErrorLog "${pipeRotateLogs} ${escapeQuote}${logsDirPath}/error_${logNameSuffix}.log${escapeQuote} ${rotatePeriod}"
"""
        if (logConfig.enableAccessLog) {
            if (logConfig.minimizeLogging) {
                conf += """# Don't log GET/PROPFIND /!svn/ requests
SetEnvIf Request_URI "^" in_repos=0
SetEnvIf Request_URI "/!svn/" in_repos=1
SetEnvIf Request_Method "GET" do_not_log
SetEnvIf Request_Method "PROPFIND" do_not_log
SetEnvIf in_repos 0 !do_not_log
"""
            }
            conf += """LogFormat "${advConfig.accessLogFormat}" common
CustomLog "${pipeRotateLogs} ${escapeQuote}${logsDirPath}/access_${logNameSuffix}.log${escapeQuote} ${rotatePeriod}" common env=!do_not_log
"""
        }
        if (logConfig.enableSubversionLog) {
            conf += "CustomLog \"${pipeRotateLogs} " + 
                    "${escapeQuote}${logsDirPath}/subversion_${logNameSuffix}.log${escapeQuote} " +
                    "${rotatePeriod}\" \"${advConfig.svnLogFormat}\" env=SVN-ACTION\n"
        }
        new File(confDirPath(), "csvn_logging.conf").write(conf)
    }

    private def createMissingCtfWinConf() {
        File ctfHttpdConf = new File(confDirPath(), "ctf_httpd.conf")
        if (!ctfHttpdConf.exists()) {
            ctfHttpdConf.text = WIN_CTF_COMMENT
        }
    }

    private def writeSvnViewvcConf(server) {
        boolean ctfMode = server.managedByCtf() ||
            server.convertingToManagedByCtf()
        createMissingCtfWinConf()
        def confDirPath = confDirPath()
        def conf = """${DONT_EDIT}
Include "${confDirPath}/ctf_httpd.conf"
"""
        AdvancedConfiguration advConfig = AdvancedConfiguration.getConfig()
        if (advConfig.compressionLevel != 5) {
            conf += "SVNCompressionLevel ${advConfig.compressionLevel}\n"
        }
        if (advConfig.useUtf8) {
            conf += 'SVNUseUTF8 On\n'
        }
        if (advConfig.inMemoryCacheSize != 16) {
            conf += 'SVNInMemoryCacheSize ' + (advConfig.inMemoryCacheSize * 1024) + '\n'
        }

        boolean isLdapLoginEnabled = isLdapLoginEnabled(server)
        if (isLdapLoginEnabled) {
            conf += "<VirtualHost *:${server.port}>\n"
        }

        conf += server.useSsl ? "SSLEngine On\n" : "# SSL is off\n"
        if (isLdapLoginEnabled || ctfMode) {
            conf += """
# Required for SCRIPT_URI/URL in viewvc libs, not just rewrite rules
RewriteEngine on
"""
            if (isLdapLoginEnabled) {
                conf += "RewriteOptions inherit\n"
            }
        }

        conf += """
# Apache will issue sub_req for PATH_INFO on ScriptAlias which
# gives a spurious error message. Setting auth at root level to avoid clogging logs.
<Location />
"""
        conf += ctfMode ? getCtfBasicAuth(server) : getViewVCHttpdConf(server)
        conf += "</Location>\n"
                    
        String contextPath = server.getSvnBasePath()
        if (server.mode == ServerMode.REPLICA) {
            def replicaConfig = ReplicaConfiguration.getCurrentConfig()
            if (server.useSsl) {
                conf += "SSLProxyEngine on\n"
            }
            if (replicaConfig.svnMasterUrl) {
                // proxy to the svn master might be needed
                def netCfg = networkingService.getNetworkConfiguration()
                if (netCfg?.httpProxyHost) {
                    conf += "ProxyRemote ${replicaConfig.svnMasterUrl}"
                    conf += " http://${netCfg.httpProxyHost}:${netCfg.httpProxyPort ?: 80}\n"
                }
                contextPath = replicaConfig.contextPath()
            }
            conf += "<Location " + contextPath + ">"
        } else {
            if (advConfig.listParentPath) {
                conf += """
# Work around authz and SVNListParentPath issue
RedirectMatch ^(${contextPath})\$ \$1/
"""
            }
            conf += "<Location ${contextPath}/>"
        }
        conf += """   
   DAV svn
   SVNParentPath "${escapePath(server.repoParentDir)}"
   SVNReposName "${advConfig.svnRealm}"
"""
        if (advConfig.compressionLevel > 0) {
            conf += """
  <IfModule deflate_module>
    SetOutputFilter DEFLATE
  </IfModule>
"""
        }
        if (!advConfig.allowBulkUpdates) {
            conf += '  SVNAllowBulkUpdates Off\n'
        } else if (advConfig.preferBulkUpdates) {
            conf += '  SVNAllowBulkUpdates Prefer\n'
        }
        
        if (advConfig.autoVersioning) {
            conf += """  SVNAutoversioning On
  ModMimeUsePathInfo On
"""
        }
        if (advConfig.hooksEnv) {
            conf += "  SVNHooksEnv ${advConfig.hooksEnv}\n"
            
        }
        if (advConfig.inMemoryCacheSize > 0) {
            if (advConfig.cacheFullTexts) {
                conf += '  SVNCacheFullTexts On\n'
            }
            if (advConfig.cacheTextDeltas) {
                conf += '  SVNCacheTextDeltas On\n'
            }
            if (advConfig.cacheRevProps) {
                conf += '  SVNCacheRevProps On\n'
            }
        }

        if (ctfMode) {
            conf += """   ${getAuthnzCTFDirective()}
   SVNPathAuthz short_circuit
"""
            conf += getSvnMasterDirectiveIfReplica(server)
        } else {
            conf += getSVNHttpdConf(server, advConfig)
        }  
        conf += "</Location>\n\n"

        def viewvcTemplateDirPath = getViewvcTemplateDir(server)
        conf += """
<Directory "${viewvcTemplateDirPath}/docroot">
  AllowOverride None
  Options None
</Directory>
<Location /viewvc-static>
  Require all granted
</Location>
Alias /viewvc-static "${viewvcTemplateDirPath}/docroot"
"""

        conf += ctfMode ? """
RewriteRule ^/viewcvs(.*)\$ /viewvc\$1 [R,L]
ScriptAlias /viewvc "${ConfigUtil.binDirPath()}/cgi-bin/ctf_viewvc.cgi"
""" : """
ScriptAlias /viewvc "${ConfigUtil.binDirPath()}/cgi-bin/viewvc.cgi"
"""
        conf += """
<Location /viewvc>
  AddDefaultCharset UTF-8
  SetEnv CSVN_HOME "${ConfigUtil.appHome()}"
"""
        if (ctfMode) {
            conf += getCtfViewVCHttpdConf(server)
        }
        conf += "</Location>\n"
        
        if (isLdapLoginEnabled) {
            conf += """
</VirtualHost>

#
# auth helper endpoint for use by SvnEdge
#        
<VirtualHost localhost:${csvnAuthenticationProvider.getAuthHelperPort(server, false)}>
  <Location "/">
${getAuthBasic(server)}
  Require valid-user
  </Location>
</VirtualHost>
"""
        }
        new File(confDirPath, "svn_viewvc_httpd.conf").write(conf)
    }
    
    /**
     * returns the path to the appropriate viewvc template dir based
     * on server mode
     * @param server the Server instance
     * @return absolute path to the appropriate template dir
     */
    private def getViewvcTemplateDir(server) {
        return ConfigUtil.viewvcTemplateDir() 
    }

    private def getAuthHelperListen(server) {
        def conf = ""
        if (isLdapLoginEnabled(server)) {
            conf += "Listen ${csvnAuthenticationProvider.getAuthHelperPort(server, true)}"
        }
        conf
    }
    
    private boolean isLdapLoginEnabled(Server server) {
        return server.mode == ServerMode.STANDALONE && server.ldapEnabled && server.ldapEnabledConsole
    }

    private def getDefaultHttpdSnippets() {
        def conf = ""
        if (isWindows() == false) {
            Server server = Server.getServer()
            def repoParentPath = server.repoParentDir
            conf = """User ${getHttpdUser()}
Group ${getHttpdGroup()}
"""
        }
        conf += """PidFile "${ConfigUtil.dataDirPath()}/run/httpd.pid"
MaxKeepAliveRequests 10000
"""
    }    

    private def getLdapURL(server) {
        def ldapUrl = server.ldapURL()
        if (server.ldapAuthBasedn) {
            ldapUrl += server.ldapAuthBasedn
        }
        if (server.ldapLoginAttribute) {
            ldapUrl += "?${server.ldapLoginAttribute}"
        } else {
            ldapUrl += "?uid"
        }
        if (server.ldapSearchScope) {
            ldapUrl += "?${server.ldapSearchScope}"
        } else {
            ldapUrl += "?sub"
        }
        if (server.ldapFilter) {
            ldapUrl += "?${server.ldapFilter}"
        }
        ldapUrl
    }

    private def getLdapHttpdConf(server) {
        def conf = ""
        if (server.ldapEnabled) {
            def ldapURL = getLdapURL(server)
            conf = """<AuthnProviderAlias ldap ldap-users>
  AuthLDAPUrl "${ldapURL}" "${server.ldapSecurityLevel}"
"""

            if (server.ldapAuthBinddn && server.ldapAuthBindPassword) {
                conf = """${conf}
  AuthLDAPBindDN "${server.ldapAuthBinddn}"
  AuthLDAPBindPassword "${server.ldapAuthBindPassword}"
"""
            }
            conf = """${conf}
</AuthnProviderAlias>
"""
            if (!server.ldapServerCertVerificationNeeded) {
            conf = """${conf}
LDAPVerifyServerCert Off
"""
            }
            
            AdvancedConfiguration advConfig = server.advancedConfig()
            if (advConfig.ldapConnectionPoolTtl > 0) {
                conf += "LDAPConnectionPoolTTL ${advConfig.ldapConnectionPoolTtl}\n"
            }
            if (advConfig.ldapTimeout > 0) {
                conf += "LDAPTimeout ${advConfig.ldapTimeout}\n"
            }
        }
        conf
    }

    private def getFileAuthHttpdConf(server) {
        def conf = ""
        if (server.fileLoginEnabled) {
            conf = """
<AuthnProviderAlias file csvn-file-users>
  AuthUserFile "${confDirPath()}/svn_auth_file"
</AuthnProviderAlias>
"""
        }
        conf
    }

    private def getPythonVersion() {
        String version;
        if (isWindows()) {
            version = ""
        } else {
            version = commandLineService
                .executeWithOutput("python", "-c", "import platform; " + 
                "print str(platform.python_version_tuple()[0]) + '.' + " +
                "str(platform.python_version_tuple()[1])")
            if (version.length()) {
                version = "." + version
                /*Remove the trailing new line */
                version = version.trim()
            }
        }
        version
    }

    def getSvnMasterDirectiveIfReplica(server) {
        def conf = ""
        if (server.mode == ServerMode.REPLICA) {
            def replicaConfig = ReplicaConfiguration.getCurrentConfig()
            def masterSVNURI = replicaConfig.svnMasterUrl
            conf += "   SVNMasterURI ${masterSVNURI}\n"
            def masterVersion = ConfigUtil.replicaSvnMasterVersion()
            if (masterVersion) {
                conf += """   # SVNMasterVersion value is defined in <conf>/overrides.properties
   SVNMasterVersion ${masterVersion}
"""
            } else {
                masterVersion = syncReplicaMasterVersion()
                if (masterVersion) {
                    conf += "   SVNMasterVersion ${masterVersion}\n"
                } else {
                    conf += "   # SVNMasterVersion unknown; Cannot check version until repository is added\n"
                }
            }
        }
        return conf
    }

    def getCtfBasicAuth(server) {
        return """  AuthType Basic
  AuthName "TeamForge Authorization Realm"
  AuthBasicAuthoritative Off
  AuthUserFile /dev/null
  Require valid-user
"""
    }

    def getAuthnzCTFDirective() {
        return """AuthnzCTFPropertiesFile "${escapePath(new File(confDirPath(), "teamforge.properties").absolutePath)}"
"""
    }
    
    private def getSSLHttpdConf(server) {
        def conf = ""
        def confDir = confDirPath()
        if (server.useSsl) {
            conf = """
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLCertificateFile    "${confDir}/server.crt"
SSLCertificateKeyFile "${confDir}/server.key"
SSLSessionCache       "shmcb:${ConfigUtil.dataDirPath()}/run/ssl_scache(512000)"
"""
            File f = new File(confDir, "server.ca")
            def chainFilePath = f.absolutePath
            if (f.exists()) {
                conf += """SSLCertificateChainFile "${chainFilePath}"
"""
            }
            File extraConfig = new File(confDir, "ssl_httpd.conf")
            if (extraConfig.exists()) {
                if (extraConfig.length() > 0) {
                    conf += """Include "${confDir}/ssl_httpd.conf"
{'fraction_non_alphanumeric': 0.10689398552195387, 'fraction_numerical': 0.0033285975238481836, 'mean_word_length': 2.1845053429851777, 'pattern_counts': {'":': 2, '<': 79, '<?xml version=': 2, '>': 89, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1892215', 'n_tokens_mistral': 22065, 'n_tokens_neox': 21271, 'n_words': 4575}