1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <cf3.defs.h>
26 #include <cf-execd-runner.h>
27 
28 #include <files_names.h>
29 #include <files_interfaces.h>
30 #include <string_lib.h>
31 #include <pipes.h>
32 #include <unix.h>
33 #include <mutex.h>
34 #include <global_mutex.h>
35 #include <signals.h>
36 #include <exec_tools.h>
37 #include <misc_lib.h>
38 #include <file_lib.h>
39 #include <assert.h>
40 #include <crypto.h>
41 #include <known_dirs.h>
42 #include <bootstrap.h>
43 #include <policy_server.h>
44 #include <hash.h>
45 #include <item_lib.h>
46 #include <regex.h>              /* StringMatchFullWithPrecompiledRegex() */
47 #include <processes_select.h>   /* LoadProcessTable()/SelectProcesses() */
48 
49 #include <cf-windows-functions.h>
50 
51 /*******************************************************************/
52 
53 static const int INF_LINES = -2;
54 
55 /*******************************************************************/
56 
57 static void MailResult(const ExecConfig *config, const char *file);
58 static bool Dialogue(int sd, const char *s);
59 
60 /******************************************************************************/
61 
62 # if defined(__MINGW32__)
63 
ThreadUniqueName(void)64 static void *ThreadUniqueName(void)
65 {
66     return pthread_self().p;
67 }
68 
69 # else /* __MINGW32__ */
70 
ThreadUniqueName(void)71 static void *ThreadUniqueName(void)
72 {
73     return (void *)pthread_self();
74 }
75 
76 # endif /* __MINGW32__ */
77 
TwinFilename(void)78 static const char *TwinFilename(void)
79 {
80 #if defined(_WIN32)
81     return "bin-twin/cf-agent.exe";
82 #else
83     return "bin/cf-twin";
84 #endif
85 }
86 
AgentFilename(void)87 static const char *AgentFilename(void)
88 {
89 #if defined(_WIN32)
90     return "bin/cf-agent.exe";
91 #else
92     return "bin/cf-agent";
93 #endif
94 }
95 
TwinExists(void)96 static bool TwinExists(void)
97 {
98     char twinfilename[CF_BUFSIZE];
99     struct stat sb;
100 
101     snprintf(twinfilename, CF_BUFSIZE, "%s/%s", GetWorkDir(), TwinFilename());
102 
103     MapName(twinfilename);
104 
105     return (stat(twinfilename, &sb) == 0) && (IsExecutable(twinfilename));
106 }
107 
108 /* Buffer has to be at least CF_BUFSIZE bytes long */
ConstructFailsafeCommand(bool scheduled_run,char * buffer)109 static void ConstructFailsafeCommand(bool scheduled_run, char *buffer)
110 {
111     bool twin_exists = TwinExists();
112 
113     const char* const workdir = GetWorkDir();
114 
115     snprintf(buffer, CF_BUFSIZE,
116              "\"%s%c%s\" -f failsafe.cf "
117              "&& \"%s%c%s\" -Dfrom_cfexecd%s",
118              workdir, FILE_SEPARATOR, twin_exists ? TwinFilename() : AgentFilename(),
119              workdir, FILE_SEPARATOR, AgentFilename(), scheduled_run ? ",scheduled_run" : "");
120 }
121 
122 #ifndef __MINGW32__
123 
124 #if defined(__hpux) && defined(__GNUC__)
125 #pragma GCC diagnostic ignored "-Wstrict-aliasing"
126 // Avoid spurious HP-UX GCC type-pun warning on FD_SET() macro
127 #endif
128 
IsReadReady(int fd,int timeout_sec)129 static bool IsReadReady(int fd, int timeout_sec)
130 {
131     fd_set  rset;
132     FD_ZERO(&rset);
133     FD_SET(fd, &rset);
134 
135     struct timeval tv = {
136         .tv_sec = timeout_sec,
137         .tv_usec = 0,
138     };
139 
140     int ret = select(fd + 1, &rset, NULL, NULL, &tv);
141 
142     if(ret < 0)
143     {
144         Log(LOG_LEVEL_ERR, "IsReadReady: Failed checking for data. (select: %s)", GetErrorStr());
145         return false;
146     }
147 
148     if(FD_ISSET(fd, &rset))
149     {
150         return true;
151     }
152 
153     if(ret == 0)  // timeout
154     {
155         return false;
156     }
157 
158     // can we get here?
159     Log(LOG_LEVEL_ERR, "IsReadReady: Unknown outcome (ret > 0 but our only fd is not set). (select: %s)", GetErrorStr());
160 
161     return false;
162 }
163 #if defined(__hpux) && defined(__GNUC__)
164 #pragma GCC diagnostic warning "-Wstrict-aliasing"
165 #endif
166 
167 #endif  /* __MINGW32__ */
168 
LocalExec(const ExecConfig * config)169 void LocalExec(const ExecConfig *config)
170 {
171     time_t starttime = time(NULL);
172 
173     void *thread_name = ThreadUniqueName();
174 
175     {
176         char starttime_str[64];
177         cf_strtimestamp_local(starttime, starttime_str);
178 
179         Log(LOG_LEVEL_VERBOSE, "----------------------------------------------------------------");
180         Log(LOG_LEVEL_VERBOSE, "  LocalExec(%sscheduled) at %s", config->scheduled_run ? "" : "not ", starttime_str);
181         Log(LOG_LEVEL_VERBOSE, "----------------------------------------------------------------");
182     }
183 
184 /* Need to make sure we have LD_LIBRARY_PATH here or children will die  */
185 
186     char cmd[CF_BUFSIZE];
187     if (strlen(config->exec_command) > 0)
188     {
189         strlcpy(cmd, config->exec_command, CF_BUFSIZE);
190     }
191     else
192     {
193         ConstructFailsafeCommand(config->scheduled_run, cmd);
194     }
195 
196     char esc_command[CF_BUFSIZE];
197     strlcpy(esc_command, MapName(cmd), CF_BUFSIZE);
198 
199 
200     char filename[CF_BUFSIZE];
201     {
202         // 2 underscores, longest 64 bit integer, -1 for NUL byte, 26 for ctime (including NUL)
203         char line[2 + sizeof("-9223372036854775808") - 1 + 26];
204         snprintf(line, sizeof(line), "_%jd_%s", (intmax_t) starttime, CanonifyName(ctime(&starttime)));
205         {
206             char canonified_fq_name[sizeof(VFQNAME)];
207 
208             strlcpy(canonified_fq_name, config->fq_name, sizeof(canonified_fq_name));
209             CanonifyNameInPlace(canonified_fq_name);
210 
211             snprintf(filename, CF_BUFSIZE, "%s/outputs/cf_%s_%s_%p",
212                      GetWorkDir(), canonified_fq_name, line, thread_name);
213 
214             MapName(filename);
215         }
216     }
217 
218 
219 /* What if no more processes? Could sacrifice and exec() - but we need a sentinel */
220 
221     FILE *fp = safe_fopen(filename, "w");
222     if (fp == NULL)
223     {
224         Log(LOG_LEVEL_ERR, "Couldn't open '%s' - aborting exec. (fopen: %s)", filename, GetErrorStr());
225         return;
226     }
227 
228 /*
229  * Don't inherit this file descriptor on fork/exec
230  */
231 
232     if (fileno(fp) != -1)
233     {
234         SetCloseOnExec(fileno(fp), true);
235     }
236 
237     Log(LOG_LEVEL_VERBOSE, "Command => %s", cmd);
238 
239     FILE *pp = cf_popen_sh(esc_command, "r");
240     if (!pp)
241     {
242         Log(LOG_LEVEL_ERR, "Couldn't open pipe to command '%s'. (cf_popen: %s)", cmd, GetErrorStr());
243         fclose(fp);
244         return;
245     }
246 
247     Log(LOG_LEVEL_VERBOSE, "Command is executing...%s", esc_command);
248 
249     int count = 0;
250     int complete = false;
251     size_t line_size = CF_BUFSIZE;
252     char *line = xmalloc(line_size);
253 
254     while (!IsPendingTermination())
255     {
256         if (!IsReadReady(fileno(pp),
257                          config->agent_expireafter * SECONDS_PER_MINUTE))
258         {
259             char errmsg[] =
260                 "cf-execd: timeout waiting for output from agent"
261                 " (agent_expireafter=%d) - terminating it\n";
262 
263             fprintf(fp, errmsg, config->agent_expireafter);
264             /* Trim '\n' before Log()ing. */
265             errmsg[strlen(errmsg) - 1] = '\0';
266             Log(LOG_LEVEL_NOTICE, errmsg, config->agent_expireafter);
267             count++;
268 
269             pid_t pid_shell;
270 
271             if (PipeToPid(&pid_shell, pp))
272             {
273                 /* Default to killing the shell process (if we fail to get
274                  * more precise target). */
275                 pid_t pid_to_kill = pid_shell;
276 
277 #ifndef __MINGW32__
278                 /* The agent command is executed in a shell. Trying to kill the
279                  * shell may end up sending it SIGKILL which is not propagated
280                  * to the subprocesses of the shell and thus the cf-agent
281                  * process. The shell, however, creates a new process group
282                  * (with the PGID equal to the PID of the child process) for the
283                  * agent which then allows us to kill the whole process group
284                  * here. */
285 
286                 /* We need to determine the PID of the agent (and thus its
287                  * process group) first.*/
288                 ClearProcessTable();
289                 if (LoadProcessTable())
290                 {
291                     ProcessSelect ps = PROCESS_SELECT_INIT;
292                     ps.min_ppid = pid_shell;
293                     ps.max_ppid = pid_shell;
294                     Item *procs = SelectProcesses(".*" /* any command */, &ps, true /* apply ps */);
295                     if (procs != NULL)
296                     {
297                         pid_to_kill = procs->counter;
298 
299                         /* There should only be one child process of the shell
300                          * running by default. But it doesn't apply to all
301                          * values of exec_command in general. */
302                         assert(procs->next == NULL);
303                     }
304                 }
305 
306                 /* kill(-pid) is actually kill(pgid=pid) and kill(pid, 0) just
307                  * checks if it's possible to send signals to pid (or pgid in
308                  * our case). Kill the whole process group if possible. */
309                 if ((getpgid(pid_to_kill) == pid_to_kill)
310                     && (kill(-pid_to_kill, 0) == 0))
311                 {
312                     pid_to_kill = -pid_to_kill;
313                 }
314 #endif
315                 ProcessSignalTerminate(pid_to_kill);
316             }
317             else
318             {
319                 Log(LOG_LEVEL_ERR, "Could not get PID of agent");
320             }
321 
322             break;
323         }
324 
325         ssize_t res = CfReadLine(&line, &line_size, pp);
326         if (res == -1)
327         {
328             if (feof(pp))
329             {
330                 complete = true;
331             }
332             else
333             {
334                 Log(LOG_LEVEL_ERR,
335                     "Unable to read output from command '%s'. (cfread: %s)",
336                     cmd, GetErrorStr());
337             }
338             break;
339         }
340 
341         const char *sp = line;
342         while (*sp != '\0' && isspace(*sp))
343         {
344             sp++;
345         }
346 
347         if (*sp != '\0') /* line isn't entirely blank */
348         {
349             /* Need space for 2x buffer and null byte. */
350             char line_escaped[res * 2 + 1];
351             memcpy(line_escaped, line, res + 1);
352 
353             ssize_t replace_res =
354                 StringReplace(line_escaped, sizeof(line_escaped), "%", "%%");
355             if (replace_res == -1)
356             {
357                 ProgrammingError("StringReplace(): buffer overflow in %s",
358                                  __FILE__);
359                 line[0] = '\0';
360                 continue;
361             }
362 
363             fprintf(fp, "%s\n", line_escaped);
364             count++;
365 
366             /* If we can't send mail, log to syslog */
367 
368             if (strlen(config->mail_to_address) == 0)
369             {
370                 strncat(line_escaped, "\n", sizeof(line_escaped) - 1 - strlen(line_escaped));
371                 if ((strchr(line_escaped, '\n')) == NULL)
372                 {
373                     line_escaped[sizeof(line_escaped) - 2] = '\n';
374                 }
375 
376                 Log(LOG_LEVEL_INFO, "%s", line_escaped);
377             }
378 
379             line[0] = '\0';
380         }
381     }
382 
383     free(line);
384     cf_pclose(pp);
385     Log(LOG_LEVEL_VERBOSE,
386         complete ? "Command is complete" : "Terminated command");
387 
388     if (count)
389     {
390         Log(LOG_LEVEL_DEBUG, "Closing fp");
391         fclose(fp);
392         Log(LOG_LEVEL_VERBOSE, "Mailing result");
393         MailResult(config, filename);
394     }
395     else
396     {
397         Log(LOG_LEVEL_VERBOSE, "No output");
398         unlink(filename);
399         fclose(fp);
400     }
401 }
402 
403 // Returns true if line is filtered, IOW should not be included.
LineIsFiltered(const ExecConfig * config,const char * line)404 static bool LineIsFiltered(const ExecConfig *config,
405                            const char *line)
406 {
407     // Check whether the line matches mail filters
408     int include_filter_len = SeqLength(config->mailfilter_include_regex);
409     int exclude_filter_len = SeqLength(config->mailfilter_exclude_regex);
410     // Count messages as matched in include set if there is no include set.
411     bool included = (include_filter_len == 0);
412     bool excluded = false;
413     if (include_filter_len > 0)
414     {
415         for (int i = 0; i < include_filter_len; i++)
416         {
417             if (StringMatchFullWithPrecompiledRegex(SeqAt(config->mailfilter_include_regex, i),
418                                                     line))
419             {
420                 included = true;
421             }
422         }
423     }
424     if (exclude_filter_len > 0)
425     {
426         for (int i = 0; i < exclude_filter_len; i++)
427         {
428             if (StringMatchFullWithPrecompiledRegex(SeqAt(config->mailfilter_exclude_regex, i),
429                                                     line))
430             {
431                 excluded = true;
432             }
433         }
434     }
435     return !included || excluded;
436 }
437 
CompareResultEqualOrFiltered(const ExecConfig * config,const char * filename,const char * prev_file)438 static bool CompareResultEqualOrFiltered(const ExecConfig *config,
439                                          const char *filename,
440                                          const char *prev_file)
441 {
442     Log(LOG_LEVEL_VERBOSE, "Comparing files  %s with %s", prev_file, filename);
443 
444     bool rtn = true;
445 
446     FILE *old_fp = safe_fopen(prev_file, "r");
447     FILE *new_fp = safe_fopen(filename, "r");
448     if (new_fp)
449     {
450         const char *errptr;
451         int erroffset;
452         pcre_extra *regex_extra = NULL;
453         // Match timestamps and remove them. Not Y21K safe! :-)
454         pcre *regex = pcre_compile(LOGGING_TIMESTAMP_REGEX, PCRE_MULTILINE, &errptr, &erroffset, NULL);
455         if (!regex)
456         {
457             UnexpectedError("Compiling regular expression failed");
458             rtn = false;
459         }
460         else
461         {
462             regex_extra = pcre_study(regex, 0, &errptr);
463         }
464 
465         size_t old_line_size = CF_BUFSIZE;
466         char *old_line = xmalloc(old_line_size);
467         size_t new_line_size = CF_BUFSIZE;
468         char *new_line = xmalloc(new_line_size);
469         bool any_new_msg_present = false;
470         while (regex)
471         {
472             char *old_msg = NULL;
473             if (old_fp)
474             {
475                 while (CfReadLine(&old_line, &old_line_size, old_fp) >= 0)
476                 {
477                     if (!LineIsFiltered(config, old_line))
478                     {
479                         old_msg = old_line;
480                         break;
481                     }
482                 }
483             }
484 
485             char *new_msg = NULL;
486             while (CfReadLine(&new_line, &new_line_size, new_fp) >= 0)
487             {
488                 if (!LineIsFiltered(config, new_line))
489                 {
490                     any_new_msg_present = true;
491                     new_msg = new_line;
492                     break;
493                 }
494             }
495 
496             if (!old_msg || !new_msg)
497             {
498                 // Return difference in most cases, when there is a new
499                 // message line, but if there isn't one, return equal, even
500                 // if strictly speaking they aren't, since we don't want to
501                 // send an empty email.
502                 if (any_new_msg_present && old_msg != new_msg)
503                 {
504                     rtn = false;
505                 }
506                 break;
507             }
508 
509             // Remove timestamps from lines before comparison.
510             char *index;
511             if (pcre_exec(regex, regex_extra, old_msg, strlen(old_msg), 0, 0, NULL, 0) >= 0)
512             {
513                 index = strstr(old_msg, ": ");
514                 if (index != NULL)
515                 {
516                     old_msg = index + 2;
517                 }
518             }
519             if (pcre_exec(regex, regex_extra, new_msg, strlen(new_msg), 0, 0, NULL, 0) >= 0)
520             {
521                 index = strstr(new_msg, ": ");
522                 if (index != NULL)
523                 {
524                     new_msg = index + 2;
525                 }
526             }
527 
528             if (strcmp(old_msg, new_msg) != 0)
529             {
530                 rtn = false;
531                 break;
532             }
533         }
534 
535         free(old_line);
536         free(new_line);
537 
538         if (regex_extra)
539         {
540             free(regex_extra);
541         }
542         pcre_free(regex);
543     }
544     else
545     {
546         /* no previous file */
547         rtn = false;
548     }
549 
550     if (old_fp)
551     {
552         fclose(old_fp);
553     }
554     if (new_fp)
555     {
556         fclose(new_fp);
557     }
558 
559     ThreadLock(cft_count);
560 
561 /* replace old file with new*/
562 
563     unlink(prev_file);
564 
565     if (!LinkOrCopy(filename, prev_file, true))
566     {
567         Log(LOG_LEVEL_INFO, "Could not symlink or copy '%s' to '%s'", filename, prev_file);
568         rtn = false;
569     }
570 
571     ThreadUnlock(cft_count);
572     return rtn;
573 }
574 
575 #ifndef TEST_CF_EXECD
ConnectToSmtpSocket(const ExecConfig * config)576 int ConnectToSmtpSocket(const ExecConfig *config)
577 {
578     struct hostent *hp = gethostbyname(config->mail_server);
579     if (!hp)
580     {
581         Log(LOG_LEVEL_ERR, "Mail report: unknown host '%s' ('smtpserver' in body executor control). Make sure that fully qualified names can be looked up at your site.",
582                 config->mail_server);
583         return -1;
584     }
585 
586     struct servent *server = getservbyname("smtp", "tcp");
587     if (!server)
588     {
589         Log(LOG_LEVEL_ERR, "Mail report: unable to lookup smtp service. (getservbyname: %s)", GetErrorStr());
590         return -1;
591     }
592 
593     struct sockaddr_in raddr;
594     memset(&raddr, 0, sizeof(raddr));
595 
596     raddr.sin_port = (unsigned int) server->s_port;
597     raddr.sin_addr.s_addr = ((struct in_addr *) (hp->h_addr))->s_addr;
598     raddr.sin_family = AF_INET;
599 
600     Log(LOG_LEVEL_DEBUG, "Mail report: connecting...");
601 
602     int sd = socket(AF_INET, SOCK_STREAM, 0);
603     if (sd == -1)
604     {
605         Log(LOG_LEVEL_ERR, "Mail report: couldn't open a socket. (socket: %s)", GetErrorStr());
606         return -1;
607     }
608 
609     if (connect(sd, (void *) &raddr, sizeof(raddr)) == -1)
610     {
611         Log(LOG_LEVEL_ERR, "Mail report: couldn't connect to host '%s'. (connect: %s)",
612             config->mail_server, GetErrorStr());
613         cf_closesocket(sd);
614         return -1;
615     }
616 
617     return sd;
618 }
619 #endif // !TEST_CF_EXECD
620 
MailResult(const ExecConfig * config,const char * file)621 static void MailResult(const ExecConfig *config, const char *file)
622 {
623     /* Must be initialised to NULL, so that free(line) works
624        when  goto mail_err. */
625     char *line = NULL;
626 
627 #if defined __linux__ || defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__
628     time_t now = time(NULL);
629 #endif
630 
631     FILE *fp = safe_fopen(file, "r");
632     if (fp == NULL)
633     {
634         Log(LOG_LEVEL_ERR, "Mail report: couldn't open file '%s'. (fopen: %s)", file, GetErrorStr());
635         return;
636     }
637 
638     Log(LOG_LEVEL_VERBOSE, "Mail report: sending result...");
639 
640     {
641         int fd = fileno(fp);
642         struct stat statbuf;
643         if (fstat(fd, &statbuf) == -1)
644         {
645             Log(LOG_LEVEL_ERR, "Mail report: failed to stat file '%s' [errno: %d]", file, errno);
646             fclose(fp);
647             return;
648         }
649 
650         if (statbuf.st_size == 0)
651         {
652             unlink(file);
653             fclose(fp);
654             Log(LOG_LEVEL_DEBUG, "Mail report: nothing to report in file '%s'", file);
655             return;
656         }
657     }
658 
659     {
660         char prev_file[CF_BUFSIZE];
661         snprintf(prev_file, CF_BUFSIZE, "%s/outputs/previous", GetWorkDir());
662         MapName(prev_file);
663 
664         if (CompareResultEqualOrFiltered(config, file, prev_file))
665         {
666             Log(LOG_LEVEL_VERBOSE, "Mail report: previous output is the same as current so do not mail it");
667             fclose(fp);
668             return;
669         }
670     }
671 
672     if ((strlen(config->mail_server) == 0) || (strlen(config->mail_to_address) == 0))
673     {
674         /* Syslog should have done this */
675         Log(LOG_LEVEL_VERBOSE, "Mail report: empty mail server or address - skipping");
676         fclose(fp);
677         return;
678     }
679 
680     if (config->mail_max_lines == 0)
681     {
682         Log(LOG_LEVEL_DEBUG, "Mail report: not mailing because EmailMaxLines was zero");
683         fclose(fp);
684         return;
685     }
686 
687     Log(LOG_LEVEL_DEBUG, "Mail report: mailing results of '%s' to '%s'", file, config->mail_to_address);
688 
689     int sd = ConnectToSmtpSocket(config);
690     if (sd < 0)
691     {
692         fclose(fp);
693         return;
694     }
695 
696 /* read greeting */
697 
698     if (!Dialogue(sd, NULL))
699     {
700         goto mail_err;
701     }
702 
703     char vbuff[CF_BUFSIZE];
704     snprintf(vbuff, sizeof(vbuff), "HELO %s\r\n", config->fq_name);
705     Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
706 
707     if (!Dialogue(sd, vbuff))
708     {
709         goto mail_err;
710     }
711 
712     if (strlen(config->mail_from_address) == 0)
713     {
714         snprintf(vbuff, sizeof(vbuff), "MAIL FROM: <cfengine@%s>\r\n",
715                  config->fq_name);
716         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
717     }
718     else
719     {
720         snprintf(vbuff, sizeof(vbuff), "MAIL FROM: <%s>\r\n",
721                  config->mail_from_address);
722         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
723     }
724 
725     if (!Dialogue(sd, vbuff))
726     {
727         goto mail_err;
728     }
729 
730     snprintf(vbuff, sizeof(vbuff), "RCPT TO: <%s>\r\n",
731              config->mail_to_address);
732     Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
733 
734     if (!Dialogue(sd, vbuff))
735     {
736         goto mail_err;
737     }
738 
739     if (!Dialogue(sd, "DATA\r\n"))
740     {
741         goto mail_err;
742     }
743 
744     if (SafeStringLength(config->mail_subject) == 0)
745     {
746         snprintf(vbuff, sizeof(vbuff), "Subject: [%s/%s]\r\n", config->fq_name, config->ip_address);
747         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
748     }
749     else
750     {
751         snprintf(vbuff, sizeof(vbuff), "Subject: %s\r\n", config->mail_subject);
752         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
753     }
754 
755     send(sd, vbuff, strlen(vbuff), 0);
756 
757     /* Send X-CFEngine SMTP header */
758     unsigned char digest[EVP_MAX_MD_SIZE + 1];
759     char buffer[CF_HOSTKEY_STRING_SIZE];
760 
761     char *existing_policy_server = PolicyServerReadFile(GetWorkDir());
762     if (!existing_policy_server)
763     {
764         existing_policy_server = xstrdup("(none)");
765     }
766 
767     HashPubKey(PUBKEY, digest, CF_DEFAULT_DIGEST);
768 
769     snprintf(vbuff, sizeof(vbuff),
770              "X-CFEngine: vfqhost=\"%s\";ip-addresses=\"%s\";policyhub=\"%s\";pkhash=\"%s\"\r\n",
771              VFQNAME, config->ip_addresses, existing_policy_server,
772              HashPrintSafe(buffer, sizeof(buffer), digest, CF_DEFAULT_DIGEST, true));
773 
774     send(sd, vbuff, strlen(vbuff), 0);
775     free(existing_policy_server);
776 
777 #if defined __linux__ || defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__
778     {
779         struct tm tm_value;
780         struct tm *const tm_pointer = localtime_r(&now, &tm_value);
781         strftime(vbuff, CF_BUFSIZE, "Date: %a, %d %b %Y %H:%M:%S %z\r\n", tm_pointer);
782     }
783     send(sd, vbuff, strlen(vbuff), 0);
784 #endif
785 
786     if (strlen(config->mail_from_address) == 0)
787     {
788         snprintf(vbuff, sizeof(vbuff), "From: cfengine@%s\r\n",
789                  config->fq_name);
790         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
791     }
792     else
793     {
794         snprintf(vbuff, sizeof(vbuff), "From: %s\r\n",
795                  config->mail_from_address);
796         Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
797     }
798 
799     send(sd, vbuff, strlen(vbuff), 0);
800 
801     snprintf(vbuff, sizeof(vbuff), "To: %s\r\n\r\n", config->mail_to_address);
802     Log(LOG_LEVEL_DEBUG, "Mail report: %s", vbuff);
803     send(sd, vbuff, strlen(vbuff), 0);
804 
805     size_t line_size = CF_BUFSIZE;
806     line = xmalloc(line_size);
807     ssize_t n_read;
808 
809     int count = 0;
810     while ((n_read = CfReadLine(&line, &line_size, fp)) > 0)
811     {
812         if (LineIsFiltered(config, line))
813         {
814             continue;
815         }
816         if (send(sd, line, n_read, 0) == -1 ||
817             send(sd, "\r\n", 2, 0) == -1)
818         {
819             Log(LOG_LEVEL_ERR, "Error while sending mail to mailserver "
820                 "'%s'. (send: '%s')", config->mail_server, GetErrorStr());
821             goto mail_err;
822         }
823 
824         count++;
825         if ((config->mail_max_lines != INF_LINES) &&
826             (count >= config->mail_max_lines))
827         {
828             snprintf(line, line_size,
829                      "\r\n[Mail truncated by CFEngine. File is at %s on %s]\r\n",
830                      file, config->fq_name);
831             if (send(sd, line, strlen(line), 0) == -1)
832             {
833                 Log(LOG_LEVEL_ERR, "Error while sending mail to mailserver "
834                     "'%s'. (send: '%s')", config->mail_server, GetErrorStr());
835                 goto mail_err;
836             }
837             break;
838         }
839     }
840 
841     if (!Dialogue(sd, ".\r\n"))
842     {
843         Log(LOG_LEVEL_DEBUG, "Mail report: mail_err\n");
844         goto mail_err;
845     }
846 
847     Dialogue(sd, "QUIT\r\n");
848     Log(LOG_LEVEL_DEBUG, "Mail report: done sending mail");
849     free(line);
850     fclose(fp);
851     cf_closesocket(sd);
852     return;
853 
854   mail_err:
855 
856     free(line);
857     fclose(fp);
858     cf_closesocket(sd);
859     Log(LOG_LEVEL_ERR, "Mail report: cannot mail to %s.", config->mail_to_address);
860 }
861 
Dialogue(int sd,const char * s)862 static bool Dialogue(int sd, const char *s)
863 {
864     if ((s != NULL) && (*s != '\0'))
865     {
866         int sent = send(sd, s, strlen(s), 0);
867         Log(LOG_LEVEL_DEBUG, "SENT(%d) -> '%s'", sent, s);
868     }
869     else
870     {
871         Log(LOG_LEVEL_DEBUG, "Nothing to send .. waiting for opening");
872     }
873 
874     int charpos = 0;
875     int rfclinetype = ' ';
876 
877     char ch, f = '\0';
878     while (recv(sd, &ch, 1, 0))
879     {
880         charpos++;
881 
882         if (f == '\0')
883         {
884             f = ch;
885         }
886 
887         if (charpos == 4)       /* Multiline RFC in form 222-Message with hyphen at pos 4 */
888         {
889             rfclinetype = ch;
890         }
891 
892         if ((ch == '\n') || (ch == '\0'))
893         {
894             charpos = 0;
895 
896             if (rfclinetype == ' ')
897             {
898                 break;
899             }
900         }
901     }
902 
903     return ((f == '2') || (f == '3'));  /* return code 200 or 300 from smtp */
904 }
905