/*
* winrc/win_svc.c - windows services API implementation for unbound
*
* Copyright (c) 2009, NLnet Labs. All rights reserved.
*
* This software is open source.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the NLNET LABS nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/**
* \file
*
* This file contains functions to integrate with the windows services API.
* This means it handles the commandline switches to install and remove
* the service (via CreateService and DeleteService), it handles
* the ServiceMain() main service entry point when started as a service,
* and it handles the Handler[_ex]() to process requests to the service
* (such as start and stop and status).
*/
#include "config.h"
#include "winrc/win_svc.h"
#include "winrc/w_inst.h"
#include "daemon/daemon.h"
#include "daemon/worker.h"
#include "daemon/remote.h"
#include "util/config_file.h"
#include "util/netevent.h"
#include "util/ub_event.h"
#include "util/net_help.h"

/** global service status */
static SERVICE_STATUS   service_status;
/** global service status handle */
static SERVICE_STATUS_HANDLE service_status_handle;
/** global service stop event */
static WSAEVENT service_stop_event = NULL;
/** event struct for stop callbacks */
static struct ub_event* service_stop_ev = NULL;
/** if stop even means shutdown or restart */
static int service_stop_shutdown = 0;
/** config file to open. global communication to service_main() */
static char* service_cfgfile = CONFIGFILE;
/** commandline verbosity. global communication to service_main() */
static int service_cmdline_verbose = 0;
/** the cron callback */
static struct comm_timer* service_cron = NULL;
/** the cron thread */
static ub_thread_type cron_thread = NULL;
/** if cron has already done its quick check */
static int cron_was_quick = 0;

/**
* Report current service status to service control manager
* @param state: current state
* @param exitcode: error code (when stopped)
* @param wait: pending operation estimated time in milliseconds.
*/
static void report_status(DWORD state, DWORD exitcode, DWORD wait)
{
       static DWORD checkpoint = 1;
       service_status.dwCurrentState = state;
       service_status.dwWin32ExitCode = exitcode;
       service_status.dwWaitHint = wait;
       if(state == SERVICE_START_PENDING)
               service_status.dwControlsAccepted = 0;
       else    service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
       if(state == SERVICE_RUNNING || state == SERVICE_STOPPED)
               service_status.dwCheckPoint = 0;
       else    service_status.dwCheckPoint = checkpoint++;
       SetServiceStatus(service_status_handle, &service_status);
}

/**
* Service control handler. Called by serviceControlManager when a control
* code is sent to the service (with ControlService).
* @param ctrl: control code
*/
static void
hdlr(DWORD ctrl)
{
       if(ctrl == SERVICE_CONTROL_STOP) {
               report_status(SERVICE_STOP_PENDING, NO_ERROR, 0);
               service_stop_shutdown = 1;
               /* send signal to stop */
               if(!WSASetEvent(service_stop_event))
                       log_err("Could not WSASetEvent: %s",
                               wsa_strerror(WSAGetLastError()));
               return;
       } else {
               /* ctrl == SERVICE_CONTROL_INTERROGATE or whatever */
               /* update status */
               report_status(service_status.dwCurrentState, NO_ERROR, 0);
       }
}

/**
* report event to system event log
* For use during startup and shutdown.
* @param str: the error
*/
static void
reportev(const char* str)
{
       char b[256];
       char e[256];
       HANDLE* s;
       LPCTSTR msg = b;
       /* print quickly to keep GetLastError value */
       wsvc_err2str(e, sizeof(e), str, GetLastError());
       snprintf(b, sizeof(b), "%s: %s", SERVICE_NAME, e);
       s = RegisterEventSource(NULL, SERVICE_NAME);
       if(!s) return;
       ReportEvent(s, /* event log */
               EVENTLOG_ERROR_TYPE, /* event type */
               0, /* event category */
               MSG_GENERIC_ERR, /* event ID (from gen_msg.mc) */
               NULL, /* user security context */
               1, /* numstrings */
               0, /* binary size */
               &msg, /* strings */
               NULL); /* binary data */
       DeregisterEventSource(s);
}

/**
* Obtain registry string (if it exists).
* @param key: key string
* @param name: name of value to fetch.
* @return malloced string with the result or NULL if it did not
* exist on an error (logged) was encountered.
*/
static char*
lookup_reg_str(const char* key, const char* name)
{
       HKEY hk = NULL;
       DWORD type = 0;
       BYTE buf[1024];
       DWORD len = (DWORD)sizeof(buf);
       LONG ret;
       char* result = NULL;
       ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, KEY_READ, &hk);
       if(ret == ERROR_FILE_NOT_FOUND)
               return NULL; /* key does not exist */
       else if(ret != ERROR_SUCCESS) {
               reportev("RegOpenKeyEx failed");
               return NULL;
       }
       ret = RegQueryValueEx(hk, (LPCTSTR)name, 0, &type, buf, &len);
       if(RegCloseKey(hk))
               reportev("RegCloseKey");
       if(ret == ERROR_FILE_NOT_FOUND)
               return NULL; /* name does not exist */
       else if(ret != ERROR_SUCCESS) {
               reportev("RegQueryValueEx failed");
               return NULL;
       }
       if(type == REG_SZ || type == REG_MULTI_SZ || type == REG_EXPAND_SZ) {
               buf[sizeof(buf)-1] = 0;
               buf[sizeof(buf)-2] = 0; /* for multi_sz */
               result = strdup((char*)buf);
               if(!result) reportev("out of memory");
       }
       return result;
}

/**
* Obtain registry integer (if it exists).
* @param key: key string
* @param name: name of value to fetch.
* @return integer value (if it exists), or 0 on error.
*/
static int
lookup_reg_int(const char* key, const char* name)
{
       HKEY hk = NULL;
       DWORD type = 0;
       BYTE buf[1024];
       DWORD len = (DWORD)sizeof(buf);
       LONG ret;
       int result = 0;
       ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, key, 0, KEY_READ, &hk);
       if(ret == ERROR_FILE_NOT_FOUND)
               return 0; /* key does not exist */
       else if(ret != ERROR_SUCCESS) {
               reportev("RegOpenKeyEx failed");
               return 0;
       }
       ret = RegQueryValueEx(hk, (LPCTSTR)name, 0, &type, buf, &len);
       if(RegCloseKey(hk))
               reportev("RegCloseKey");
       if(ret == ERROR_FILE_NOT_FOUND)
               return 0; /* name does not exist */
       else if(ret != ERROR_SUCCESS) {
               reportev("RegQueryValueEx failed");
               return 0;
       }
       if(type == REG_SZ || type == REG_MULTI_SZ || type == REG_EXPAND_SZ) {
               buf[sizeof(buf)-1] = 0;
               buf[sizeof(buf)-2] = 0; /* for multi_sz */
               result = atoi((char*)buf);
       } else if(type == REG_DWORD) {
               DWORD r;
               memmove(&r, buf, sizeof(r));
               result = r;
       }
       return result;
}

/** wait for unbound-anchor process to finish */
static void
waitforubanchor(PROCESS_INFORMATION* pinfo)
{
       /* we have 5 seconds scheduled for it, usually it will be very fast,
        * with only a UDP message or two (100 msec or so), but the https
        * connections could take some time */
       DWORD count = 7900;
       DWORD ret = WAIT_TIMEOUT;
       /* decrease timer every 1/10 second, we are still starting up */
       while(ret == WAIT_TIMEOUT) {
               ret = WaitForSingleObject(pinfo->hProcess, 100);
               if(count > 4000) count -= 100;
               else count--; /* go slow, it is taking long */
               if(count > 3000)
                       report_status(SERVICE_START_PENDING, NO_ERROR, count);
       }
       verbose(VERB_ALGO, "unbound-anchor done");
       if(ret != WAIT_OBJECT_0) {
               return; /* did not end successfully */
       }
       if(!GetExitCodeProcess(pinfo->hProcess, &ret)) {
               log_err("GetExitCodeProcess failed");
               return;
       }
       verbose(VERB_ALGO, "unbound-anchor exit code is %d", (int)ret);
       if(ret != 0) {
               log_info("The root trust anchor has been updated.");
       }
}


/**
* Perform root anchor update if so configured, by calling that process
*/
static void
call_root_update(void)
{
       char* rootanchor;
       rootanchor = lookup_reg_str("Software\\Unbound", "RootAnchor");
       if(rootanchor && strlen(rootanchor)>0) {
               STARTUPINFO sinfo;
               PROCESS_INFORMATION pinfo;
               memset(&pinfo, 0, sizeof(pinfo));
               memset(&sinfo, 0, sizeof(sinfo));
               sinfo.cb = sizeof(sinfo);
               verbose(VERB_ALGO, "rootanchor: %s", rootanchor);
               report_status(SERVICE_START_PENDING, NO_ERROR, 8000);
               if(!CreateProcess(NULL, rootanchor, NULL, NULL, 0,
                       CREATE_NO_WINDOW, NULL, NULL, &sinfo, &pinfo))
                       log_err("CreateProcess error for unbound-anchor.exe");
               else {
                       waitforubanchor(&pinfo);
                       CloseHandle(pinfo.hProcess);
                       CloseHandle(pinfo.hThread);
               }
       }
       free(rootanchor);
}

/**
* Init service. Keeps calling status pending to tell service control
* manager that this process is not hanging.
* @param r: restart, true on restart
* @param d: daemon returned here.
* @param c: config file returned here.
* @return false if failed.
*/
static int
service_init(int r, struct daemon** d, struct config_file** c)
{
       struct config_file* cfg = NULL;
       struct daemon* daemon = NULL;

       if(!service_cfgfile) {
               char* newf = lookup_reg_str("Software\\Unbound", "ConfigFile");
               if(newf) service_cfgfile = newf;
               else    service_cfgfile = strdup(CONFIGFILE);
               if(!service_cfgfile) fatal_exit("out of memory");
       }

       /* create daemon */
       if(r)   daemon = *d;
       else    daemon = daemon_init();
       if(!daemon) return 0;
       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2800);

       /* read config */
       cfg = config_create();
       if(!cfg) return 0;
       if(!config_read(cfg, service_cfgfile, daemon->chroot)) {
               if(errno != ENOENT) {
                       log_err("error in config file");
                       return 0;
               }
               log_warn("could not open config file, using defaults");
       }
       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2600);

       verbose(VERB_QUERY, "winservice - apply settings");
       /* apply settings and init */
       verbosity = cfg->verbosity + service_cmdline_verbose;
       w_config_adjust_directory(cfg);
       if(cfg->directory && cfg->directory[0]) {
               char* dir = cfg->directory;
               if(chdir(dir)) {
                       log_err("could not chdir to %s: %s",
                               dir, strerror(errno));
                       if(errno != ENOENT)
                               return 0;
                       log_warn("could not change directory - continuing");
               } else
                       verbose(VERB_QUERY, "chdir to %s", dir);
       }
       log_init(cfg->logfile, cfg->use_syslog, cfg->chrootdir);
       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2400);
       verbose(VERB_QUERY, "winservice - apply cfg");
       daemon_apply_cfg(daemon, cfg);

       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2300);
       if(!r) {
               if(!daemon_privileged(daemon))
                       fatal_exit("could not do privileged setup");
       }
       if(!(daemon->rc = daemon_remote_create(cfg))) {
               log_err("could not set up remote-control");
               daemon_delete(daemon);
               config_delete(cfg);
               return 0;
       }
       if(cfg->ssl_service_key && cfg->ssl_service_key[0]) {
               if(!(daemon->listen_dot_sslctx = listen_sslctx_create(
                       cfg->ssl_service_key, cfg->ssl_service_pem, NULL,
                       cfg->tls_ciphers, cfg->tls_ciphersuites,
                       (cfg->tls_session_ticket_keys.first &&
                       cfg->tls_session_ticket_keys.first->str[0] != 0),
                       1, 0))) {
                       fatal_exit("could not set up listen SSL_CTX");
               }
#ifdef HAVE_NGHTTP2_NGHTTP2_H
               if(cfg_has_https(cfg)) {
                       if(!(daemon->listen_doh_sslctx = listen_sslctx_create(
                               cfg->ssl_service_key, cfg->ssl_service_pem, NULL,
                               cfg->tls_ciphers, cfg->tls_ciphersuites,
                               (cfg->tls_session_ticket_keys.first &&
                               cfg->tls_session_ticket_keys.first->str[0] != 0),
                               0, 1))) {
                               fatal_exit("could not set up listen doh SSL_CTX");
                       }
               }
#endif
#ifdef HAVE_NGTCP2
               if(cfg_has_quic(cfg)) {
                       if(!(daemon->listen_quic_sslctx = quic_sslctx_create(
                               cfg->ssl_service_key, cfg->ssl_service_pem, NULL))) {
                               fatal_exit("could not set up quic SSL_CTX");
                       }
               }
#endif /* HAVE_NGTCP2 */
       }
       if(!(daemon->connect_dot_sslctx = connect_sslctx_create(NULL, NULL,
               cfg->tls_cert_bundle, cfg->tls_win_cert)))
               fatal_exit("could not set up connect SSL_CTX");

       /* open ports */
       /* keep reporting that we are busy starting */
       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2200);
       verbose(VERB_QUERY, "winservice - open ports");
       if(!daemon_open_shared_ports(daemon)) return 0;
       verbose(VERB_QUERY, "winservice - ports opened");
       if(!r) report_status(SERVICE_START_PENDING, NO_ERROR, 2000);

       *d = daemon;
       *c = cfg;
       return 1;
}

/**
* Deinit the service
*/
static void
service_deinit(struct daemon* daemon, struct config_file* cfg)
{
       daemon_cleanup(daemon);
       config_delete(cfg);
       daemon_delete(daemon);
}

#ifdef DOXYGEN
#define ATTR_UNUSED(x) x
#endif
/**
* The main function for the service.
* Called by the services API when starting unbound on windows in background.
* Arguments could have been present in the string 'path'.
* @param argc: nr args
* @param argv: arg text.
*/
static void
service_main(DWORD ATTR_UNUSED(argc), LPTSTR* ATTR_UNUSED(argv))
{
       struct config_file* cfg = NULL;
       struct daemon* daemon = NULL;

       service_status_handle = RegisterServiceCtrlHandler(SERVICE_NAME,
               (LPHANDLER_FUNCTION)hdlr);
       if(!service_status_handle) {
               reportev("Could not RegisterServiceCtrlHandler");
               return;
       }

       service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
       service_status.dwServiceSpecificExitCode = 0;

       /* see if we have root anchor update enabled */
       call_root_update();

       /* we are now starting up */
       report_status(SERVICE_START_PENDING, NO_ERROR, 3000);
       if(!service_init(0, &daemon, &cfg)) {
               reportev("Could not service_init");
               report_status(SERVICE_STOPPED, NO_ERROR, 0);
               return;
       }

       /* event that gets signalled when we want to quit; it
        * should get registered in the worker-0 waiting loop. */
       service_stop_event = WSACreateEvent();
       if(service_stop_event == WSA_INVALID_EVENT) {
               log_err("WSACreateEvent: %s", wsa_strerror(WSAGetLastError()));
               reportev("Could not WSACreateEvent");
               report_status(SERVICE_STOPPED, NO_ERROR, 0);
               return;
       }
       if(!WSAResetEvent(service_stop_event)) {
               log_err("WSAResetEvent: %s", wsa_strerror(WSAGetLastError()));
       }

       /* SetServiceStatus SERVICE_RUNNING;*/
       report_status(SERVICE_RUNNING, NO_ERROR, 0);
       verbose(VERB_QUERY, "winservice - init complete");

       /* daemon performs work */
       while(!service_stop_shutdown) {
               daemon_fork(daemon);
               if(!service_stop_shutdown) {
                       daemon_cleanup(daemon);
                       config_delete(cfg); cfg=NULL;
                       if(!service_init(1, &daemon, &cfg)) {
                               reportev("Could not service_init");
                               report_status(SERVICE_STOPPED, NO_ERROR, 0);
                               return;
                       }
               }
       }

       /* exit */
       verbose(VERB_ALGO, "winservice - cleanup.");
       report_status(SERVICE_STOP_PENDING, NO_ERROR, 0);
       if(service_stop_event) (void)WSACloseEvent(service_stop_event);
       service_deinit(daemon, cfg);
       free(service_cfgfile);
       verbose(VERB_QUERY, "winservice - full stop");
       report_status(SERVICE_STOPPED, NO_ERROR, 0);
}

/** start the service */
static void
service_start(const char* cfgfile, int v, int c)
{
       SERVICE_TABLE_ENTRY myservices[2] = {
               {SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION)service_main},
               {NULL, NULL} };
       verbosity=v;
       if(verbosity >= VERB_QUERY) {
               /* log to file about start sequence */
               fclose(fopen("C:\\unbound.log", "w"));
               log_init("C:\\unbound.log", 0, 0);
               verbose(VERB_QUERY, "open logfile");
       } else log_init(0, 1, 0); /* otherwise, use Application log */
       if(c) {
               service_cfgfile = strdup(cfgfile);
               if(!service_cfgfile) fatal_exit("out of memory");
       } else  service_cfgfile = NULL;
       service_cmdline_verbose = v;
       /* this call returns when service has stopped. */
       if(!StartServiceCtrlDispatcher(myservices)) {
               reportev("Could not StartServiceCtrlDispatcher");
       }
}

void
wsvc_command_option(const char* wopt, const char* cfgfile, int v, int c)
{
       if(strcmp(wopt, "install") == 0)
               wsvc_install(stdout, NULL);
       else if(strcmp(wopt, "remove") == 0)
               wsvc_remove(stdout);
       else if(strcmp(wopt, "service") == 0)
               service_start(cfgfile, v, c);
       else if(strcmp(wopt, "start") == 0)
               wsvc_rc_start(stdout);
       else if(strcmp(wopt, "stop") == 0)
               wsvc_rc_stop(stdout);
       else fatal_exit("unknown option: %s", wopt);
       exit(0);
}

void
worker_win_stop_cb(int ATTR_UNUSED(fd), short ATTR_UNUSED(ev), void* arg)
{
       struct worker* worker = (struct worker*)arg;
       verbose(VERB_QUERY, "caught stop signal (wsaevent)");
       worker->need_to_exit = 1;
       comm_base_exit(worker->base);
}

/** wait for cron process to finish */
static void
waitforit(PROCESS_INFORMATION* pinfo)
{
       DWORD ret = WaitForSingleObject(pinfo->hProcess, INFINITE);
       verbose(VERB_ALGO, "cronaction done");
       if(ret != WAIT_OBJECT_0) {
               return; /* did not end successfully */
       }
       if(!GetExitCodeProcess(pinfo->hProcess, &ret)) {
               log_err("GetExitCodeProcess failed");
               return;
       }
       verbose(VERB_ALGO, "exit code is %d", (int)ret);
       if(ret != 1) {
               if(!WSASetEvent(service_stop_event))
                       log_err("Could not WSASetEvent: %s",
                       wsa_strerror(WSAGetLastError()));
       }
}

/** Do the cron action and wait for result exit value */
static void*
win_do_cron(void* ATTR_UNUSED(arg))
{
       int mynum=65;
       char* cronaction;
       log_thread_set(&mynum);
       cronaction = lookup_reg_str("Software\\Unbound", "CronAction");
       if(cronaction && strlen(cronaction)>0) {
               STARTUPINFO sinfo;
               PROCESS_INFORMATION pinfo;
               memset(&pinfo, 0, sizeof(pinfo));
               memset(&sinfo, 0, sizeof(sinfo));
               sinfo.cb = sizeof(sinfo);
               verbose(VERB_ALGO, "cronaction: %s", cronaction);
               if(!CreateProcess(NULL, cronaction, NULL, NULL, 0,
                       CREATE_NO_WINDOW, NULL, NULL, &sinfo, &pinfo))
                       log_err("CreateProcess error");
               else {
                       waitforit(&pinfo);
                       CloseHandle(pinfo.hProcess);
                       CloseHandle(pinfo.hThread);
               }
       }
       free(cronaction);
       /* stop self */
       CloseHandle(cron_thread);
       cron_thread = NULL;
       return NULL;
}

/** Set the timer for cron for the next wake up */
static void
set_cron_timer(void)
{
       struct timeval tv;
       int crontime;
       if(cron_was_quick == 0) {
               cron_was_quick = 1;
               crontime = 3600; /* first update some time after boot */
       } else {
               crontime = lookup_reg_int("Software\\Unbound", "CronTime");
               if(crontime == 0) crontime = 60*60*24; /* 24 hours */
       }
       memset(&tv, 0, sizeof(tv));
       tv.tv_sec = (time_t)crontime;
       comm_timer_set(service_cron, &tv);
}

void
wsvc_cron_cb(void* arg)
{
       struct worker* worker = (struct worker*)arg;
       /* perform cronned operation */
       verbose(VERB_ALGO, "cron timer callback");
       if(cron_thread == NULL) {
               /* create new thread to do it */
               ub_thread_create(&cron_thread, win_do_cron, worker);
       }
       /* reschedule */
       set_cron_timer();
}

void wsvc_setup_worker(struct worker* worker)
{
       /* if not started with -w service, do nothing */
       if(!service_stop_event)
               return;
       if(!(service_stop_ev = ub_winsock_register_wsaevent(
               comm_base_internal(worker->base), service_stop_event,
               &worker_win_stop_cb, worker))) {
               fatal_exit("could not register wsaevent");
               return;
       }
       if(!service_cron) {
               service_cron = comm_timer_create(worker->base,
                       wsvc_cron_cb, worker);
               if(!service_cron)
                       fatal_exit("could not create cron timer");
               set_cron_timer();
       }
}

void wsvc_desetup_worker(struct worker* ATTR_UNUSED(worker))
{
       comm_timer_delete(service_cron);
       service_cron = NULL;
}