1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2 
3 #include "remote/configpackageutility.hpp"
4 #include "remote/apilistener.hpp"
5 #include "base/application.hpp"
6 #include "base/exception.hpp"
7 #include "base/utility.hpp"
8 #include <boost/algorithm/string.hpp>
9 #include <boost/regex.hpp>
10 #include <algorithm>
11 #include <cctype>
12 #include <fstream>
13 
14 using namespace icinga;
15 
GetPackageDir()16 String ConfigPackageUtility::GetPackageDir()
17 {
18 	return Configuration::DataDir + "/api/packages";
19 }
20 
CreatePackage(const String & name)21 void ConfigPackageUtility::CreatePackage(const String& name)
22 {
23 	String path = GetPackageDir() + "/" + name;
24 
25 	if (Utility::PathExists(path))
26 		BOOST_THROW_EXCEPTION(std::invalid_argument("Package already exists."));
27 
28 	Utility::MkDirP(path, 0700);
29 	WritePackageConfig(name);
30 }
31 
DeletePackage(const String & name)32 void ConfigPackageUtility::DeletePackage(const String& name)
33 {
34 	String path = GetPackageDir() + "/" + name;
35 
36 	if (!Utility::PathExists(path))
37 		BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
38 
39 	ApiListener::Ptr listener = ApiListener::GetInstance();
40 
41 	/* config packages without API make no sense. */
42 	if (!listener)
43 		BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
44 
45 	listener->RemoveActivePackageStage(name);
46 
47 	Utility::RemoveDirRecursive(path);
48 	Application::RequestRestart();
49 }
50 
GetPackages()51 std::vector<String> ConfigPackageUtility::GetPackages()
52 {
53 	String packageDir = GetPackageDir();
54 
55 	std::vector<String> packages;
56 
57 	/* Package directory does not exist, no packages have been created thus far. */
58 	if (!Utility::PathExists(packageDir))
59 		return packages;
60 
61 	Utility::Glob(packageDir + "/*", [&packages](const String& path) { packages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
62 
63 	return packages;
64 }
65 
PackageExists(const String & name)66 bool ConfigPackageUtility::PackageExists(const String& name)
67 {
68 	auto packages (GetPackages());
69 	return std::find(packages.begin(), packages.end(), name) != packages.end();
70 }
71 
CreateStage(const String & packageName,const Dictionary::Ptr & files)72 String ConfigPackageUtility::CreateStage(const String& packageName, const Dictionary::Ptr& files)
73 {
74 	String stageName = Utility::NewUniqueID();
75 
76 	String path = GetPackageDir() + "/" + packageName;
77 
78 	if (!Utility::PathExists(path))
79 		BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
80 
81 	path += "/" + stageName;
82 
83 	Utility::MkDirP(path, 0700);
84 	Utility::MkDirP(path + "/conf.d", 0700);
85 	Utility::MkDirP(path + "/zones.d", 0700);
86 	WriteStageConfig(packageName, stageName);
87 
88 	bool foundDotDot = false;
89 
90 	if (files) {
91 		ObjectLock olock(files);
92 		for (const Dictionary::Pair& kv : files) {
93 			if (ContainsDotDot(kv.first)) {
94 				foundDotDot = true;
95 				break;
96 			}
97 
98 			String filePath = path + "/" + kv.first;
99 
100 			Log(LogInformation, "ConfigPackageUtility")
101 				<< "Updating configuration file: " << filePath;
102 
103 			// Pass the directory and generate a dir tree, if it does not already exist
104 			Utility::MkDirP(Utility::DirName(filePath), 0750);
105 			std::ofstream fp(filePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
106 			fp << kv.second;
107 			fp.close();
108 		}
109 	}
110 
111 	if (foundDotDot) {
112 		Utility::RemoveDirRecursive(path);
113 		BOOST_THROW_EXCEPTION(std::invalid_argument("Path must not contain '..'."));
114 	}
115 
116 	return stageName;
117 }
118 
WritePackageConfig(const String & packageName)119 void ConfigPackageUtility::WritePackageConfig(const String& packageName)
120 {
121 	String stageName = GetActiveStage(packageName);
122 
123 	String includePath = GetPackageDir() + "/" + packageName + "/include.conf";
124 	std::ofstream fpInclude(includePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
125 	fpInclude << "include \"*/include.conf\"\n";
126 	fpInclude.close();
127 
128 	String activePath = GetPackageDir() + "/" + packageName + "/active.conf";
129 	std::ofstream fpActive(activePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
130 	fpActive << "if (!globals.contains(\"ActiveStages\")) {\n"
131 		<< "  globals.ActiveStages = {}\n"
132 		<< "}\n"
133 		<< "\n"
134 		<< "if (globals.contains(\"ActiveStageOverride\")) {\n"
135 		<< "  var arr = ActiveStageOverride.split(\":\")\n"
136 		<< "  if (arr[0] == \"" << packageName << "\") {\n"
137 		<< "    if (arr.len() < 2) {\n"
138 		<< "      log(LogCritical, \"Config\", \"Invalid value for ActiveStageOverride\")\n"
139 		<< "    } else {\n"
140 		<< "      ActiveStages[\"" << packageName << "\"] = arr[1]\n"
141 		<< "    }\n"
142 		<< "  }\n"
143 		<< "}\n"
144 		<< "\n"
145 		<< "if (!ActiveStages.contains(\"" << packageName << "\")) {\n"
146 		<< "  ActiveStages[\"" << packageName << "\"] = \"" << stageName << "\"\n"
147 		<< "}\n";
148 	fpActive.close();
149 }
150 
WriteStageConfig(const String & packageName,const String & stageName)151 void ConfigPackageUtility::WriteStageConfig(const String& packageName, const String& stageName)
152 {
153 	String path = GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf";
154 	std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
155 	fp << "include \"../active.conf\"\n"
156 		<< "if (ActiveStages[\"" << packageName << "\"] == \"" << stageName << "\") {\n"
157 		<< "  include_recursive \"conf.d\"\n"
158 		<< "  include_zones \"" << packageName << "\", \"zones.d\"\n"
159 		<< "}\n";
160 	fp.close();
161 }
162 
ActivateStage(const String & packageName,const String & stageName)163 void ConfigPackageUtility::ActivateStage(const String& packageName, const String& stageName)
164 {
165 	SetActiveStage(packageName, stageName);
166 
167 	WritePackageConfig(packageName);
168 }
169 
TryActivateStageCallback(const ProcessResult & pr,const String & packageName,const String & stageName,bool activate,bool reload)170 void ConfigPackageUtility::TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName, bool activate, bool reload)
171 {
172 	String logFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/startup.log";
173 	std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
174 	fpLog << pr.Output;
175 	fpLog.close();
176 
177 	String statusFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/status";
178 	std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
179 	fpStatus << pr.ExitStatus;
180 	fpStatus.close();
181 
182 	/* validation went fine, activate stage and reload */
183 	if (pr.ExitStatus == 0) {
184 		if (activate) {
185 			{
186 				std::unique_lock<std::mutex> lock(GetStaticPackageMutex());
187 
188 				ActivateStage(packageName, stageName);
189 			}
190 
191 			if (reload)
192 				Application::RequestRestart();
193 		}
194 	} else {
195 		Log(LogCritical, "ConfigPackageUtility")
196 			<< "Config validation failed for package '"
197 			<< packageName << "' and stage '" << stageName << "'.";
198 	}
199 }
200 
AsyncTryActivateStage(const String & packageName,const String & stageName,bool activate,bool reload)201 void ConfigPackageUtility::AsyncTryActivateStage(const String& packageName, const String& stageName, bool activate, bool reload)
202 {
203 	VERIFY(Application::GetArgC() >= 1);
204 
205 	// prepare arguments
206 	Array::Ptr args = new Array({
207 		Application::GetExePath(Application::GetArgV()[0]),
208 	});
209 
210 	// copy all arguments of parent process
211 	for (int i = 1; i < Application::GetArgC(); i++) {
212 		String argV = Application::GetArgV()[i];
213 
214 		if (argV == "-d" || argV == "--daemonize")
215 			continue;
216 
217 		args->Add(argV);
218 	}
219 
220 	// add arguments for validation
221 	args->Add("--validate");
222 	args->Add("--define");
223 	args->Add("ActiveStageOverride=" + packageName + ":" + stageName);
224 
225 	Process::Ptr process = new Process(Process::PrepareCommand(args));
226 	process->SetTimeout(Application::GetReloadTimeout());
227 	process->Run([packageName, stageName, activate, reload](const ProcessResult& pr) { TryActivateStageCallback(pr, packageName, stageName, activate, reload); });
228 }
229 
DeleteStage(const String & packageName,const String & stageName)230 void ConfigPackageUtility::DeleteStage(const String& packageName, const String& stageName)
231 {
232 	String path = GetPackageDir() + "/" + packageName + "/" + stageName;
233 
234 	if (!Utility::PathExists(path))
235 		BOOST_THROW_EXCEPTION(std::invalid_argument("Stage does not exist."));
236 
237 	if (GetActiveStage(packageName) == stageName)
238 		BOOST_THROW_EXCEPTION(std::invalid_argument("Active stage cannot be deleted."));
239 
240 	Utility::RemoveDirRecursive(path);
241 }
242 
GetStages(const String & packageName)243 std::vector<String> ConfigPackageUtility::GetStages(const String& packageName)
244 {
245 	std::vector<String> stages;
246 	Utility::Glob(GetPackageDir() + "/" + packageName + "/*", [&stages](const String& path) { stages.emplace_back(Utility::BaseName(path)); }, GlobDirectory);
247 	return stages;
248 }
249 
GetActiveStageFromFile(const String & packageName)250 String ConfigPackageUtility::GetActiveStageFromFile(const String& packageName)
251 {
252 	/* Lock the transaction, reading this only happens on startup or when something really is broken. */
253 	std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
254 
255 	String path = GetPackageDir() + "/" + packageName + "/active-stage";
256 
257 	std::ifstream fp;
258 	fp.open(path.CStr());
259 
260 	String stage;
261 	std::getline(fp, stage.GetData());
262 
263 	fp.close();
264 
265 	if (fp.fail())
266 		return ""; /* Don't use exceptions here. The caller must deal with empty stages at this point. Happens on initial package creation for example. */
267 
268 	return stage.Trim();
269 }
270 
SetActiveStageToFile(const String & packageName,const String & stageName)271 void ConfigPackageUtility::SetActiveStageToFile(const String& packageName, const String& stageName)
272 {
273 	std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
274 
275 	String activeStagePath = GetPackageDir() + "/" + packageName + "/active-stage";
276 
277 	std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); //TODO: fstream exceptions
278 	fpActiveStage << stageName;
279 	fpActiveStage.close();
280 }
281 
GetActiveStage(const String & packageName)282 String ConfigPackageUtility::GetActiveStage(const String& packageName)
283 {
284 	String activeStage;
285 
286 	ApiListener::Ptr listener = ApiListener::GetInstance();
287 
288 	/* If we don't have an API feature, just use the file storage without caching this.
289 	 * This happens when ScheduledDowntime objects generate Downtime objects.
290 	 * TODO: Make the API a first class citizen.
291 	 */
292 	if (!listener)
293 		return GetActiveStageFromFile(packageName);
294 
295 	/* First use runtime state. */
296 	try {
297 		activeStage = listener->GetActivePackageStage(packageName);
298 	} catch (const std::exception& ex) {
299 		/* Fallback to reading the file, happens on restarts. */
300 		activeStage = GetActiveStageFromFile(packageName);
301 
302 		/* When we've read something, correct memory. */
303 		if (!activeStage.IsEmpty())
304 			listener->SetActivePackageStage(packageName, activeStage);
305 	}
306 
307 	return activeStage;
308 }
309 
SetActiveStage(const String & packageName,const String & stageName)310 void ConfigPackageUtility::SetActiveStage(const String& packageName, const String& stageName)
311 {
312 	/* Update the marker on disk for restarts. */
313 	SetActiveStageToFile(packageName, stageName);
314 
315 	ApiListener::Ptr listener = ApiListener::GetInstance();
316 
317 	/* No API, no caching. */
318 	if (!listener)
319 		return;
320 
321 	listener->SetActivePackageStage(packageName, stageName);
322 }
323 
GetFiles(const String & packageName,const String & stageName)324 std::vector<std::pair<String, bool> > ConfigPackageUtility::GetFiles(const String& packageName, const String& stageName)
325 {
326 	std::vector<std::pair<String, bool> > paths;
327 	Utility::GlobRecursive(GetPackageDir() + "/" + packageName + "/" + stageName, "*", [&paths](const String& path) {
328 		CollectPaths(path, paths);
329 	}, GlobDirectory | GlobFile);
330 
331 	return paths;
332 }
333 
CollectPaths(const String & path,std::vector<std::pair<String,bool>> & paths)334 void ConfigPackageUtility::CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths)
335 {
336 #ifndef _WIN32
337 	struct stat statbuf;
338 	int rc = lstat(path.CStr(), &statbuf);
339 	if (rc < 0)
340 		BOOST_THROW_EXCEPTION(posix_error()
341 			<< boost::errinfo_api_function("lstat")
342 			<< boost::errinfo_errno(errno)
343 			<< boost::errinfo_file_name(path));
344 
345 	paths.emplace_back(path, S_ISDIR(statbuf.st_mode));
346 #else /* _WIN32 */
347 	struct _stat statbuf;
348 	int rc = _stat(path.CStr(), &statbuf);
349 	if (rc < 0)
350 		BOOST_THROW_EXCEPTION(posix_error()
351 			<< boost::errinfo_api_function("_stat")
352 			<< boost::errinfo_errno(errno)
353 			<< boost::errinfo_file_name(path));
354 
355 	paths.emplace_back(path, ((statbuf.st_mode & S_IFMT) == S_IFDIR));
356 #endif /* _WIN32 */
357 }
358 
ContainsDotDot(const String & path)359 bool ConfigPackageUtility::ContainsDotDot(const String& path)
360 {
361 	std::vector<String> tokens = path.Split("/\\");
362 
363 	for (const String& part : tokens) {
364 		if (part == "..")
365 			return true;
366 	}
367 
368 	return false;
369 }
370 
ValidatePackageName(const String & packageName)371 bool ConfigPackageUtility::ValidatePackageName(const String& packageName)
372 {
373 	return ValidateFreshName(packageName) || PackageExists(packageName);
374 }
375 
ValidateFreshName(const String & name)376 bool ConfigPackageUtility::ValidateFreshName(const String& name)
377 {
378 	if (name.IsEmpty())
379 		return false;
380 
381 	/* check for path injection */
382 	if (ContainsDotDot(name))
383 		return false;
384 
385 	return std::all_of(name.Begin(), name.End(), [](char c) {
386 		return std::isalnum(c, std::locale::classic()) || c == '_' || c == '-';
387 	});
388 }
389 
GetStaticPackageMutex()390 std::mutex& ConfigPackageUtility::GetStaticPackageMutex()
391 {
392 	static std::mutex mutex;
393 	return mutex;
394 }
395 
GetStaticActiveStageMutex()396 std::mutex& ConfigPackageUtility::GetStaticActiveStageMutex()
397 {
398 	static std::mutex mutex;
399 	return mutex;
400 }
401