1 /*
2 * Error message and syslog output.
3 *
4 * Copyright (c) 2014-2017 by Farsight Security, Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 #include <axa/axa.h>
20
21 #include <syslog.h>
22 #include <errno.h>
23 #include <paths.h>
24 #include <sysexits.h>
25 #include <unistd.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #ifdef __linux
29 #include <bsd/string.h> /* for strlcpy() */
30 #endif
31 #include <fcntl.h>
32 #include <sys/stat.h>
33 #include <sys/resource.h>
34
35
36
37 static struct {
38 int priority; /* syslog(3) facility|level */
39 bool set;
40 bool on;
41 bool out_stdout; /* send messages to stdout */
42 bool out_stderr; /* send messages to stderr */
43 } ss[3]; /* AXA_SYSLOG_{TRACE,ERROR,ACCT} */
44
45 static bool syslog_set;
46 static bool syslog_open;
47
48 char axa_prog_name[256];
49
50
51 /* Crash immediately on malloc failures. */
52 void *
axa_malloc(size_t s)53 axa_malloc(size_t s)
54 {
55 void *p;
56
57 p = malloc(s);
58 AXA_ASSERT(p != NULL);
59 return (p);
60 }
61
62 void *
axa_zalloc(size_t s)63 axa_zalloc(size_t s)
64 {
65 void *p;
66
67 p = calloc(1, s);
68 AXA_ASSERT(p != NULL);
69 return (p);
70 }
71
72 char *
axa_strdup(const char * s)73 axa_strdup(const char *s)
74 {
75 char *p;
76
77 p = strdup(s);
78 AXA_ASSERT(p != NULL);
79 return (p);
80 }
81
82 char *
axa_strndup(const char * s,size_t len)83 axa_strndup(const char *s, size_t len)
84 {
85 char *p;
86
87 p = strndup(s, len);
88 AXA_ASSERT(p != NULL);
89 return (p);
90 }
91
92 void
axa_vasprintf(char ** bufp,const char * p,va_list args)93 axa_vasprintf(char **bufp, const char *p, va_list args)
94 {
95 int i;
96
97 i = vasprintf(bufp, p, args);
98 AXA_ASSERT(i >= 0);
99 }
100
101 void AXA_PF(2,3)
axa_asprintf(char ** bufp,const char * p,...)102 axa_asprintf(char **bufp, const char *p, ...)
103 {
104 va_list args;
105
106 va_start(args, p);
107 axa_vasprintf(bufp, p, args);
108 va_end(args);
109 }
110
111 /* Try to enable core files. */
112 void
axa_set_core(void)113 axa_set_core(void)
114 {
115 struct rlimit rl;
116
117 if (0 > getrlimit(RLIMIT_CORE, &rl)) {
118 axa_error_msg("getrlimit(RLIMIT_CORE): %s\n",
119 strerror(errno));
120 return;
121 }
122 if (rl.rlim_cur != 0)
123 return;
124 if (rl.rlim_max < 10*1024) {
125 axa_error_msg("getrlimit(RLIMIT_CORE) max = %ld\n",
126 (long)rl.rlim_max);
127 }
128 rl.rlim_cur = RLIM_INFINITY;
129 if (0 > setrlimit(RLIMIT_CORE, &rl)) {
130 axa_error_msg("setrlimit(RLIMIT_CORE %ld %ld): %s\n",
131 (long)rl.rlim_cur, (long)rl.rlim_max, strerror(errno));
132 return;
133 }
134 }
135
136 void
axa_set_me(const char * me)137 axa_set_me(const char *me)
138 {
139 const char *p;
140
141 p = strrchr(me, '/');
142 if (p != NULL)
143 me = p+1;
144 strlcpy(axa_prog_name, me, sizeof(axa_prog_name));
145 if (syslog_open)
146 axa_syslog_init();
147 }
148
149 static int
parse_syslog_level(const char * level)150 parse_syslog_level(const char *level)
151 {
152 static struct {
153 const char *str;
154 int level;
155 } level_tbl[] = {
156 {"LOG_EMERG", LOG_EMERG},
157 {"LOG_ALERT", LOG_ALERT},
158 {"LOG_CRIT", LOG_CRIT},
159 {"LOG_ERR", LOG_ERR},
160 {"LOG_WARNING", LOG_WARNING},
161 {"LOG_NOTICE", LOG_NOTICE},
162 {"LOG_INFO", LOG_INFO},
163 {"LOG_DEBUG", LOG_DEBUG},
164 };
165 int i;
166
167 for (i = 0; i < AXA_DIM(level_tbl); ++i) {
168 if (strcasecmp(level, level_tbl[i].str) == 0)
169 return (level_tbl[i].level);
170 }
171 return (-1);
172 }
173
174 static int
parse_syslog_facility(const char * facility)175 parse_syslog_facility(const char *facility)
176 {
177 static struct {
178 const char *str;
179 int facility;
180 } facility_tbl[] = {
181 {"LOG_AUTH", LOG_AUTH},
182 #ifdef LOG_AUTHPRIV
183 {"LOG_AUTHPRIV",LOG_AUTHPRIV},
184 #endif
185 {"LOG_CRON", LOG_CRON},
186 {"LOG_DAEMON", LOG_DAEMON},
187 #ifdef LOG_FTP
188 {"LOG_FTP", LOG_FTP},
189 #endif
190 {"LOG_KERN", LOG_KERN},
191 {"LOG_LPR", LOG_LPR},
192 {"LOG_MAIL", LOG_MAIL},
193 {"LOG_NEWS", LOG_NEWS},
194 {"LOG_USER", LOG_USER},
195 {"LOG_UUCP", LOG_UUCP},
196 {"LOG_LOCAL0", LOG_LOCAL0},
197 {"LOG_LOCAL1", LOG_LOCAL1},
198 {"LOG_LOCAL2", LOG_LOCAL2},
199 {"LOG_LOCAL3", LOG_LOCAL3},
200 {"LOG_LOCAL4", LOG_LOCAL4},
201 {"LOG_LOCAL5", LOG_LOCAL5},
202 {"LOG_LOCAL6", LOG_LOCAL6},
203 {"LOG_LOCAL7", LOG_LOCAL7},
204 };
205 int i;
206
207 for (i = 0; i < AXA_DIM(facility_tbl); ++i) {
208 if (strcasecmp(facility, facility_tbl[i].str) == 0)
209 return (facility_tbl[i].facility);
210 }
211 return (-1);
212 }
213
214 /*
215 * Parse
216 * {trace|error|acct},{off|FACILITY.LEVEL}[,{none,stderr,stdout}]
217 */
218 bool
axa_parse_log_opt(axa_emsg_t * emsg,const char * arg)219 axa_parse_log_opt(axa_emsg_t *emsg, const char *arg)
220 {
221 char type_buf[32], syslog_buf[32], syslog1_buf[32];
222 const char *arg1, *syslog2_str;
223 int facility, level;
224 axa_syslog_type_t type;
225 bool on, out_stdout, out_stderr;
226
227 arg1 = arg;
228 axa_get_token(type_buf, sizeof(type_buf), &arg1, ",");
229 if (strcasecmp(type_buf, "trace") == 0) {
230 type = AXA_SYSLOG_TRACE;
231 } else if (strcasecmp(type_buf, "error") == 0) {
232 type = AXA_SYSLOG_ERROR;
233 } else if (strcasecmp(type_buf, "acct") == 0) {
234 type = AXA_SYSLOG_ACCT;
235 } else {
236 axa_pemsg(emsg, "\"%s\" in \"-L %s\""
237 " is neither \"trace\", \"error\", nor \"acct\"",
238 type_buf, arg);
239 return (false);
240 }
241
242 axa_get_token(syslog_buf, sizeof(syslog_buf), &arg1, ",");
243 if (strcasecmp(syslog_buf, "off") == 0) {
244 on = false;
245 facility = 0;
246 level = 0;
247 } else {
248 syslog2_str = syslog_buf;
249 axa_get_token(syslog1_buf, sizeof(syslog1_buf),
250 &syslog2_str, ".");
251
252 facility = parse_syslog_facility(syslog1_buf);
253 level = parse_syslog_level(syslog2_str);
254 if (facility < 0 && level < 0) {
255 /* Recognize both LEVEL.FACILITY and FACILITY.LEVEL */
256 facility = parse_syslog_facility(syslog2_str);
257 level = parse_syslog_level(syslog1_buf);
258 }
259 if (facility < 0) {
260 axa_pemsg(emsg,
261 "unrecognized syslog facility in \"%s\"",
262 arg);
263 return (false);
264 }
265 if (level < 0) {
266 axa_pemsg(emsg, "unrecognized syslog level in \"%s\"",
267 arg);
268 return (false);
269 }
270 on = true;
271 }
272
273 if (arg1[0] == '\0' || AXA_CLITCMP(arg1, "stderr")) {
274 out_stdout = false;
275 out_stderr = true;
276 } else if (AXA_CLITCMP(arg1, "off") || AXA_CLITCMP(arg1, "none")) {
277 out_stdout = false;
278 out_stderr = false;
279 } else if (AXA_CLITCMP(arg1, "stdout")) {
280 out_stdout = true;
281 out_stderr = false;
282 } else {
283 axa_pemsg(emsg, "\"%s\" in \"-L %s\" is neither"
284 " NONE, STDERR, nor STDOUT",
285 arg1, arg);
286 return (false);
287 }
288
289 ss[type].on = on;
290 ss[type].priority = facility | level;
291 ss[type].out_stdout = out_stdout;
292 ss[type].out_stderr = out_stderr;
293
294 if (ss[type].set)
295 axa_error_msg("warning: \"-L %s,...\" already set", type_buf);
296 ss[type].set = true;
297
298 return (true);
299 }
300
301 /*
302 * Initialize AXA default logging.
303 * axa_parse_log_opt() can override these values.
304 */
305 static void
set_syslog(void)306 set_syslog(void)
307 {
308 axa_emsg_t emsg;
309
310 if (syslog_set)
311 return;
312
313 if (!ss[AXA_SYSLOG_TRACE].set) {
314 AXA_ASSERT(axa_parse_log_opt(&emsg,
315 "trace,LOG_DEBUG.LOG_DAEMON"));
316 ss[AXA_SYSLOG_TRACE].set = false;
317 }
318 if (!ss[AXA_SYSLOG_ERROR].set) {
319 /* transposed facility and level to check axa_parse_log_opt() */
320 AXA_ASSERT(axa_parse_log_opt(&emsg,
321 "error,LOG_DAEMON.LOG_ERR"));
322 ss[AXA_SYSLOG_ERROR].set = false;
323 }
324 if (!ss[AXA_SYSLOG_ACCT].set) {
325 AXA_ASSERT(axa_parse_log_opt(&emsg,
326 "acct,LOG_NOTICE.LOG_AUTH,none"));
327 ss[AXA_SYSLOG_ACCT].set = false;
328 }
329 syslog_set = true;
330 }
331
332 void
axa_syslog_init(void)333 axa_syslog_init(void)
334 {
335 set_syslog();
336 if (axa_prog_name[0] != '\0') {
337 if (syslog_open)
338 closelog();
339 openlog(axa_prog_name, LOG_PID, LOG_DAEMON);
340 syslog_open = true;
341 }
342 }
343
344 static void
clean_stdfd(int stdfd)345 clean_stdfd(int stdfd)
346 {
347 struct stat sb;
348 int fd;
349
350 if (0 > fstat(stdfd, &sb) && errno == EBADF) {
351 fd = open(_PATH_DEVNULL, 0, O_RDWR | O_CLOEXEC);
352 if (fd < 0) /* ignore errors we can't help */
353 return;
354 if (fd != stdfd) {
355 dup2(fd, stdfd);
356 close(fd);
357 }
358 }
359 }
360
361 /*
362 * Add text to an error or other message buffer.
363 * If we run out of room, add "...". */
364 void AXA_PF(3,4)
axa_buf_print(char ** bufp,size_t * buf_lenp,const char * p,...)365 axa_buf_print(char **bufp, size_t *buf_lenp, const char *p, ...)
366 {
367 size_t buf_len, len;
368 va_list args;
369
370 buf_len = *buf_lenp;
371 if (buf_len < sizeof("...")) {
372 if (buf_len != 0) {
373 strlcpy(*bufp, "...", buf_len);
374 *bufp += buf_len-1;
375 *buf_lenp = 1;
376 }
377 return;
378 }
379
380 va_start(args, p);
381 len = vsnprintf(*bufp, *buf_lenp, p, args);
382 va_end(args);
383 if (len+sizeof("...") > buf_len) {
384 strcpy(*bufp+buf_len-sizeof("..."), "...");
385 *bufp += buf_len-1;
386 *buf_lenp = 1;
387 } else {
388 *buf_lenp -= len;
389 *bufp += len;
390 }
391 }
392
393
394 /* Prevent surprises from uses of stdio FDs by ensuring that the FDs are open */
395 void
axa_clean_stdio(void)396 axa_clean_stdio(void)
397 {
398 clean_stdfd(STDIN_FILENO);
399 clean_stdfd(STDOUT_FILENO);
400 clean_stdfd(STDERR_FILENO);
401 }
402
403 void
axa_vlog_msg(axa_syslog_type_t type,bool fatal,const char * p,va_list args)404 axa_vlog_msg(axa_syslog_type_t type, bool fatal, const char *p, va_list args)
405 {
406 char buf[512], *bufp;
407 size_t buf_len, n;
408 FILE *stdio;
409 # define FMSG "; fatal error"
410
411 /*
412 * This function cannot use axa_vasprintf() and other axa_*()
413 * functions that would themselves call this function.
414 */
415
416 bufp = buf;
417 buf_len = sizeof(buf);
418 if (fatal)
419 buf_len -= sizeof(FMSG)-1;
420
421 n = vsnprintf(bufp, buf_len, p, args);
422
423 if (n >= buf_len)
424 n = buf_len-1;
425 if (n != 0 && buf[n-1] == '\n')
426 buf[--n] = '\0';
427 if (n == 0) {
428 strlcat(bufp, "(empty error message)", buf_len);
429 n = sizeof("(empty error message)")-1;
430 }
431 if (n >= buf_len)
432 strcpy(&buf[buf_len-sizeof("...")], "...");
433 if (fatal)
434 strlcat(buf, FMSG, sizeof(buf));
435
436 /* keep stderr and stdout straight despite syslog output
437 * to stdout or stderr */
438 fflush(stdout);
439 fflush(stderr);
440
441 set_syslog();
442
443 if (ss[type].out_stderr)
444 stdio = stderr;
445 else if (ss[type].out_stdout)
446 stdio = stdout;
447 else
448 stdio = NULL;
449 if (stdio != NULL)
450 fprintf(stdio, "%s\n", buf);
451
452 if (ss[type].on)
453 syslog(ss[type].priority, "%s", buf);
454
455 /* Error messges also go to the trace stream. */
456 if (type == AXA_SYSLOG_ERROR && ss[AXA_SYSLOG_TRACE].on
457 && ss[AXA_SYSLOG_TRACE].priority != ss[AXA_SYSLOG_ERROR].priority)
458 syslog(ss[AXA_SYSLOG_TRACE].priority, "%s", buf);
459
460 fflush(stdout);
461 fflush(stderr);
462 }
463
464 /*
465 * Generate an error message string in a buffer, if we have a buffer.
466 * Log or print the message if there is no buffer
467 */
468 void
axa_vpemsg(axa_emsg_t * emsg,const char * p,va_list args)469 axa_vpemsg(axa_emsg_t *emsg, const char *p, va_list args)
470 {
471 if (emsg == NULL) {
472 axa_vlog_msg(AXA_SYSLOG_ERROR, false, p, args);
473 } else {
474 vsnprintf(emsg->c, sizeof(axa_emsg_t), p, args);
475 }
476 }
477
478 void AXA_PF(2,3)
axa_pemsg(axa_emsg_t * emsg,const char * p,...)479 axa_pemsg(axa_emsg_t *emsg, const char *p, ...)
480 {
481 va_list args;
482
483 va_start(args, p);
484 axa_vpemsg(emsg, p, args);
485 va_end(args);
486 }
487
488 void
axa_verror_msg(const char * p,va_list args)489 axa_verror_msg(const char *p, va_list args)
490 {
491 axa_vlog_msg(AXA_SYSLOG_ERROR, false, p, args);
492 }
493
494 void AXA_PF(1,2)
axa_error_msg(const char * p,...)495 axa_error_msg(const char *p, ...)
496 {
497 va_list args;
498
499 va_start(args, p);
500 axa_vlog_msg(AXA_SYSLOG_ERROR, false, p, args);
501 va_end(args);
502 }
503
504 void
axa_io_error(const char * op,const char * src,ssize_t len)505 axa_io_error(const char *op, const char *src, ssize_t len)
506 {
507 if (len >= 0) {
508 axa_error_msg("%s(%s)=%zd", op, src, len);
509 } else {
510 axa_error_msg("%s(%s): %s", op, src, strerror(errno));
511 }
512 }
513
514 void
axa_vtrace_msg(const char * p,va_list args)515 axa_vtrace_msg(const char *p, va_list args)
516 {
517 axa_vlog_msg(AXA_SYSLOG_TRACE, false, p, args);
518 }
519
520 void AXA_PF(1,2)
axa_trace_msg(const char * p,...)521 axa_trace_msg(const char *p, ...)
522 {
523 va_list args;
524
525 va_start(args, p);
526 axa_vlog_msg(AXA_SYSLOG_TRACE, false, p, args);
527 va_end(args);
528 }
529
530 void AXA_NORETURN
axa_vfatal_msg(int ex_code,const char * p,va_list args)531 axa_vfatal_msg(int ex_code, const char *p, va_list args)
532 {
533 axa_vlog_msg(AXA_SYSLOG_ERROR, true, p, args);
534
535 if (ex_code == 0 || ex_code == EX_SOFTWARE)
536 abort();
537 exit(ex_code);
538 }
539
540 /*
541 * Things are so sick that we must bail out.
542 */
543 void AXA_PF(2,3) AXA_NORETURN
axa_fatal_msg(int ex_code,const char * p,...)544 axa_fatal_msg(int ex_code, const char *p, ...)
545 {
546 va_list args;
547
548 va_start(args, p);
549 axa_vfatal_msg(ex_code, p, args);
550 }
551
552 /*
553 * Get a logical line from a stdio stream, with leading and trailing
554 * whitespace trimmed and "\\\n" deleted as a continuation.
555 * The file name and line number must be provided for error messages.
556 * The line number is updated.
557 * The buffer can be NULL.
558 * If the buffer is NULL or it is not big enough, it is freed and a new
559 * buffer is allocated.
560 * The must be freed after the last use of this function.
561 * Except at error or EOF, the start of the next line is returned,
562 * which might not be at the start of the buffer.
563 * The return value is NULL and emsg->c[0]=='\0' at EOF.
564 * The return value is NULL and emsg->c[0]!='\0' after an error.
565 */
566 char *
axa_fgetln(FILE * f,const char * file_name,uint * line_num,char ** bufp,size_t * buf_sizep)567 axa_fgetln(FILE *f, /* source */
568 const char *file_name, /* for error messages */
569 uint *line_num,
570 char **bufp, /* destination must be freed */
571 size_t *buf_sizep)
572 {
573 char *buf, *p, *line;
574 size_t buf_size, len, delta;
575
576 if (*bufp == NULL) {
577 AXA_ASSERT(*buf_sizep == 0);
578 buf = axa_malloc(*buf_sizep = 81);
579 *bufp = buf;
580 }
581 for (;;) {
582 buf = *bufp;
583 buf_size = *buf_sizep;
584 for (;;) {
585 if (buf_size < 80) {
586 delta = (*buf_sizep/81+2)*81 - buf_size;
587 p = axa_malloc(*buf_sizep + delta);
588 len = buf - *bufp;
589 if (len > 0)
590 memcpy(p, *bufp, len);
591 *buf_sizep += delta;
592 buf_size += delta;
593 free(*bufp);
594 *bufp = p;
595 buf = p+len;
596 }
597
598 if (fgets(buf, buf_size, f) == NULL) {
599 *buf = '\0';
600 if (ferror(f) != 0) {
601 axa_error_msg("fgets(%s): \"%s\"",
602 file_name,
603 strerror(errno));
604 return (NULL);
605 }
606 break;
607 }
608
609 /* Expand the buffer and get more if the buffer
610 * was too small for the line */
611 len = strlen(buf);
612 if (len >= buf_size-1 && buf[len-1] != '\n') {
613 buf_size -= len;
614 buf += len;
615 continue;
616 }
617
618 ++*line_num;
619
620 /* trim trailing '\n' and check for continuation */
621 while (len >0
622 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
623 buf[--len] = '\0';
624 }
625 if (len == 0
626 || ( buf[--len] != '\\' || len >= 10*1024))
627 break;
628 buf[len]= '\0';
629 buf_size -= len;
630 buf += len;
631 }
632
633 /* Trim leading blanks and comments */
634 line = *bufp+strspn(*bufp, AXA_WHITESPACE);
635 p = strpbrk(line, "\r\n#");
636 if (p != NULL)
637 *p = '\0';
638
639 /* skip blank lines */
640 if (*line != '\0')
641 return (line);
642 if (feof(f))
643 return (NULL);
644 }
645 }
646
647 /*
648 * Strip leading and trailing white space.
649 */
650 const char *
axa_strip_white(const char * str,size_t * lenp)651 axa_strip_white(const char *str, size_t *lenp)
652 {
653 const char *end;
654 char c;
655
656 str += strspn(str, AXA_WHITESPACE);
657 end = str+strlen(str);
658 while (end > str) {
659 c = *(end-1);
660 if (c != ' ' && c != '\t' && c != '\r' && c != '\n')
661 break;
662 --end;
663 }
664 *lenp = end-str;
665 return (str);
666 }
667
668 /* Copy the next token from a string to a buffer and return the
669 * size of the string put into the buffer.
670 * Honor quotes and backslash.
671 * The caller must skip leading token separators (e.g. blanks) if necessary.
672 * When the separators include whitespace and whitespace ends the token,
673 * then all trailing whitespace is skipped */
674 ssize_t /* # of bytes or <0 for failure */
axa_get_token(char * buf,size_t buf_len,const char ** stringp,const char * seps)675 axa_get_token(char *buf, /* put the token here */
676 size_t buf_len,
677 const char **stringp, /* input string pointer */
678 const char *seps) /* string of token separators */
679 {
680 int token_len;
681 bool quot_ok, esc_ok;
682 const char *string;
683 char c, quote;
684
685 token_len = 0;
686
687 /* Quietly skip without a buffer but fail with a zero-length buffer. */
688 if (buf_len == 0 && buf != NULL)
689 return (-1);
690
691 quot_ok = (strpbrk(seps, "\"'") == NULL);
692 esc_ok = (strchr(seps, '\\') == NULL);
693 string = *stringp;
694
695 for (;;) {
696 c = *string;
697 if (c == '\0') {
698 if (buf != NULL)
699 *buf = '\0';
700 *stringp = string;
701 return (token_len);
702 }
703 if (quot_ok && strchr("\"'",c ) != NULL) {
704 quote = c;
705 while ((c = *++string) != quote) {
706 if (c == '\0') {
707 if (buf != NULL)
708 *buf = '\0';
709 *stringp = string;
710 return (token_len);
711 }
712 ++token_len;
713
714 if (buf == NULL)
715 continue;
716 if (--buf_len == 0) {
717 *buf = '\0';
718 *stringp = string;
719 return (-1);
720 }
721 *buf++ = c;
722 }
723 ++string;
724 continue;
725 }
726
727 ++string;
728 if (c == '\\' && esc_ok) {
729 c = *string++;
730 } else if (strchr(seps, c) != NULL) {
731 /* We found a separator. Eat it and stop.
732 * If it is whitespace, then eat all trailing
733 * whitespace. */
734 if (strchr(AXA_WHITESPACE, c) != NULL)
735 string += strspn(string, AXA_WHITESPACE);
736
737 if (buf != NULL)
738 *buf = '\0';
739 *stringp = string;
740 return (token_len);
741 }
742 ++token_len;
743
744 if (buf == NULL)
745 continue;
746 if (--buf_len == 0) {
747 *buf = '\0';
748 *stringp = string;
749 return (-1);
750 }
751 *buf++ = c;
752 }
753 }
754