1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
4 This file is part of systemd.
6 Copyright 2012 Lennart Poettering
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
27 #include <microhttpd.h>
29 #include <gnutls/gnutls.h>
32 #include "sd-journal.h"
33 #include "sd-daemon.h"
38 #include "hostname-util.h"
40 #include "logs-show.h"
41 #include "microhttpd-util.h"
45 static char *arg_key_pem
= NULL
;
46 static char *arg_cert_pem
= NULL
;
47 static char *arg_trust_pem
= NULL
;
49 typedef struct RequestMeta
{
62 int argument_parse_error
;
71 static const char* const mime_types
[_OUTPUT_MODE_MAX
] = {
72 [OUTPUT_SHORT
] = "text/plain",
73 [OUTPUT_JSON
] = "application/json",
74 [OUTPUT_JSON_SSE
] = "text/event-stream",
75 [OUTPUT_EXPORT
] = "application/vnd.fdo.journal",
78 static RequestMeta
*request_meta(void **connection_cls
) {
81 assert(connection_cls
);
83 return *connection_cls
;
85 m
= new0(RequestMeta
, 1);
93 static void request_meta_free(
95 struct MHD_Connection
*connection
,
96 void **connection_cls
,
97 enum MHD_RequestTerminationCode toe
) {
99 RequestMeta
*m
= *connection_cls
;
104 sd_journal_close(m
->journal
);
112 static int open_journal(RequestMeta
*m
) {
118 return sd_journal_open(&m
->journal
, SD_JOURNAL_LOCAL_ONLY
|SD_JOURNAL_SYSTEM
);
121 static int request_meta_ensure_tmp(RequestMeta
*m
) {
127 fd
= open_tmpfile("/tmp", O_RDWR
|O_CLOEXEC
);
131 m
->tmp
= fdopen(fd
, "w+");
141 static ssize_t
request_reader_entries(
147 RequestMeta
*m
= cls
;
154 assert(pos
>= m
->delta
);
158 while (pos
>= m
->size
) {
161 /* End of this entry, so let's serialize the next
164 if (m
->n_entries_set
&&
166 return MHD_CONTENT_READER_END_OF_STREAM
;
169 r
= sd_journal_previous_skip(m
->journal
, (uint64_t) -m
->n_skip
+ 1);
170 else if (m
->n_skip
> 0)
171 r
= sd_journal_next_skip(m
->journal
, (uint64_t) m
->n_skip
+ 1);
173 r
= sd_journal_next(m
->journal
);
176 log_error_errno(r
, "Failed to advance journal pointer: %m");
177 return MHD_CONTENT_READER_END_WITH_ERROR
;
181 r
= sd_journal_wait(m
->journal
, (uint64_t) -1);
183 log_error_errno(r
, "Couldn't wait for journal event: %m");
184 return MHD_CONTENT_READER_END_WITH_ERROR
;
190 return MHD_CONTENT_READER_END_OF_STREAM
;
196 r
= sd_journal_test_cursor(m
->journal
, m
->cursor
);
198 log_error_errno(r
, "Failed to test cursor: %m");
199 return MHD_CONTENT_READER_END_WITH_ERROR
;
203 return MHD_CONTENT_READER_END_OF_STREAM
;
209 if (m
->n_entries_set
)
214 r
= request_meta_ensure_tmp(m
);
216 log_error_errno(r
, "Failed to create temporary file: %m");
217 return MHD_CONTENT_READER_END_WITH_ERROR
;
220 r
= output_journal(m
->tmp
, m
->journal
, m
->mode
, 0, OUTPUT_FULL_WIDTH
, NULL
);
222 log_error_errno(r
, "Failed to serialize item: %m");
223 return MHD_CONTENT_READER_END_WITH_ERROR
;
227 if (sz
== (off_t
) -1) {
228 log_error_errno(errno
, "Failed to retrieve file position: %m");
229 return MHD_CONTENT_READER_END_WITH_ERROR
;
232 m
->size
= (uint64_t) sz
;
235 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
236 log_error_errno(errno
, "Failed to seek to position: %m");
237 return MHD_CONTENT_READER_END_WITH_ERROR
;
245 k
= fread(buf
, 1, n
, m
->tmp
);
247 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
248 return MHD_CONTENT_READER_END_WITH_ERROR
;
254 static int request_parse_accept(
256 struct MHD_Connection
*connection
) {
263 header
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Accept");
267 if (streq(header
, mime_types
[OUTPUT_JSON
]))
268 m
->mode
= OUTPUT_JSON
;
269 else if (streq(header
, mime_types
[OUTPUT_JSON_SSE
]))
270 m
->mode
= OUTPUT_JSON_SSE
;
271 else if (streq(header
, mime_types
[OUTPUT_EXPORT
]))
272 m
->mode
= OUTPUT_EXPORT
;
274 m
->mode
= OUTPUT_SHORT
;
279 static int request_parse_range(
281 struct MHD_Connection
*connection
) {
283 const char *range
, *colon
, *colon2
;
289 range
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Range");
293 if (!startswith(range
, "entries="))
297 range
+= strspn(range
, WHITESPACE
);
299 colon
= strchr(range
, ':');
301 m
->cursor
= strdup(range
);
305 colon2
= strchr(colon
+ 1, ':');
307 _cleanup_free_
char *t
;
309 t
= strndup(colon
+ 1, colon2
- colon
- 1);
313 r
= safe_atoi64(t
, &m
->n_skip
);
318 p
= (colon2
? colon2
: colon
) + 1;
320 r
= safe_atou64(p
, &m
->n_entries
);
324 if (m
->n_entries
<= 0)
327 m
->n_entries_set
= true;
330 m
->cursor
= strndup(range
, colon
- range
);
336 m
->cursor
[strcspn(m
->cursor
, WHITESPACE
)] = 0;
337 if (isempty(m
->cursor
))
338 m
->cursor
= mfree(m
->cursor
);
343 static int request_parse_arguments_iterator(
345 enum MHD_ValueKind kind
,
349 RequestMeta
*m
= cls
;
350 _cleanup_free_
char *p
= NULL
;
356 m
->argument_parse_error
= -EINVAL
;
360 if (streq(key
, "follow")) {
361 if (isempty(value
)) {
366 r
= parse_boolean(value
);
368 m
->argument_parse_error
= r
;
376 if (streq(key
, "discrete")) {
377 if (isempty(value
)) {
382 r
= parse_boolean(value
);
384 m
->argument_parse_error
= r
;
392 if (streq(key
, "boot")) {
396 r
= parse_boolean(value
);
398 m
->argument_parse_error
= r
;
404 char match
[9 + 32 + 1] = "_BOOT_ID=";
407 r
= sd_id128_get_boot(&bid
);
409 log_error_errno(r
, "Failed to get boot ID: %m");
413 sd_id128_to_string(bid
, match
+ 9);
414 r
= sd_journal_add_match(m
->journal
, match
, sizeof(match
)-1);
416 m
->argument_parse_error
= r
;
424 p
= strjoin(key
, "=", strempty(value
), NULL
);
426 m
->argument_parse_error
= log_oom();
430 r
= sd_journal_add_match(m
->journal
, p
, 0);
432 m
->argument_parse_error
= r
;
439 static int request_parse_arguments(
441 struct MHD_Connection
*connection
) {
446 m
->argument_parse_error
= 0;
447 MHD_get_connection_values(connection
, MHD_GET_ARGUMENT_KIND
, request_parse_arguments_iterator
, m
);
449 return m
->argument_parse_error
;
452 static int request_handler_entries(
453 struct MHD_Connection
*connection
,
454 void *connection_cls
) {
456 struct MHD_Response
*response
;
457 RequestMeta
*m
= connection_cls
;
465 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
467 if (request_parse_accept(m
, connection
) < 0)
468 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.\n");
470 if (request_parse_range(m
, connection
) < 0)
471 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Range header.\n");
473 if (request_parse_arguments(m
, connection
) < 0)
474 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse URL arguments.\n");
478 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Discrete seeks require a cursor specification.\n");
481 m
->n_entries_set
= true;
485 r
= sd_journal_seek_cursor(m
->journal
, m
->cursor
);
486 else if (m
->n_skip
>= 0)
487 r
= sd_journal_seek_head(m
->journal
);
488 else if (m
->n_skip
< 0)
489 r
= sd_journal_seek_tail(m
->journal
);
491 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to seek in journal.\n");
493 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_entries
, m
, NULL
);
495 return respond_oom(connection
);
497 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
]);
499 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
500 MHD_destroy_response(response
);
505 static int output_field(FILE *f
, OutputMode m
, const char *d
, size_t l
) {
509 eq
= memchr(d
, '=', l
);
513 j
= l
- (eq
- d
+ 1);
515 if (m
== OUTPUT_JSON
) {
516 fprintf(f
, "{ \"%.*s\" : ", (int) (eq
- d
), d
);
517 json_escape(f
, eq
+1, j
, OUTPUT_FULL_WIDTH
);
520 fwrite(eq
+1, 1, j
, f
);
527 static ssize_t
request_reader_fields(
533 RequestMeta
*m
= cls
;
540 assert(pos
>= m
->delta
);
544 while (pos
>= m
->size
) {
549 /* End of this field, so let's serialize the next
552 if (m
->n_fields_set
&&
554 return MHD_CONTENT_READER_END_OF_STREAM
;
556 r
= sd_journal_enumerate_unique(m
->journal
, &d
, &l
);
558 log_error_errno(r
, "Failed to advance field index: %m");
559 return MHD_CONTENT_READER_END_WITH_ERROR
;
561 return MHD_CONTENT_READER_END_OF_STREAM
;
569 r
= request_meta_ensure_tmp(m
);
571 log_error_errno(r
, "Failed to create temporary file: %m");
572 return MHD_CONTENT_READER_END_WITH_ERROR
;
575 r
= output_field(m
->tmp
, m
->mode
, d
, l
);
577 log_error_errno(r
, "Failed to serialize item: %m");
578 return MHD_CONTENT_READER_END_WITH_ERROR
;
582 if (sz
== (off_t
) -1) {
583 log_error_errno(errno
, "Failed to retrieve file position: %m");
584 return MHD_CONTENT_READER_END_WITH_ERROR
;
587 m
->size
= (uint64_t) sz
;
590 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
591 log_error_errno(errno
, "Failed to seek to position: %m");
592 return MHD_CONTENT_READER_END_WITH_ERROR
;
600 k
= fread(buf
, 1, n
, m
->tmp
);
602 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
603 return MHD_CONTENT_READER_END_WITH_ERROR
;
609 static int request_handler_fields(
610 struct MHD_Connection
*connection
,
612 void *connection_cls
) {
614 struct MHD_Response
*response
;
615 RequestMeta
*m
= connection_cls
;
623 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
625 if (request_parse_accept(m
, connection
) < 0)
626 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.\n");
628 r
= sd_journal_query_unique(m
->journal
, field
);
630 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to query unique fields.\n");
632 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_fields
, m
, NULL
);
634 return respond_oom(connection
);
636 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
== OUTPUT_JSON
? OUTPUT_JSON
: OUTPUT_SHORT
]);
638 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
639 MHD_destroy_response(response
);
644 static int request_handler_redirect(
645 struct MHD_Connection
*connection
,
646 const char *target
) {
649 struct MHD_Response
*response
;
655 if (asprintf(&page
, "<html><body>Please continue to the <a href=\"%s\">journal browser</a>.</body></html>", target
) < 0)
656 return respond_oom(connection
);
658 response
= MHD_create_response_from_buffer(strlen(page
), page
, MHD_RESPMEM_MUST_FREE
);
661 return respond_oom(connection
);
664 MHD_add_response_header(response
, "Content-Type", "text/html");
665 MHD_add_response_header(response
, "Location", target
);
667 ret
= MHD_queue_response(connection
, MHD_HTTP_MOVED_PERMANENTLY
, response
);
668 MHD_destroy_response(response
);
673 static int request_handler_file(
674 struct MHD_Connection
*connection
,
676 const char *mime_type
) {
678 struct MHD_Response
*response
;
680 _cleanup_close_
int fd
= -1;
687 fd
= open(path
, O_RDONLY
|O_CLOEXEC
);
689 return mhd_respondf(connection
, MHD_HTTP_NOT_FOUND
, "Failed to open file %s: %m\n", path
);
691 if (fstat(fd
, &st
) < 0)
692 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to stat file: %m\n");
694 response
= MHD_create_response_from_fd_at_offset(st
.st_size
, fd
, 0);
696 return respond_oom(connection
);
700 MHD_add_response_header(response
, "Content-Type", mime_type
);
702 ret
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
703 MHD_destroy_response(response
);
708 static int get_virtualization(char **v
) {
709 _cleanup_bus_unref_ sd_bus
*bus
= NULL
;
713 r
= sd_bus_default_system(&bus
);
717 r
= sd_bus_get_property_string(
719 "org.freedesktop.systemd1",
720 "/org/freedesktop/systemd1",
721 "org.freedesktop.systemd1.Manager",
738 static int request_handler_machine(
739 struct MHD_Connection
*connection
,
740 void *connection_cls
) {
742 struct MHD_Response
*response
;
743 RequestMeta
*m
= connection_cls
;
745 _cleanup_free_
char* hostname
= NULL
, *os_name
= NULL
;
746 uint64_t cutoff_from
= 0, cutoff_to
= 0, usage
= 0;
749 _cleanup_free_
char *v
= NULL
;
756 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
758 r
= sd_id128_get_machine(&mid
);
760 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine machine ID: %s\n", strerror(-r
));
762 r
= sd_id128_get_boot(&bid
);
764 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine boot ID: %s\n", strerror(-r
));
766 hostname
= gethostname_malloc();
768 return respond_oom(connection
);
770 r
= sd_journal_get_usage(m
->journal
, &usage
);
772 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %s\n", strerror(-r
));
774 r
= sd_journal_get_cutoff_realtime_usec(m
->journal
, &cutoff_from
, &cutoff_to
);
776 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %s\n", strerror(-r
));
778 if (parse_env_file("/etc/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
) == -ENOENT
)
779 (void) parse_env_file("/usr/lib/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
);
781 get_virtualization(&v
);
784 "{ \"machine_id\" : \"" SD_ID128_FORMAT_STR
"\","
785 "\"boot_id\" : \"" SD_ID128_FORMAT_STR
"\","
786 "\"hostname\" : \"%s\","
787 "\"os_pretty_name\" : \"%s\","
788 "\"virtualization\" : \"%s\","
789 "\"usage\" : \"%"PRIu64
"\","
790 "\"cutoff_from_realtime\" : \"%"PRIu64
"\","
791 "\"cutoff_to_realtime\" : \"%"PRIu64
"\" }\n",
792 SD_ID128_FORMAT_VAL(mid
),
793 SD_ID128_FORMAT_VAL(bid
),
794 hostname_cleanup(hostname
),
795 os_name
? os_name
: "Linux",
802 return respond_oom(connection
);
804 response
= MHD_create_response_from_buffer(strlen(json
), json
, MHD_RESPMEM_MUST_FREE
);
807 return respond_oom(connection
);
810 MHD_add_response_header(response
, "Content-Type", "application/json");
811 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
812 MHD_destroy_response(response
);
817 static int request_handler(
819 struct MHD_Connection
*connection
,
823 const char *upload_data
,
824 size_t *upload_data_size
,
825 void **connection_cls
) {
829 assert(connection_cls
);
833 if (!streq(method
, "GET"))
834 return mhd_respond(connection
, MHD_HTTP_METHOD_NOT_ACCEPTABLE
,
835 "Unsupported method.\n");
838 if (!*connection_cls
) {
839 if (!request_meta(connection_cls
))
840 return respond_oom(connection
);
845 r
= check_permissions(connection
, &code
, NULL
);
851 return request_handler_redirect(connection
, "/browse");
853 if (streq(url
, "/entries"))
854 return request_handler_entries(connection
, *connection_cls
);
856 if (startswith(url
, "/fields/"))
857 return request_handler_fields(connection
, url
+ 8, *connection_cls
);
859 if (streq(url
, "/browse"))
860 return request_handler_file(connection
, DOCUMENT_ROOT
"/browse.html", "text/html");
862 if (streq(url
, "/machine"))
863 return request_handler_machine(connection
, *connection_cls
);
865 return mhd_respond(connection
, MHD_HTTP_NOT_FOUND
, "Not found.\n");
868 static void help(void) {
869 printf("%s [OPTIONS...] ...\n\n"
870 "HTTP server for journal events.\n\n"
871 " -h --help Show this help\n"
872 " --version Show package version\n"
873 " --cert=CERT.PEM Server certificate in PEM format\n"
874 " --key=KEY.PEM Server key in PEM format\n"
875 " --trust=CERT.PEM Certificat authority certificate in PEM format\n",
876 program_invocation_short_name
);
879 static int parse_argv(int argc
, char *argv
[]) {
889 static const struct option options
[] = {
890 { "help", no_argument
, NULL
, 'h' },
891 { "version", no_argument
, NULL
, ARG_VERSION
},
892 { "key", required_argument
, NULL
, ARG_KEY
},
893 { "cert", required_argument
, NULL
, ARG_CERT
},
894 { "trust", required_argument
, NULL
, ARG_TRUST
},
901 while ((c
= getopt_long(argc
, argv
, "h", options
, NULL
)) >= 0)
914 log_error("Key file specified twice");
917 r
= read_full_file(optarg
, &arg_key_pem
, NULL
);
919 return log_error_errno(r
, "Failed to read key file: %m");
925 log_error("Certificate file specified twice");
928 r
= read_full_file(optarg
, &arg_cert_pem
, NULL
);
930 return log_error_errno(r
, "Failed to read certificate file: %m");
931 assert(arg_cert_pem
);
937 log_error("CA certificate file specified twice");
940 r
= read_full_file(optarg
, &arg_trust_pem
, NULL
);
942 return log_error_errno(r
, "Failed to read CA certificate file: %m");
943 assert(arg_trust_pem
);
946 log_error("Option --trust is not available.");
953 assert_not_reached("Unhandled option");
957 log_error("This program does not take arguments.");
961 if (!!arg_key_pem
!= !!arg_cert_pem
) {
962 log_error("Certificate and key files must be specified together");
966 if (arg_trust_pem
&& !arg_key_pem
) {
967 log_error("CA certificate can only be used with certificate file");
974 int main(int argc
, char *argv
[]) {
975 struct MHD_Daemon
*d
= NULL
;
978 log_set_target(LOG_TARGET_AUTO
);
979 log_parse_environment();
982 r
= parse_argv(argc
, argv
);
990 r
= setup_gnutls_logger(NULL
);
994 n
= sd_listen_fds(1);
996 log_error_errno(n
, "Failed to determine passed sockets: %m");
999 log_error("Can't listen on more than one socket.");
1002 struct MHD_OptionItem opts
[] = {
1003 { MHD_OPTION_NOTIFY_COMPLETED
,
1004 (intptr_t) request_meta_free
, NULL
},
1005 { MHD_OPTION_EXTERNAL_LOGGER
,
1006 (intptr_t) microhttpd_logger
, NULL
},
1007 { MHD_OPTION_END
, 0, NULL
},
1008 { MHD_OPTION_END
, 0, NULL
},
1009 { MHD_OPTION_END
, 0, NULL
},
1010 { MHD_OPTION_END
, 0, NULL
},
1011 { MHD_OPTION_END
, 0, NULL
}};
1014 /* We force MHD_USE_PIPE_FOR_SHUTDOWN here, in order
1015 * to make sure libmicrohttpd doesn't use shutdown()
1016 * on our listening socket, which would break socket
1017 * re-activation. See
1019 * https://lists.gnu.org/archive/html/libmicrohttpd/2015-09/msg00014.html
1020 * https://github.com/systemd/systemd/pull/1286
1025 MHD_USE_DUAL_STACK
|
1026 MHD_USE_PIPE_FOR_SHUTDOWN
|
1028 MHD_USE_THREAD_PER_CONNECTION
;
1031 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1032 {MHD_OPTION_LISTEN_SOCKET
, SD_LISTEN_FDS_START
};
1034 assert(arg_cert_pem
);
1035 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1036 {MHD_OPTION_HTTPS_MEM_KEY
, 0, arg_key_pem
};
1037 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1038 {MHD_OPTION_HTTPS_MEM_CERT
, 0, arg_cert_pem
};
1039 flags
|= MHD_USE_SSL
;
1041 if (arg_trust_pem
) {
1042 assert(flags
& MHD_USE_SSL
);
1043 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1044 {MHD_OPTION_HTTPS_MEM_TRUST
, 0, arg_trust_pem
};
1047 d
= MHD_start_daemon(flags
, 19531,
1049 request_handler
, NULL
,
1050 MHD_OPTION_ARRAY
, opts
,
1055 log_error("Failed to start daemon!");