SmbConfig.cpp   [plain text]


/*
 * Copyright (c) 2007 Apple Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

#include "smb_server_prefs.h"
#include "macros.hpp"
#include "common.hpp"
#include "SmbConfig.hpp"

#include <sstream>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>

#define SMB_CONFIG_PATH		"/etc/smb.conf"
#define SMB_RUN_CONFIG_PATH	"/var/db/smb.conf"
#define SMB_RUN_TEMPLATE	"/var/db/.smb.conf.XXXXXXXX"

#define LAUNCHD_PLIST(name) ("/System/Library/LaunchDaemons/" name ".plist")

static const char config_header[] =
"#\n"
"# Configuration options for smbd(8), nmbd(8) and winbindd(8).\n"
"#\n"
"# This file is automatically generated, DO NOT EDIT!\n"
"#\n";

template<> SmbConfig::param_type
make_smb_param<std::string>(const std::string& key, const std::string& val)
{
    return SmbConfig::param_type(key, val);
}

template<> SmbConfig::param_type
make_smb_param<bool>(const std::string& key, const bool& val)
{
    return make_smb_param(key, val ? "yes" : "no");
}

static void
load_smbconf_metadata(SmbConfig::paramlist_type& params)
{
    int err;
    std::string	line;
    RegularExpression regex;
    std::ifstream smbconf(SMB_RUN_CONFIG_PATH);

    if (!smbconf) {
	return;
    }

    /* Config metadata must be of the form "# Param name : param value".
     */
    err = regex.compile("^# ([^ \t][^:]*)[ \t]*:[ \t]*([^ \t].*)[ \t]*$");
    if (err != 0) {
	VERBOSE("regex compilation error: %s\n", regex.errstring(err));
	return;
    }

    while (std::getline(smbconf, line)) {
	err = regex.match(line, 3);
	if (err != 0 && err != RegularExpression::NOMATCH) {
	    VERBOSE("regex match error: %s\n", regex.errstring(err));
	    return;
	}

	if (err == RegularExpression::NOMATCH) {
	    DEBUGMSG("no match for line '%s'\n", line.c_str());
	    continue;
	}

	if (regex.get_matches().size() != 3) {
	    DEBUGMSG("expected 3 matches, but got %zd\n",
		regex.get_matches().size());
	    continue;
	}

	/* Remember that match[0] is the string the entire regex matched. */
	params.push_back(make_smb_param(regex.get_matches()[1],
					regex.get_matches()[2]));

	VERBOSE("updated metadata param='%s' value='%s'\n",
		params.back().first.c_str(),
		params.back().second.c_str());
    }
}

SmbConfig::SmbConfig()
{
    /* Initialise service status defaults. Note that we require winbindd
     * by default.
     */
    this->m_smbd = new LaunchService("org.samba.smbd",
					LAUNCHD_PLIST("smbd"));

    this->m_nmbd = new LaunchService("org.samba.nmbd",
					LAUNCHD_PLIST("nmbd"));

    this->m_winbind = new LaunchService("org.samba.winbindd",
					LAUNCHD_PLIST("org.samba.winbindd"));

    this->m_smbd->required(false);
    this->m_nmbd->required(false);
    this->m_winbind->required(false);

    this->m_smbconf.insert(std::make_pair(GLOBAL, paramlist_type()));
    this->m_smbconf.insert(std::make_pair(PRINTER, paramlist_type()));
    this->m_smbconf.insert(std::make_pair(HOMES, paramlist_type()));
    this->m_smbconf.insert(std::make_pair(NETLOGON, paramlist_type()));
    this->m_smbconf.insert(std::make_pair(PROFILES, paramlist_type()));

    load_smbconf_metadata(this->m_lastmeta);
}

SmbConfig::~SmbConfig()
{
    delete this->m_smbd;
    delete this->m_nmbd;
    delete this->m_winbind;
}

template <class T> static T
find_by_name(T begin, T end, const SmbConfig::param_type& param)
{
    for (; begin != end; ++begin) {
	if (begin->first == param.first) {
	    return begin;
	}
    }

    return end;
}

void SmbConfig::set_param(section_type section, const param_type& param)
{
    /* XXX using a vector to the paramlist_type means that we don't uniquify
     * the parameters. This shouldn't matter.
     */
    this->m_smbconf[section].push_back(param);
}

void SmbConfig::set_meta(const param_type& param)
{
    /* XXX using a vector to the paramlist_type means that we don't uniquify
     * the parameters. This shouldn't matter.
     */
    this->m_metaconf.push_back(param);
}

bool SmbConfig::meta_changed(param_type& param) const
{
    SmbConfig::paramlist_type::const_iterator last;
    SmbConfig::paramlist_type::const_iterator current;

    last = find_by_name(this->m_lastmeta.begin(), this->m_lastmeta.end(),
			param);
    if (last == this->m_lastmeta.end()) {
	return true;
    }

    current = find_by_name(this->m_metaconf.begin(), this->m_metaconf.end(),
			param);
    if (last == this->m_metaconf.end()) {
	return true;
    }

    VERBOSE("last='%s' current='%s'\n",
	    last->second.c_str(), current->second.c_str());
    return current->second != last->second;
}

static void
format_paramlist(const char * section,
		SmbConfig::smbconf_type::const_iterator params,
		std::ostream& out)
{
    SmbConfig::paramlist_type::const_iterator p;
    std::ostringstream tmp;

    out << "\n[" << section << "]\n";

    for (p = params->second.begin(); p != params->second.end(); ++p) {
	/* If the parameter value formats to an empty string, don't emit it.
	 * It's possible in principle that a present but empty parameter might
	 * be necessary, but there aren't any cases of this right now, so let's
	 * not do it.
	 */
	tmp.str("");
	tmp << p->second;
	if (tmp.str().size() == 0) {
	    continue;
	}

	out << "\t" << p->first << " = " << tmp.str() << "\n";
    }

}

static void
format_metadata(const SmbConfig * smb,
		const SmbConfig::paramlist_type& meta,
		std::ostream& out)
{
    SmbConfig::paramlist_type::const_iterator p;

    for (p = meta.begin(); p != meta.end(); ++p) {
	out << "# " << p->first << ": " << p->second << "\n";
    }

    out << "# " << "Services required:";
    if (smb->SmbdService().required() ||
	smb->NmbdService().required() ||
	smb->WinbindService().required()) {
	if (smb->SmbdService().required()) {
	    out << ' ' << smb->SmbdService().label();
	}
	if (smb->NmbdService().required()) {
	    out << ' ' << smb->NmbdService().label();
	}
	if (smb->WinbindService().required()) {
	    out << ' ' << smb->WinbindService().label();
	}
    } else {
	out << " none";
    }

    out << "\n#\n";
}

static SmbConfig::paramlist_type::size_type
count(SmbConfig::smbconf_type::const_iterator params)
{
    return params->second.size();
}

void SmbConfig::format(std::ostream& out) const
{
    SmbConfig::smbconf_type::const_iterator params;

    out << config_header;
    format_metadata(this, this->m_metaconf, out);

    /* We should be able to use std::map::at() here, but GCC 4.0.1 doesn't
     * appear to have it. std::map::find is clunkier but equivalent. All this
     * jazz is necessary because operator[] is non-const;
     */

    /* NOTE: We have to guarantee that [global] is the first sectino formatted
     * because we don't know what the current section is when this config file
     * is included.
     */
    params = this->m_smbconf.find(GLOBAL);
    if (count(params) > 0) {
	format_paramlist("global", params, out);
    }

    params = this->m_smbconf.find(NETLOGON);
    if (count(params) > 0) {
	format_paramlist("netlogon", params, out);
    }

    params = this->m_smbconf.find(PROFILES);
    if (count(params) > 0) {
	format_paramlist("profiles", params, out);
    }

    params = this->m_smbconf.find(HOMES);
    if (count(params) > 0) {
	format_paramlist("homes", params, out);
    }

    params = this->m_smbconf.find(PRINTER);
    if (count(params) > 0) {
	format_paramlist("printers", params, out);
    }


    /* Make sure that [global] is the active section at the end of the file.
     * This guarantees that and directives following the inclusino of this file
     * will be in the global section.
     */
    out << "\n[global]\n";
    out.flush();
}

bool SmbConfig::writeback() const
{
    bool ret = false;
    int fd = -1;
    char * runpath = NULL;

    if ((runpath = ::strdup(SMB_RUN_TEMPLATE)) == NULL) {
	throw std::runtime_error("out of memory");
    }

    fd = ::mkstemp(runpath);
    if (fd == -1) {
	VERBOSE("failed to create temp file: %s\n",
		::strerror(errno));
	goto done;
    }

    /* In principle, we should unlink(2) here, but there's no way to rename an
     * open file descriptor, so we have to leave it.
     */

    /* mkstemp(3) creates with 0600 permissions which rename(2) preserves. We
     * prefer 0644 because it's easier to debug people's systems if you don't
     * have to be root to read the config file. There's no security issue here,
     * move along.
     */
    ::fchmod(fd, S_IRUSR | S_IWUSR  | S_IRGRP | S_IROTH);

    /* Format the config file into memory and then write it out. There's no
     * standard C++ way to construct an ostream from a fd. GCC has an
     * extension (stdio_filebuf), but since we are already playing on the BSD
     * layer, we might as well stay here.
     */
    try {
	std::ostringstream out;
	size_t remain;

	this->format(out);

	remain = out.str().size();
	while (remain) {
	    ssize_t err;
	    size_t begin = out.str().size() - remain;

	    err = write(fd, out.str().c_str() + begin, remain);
	    if (err == -1) {
		VERBOSE("failed to write config file: %s\n",
			strerror(errno));
		if (errno != EAGAIN && errno != EINTR) {
		    break;
		}
	    } else {
		remain -= err;
	    }
	}
	fsync(fd);
    } catch (...) {
	/* I'm being really paranoid here, because we are cleaning up
	 * resources by hand and can't leak.
	 */
	VERBOSE("caught exception writing config file\n");
	goto done;
    }

    if (::rename(runpath, SMB_RUN_CONFIG_PATH) == -1) {
	VERBOSE("failed to rename temp file: %s\n",
		::strerror(errno));
	goto done;
    }

    ret = true;

done:

    ::unlink(runpath);
    ::close(fd); /* Paranoia. The stdio_filebuf is supposed to do the close. */

    if (runpath) {
	::free(runpath);
    }

    return ret;
}

/* vim: set cindent ts=8 sts=4 tw=79 : */