#!/usr/bin/php -q
<?

function filecheck($path,$data="") {
  $name=substr($path,25);
  if (!($info=@stat($path))) {
    printf("Creating file %s\n",$path);
    $f=fopen($path,"w+");
    fwrite($f,$data);
    fclose($f);
    chown($path,503);
    chgrp($path,406);
    printf("Missing File %s, created\n",$name);
    return;
  }
/*   if ($info["uid"]!=503 || $info["gid"]!=406) { */
/*     printf("Setting OwnerShip of %s to mail:mail\n",$name); */
/*     return; */
/*     chown($path,503); */
/*     chgrp($path,406); */
/*   } */
}

function directorycheck($path) {
  $name=substr($path,25);
  if (!($info=@stat($path))) {
    printf("making directory %s\n",$path);
    mkdir($path,0755);
    chown($path,8);
    chgrp($path,12);
    printf("Missing Directory %s, created\n",$name);
    return;
  }
/*   if ($info["uid"]!=8 || $info["gid"]!=12) { */
/*     printf("Setting OwnerShip of %s to mail:mail\n",$name); */
/*     chown($path,8); */
/*     chgrp($path,12); */
/*   } */
}

function setupdirectories($path) {
  directorycheck($path."/log");
  directorycheck($path."/html");
  directorycheck($path."/graph");
  directorycheck($path."/etc");
/*   directorycheck($path."/bin"); */
  $config="<VirtualHost *:80>\n".
    "DocumentRoot /usr/daemon/httpd/domain/base.dom/html\n".
    "  ServerName base.dom\n".
    "  TransferLog /usr/daemon/httpd/domain/base.dom/log/access.log\n".
    "  #AddType application/x-httpd-php .htp\n".
    "  <Directory \"/usr/daemon/httpd/domain/base.dom/html\">\n".
    "    Options FollowSymLinks MultiViews\n".
    "    AllowOverride None\n".
    "    Require all granted\n".
    "  </Directory>\n".
    "</VirtualHost>\n\n";

  $config=
    "<VirtualHost *:80>\n".
    "  DocumentRoot /usr/daemon/httpd/domain/base.dom/html\n".
    "  ServerName base.dom\n".
    "  TransferLog /usr/daemon/httpd/domain/base.dom/log/access.log\n".
    "  #AddType application/x-httpd-php .htp\n".
    "  AddType application/x-httpd-php .php\n".
    "  <Directory \"/usr/daemon/httpd/domain/base.dom/html\">\n".
    "    Options FollowSymLinks MultiViews\n".
    "    AllowOverride None\n".
    "    Require all granted\n".
    "  </Directory>\n".
    "  <Files ~ \"\.phpt$|~$|ChangeLog\">\n".
    "    Order allow,deny\n".
    "    Deny from all\n".
    "  </Files>\n".
    "  #RewriteEngine on\n".
    "  #RewriteRule ^(.*[.])ver[0-9]+[.]([a-zA-z]+)$ $1$2\n".
    "  #RemoteIPHeader X-Forwarded-For\n".
    "  #RemoteIPInternalProxy 10.3.1.0/24\n".
    "  #SSLEngine on\n".
    "  #SSLOptions +StrictRequire\n".
    "  #<Directory />\n".
    "    #SSLRequireSSL\n".
    "  #</Directory>\n".
    "  #SSLProtocol -all +TLSv1 +TLSv1.1 +TLSv1.2 -SSLv2 -SSLv3\n".
    "  #SSLCipherSuite HIGH:MEDIUM:!aNULL:+SHA1:+SHA256:+MD5:+HIGH:+MEDIUM:!RC4\n".
    "  #SSLSessionCacheTimeout 600\n".
    "  #SSLVerifyClient none\n".
    "  #SSLProxyEngine off\n".
    "  #SSLCertificateFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/cert.pem\n".
    "  #SSLCertificateKeyFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/privkey.pem\n".
    "  #SSLCACertificateFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/chain.pem\n".
    "</VirtualHost>\n\n";
  
 filecheck($path."/etc/httpd.conf",$config);
}

function stripdbname($db) {
  if ($pos=strrpos($db,".")) {
    $db=substr($db,0,$pos);
  }
  if ($pos=strpos($db,".")) {
    $db=substr($db,$pos+1);
  }
  return($db);
}

function getmysqldblist($httproot) {
  $dir=@opendir($httproot);
  if (!$dir) {
    printf("No apache directory %s.  Update canceled.\n",$httproot);
    exit;
  }
  while($file=readdir($dir)) {
    if ($file==".." || $file==".")
      continue;
    $domaindir=sprintf("%s/%s",$httproot,$file);
    $dirname=sprintf("%s/%s/mysql",$httproot,$file);
    if (is_link($dirname) || is_link($domaindir))
      continue;
    if (is_dir($dirname)) {
      $db=stripdbname($file);
      $dir2=@opendir($dirname);
      while($file2=readdir($dir2)) {
	if ($file2==".." || $file2==".")
	  continue;
	$dbdir=sprintf("%s/%s/mysql/%s",$httproot,$file,$file2);
	if (!is_dir($dbdir))
	  continue;
	if ($file2=="base" || $file2=="base.mysql")
	  $linkname=$db;
	else
	  $linkname=$file2;
	if ($links[$linkname]) {
	  printf("Error: mysql database declared twice\n%s\n%s\nNo mysql changes made\n",$links[$linkname],$dbdir);
	  exit;
	}
	$links[$linkname]=$dbdir;
      }
    }
  }
  return($links);
}

function getmysqllinks($httproot,$mysqlroot) {
  $dir=@opendir($mysqlroot);
  while($file=readdir($dir)) {
    if ($file==".." || $file==".")
      continue;
    $dirname=sprintf("%s/%s",$mysqlroot,$file);
    if (!is_link($dirname))
      continue;
    $target=readlink($dirname);
    $len=strlen($httproot);
    if ($httproot!=substr($target,0,$len))
      continue;
    $db=substr($target,$len+1);
    $db=substr($db,0,strpos($db,"/"));
    $db=stripdbname($db);
    $links[$db]=$target;
  }
  return($links);
}

function resyncmysql($httproot,$mysqlroot) {
  $httpdlinks=getmysqldblist($httproot);
  $mysqllinks=getmysqllinks($httproot,$mysqlroot);
  reset($mysqllinks);
  while(list($key,$val)=each($mysqllinks)) {
    $dir1=$mysqllinks[$key];
    $dir2=$httpdlinks[$key];
    if ($dir1 && $dir1==$dir2)
      continue;
    $link=sprintf("%s/%s",$mysqlroot,$key);
    if (!is_link($link)) {
      printf("Code Error: removing %s, aborting.\n",$link);
      exit;
    }
    printf("removing database link %s for %s\n",$key,$link);
    unlink($link);
    $flag=1;
  }  
  reset($httpdlinks);
  while(list($key,$val)=each($httpdlinks)) {
    $dir1=$httpdlinks[$key];
    $dir2=$mysqllinks[$key];
    if ($dir1 && $dir1==$dir2)
      continue;
    printf("adding mysql database %s at %s\n",$key,$dir1);
    $link=sprintf("%s/%s",$mysqlroot,$key);
    if (is_dir($link)) {
      printf("Error: can not create link at %s, already in use.  Aborting\n",$link);
      exit;
    }
    symlink($dir1,$link);
    $flag=1;
  }  
  if ($flag) {
    printf("mysql databases changed, requesting server reload\n");
  }
}

/**
 * Build HMAC-SHA256 API auth headers for a JSON body.
 * Used for the api_key=... in syncnginx.conf (the only outbound peer).
 */
function api_headers(string $apiKey, string $method, string $urlPath, string $jsonBody): array {
  $ts        = time();
  $bodyHash  = hash('sha256', $jsonBody);
  $canonical = "{$ts}\n{$method}\n{$urlPath}\n{$bodyHash}";
  $sig       = hash_hmac('sha256', $canonical, $apiKey);
  return [
    'Content-Type: application/json',
    'X-API-Key: '       . $apiKey,
    'X-API-Timestamp: ' . $ts,
    'X-API-Signature: ' . $sig,
  ];
}

/**
 * Make an HMAC-authenticated API call to the backup server.
 */
function backup_api_call(array $cfg, string $method, string $path, array $body = []): ?array {
  $apiKey  = $cfg['api_key'] ?? '';
  $baseUrl = rtrim($cfg['url'] ?? '', '/');
  if (!$baseUrl || !$apiKey) return null;

  $fullUrl  = $baseUrl . $path;
  $jsonBody = ($method !== 'GET' && !empty($body)) ? json_encode($body) : '';
  $urlPath  = parse_url($fullUrl, PHP_URL_PATH);
  $headers  = api_headers($apiKey, $method, $urlPath, $jsonBody);

  $ch = curl_init($fullUrl);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
  curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  if ($jsonBody !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);

  $result = curl_exec($ch);
  $err    = curl_error($ch);
  curl_close($ch);

  if ($err) {
    printf("syncbackup: curl error %s %s: %s\n", $method, $path, $err);
    return null;
  }
  return json_decode($result, true) ?: null;
}

/**
 * For each domain with an etc/backup.conf, register modules on the backup server,
 * obtain SSH keys, write per-module backup scripts, and schedule them via cron.d.
 *
 * Global config: /usr/daemon/httpd/etc/syncbackup.conf
 *   url=http://<backup-ip>/api/
 *   api_key=<client-management-key>
 *   client=<this-container-name>
 *   ssh_host=<backup-ip>
 *   ssh_user=bkp_<client>        (default: bkp_<client>)
 *   schedule=0 2 * * *           (default: 2am daily)
 *
 * Per-domain: /usr/daemon/httpd/domain/<fqdn>/etc/backup.conf
 *   module=mysite
 *   dirs=/path/to/html:/path/to/etc
 *   due_hours=48
 */
/**
 * Discover which backup modules this host needs, from each domain's
 * etc/backup.conf. No API calls happen here any more: the request is handed to
 * the nginx server, which relays it to the backup server on our behalf.
 *
 * Per-domain: /usr/daemon/httpd/domain/<fqdn>/etc/backup.conf
 *   module=mysite
 *   dirs=/path/to/html:/path/to/etc
 *   due_hours=48
 *
 * Returns [ module => ['domain'=>..., 'dirs'=>[...], 'due_hours'=>int] ]
 */
function collect_backup_requests(): array {
  $domainBase = '/usr/daemon/httpd/domain';
  $dh = @opendir($domainBase);
  if (!$dh) return [];

  $req = [];
  while (($domain = readdir($dh)) !== false) {
    if ($domain === '.' || $domain === '..') continue;
    if (is_link("$domainBase/$domain")) continue;
    $backupConf = "$domainBase/$domain/etc/backup.conf";
    if (!file_exists($backupConf)) continue;

    $bcfg = [];
    foreach (file($backupConf) as $line) {
      $line = trim($line);
      if ($line === '' || $line[0] === '#') continue;
      if (preg_match('/^(\w+)\s*=\s*(.+)$/', $line, $m)) $bcfg[$m[1]] = $m[2];
    }

    $module  = $bcfg['module'] ?? preg_replace('/[^a-z0-9_-]/', '-', strtolower($domain));
    $dirsRaw = $bcfg['dirs']   ?? '';
    if (empty($dirsRaw)) {
      printf("syncbackup: %s: no dirs configured in backup.conf\n", $domain);
      continue;
    }
    $req[$module] = [
      'domain'    => $domain,
      'dirs'      => array_values(array_filter(array_map('trim', explode(':', $dirsRaw)))),
      'due_hours' => (int)($bcfg['due_hours'] ?? 48),
    ];
  }
  closedir($dh);
  return $req;
}

/**
 * Apply what nginx relayed back from the backup server: install the issued SSH
 * key, then write the per-module rsync script and its cron.d entry.
 * Both are rewritten on every run, so they stay idempotent.
 */
function apply_backup_results(array $results, array $requests, array $cfg): void {
  if (empty($results)) return;
  $keyDir  = '/usr/daemon/httpd/etc/backup-keys';
  $logFile = '/var/log/backup.log';
  if (!is_dir($keyDir)) mkdir($keyDir, 0700, true);

  foreach ($results as $module => $res) {
    if (!is_array($res)) continue;
    if (isset($res['error'])) {
      printf("syncbackup: %s: %s\n", $module, $res['error']);
      continue;
    }
    $dirs = $requests[$module]['dirs'] ?? [];
    if (empty($dirs)) continue;

    $domain   = $requests[$module]['domain'] ?? $module;
    $sshHost  = $res['ssh_host'] ?? ($cfg['backup_ssh_host'] ?? '');
    $sshUser  = $res['ssh_user'] ?? ($cfg['backup_ssh_user'] ?? '');
    $schedule = $cfg['backup_schedule'] ?? '0 2 * * *';
    if ($sshHost === '' || $sshUser === '') {
      printf("syncbackup: %s: backup server did not supply ssh_host/ssh_user\n", $module);
      continue;
    }

    $pemFile = "$keyDir/$module.pem";
    if (!empty($res['private_key']) && !file_exists($pemFile)) {
      file_put_contents($pemFile, $res['private_key']);
      chmod($pemFile, 0600);
      @chown($pemFile, 'httpd');
      printf("syncbackup: installed backup key for module '%s'\n", $module);
    }
    if (!file_exists($pemFile)) {
      printf("syncbackup: %s: no key available; skipping script\n", $module);
      continue;
    }

    $scriptFile  = "/usr/daemon/httpd/etc/backup-$module.sh";
    $scriptLines = [
      "#!/bin/bash",
      "# Auto-generated by synchttpd syncbackup - do not edit manually",
      "# module: $module  domain: $domain",
      "RSYNC=/usr/daemon/rsync/bin/rsync",
      "KEY=" . escapeshellarg($pemFile),
      'SSH_OPTS="-i $KEY -o StrictHostKeyChecking=no -o BatchMode=yes"',
      "",
    ];
    foreach ($dirs as $dir) {
      $subdir = basename(rtrim($dir, '/'));
      $scriptLines[] = "\"\$RSYNC\" -az --delete"
        . " -e \"ssh \$SSH_OPTS\""
        . " " . escapeshellarg(rtrim($dir, '/') . '/')
        . " " . escapeshellarg("$sshUser@$sshHost:/$subdir/");
    }
    $scriptLines[] = "";
    $scriptLines[] = "echo \"backup complete: \$(date)\"";
    file_put_contents($scriptFile, implode("\n", $scriptLines) . "\n");
    chmod($scriptFile, 0750);
    @chown($scriptFile, 'httpd');

    $cronName    = 'backup-' . preg_replace('/[^a-z0-9]/', '-', strtolower($module));
    $cronFile    = "/etc/cron.d/$cronName";
    $cronContent = "# Backup module '$module' ($domain) - managed by synchttpd\n"
                 . "SHELL=/bin/bash\n"
                 . "$schedule httpd /bin/bash " . escapeshellarg($scriptFile)
                 . " >> " . escapeshellarg($logFile) . " 2>&1\n";
    file_put_contents($cronFile, $cronContent);
    chmod($cronFile, 0644);

    printf("syncbackup: configured backup for '%s' (%d dirs, schedule: %s)\n",
           $module, count($dirs), $schedule);
  }
}

/**
 * Read /usr/daemon/httpd/etc/syncnginx.conf.
 * Returns ['_missing' => true] when the file does not exist at all, so the
 * caller can tell "never configured" apart from "configured but incomplete".
 */
function syncnginx_config(): array {
  $cfgFile = '/usr/daemon/httpd/etc/syncnginx.conf';
  if (!file_exists($cfgFile)) return ['_missing' => true];
  $cfg = [];
  foreach (file($cfgFile) as $line) {
    $line = trim($line);
    if ($line === '' || $line[0] === '#') continue;
    if (preg_match('/^(\w+)\s*=\s*(.+)$/', $line, $m)) $cfg[$m[1]] = $m[2];
  }
  return $cfg;
}

/**
 * Print instructions for setting up nginx credentials (--credentials-help).
 */
function credentials_help(): void {
  $f = '/usr/daemon/httpd/etc/syncnginx.conf';
  printf("synchttpd talks to exactly one server: the nginx proxy. It relays cert\n");
  printf("requests and backup registration on this host's behalf.\n\n");
  printf("Create %s containing:\n\n", $f);
  printf("    url=http://<nginx-ip>/api/sync\n");
  printf("    api_key=<key issued by the nginx server>\n");
  printf("    #ip=<this host's IP>      # optional; autodetected from hostname -I\n\n");
  printf("Issue a key on the nginx server with:\n\n");
  printf("    curl -s -X POST http://<nginx-ip>/api/keys \\\n");
  printf("         -H 'Content-Type: application/json' \\\n");
  printf("         -d '{\"name\":\"%s\"}'\n\n", trim(shell_exec('hostname') ?: 'this-host'));
  printf("Then chmod 600 %s (it holds a shared secret).\n\n", $f);
  printf("If this host is deliberately not proxied (e.g. a DNS server), add:\n\n");
  printf("    no_sync=yes\n\n");
  printf("to %s to silence this warning permanently. Use --quiet to\n", $f);
  printf("silence it for a single run.\n");
}

/**
 * Warn when nginx credentials are absent or incomplete.
 * Suppressed by --quiet, or permanently by no_sync=yes in syncnginx.conf.
 */
function warn_nginx_credentials(array $cfg, bool $quiet): void {
  if ($quiet) return;
  $noSync = strtolower(trim($cfg['no_sync'] ?? ''));
  if (in_array($noSync, ['1', 'yes', 'true', 'on'], true)) return;

  $missing = !empty($cfg['_missing']);
  $reason  = null;
  if ($missing) {
    $reason = 'no /usr/daemon/httpd/etc/syncnginx.conf';
  } elseif (empty($cfg['url'])) {
    $reason = 'syncnginx.conf has no url=';
  } elseif (empty($cfg['api_key'])) {
    $reason = 'syncnginx.conf has no api_key=';
  }
  if ($reason === null) return;

  fwrite(STDERR, "\n");
  fwrite(STDERR, "WARNING: nginx credentials are not set up ($reason).\n");
  fwrite(STDERR, "         This host will NOT be proxied, will NOT receive certificates,\n");
  fwrite(STDERR, "         and its backup modules will NOT be registered.\n");
  fwrite(STDERR, "         Run 'synchttpd --credentials-help' for setup instructions,\n");
  fwrite(STDERR, "         '--quiet' to silence this once, or set no_sync=yes if this\n");
  fwrite(STDERR, "         host is intentionally not proxied.\n\n");
}

/**
 * Map each vhost ServerName to the domain directory whose fragment produced it.
 * resyncapache() writes a "#From file .../domain/<dir>/etc/httpd.conf" marker
 * ahead of every fragment, which is what makes this association reliable.
 */
function map_servernames_to_domaindirs(string $config): array {
  $map     = [];
  $current = null;
  foreach (explode("\n", $config) as $line) {
    // NB: '~' delimiter — the pattern begins with a literal '#', which would
    // terminate a '#'-delimited pattern immediately and silently never match.
    if (preg_match('~^#From file /usr/daemon/httpd/domain/([^/]+)/etc/httpd\.conf~', $line, $m)) {
      $current = $m[1];
      continue;
    }
    if ($current !== null && preg_match('/^\s*ServerName\s+(\S+)/i', $line, $sm)) {
      $sn = preg_replace('/:\d+$/', '', trim($sm[1]));
      if (!isset($map[$sn])) $map[$sn] = $current;
    }
  }
  return $map;
}

/**
 * Write a cert bundle returned by nginx into the estate's standard location:
 *   /usr/daemon/httpd/domain/<domain-dir>/cert/<site-name>/{cert,privkey,fullchain,chain}.pem
 */
function store_cert(string $domainDir, string $site, array $bundle): bool {
  $dir = "/usr/daemon/httpd/domain/$domainDir/cert/$site";
  if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
    printf("syncnginx: cannot create %s\n", $dir);
    return false;
  }
  $files = [
    'cert.pem'      => $bundle['cert']      ?? null,
    'privkey.pem'   => $bundle['privkey']   ?? null,
    'fullchain.pem' => $bundle['fullchain'] ?? null,
    'chain.pem'     => $bundle['chain']     ?? null,
  ];
  // fullchain is the compatibility fallback when no explicit chain is supplied
  if ($files['chain.pem'] === null) $files['chain.pem'] = $files['fullchain.pem'];
  $wrote = false;
  foreach ($files as $name => $b64) {
    if ($b64 === null) continue;
    $data = base64_decode($b64, true);
    if ($data === false || $data === '') continue;
    $path = "$dir/$name";
    if (file_put_contents("$path.tmp", $data) === false) continue;
    chmod("$path.tmp", $name === 'privkey.pem' ? 0600 : 0644);
    rename("$path.tmp", $path);   // atomic: never leave a half-written key
    $wrote = true;
  }
  return $wrote;
}

function posttosyncnginx($confFile, array $cfg) {
  if (!empty($cfg['_missing'])) return;
  $url = $cfg['url'] ?? '';
  if (empty($url)) return;
  $apiKey = $cfg['api_key'] ?? '';
  if ($apiKey === '') return;   // HMAC is the only supported auth
  $ip     = $cfg['ip'] ?? trim(shell_exec("hostname -I | awk '{print $1}'"));
  // nginx names the sync/<host>/ directory after this; it falls back to the IP
  // when absent, so an older backend still syncs, just under a numeric name.
  $host   = $cfg['hostname'] ?? trim(shell_exec('hostname') ?: '');
  $config = file_get_contents($confFile);

  // Gather SSL certs for vhosts that have RemoteIPHeader
  $certs = [];
  preg_match_all('/^<VirtualHost[^>]*>(.*?)<\/VirtualHost>/smi', $config, $vmatches);
  foreach ($vmatches[1] as $vhost) {
    if (!preg_match('/RemoteIPHeader/i', $vhost)) continue;
    if (!preg_match('/^\s*ServerName\s+(\S+)/mi', $vhost, $sm)) continue;
    $sn = preg_replace('/:\d+$/', '', trim($sm[1]));
    $privkey   = null;
    $fullchain = null;
    if (preg_match('/^\s*SSLCertificateKeyFile\s+(\S+)/mi', $vhost, $km)) {
      $kf = trim($km[1]);
      if (file_exists($kf)) $privkey = base64_encode(file_get_contents($kf));
    }
    if (preg_match('/^\s*SSLCertificateFile\s+(\S+)/mi', $vhost, $cm)) {
      $cf  = trim($cm[1]);
      $dir = dirname($cf);
      if (file_exists("$dir/fullchain.pem")) {
        $fullchain = base64_encode(file_get_contents("$dir/fullchain.pem"));
      } elseif (file_exists($cf)) {
        if (preg_match('/^\s*SSLCACertificateFile\s+(\S+)/mi', $vhost, $chm) && file_exists(trim($chm[1]))) {
          $fullchain = base64_encode(file_get_contents($cf) . file_get_contents(trim($chm[1])));
        } else {
          $fullchain = base64_encode(file_get_contents($cf));
        }
      }
    }
    if ($privkey !== null && $fullchain !== null) {
      $certs[$sn] = ['privkey' => $privkey, 'fullchain' => $fullchain];
    }
  }

  // Which SSL vhosts need a certificate obtained or renewed? nginx performs the
  // actual request against the cert server; this host only states the need.
  $renewDays  = (int)($cfg['cert_renew_days'] ?? 30);
  $domainDirs = map_servernames_to_domaindirs($config);
  $certReq    = [];
  foreach ($vmatches[1] as $vhost) {
    if (!preg_match('/^\s*SSLCertificateFile\s+(\S+)/mi', $vhost, $cm)) continue;
    if (!preg_match('/^\s*ServerName\s+(\S+)/mi', $vhost, $sm)) continue;
    $sn        = preg_replace('/:\d+$/', '', trim($sm[1]));
    $certPath  = trim($cm[1]);
    $needsCert = true;
    if (file_exists($certPath)) {
      $info = @openssl_x509_parse(file_get_contents($certPath));
      if ($info && isset($info['validTo_time_t'])) {
        $needsCert = (($info['validTo_time_t'] - time()) / 86400) <= $renewDays;
      }
    }
    if ($needsCert && !in_array($sn, $certReq, true)) $certReq[] = $sn;
  }

  // Backup module registration, previously posted straight to the backup
  // server; nginx now relays it so this host has a single outbound peer.
  $backup = collect_backup_requests();

  $apiUrl  = preg_replace('/\/syncnginx\.php$/i', '/api/sync', $url);
  $body    = json_encode([
    'config'       => $config,
    'ip'           => $ip,
    'hostname'     => $host,
    'certs'        => $certs   ?: new stdClass(),
    'cert_request' => $certReq,
    'backup'       => $backup  ?: new stdClass(),
  ]);
  $urlPath = parse_url($apiUrl, PHP_URL_PATH);

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
  // Generous: nginx may be waiting on certbot DNS propagation on our behalf.
  curl_setopt($ch, CURLOPT_TIMEOUT, empty($certReq) ? 30 : 300);
  curl_setopt($ch, CURLOPT_URL, $apiUrl);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  curl_setopt($ch, CURLOPT_HTTPHEADER, api_headers($apiKey, 'POST', $urlPath, $body));

  $result = curl_exec($ch);
  $err    = curl_error($ch);
  curl_close($ch);

  if ($err) {
    printf("syncnginx: curl error: %s\n", $err);
    return;
  }

  $data = json_decode($result, true);
  $ok   = is_array($data)
          && (($data['status'] ?? null) === 'ok' || ($data['ok'] ?? null) === true);
  if (!$ok) {
    printf("syncnginx: response: %s\n", $result);
    return;
  }

  printf("syncnginx: ok, proxied domains: %s\n", implode(', ', array_keys($data['proxied'] ?? [])));

  // Store any certs nginx obtained for us.
  if (!empty($certReq) && empty($data['certs'])) {
    printf("syncnginx: requested certs for %s but nginx returned none\n", implode(', ', $certReq));
  }
  foreach (($data['certs'] ?? []) as $site => $bundle) {
    if (!is_array($bundle)) {
      printf("syncnginx: cert for %s: malformed response\n", $site);
      continue;
    }
    // A relay failure comes back as an error bundle; report it rather than
    // letting store_cert() quietly do nothing.
    if (isset($bundle['error'])) {
      printf("syncnginx: cert for %s FAILED: %s\n", $site, $bundle['error']);
      continue;
    }
    $dir = $domainDirs[$site] ?? null;
    if ($dir === null) {
      printf("syncnginx: cert for %s returned but no matching domain dir; skipped\n", $site);
      continue;
    }
    if (store_cert($dir, $site, $bundle)) {
      printf("syncnginx: stored cert for %s in domain/%s/cert/%s/\n", $site, $dir, $site);
    } else {
      printf("syncnginx: cert for %s: nothing written (empty bundle)\n", $site);
    }
  }

  apply_backup_results($data['backup'] ?? [], $backup, $cfg);
}


function generate_self_signed_cert($domain, $liveDir) {
  if (!is_dir($liveDir)) mkdir($liveDir, 0755, true);
  $keyFile  = "$liveDir/privkey.pem";
  $certFile = "$liveDir/cert.pem";
  $cmd = sprintf(
    'openssl req -x509 -nodes -days 3650 -newkey rsa:2048 ' .
    '-keyout %s -out %s -subj %s 2>&1',
    escapeshellarg($keyFile),
    escapeshellarg($certFile),
    escapeshellarg("/CN=$domain")
  );
  exec($cmd, $out, $ret);
  if ($ret !== 0) {
    printf("ssl: openssl failed: %s\n", implode("\n", $out));
    return false;
  }
  copy($certFile, "$liveDir/fullchain.pem");
  copy($certFile, "$liveDir/chain.pem");
  chmod($keyFile, 0640);
  printf("ssl: generated self-signed cert for %s\n", $domain);
  return true;
}

function ensure_ssl_cert($domain) {
  // Certificates are obtained by the nginx server during sync and written back
  // into domain/<domain>/cert/<site>/. This only guarantees *something* usable
  // exists so httpd -t passes; the real cert arrives on the next sync.
  $certDir  = "/usr/daemon/httpd/domain/$domain/cert/$domain";
  $certFile = "$certDir/cert.pem";
  $keyFile  = "$certDir/privkey.pem";

  if (file_exists($certFile) && file_exists($keyFile)) {
    $info = @openssl_x509_parse(file_get_contents($certFile));
    if ($info && isset($info['validTo_time_t']) && $info['validTo_time_t'] > time() + 86400) {
      printf("ssl: existing cert for %s is valid\n", $domain);
      return true;
    }
  }

  printf("ssl: no valid cert for %s; generating a self-signed placeholder\n", $domain);
  printf("ssl: run synchttpd once nginx credentials are configured to obtain a real cert\n");
  return generate_self_signed_cert($domain, $certDir);
}

function enable_ssl_vhost($domain) {
  $confFile = "/usr/daemon/httpd/domain/$domain/etc/httpd.conf";
  if (!file_exists($confFile)) {
    printf("ssl: no httpd.conf found for %s\n", $domain);
    return false;
  }

  if (!ensure_ssl_cert($domain)) {
    printf("ssl: could not obtain cert for %s\n", $domain);
    return false;
  }

  $conf = file_get_contents($confFile);

  // Update existing 443 block if present
  if (preg_match('/<VirtualHost[^>]*:443[^>]*>/i', $conf)) {
    printf("ssl: 443 vhost already present for %s\n", $domain);
    return true;
  }

  $ssl_block =
    "\n<VirtualHost *:443>\n" .
    "  DocumentRoot /usr/daemon/httpd/domain/base.dom/html\n" .
    "  ServerName base.dom\n" .
    "  TransferLog /usr/daemon/httpd/domain/base.dom/log/access.log\n" .
    "  AddType application/x-httpd-php .php\n" .
    "  <Directory \"/usr/daemon/httpd/domain/base.dom/html\">\n" .
    "    Options FollowSymLinks MultiViews\n" .
    "    AllowOverride None\n" .
    "    Require all granted\n" .
    "  </Directory>\n" .
    "  RemoteIPHeader X-Forwarded-For\n" .
    "  SSLEngine on\n" .
    "  SSLCertificateFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/cert.pem\n" .
    "  SSLCertificateKeyFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/privkey.pem\n" .
    "  SSLCACertificateFile /usr/daemon/httpd/domain/base.dom/cert/base.dom/chain.pem\n" .
    "</VirtualHost>\n";

  file_put_contents($confFile, $conf . $ssl_block);
  printf("ssl: added 443 vhost for %s\n", $domain);
  return true;
}

function resyncapache($root) {
  copy("/usr/daemon/httpd/conf/settings.httpd.conf","/usr/daemon/httpd/conf/new.httpd.conf");
  $f=fopen("/usr/daemon/httpd/conf/new.httpd.conf","a+");

  $dir=@opendir($basedir="/usr/daemon/httpd/domain");
  if (!$dir) {
    printf("No /usr/daemon/httpd/domain.  Update canceled.\n");
    exit;
  }
  
  while($file=readdir($dir)) {
    if ($file==".." || $file==".")
      continue;
    setupdirectories($basedir."/".$file);
    $fname=sprintf("/usr/daemon/httpd/domain/%s/etc/httpd.conf",$file);
    if (!is_file($fname)) {
      printf("Did not find a config segment for %s.  No changes made.\n",$file);
      exit;
    }
    fwrite($f,sprintf("\n#From file %s\n",$fname));
    $conf=fopen($fname,"r");
    while($buf=fgets($conf,2048)) {
      /* Both placeholders mean "this domain's directory name". "base.dom" is the
         original and every existing per-domain httpd.conf uses it, so it keeps
         working untouched; "[Domain]" reads as a placeholder rather than as a
         plausible hostname, which is what it was mistaken for. Accepting both
         means a config can be converted whenever, or never. */
      fwrite($f,sprintf("%s\n",str_replace(array("base.dom","[Domain]"),$file,rtrim($buf))));
    }
/*     system(sprintf("cat $fname | sed s/base.dom/$file/ >> /usr/daemon/httpd/conf/new.httpd.conf")); */
    fclose($conf);
  } 
  fclose($f);
  closedir($dir);
  
  system("/usr/daemon/httpd/bin/httpd -t -f /usr/daemon/httpd/conf/new.httpd.conf");
  
/*  $p=popen("/usr/sbin/httpd -t -f /usr/daemon/httpd/conf/new.httpd.conf 2>&1","r"); */
  $p=popen("/usr/daemon/httpd/bin/httpd -t -f /usr/daemon/httpd/conf/new.httpd.conf 2>&1","r");
  if (!$p) {
    printf("Error: install check failed to run httpd -t\n");
    exit;
  }

  $buf=trim(fgets($p,2048));
  if ($buf=="Syntax OK") {
    printf("Current configuration has been accepted\n");
    copy("/usr/daemon/httpd/conf/new.httpd.conf","/usr/daemon/httpd/conf/httpd.conf");
    system("killall -1 httpd");
    // nginx is now this host's only outbound peer: it relays cert requests to
    // the cert server and backup registration to the backup server on our behalf.
    posttosyncnginx("/usr/daemon/httpd/conf/httpd.conf",
                    $GLOBALS['SYNCNGINX_CFG'] ?? syncnginx_config());
  } else {
    printf("Due to errors, no changes have been\n");
  }
}

// A host with a colocated nginx frontend uses the serialized two-service
// transaction engine. Ordinary remote-nginx deployments keep the existing path.
if (empty($GLOBALS['_SQTEST_']) && is_file('/usr/daemon/httpd/etc/local-nginx.json')) {
  $tool='/usr/daemon/httpd/local/bin/synchttpd-local';
  if (!is_executable($tool)) { fwrite(STDERR,"Local nginx synchronization helper is unavailable.\n"); exit(1); }
  $cmd=escapeshellarg($tool);
  foreach (array_slice($argv,1) as $arg) $cmd.=' '.escapeshellarg($arg);
  passthru($cmd,$code); exit($code);
}

if (!$GLOBALS["_SQTEST_"]) {
  $ssl_domain = null;
  $quiet      = false;
  for ($i = 1; $i < $argc; $i++) {
    switch ($argv[$i]) {
      case '--ssl':
        if (isset($argv[$i + 1])) $ssl_domain = $argv[++$i];
        break;
      case '-q':
      case '--quiet':
        $quiet = true;
        break;
      case '--credentials-help':
        credentials_help();
        exit(0);
      case '-h':
      case '--help':
        printf("Usage: synchttpd [--ssl <domain>] [-q|--quiet] [--credentials-help]\n\n");
        printf("  --ssl <domain>       add a 443 vhost for <domain>, then resync\n");
        printf("  -q, --quiet          suppress the missing-credentials warning\n");
        printf("      --credentials-help  explain how to configure nginx credentials\n");
        exit(0);
    }
  }

  $GLOBALS['SYNCNGINX_CFG']   = syncnginx_config();
  $GLOBALS['SYNCNGINX_QUIET'] = $quiet;
  warn_nginx_credentials($GLOBALS['SYNCNGINX_CFG'], $quiet);

  if ($ssl_domain !== null) {
    if (enable_ssl_vhost($ssl_domain)) resyncapache("/usr/daemon/httpd/domain");
  } else {
    resyncapache("/usr/daemon/httpd/domain");
  }
}

?>