1 /* SPDX-License-Identifier: LGPL-2.1+ */
3 This file is part of systemd.
5 Copyright 2012 Lennart Poettering
7 systemd is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation; either version 2.1 of the License, or
10 (at your option) any later version.
12 systemd is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with systemd; If not, see <http://www.gnu.org/licenses/>.
23 #include <microhttpd.h>
29 #include "sd-daemon.h"
30 #include "sd-journal.h"
32 #include "alloc-util.h"
36 #include "hostname-util.h"
38 #include "logs-show.h"
39 #include "microhttpd-util.h"
40 #include "parse-util.h"
44 #define JOURNAL_WAIT_TIMEOUT (10*USEC_PER_SEC)
46 static char *arg_key_pem
= NULL
;
47 static char *arg_cert_pem
= NULL
;
48 static char *arg_trust_pem
= NULL
;
49 static char *arg_directory
= NULL
;
51 typedef struct RequestMeta
{
64 int argument_parse_error
;
73 static const char* const mime_types
[_OUTPUT_MODE_MAX
] = {
74 [OUTPUT_SHORT
] = "text/plain",
75 [OUTPUT_JSON
] = "application/json",
76 [OUTPUT_JSON_SSE
] = "text/event-stream",
77 [OUTPUT_EXPORT
] = "application/vnd.fdo.journal",
80 static RequestMeta
*request_meta(void **connection_cls
) {
83 assert(connection_cls
);
85 return *connection_cls
;
87 m
= new0(RequestMeta
, 1);
95 static void request_meta_free(
97 struct MHD_Connection
*connection
,
98 void **connection_cls
,
99 enum MHD_RequestTerminationCode toe
) {
101 RequestMeta
*m
= *connection_cls
;
106 sd_journal_close(m
->journal
);
114 static int open_journal(RequestMeta
*m
) {
121 return sd_journal_open_directory(&m
->journal
, arg_directory
, 0);
123 return sd_journal_open(&m
->journal
, SD_JOURNAL_LOCAL_ONLY
|SD_JOURNAL_SYSTEM
);
126 static int request_meta_ensure_tmp(RequestMeta
*m
) {
134 fd
= open_tmpfile_unlinkable("/tmp", O_RDWR
|O_CLOEXEC
);
138 m
->tmp
= fdopen(fd
, "w+");
148 static ssize_t
request_reader_entries(
154 RequestMeta
*m
= cls
;
161 assert(pos
>= m
->delta
);
165 while (pos
>= m
->size
) {
168 /* End of this entry, so let's serialize the next
171 if (m
->n_entries_set
&&
173 return MHD_CONTENT_READER_END_OF_STREAM
;
176 r
= sd_journal_previous_skip(m
->journal
, (uint64_t) -m
->n_skip
+ 1);
177 else if (m
->n_skip
> 0)
178 r
= sd_journal_next_skip(m
->journal
, (uint64_t) m
->n_skip
+ 1);
180 r
= sd_journal_next(m
->journal
);
183 log_error_errno(r
, "Failed to advance journal pointer: %m");
184 return MHD_CONTENT_READER_END_WITH_ERROR
;
188 r
= sd_journal_wait(m
->journal
, (uint64_t) JOURNAL_WAIT_TIMEOUT
);
190 log_error_errno(r
, "Couldn't wait for journal event: %m");
191 return MHD_CONTENT_READER_END_WITH_ERROR
;
193 if (r
== SD_JOURNAL_NOP
)
199 return MHD_CONTENT_READER_END_OF_STREAM
;
205 r
= sd_journal_test_cursor(m
->journal
, m
->cursor
);
207 log_error_errno(r
, "Failed to test cursor: %m");
208 return MHD_CONTENT_READER_END_WITH_ERROR
;
212 return MHD_CONTENT_READER_END_OF_STREAM
;
218 if (m
->n_entries_set
)
223 r
= request_meta_ensure_tmp(m
);
225 log_error_errno(r
, "Failed to create temporary file: %m");
226 return MHD_CONTENT_READER_END_WITH_ERROR
;
229 r
= output_journal(m
->tmp
, m
->journal
, m
->mode
, 0, OUTPUT_FULL_WIDTH
, NULL
, NULL
);
231 log_error_errno(r
, "Failed to serialize item: %m");
232 return MHD_CONTENT_READER_END_WITH_ERROR
;
236 if (sz
== (off_t
) -1) {
237 log_error_errno(errno
, "Failed to retrieve file position: %m");
238 return MHD_CONTENT_READER_END_WITH_ERROR
;
241 m
->size
= (uint64_t) sz
;
244 if (m
->tmp
== NULL
&& m
->follow
)
247 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
248 log_error_errno(errno
, "Failed to seek to position: %m");
249 return MHD_CONTENT_READER_END_WITH_ERROR
;
259 k
= fread(buf
, 1, n
, m
->tmp
);
261 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
262 return MHD_CONTENT_READER_END_WITH_ERROR
;
268 static int request_parse_accept(
270 struct MHD_Connection
*connection
) {
277 header
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Accept");
281 if (streq(header
, mime_types
[OUTPUT_JSON
]))
282 m
->mode
= OUTPUT_JSON
;
283 else if (streq(header
, mime_types
[OUTPUT_JSON_SSE
]))
284 m
->mode
= OUTPUT_JSON_SSE
;
285 else if (streq(header
, mime_types
[OUTPUT_EXPORT
]))
286 m
->mode
= OUTPUT_EXPORT
;
288 m
->mode
= OUTPUT_SHORT
;
293 static int request_parse_range(
295 struct MHD_Connection
*connection
) {
297 const char *range
, *colon
, *colon2
;
303 range
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Range");
307 if (!startswith(range
, "entries="))
311 range
+= strspn(range
, WHITESPACE
);
313 colon
= strchr(range
, ':');
315 m
->cursor
= strdup(range
);
319 colon2
= strchr(colon
+ 1, ':');
321 _cleanup_free_
char *t
;
323 t
= strndup(colon
+ 1, colon2
- colon
- 1);
327 r
= safe_atoi64(t
, &m
->n_skip
);
332 p
= (colon2
? colon2
: colon
) + 1;
334 r
= safe_atou64(p
, &m
->n_entries
);
338 if (m
->n_entries
<= 0)
341 m
->n_entries_set
= true;
344 m
->cursor
= strndup(range
, colon
- range
);
350 m
->cursor
[strcspn(m
->cursor
, WHITESPACE
)] = 0;
351 if (isempty(m
->cursor
))
352 m
->cursor
= mfree(m
->cursor
);
357 static int request_parse_arguments_iterator(
359 enum MHD_ValueKind kind
,
363 RequestMeta
*m
= cls
;
364 _cleanup_free_
char *p
= NULL
;
370 m
->argument_parse_error
= -EINVAL
;
374 if (streq(key
, "follow")) {
375 if (isempty(value
)) {
380 r
= parse_boolean(value
);
382 m
->argument_parse_error
= r
;
390 if (streq(key
, "discrete")) {
391 if (isempty(value
)) {
396 r
= parse_boolean(value
);
398 m
->argument_parse_error
= r
;
406 if (streq(key
, "boot")) {
410 r
= parse_boolean(value
);
412 m
->argument_parse_error
= r
;
418 char match
[9 + 32 + 1] = "_BOOT_ID=";
421 r
= sd_id128_get_boot(&bid
);
423 log_error_errno(r
, "Failed to get boot ID: %m");
427 sd_id128_to_string(bid
, match
+ 9);
428 r
= sd_journal_add_match(m
->journal
, match
, sizeof(match
)-1);
430 m
->argument_parse_error
= r
;
438 p
= strjoin(key
, "=", strempty(value
));
440 m
->argument_parse_error
= log_oom();
444 r
= sd_journal_add_match(m
->journal
, p
, 0);
446 m
->argument_parse_error
= r
;
453 static int request_parse_arguments(
455 struct MHD_Connection
*connection
) {
460 m
->argument_parse_error
= 0;
461 MHD_get_connection_values(connection
, MHD_GET_ARGUMENT_KIND
, request_parse_arguments_iterator
, m
);
463 return m
->argument_parse_error
;
466 static int request_handler_entries(
467 struct MHD_Connection
*connection
,
468 void *connection_cls
) {
470 struct MHD_Response
*response
;
471 RequestMeta
*m
= connection_cls
;
479 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %m");
481 if (request_parse_accept(m
, connection
) < 0)
482 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.");
484 if (request_parse_range(m
, connection
) < 0)
485 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Range header.");
487 if (request_parse_arguments(m
, connection
) < 0)
488 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse URL arguments.");
492 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Discrete seeks require a cursor specification.");
495 m
->n_entries_set
= true;
499 r
= sd_journal_seek_cursor(m
->journal
, m
->cursor
);
500 else if (m
->n_skip
>= 0)
501 r
= sd_journal_seek_head(m
->journal
);
502 else if (m
->n_skip
< 0)
503 r
= sd_journal_seek_tail(m
->journal
);
505 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to seek in journal.");
507 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_entries
, m
, NULL
);
509 return respond_oom(connection
);
511 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
]);
513 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
514 MHD_destroy_response(response
);
519 static int output_field(FILE *f
, OutputMode m
, const char *d
, size_t l
) {
523 eq
= memchr(d
, '=', l
);
527 j
= l
- (eq
- d
+ 1);
529 if (m
== OUTPUT_JSON
) {
530 fprintf(f
, "{ \"%.*s\" : ", (int) (eq
- d
), d
);
531 json_escape(f
, eq
+1, j
, OUTPUT_FULL_WIDTH
);
534 fwrite(eq
+1, 1, j
, f
);
541 static ssize_t
request_reader_fields(
547 RequestMeta
*m
= cls
;
554 assert(pos
>= m
->delta
);
558 while (pos
>= m
->size
) {
563 /* End of this field, so let's serialize the next
566 if (m
->n_fields_set
&&
568 return MHD_CONTENT_READER_END_OF_STREAM
;
570 r
= sd_journal_enumerate_unique(m
->journal
, &d
, &l
);
572 log_error_errno(r
, "Failed to advance field index: %m");
573 return MHD_CONTENT_READER_END_WITH_ERROR
;
575 return MHD_CONTENT_READER_END_OF_STREAM
;
583 r
= request_meta_ensure_tmp(m
);
585 log_error_errno(r
, "Failed to create temporary file: %m");
586 return MHD_CONTENT_READER_END_WITH_ERROR
;
589 r
= output_field(m
->tmp
, m
->mode
, d
, l
);
591 log_error_errno(r
, "Failed to serialize item: %m");
592 return MHD_CONTENT_READER_END_WITH_ERROR
;
596 if (sz
== (off_t
) -1) {
597 log_error_errno(errno
, "Failed to retrieve file position: %m");
598 return MHD_CONTENT_READER_END_WITH_ERROR
;
601 m
->size
= (uint64_t) sz
;
604 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
605 log_error_errno(errno
, "Failed to seek to position: %m");
606 return MHD_CONTENT_READER_END_WITH_ERROR
;
614 k
= fread(buf
, 1, n
, m
->tmp
);
616 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
617 return MHD_CONTENT_READER_END_WITH_ERROR
;
623 static int request_handler_fields(
624 struct MHD_Connection
*connection
,
626 void *connection_cls
) {
628 struct MHD_Response
*response
;
629 RequestMeta
*m
= connection_cls
;
637 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %m");
639 if (request_parse_accept(m
, connection
) < 0)
640 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.");
642 r
= sd_journal_query_unique(m
->journal
, field
);
644 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to query unique fields.");
646 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_fields
, m
, NULL
);
648 return respond_oom(connection
);
650 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
== OUTPUT_JSON
? OUTPUT_JSON
: OUTPUT_SHORT
]);
652 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
653 MHD_destroy_response(response
);
658 static int request_handler_redirect(
659 struct MHD_Connection
*connection
,
660 const char *target
) {
663 struct MHD_Response
*response
;
669 if (asprintf(&page
, "<html><body>Please continue to the <a href=\"%s\">journal browser</a>.</body></html>", target
) < 0)
670 return respond_oom(connection
);
672 response
= MHD_create_response_from_buffer(strlen(page
), page
, MHD_RESPMEM_MUST_FREE
);
675 return respond_oom(connection
);
678 MHD_add_response_header(response
, "Content-Type", "text/html");
679 MHD_add_response_header(response
, "Location", target
);
681 ret
= MHD_queue_response(connection
, MHD_HTTP_MOVED_PERMANENTLY
, response
);
682 MHD_destroy_response(response
);
687 static int request_handler_file(
688 struct MHD_Connection
*connection
,
690 const char *mime_type
) {
692 struct MHD_Response
*response
;
694 _cleanup_close_
int fd
= -1;
701 fd
= open(path
, O_RDONLY
|O_CLOEXEC
);
703 return mhd_respondf(connection
, errno
, MHD_HTTP_NOT_FOUND
, "Failed to open file %s: %m", path
);
705 if (fstat(fd
, &st
) < 0)
706 return mhd_respondf(connection
, errno
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to stat file: %m");
708 response
= MHD_create_response_from_fd_at_offset64(st
.st_size
, fd
, 0);
710 return respond_oom(connection
);
714 MHD_add_response_header(response
, "Content-Type", mime_type
);
716 ret
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
717 MHD_destroy_response(response
);
722 static int get_virtualization(char **v
) {
723 _cleanup_(sd_bus_unrefp
) sd_bus
*bus
= NULL
;
727 r
= sd_bus_default_system(&bus
);
731 r
= sd_bus_get_property_string(
733 "org.freedesktop.systemd1",
734 "/org/freedesktop/systemd1",
735 "org.freedesktop.systemd1.Manager",
752 static int request_handler_machine(
753 struct MHD_Connection
*connection
,
754 void *connection_cls
) {
756 struct MHD_Response
*response
;
757 RequestMeta
*m
= connection_cls
;
759 _cleanup_free_
char* hostname
= NULL
, *os_name
= NULL
;
760 uint64_t cutoff_from
= 0, cutoff_to
= 0, usage
= 0;
763 _cleanup_free_
char *v
= NULL
;
770 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %m");
772 r
= sd_id128_get_machine(&mid
);
774 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine machine ID: %m");
776 r
= sd_id128_get_boot(&bid
);
778 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine boot ID: %m");
780 hostname
= gethostname_malloc();
782 return respond_oom(connection
);
784 r
= sd_journal_get_usage(m
->journal
, &usage
);
786 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %m");
788 r
= sd_journal_get_cutoff_realtime_usec(m
->journal
, &cutoff_from
, &cutoff_to
);
790 return mhd_respondf(connection
, r
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %m");
792 if (parse_env_file("/etc/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
) == -ENOENT
)
793 (void) parse_env_file("/usr/lib/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
);
795 get_virtualization(&v
);
798 "{ \"machine_id\" : \"" SD_ID128_FORMAT_STR
"\","
799 "\"boot_id\" : \"" SD_ID128_FORMAT_STR
"\","
800 "\"hostname\" : \"%s\","
801 "\"os_pretty_name\" : \"%s\","
802 "\"virtualization\" : \"%s\","
803 "\"usage\" : \"%"PRIu64
"\","
804 "\"cutoff_from_realtime\" : \"%"PRIu64
"\","
805 "\"cutoff_to_realtime\" : \"%"PRIu64
"\" }\n",
806 SD_ID128_FORMAT_VAL(mid
),
807 SD_ID128_FORMAT_VAL(bid
),
808 hostname_cleanup(hostname
),
809 os_name
? os_name
: "Linux",
816 return respond_oom(connection
);
818 response
= MHD_create_response_from_buffer(strlen(json
), json
, MHD_RESPMEM_MUST_FREE
);
821 return respond_oom(connection
);
824 MHD_add_response_header(response
, "Content-Type", "application/json");
825 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
826 MHD_destroy_response(response
);
831 static int request_handler(
833 struct MHD_Connection
*connection
,
837 const char *upload_data
,
838 size_t *upload_data_size
,
839 void **connection_cls
) {
843 assert(connection_cls
);
847 if (!streq(method
, "GET"))
848 return mhd_respond(connection
, MHD_HTTP_NOT_ACCEPTABLE
, "Unsupported method.");
851 if (!*connection_cls
) {
852 if (!request_meta(connection_cls
))
853 return respond_oom(connection
);
858 r
= check_permissions(connection
, &code
, NULL
);
864 return request_handler_redirect(connection
, "/browse");
866 if (streq(url
, "/entries"))
867 return request_handler_entries(connection
, *connection_cls
);
869 if (startswith(url
, "/fields/"))
870 return request_handler_fields(connection
, url
+ 8, *connection_cls
);
872 if (streq(url
, "/browse"))
873 return request_handler_file(connection
, DOCUMENT_ROOT
"/browse.html", "text/html");
875 if (streq(url
, "/machine"))
876 return request_handler_machine(connection
, *connection_cls
);
878 return mhd_respond(connection
, MHD_HTTP_NOT_FOUND
, "Not found.");
881 static void help(void) {
882 printf("%s [OPTIONS...] ...\n\n"
883 "HTTP server for journal events.\n\n"
884 " -h --help Show this help\n"
885 " --version Show package version\n"
886 " --cert=CERT.PEM Server certificate in PEM format\n"
887 " --key=KEY.PEM Server key in PEM format\n"
888 " --trust=CERT.PEM Certificate authority certificate in PEM format\n"
889 " -D --directory=PATH Serve journal files in directory\n",
890 program_invocation_short_name
);
893 static int parse_argv(int argc
, char *argv
[]) {
903 static const struct option options
[] = {
904 { "help", no_argument
, NULL
, 'h' },
905 { "version", no_argument
, NULL
, ARG_VERSION
},
906 { "key", required_argument
, NULL
, ARG_KEY
},
907 { "cert", required_argument
, NULL
, ARG_CERT
},
908 { "trust", required_argument
, NULL
, ARG_TRUST
},
909 { "directory", required_argument
, NULL
, 'D' },
916 while ((c
= getopt_long(argc
, argv
, "hD:", options
, NULL
)) >= 0)
929 log_error("Key file specified twice");
932 r
= read_full_file(optarg
, &arg_key_pem
, NULL
);
934 return log_error_errno(r
, "Failed to read key file: %m");
940 log_error("Certificate file specified twice");
943 r
= read_full_file(optarg
, &arg_cert_pem
, NULL
);
945 return log_error_errno(r
, "Failed to read certificate file: %m");
946 assert(arg_cert_pem
);
952 log_error("CA certificate file specified twice");
955 r
= read_full_file(optarg
, &arg_trust_pem
, NULL
);
957 return log_error_errno(r
, "Failed to read CA certificate file: %m");
958 assert(arg_trust_pem
);
961 log_error("Option --trust is not available.");
965 arg_directory
= optarg
;
972 assert_not_reached("Unhandled option");
976 log_error("This program does not take arguments.");
980 if (!!arg_key_pem
!= !!arg_cert_pem
) {
981 log_error("Certificate and key files must be specified together");
985 if (arg_trust_pem
&& !arg_key_pem
) {
986 log_error("CA certificate can only be used with certificate file");
993 int main(int argc
, char *argv
[]) {
994 struct MHD_Daemon
*d
= NULL
;
997 log_set_target(LOG_TARGET_AUTO
);
998 log_parse_environment();
1001 r
= parse_argv(argc
, argv
);
1003 return EXIT_FAILURE
;
1005 return EXIT_SUCCESS
;
1009 r
= setup_gnutls_logger(NULL
);
1011 return EXIT_FAILURE
;
1013 n
= sd_listen_fds(1);
1015 log_error_errno(n
, "Failed to determine passed sockets: %m");
1018 log_error("Can't listen on more than one socket.");
1021 struct MHD_OptionItem opts
[] = {
1022 { MHD_OPTION_NOTIFY_COMPLETED
,
1023 (intptr_t) request_meta_free
, NULL
},
1024 { MHD_OPTION_EXTERNAL_LOGGER
,
1025 (intptr_t) microhttpd_logger
, NULL
},
1026 { MHD_OPTION_END
, 0, NULL
},
1027 { MHD_OPTION_END
, 0, NULL
},
1028 { MHD_OPTION_END
, 0, NULL
},
1029 { MHD_OPTION_END
, 0, NULL
},
1030 { MHD_OPTION_END
, 0, NULL
}};
1033 /* We force MHD_USE_ITC here, in order to make sure
1034 * libmicrohttpd doesn't use shutdown() on our listening
1035 * socket, which would break socket re-activation. See
1037 * https://lists.gnu.org/archive/html/libmicrohttpd/2015-09/msg00014.html
1038 * https://github.com/systemd/systemd/pull/1286
1043 MHD_USE_DUAL_STACK
|
1045 MHD_USE_POLL_INTERNAL_THREAD
|
1046 MHD_USE_THREAD_PER_CONNECTION
;
1049 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1050 {MHD_OPTION_LISTEN_SOCKET
, SD_LISTEN_FDS_START
};
1052 assert(arg_cert_pem
);
1053 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1054 {MHD_OPTION_HTTPS_MEM_KEY
, 0, arg_key_pem
};
1055 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1056 {MHD_OPTION_HTTPS_MEM_CERT
, 0, arg_cert_pem
};
1057 flags
|= MHD_USE_TLS
;
1059 if (arg_trust_pem
) {
1060 assert(flags
& MHD_USE_TLS
);
1061 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1062 {MHD_OPTION_HTTPS_MEM_TRUST
, 0, arg_trust_pem
};
1065 d
= MHD_start_daemon(flags
, 19531,
1067 request_handler
, NULL
,
1068 MHD_OPTION_ARRAY
, opts
,
1073 log_error("Failed to start daemon!");