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/>.
28 #include <microhttpd.h>
31 #include <gnutls/gnutls.h>
34 #include "sd-journal.h"
35 #include "sd-daemon.h"
40 #include "logs-show.h"
41 #include "microhttpd-util.h"
45 #include "hostname-util.h"
47 static char *arg_key_pem
= NULL
;
48 static char *arg_cert_pem
= NULL
;
49 static char *arg_trust_pem
= 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
;
107 sd_journal_close(m
->journal
);
116 static int open_journal(RequestMeta
*m
) {
122 return sd_journal_open(&m
->journal
, SD_JOURNAL_LOCAL_ONLY
|SD_JOURNAL_SYSTEM
);
125 static int request_meta_ensure_tmp(RequestMeta
*m
) {
131 fd
= open_tmpfile("/tmp", O_RDWR
|O_CLOEXEC
);
135 m
->tmp
= fdopen(fd
, "rw");
145 static ssize_t
request_reader_entries(
151 RequestMeta
*m
= cls
;
158 assert(pos
>= m
->delta
);
162 while (pos
>= m
->size
) {
165 /* End of this entry, so let's serialize the next
168 if (m
->n_entries_set
&&
170 return MHD_CONTENT_READER_END_OF_STREAM
;
173 r
= sd_journal_previous_skip(m
->journal
, (uint64_t) -m
->n_skip
+ 1);
174 else if (m
->n_skip
> 0)
175 r
= sd_journal_next_skip(m
->journal
, (uint64_t) m
->n_skip
+ 1);
177 r
= sd_journal_next(m
->journal
);
180 log_error_errno(r
, "Failed to advance journal pointer: %m");
181 return MHD_CONTENT_READER_END_WITH_ERROR
;
185 r
= sd_journal_wait(m
->journal
, (uint64_t) -1);
187 log_error_errno(r
, "Couldn't wait for journal event: %m");
188 return MHD_CONTENT_READER_END_WITH_ERROR
;
194 return MHD_CONTENT_READER_END_OF_STREAM
;
200 r
= sd_journal_test_cursor(m
->journal
, m
->cursor
);
202 log_error_errno(r
, "Failed to test cursor: %m");
203 return MHD_CONTENT_READER_END_WITH_ERROR
;
207 return MHD_CONTENT_READER_END_OF_STREAM
;
213 if (m
->n_entries_set
)
218 r
= request_meta_ensure_tmp(m
);
220 log_error_errno(r
, "Failed to create temporary file: %m");
221 return MHD_CONTENT_READER_END_WITH_ERROR
;
224 r
= output_journal(m
->tmp
, m
->journal
, m
->mode
, 0, OUTPUT_FULL_WIDTH
, NULL
);
226 log_error_errno(r
, "Failed to serialize item: %m");
227 return MHD_CONTENT_READER_END_WITH_ERROR
;
231 if (sz
== (off_t
) -1) {
232 log_error_errno(errno
, "Failed to retrieve file position: %m");
233 return MHD_CONTENT_READER_END_WITH_ERROR
;
236 m
->size
= (uint64_t) sz
;
239 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
240 log_error_errno(errno
, "Failed to seek to position: %m");
241 return MHD_CONTENT_READER_END_WITH_ERROR
;
249 k
= fread(buf
, 1, n
, m
->tmp
);
251 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
252 return MHD_CONTENT_READER_END_WITH_ERROR
;
258 static int request_parse_accept(
260 struct MHD_Connection
*connection
) {
267 header
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Accept");
271 if (streq(header
, mime_types
[OUTPUT_JSON
]))
272 m
->mode
= OUTPUT_JSON
;
273 else if (streq(header
, mime_types
[OUTPUT_JSON_SSE
]))
274 m
->mode
= OUTPUT_JSON_SSE
;
275 else if (streq(header
, mime_types
[OUTPUT_EXPORT
]))
276 m
->mode
= OUTPUT_EXPORT
;
278 m
->mode
= OUTPUT_SHORT
;
283 static int request_parse_range(
285 struct MHD_Connection
*connection
) {
287 const char *range
, *colon
, *colon2
;
293 range
= MHD_lookup_connection_value(connection
, MHD_HEADER_KIND
, "Range");
297 if (!startswith(range
, "entries="))
301 range
+= strspn(range
, WHITESPACE
);
303 colon
= strchr(range
, ':');
305 m
->cursor
= strdup(range
);
309 colon2
= strchr(colon
+ 1, ':');
311 _cleanup_free_
char *t
;
313 t
= strndup(colon
+ 1, colon2
- colon
- 1);
317 r
= safe_atoi64(t
, &m
->n_skip
);
322 p
= (colon2
? colon2
: colon
) + 1;
324 r
= safe_atou64(p
, &m
->n_entries
);
328 if (m
->n_entries
<= 0)
331 m
->n_entries_set
= true;
334 m
->cursor
= strndup(range
, colon
- range
);
340 m
->cursor
[strcspn(m
->cursor
, WHITESPACE
)] = 0;
341 if (isempty(m
->cursor
)) {
349 static int request_parse_arguments_iterator(
351 enum MHD_ValueKind kind
,
355 RequestMeta
*m
= cls
;
356 _cleanup_free_
char *p
= NULL
;
362 m
->argument_parse_error
= -EINVAL
;
366 if (streq(key
, "follow")) {
367 if (isempty(value
)) {
372 r
= parse_boolean(value
);
374 m
->argument_parse_error
= r
;
382 if (streq(key
, "discrete")) {
383 if (isempty(value
)) {
388 r
= parse_boolean(value
);
390 m
->argument_parse_error
= r
;
398 if (streq(key
, "boot")) {
402 r
= parse_boolean(value
);
404 m
->argument_parse_error
= r
;
410 char match
[9 + 32 + 1] = "_BOOT_ID=";
413 r
= sd_id128_get_boot(&bid
);
415 log_error_errno(r
, "Failed to get boot ID: %m");
419 sd_id128_to_string(bid
, match
+ 9);
420 r
= sd_journal_add_match(m
->journal
, match
, sizeof(match
)-1);
422 m
->argument_parse_error
= r
;
430 p
= strjoin(key
, "=", strempty(value
), NULL
);
432 m
->argument_parse_error
= log_oom();
436 r
= sd_journal_add_match(m
->journal
, p
, 0);
438 m
->argument_parse_error
= r
;
445 static int request_parse_arguments(
447 struct MHD_Connection
*connection
) {
452 m
->argument_parse_error
= 0;
453 MHD_get_connection_values(connection
, MHD_GET_ARGUMENT_KIND
, request_parse_arguments_iterator
, m
);
455 return m
->argument_parse_error
;
458 static int request_handler_entries(
459 struct MHD_Connection
*connection
,
460 void *connection_cls
) {
462 struct MHD_Response
*response
;
463 RequestMeta
*m
= connection_cls
;
471 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
473 if (request_parse_accept(m
, connection
) < 0)
474 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.\n");
476 if (request_parse_range(m
, connection
) < 0)
477 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Range header.\n");
479 if (request_parse_arguments(m
, connection
) < 0)
480 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse URL arguments.\n");
484 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Discrete seeks require a cursor specification.\n");
487 m
->n_entries_set
= true;
491 r
= sd_journal_seek_cursor(m
->journal
, m
->cursor
);
492 else if (m
->n_skip
>= 0)
493 r
= sd_journal_seek_head(m
->journal
);
494 else if (m
->n_skip
< 0)
495 r
= sd_journal_seek_tail(m
->journal
);
497 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to seek in journal.\n");
499 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_entries
, m
, NULL
);
501 return respond_oom(connection
);
503 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
]);
505 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
506 MHD_destroy_response(response
);
511 static int output_field(FILE *f
, OutputMode m
, const char *d
, size_t l
) {
515 eq
= memchr(d
, '=', l
);
519 j
= l
- (eq
- d
+ 1);
521 if (m
== OUTPUT_JSON
) {
522 fprintf(f
, "{ \"%.*s\" : ", (int) (eq
- d
), d
);
523 json_escape(f
, eq
+1, j
, OUTPUT_FULL_WIDTH
);
526 fwrite(eq
+1, 1, j
, f
);
533 static ssize_t
request_reader_fields(
539 RequestMeta
*m
= cls
;
546 assert(pos
>= m
->delta
);
550 while (pos
>= m
->size
) {
555 /* End of this field, so let's serialize the next
558 if (m
->n_fields_set
&&
560 return MHD_CONTENT_READER_END_OF_STREAM
;
562 r
= sd_journal_enumerate_unique(m
->journal
, &d
, &l
);
564 log_error_errno(r
, "Failed to advance field index: %m");
565 return MHD_CONTENT_READER_END_WITH_ERROR
;
567 return MHD_CONTENT_READER_END_OF_STREAM
;
575 r
= request_meta_ensure_tmp(m
);
577 log_error_errno(r
, "Failed to create temporary file: %m");
578 return MHD_CONTENT_READER_END_WITH_ERROR
;
581 r
= output_field(m
->tmp
, m
->mode
, d
, l
);
583 log_error_errno(r
, "Failed to serialize item: %m");
584 return MHD_CONTENT_READER_END_WITH_ERROR
;
588 if (sz
== (off_t
) -1) {
589 log_error_errno(errno
, "Failed to retrieve file position: %m");
590 return MHD_CONTENT_READER_END_WITH_ERROR
;
593 m
->size
= (uint64_t) sz
;
596 if (fseeko(m
->tmp
, pos
, SEEK_SET
) < 0) {
597 log_error_errno(errno
, "Failed to seek to position: %m");
598 return MHD_CONTENT_READER_END_WITH_ERROR
;
606 k
= fread(buf
, 1, n
, m
->tmp
);
608 log_error("Failed to read from file: %s", errno
? strerror(errno
) : "Premature EOF");
609 return MHD_CONTENT_READER_END_WITH_ERROR
;
615 static int request_handler_fields(
616 struct MHD_Connection
*connection
,
618 void *connection_cls
) {
620 struct MHD_Response
*response
;
621 RequestMeta
*m
= connection_cls
;
629 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
631 if (request_parse_accept(m
, connection
) < 0)
632 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to parse Accept header.\n");
634 r
= sd_journal_query_unique(m
->journal
, field
);
636 return mhd_respond(connection
, MHD_HTTP_BAD_REQUEST
, "Failed to query unique fields.\n");
638 response
= MHD_create_response_from_callback(MHD_SIZE_UNKNOWN
, 4*1024, request_reader_fields
, m
, NULL
);
640 return respond_oom(connection
);
642 MHD_add_response_header(response
, "Content-Type", mime_types
[m
->mode
== OUTPUT_JSON
? OUTPUT_JSON
: OUTPUT_SHORT
]);
644 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
645 MHD_destroy_response(response
);
650 static int request_handler_redirect(
651 struct MHD_Connection
*connection
,
652 const char *target
) {
655 struct MHD_Response
*response
;
661 if (asprintf(&page
, "<html><body>Please continue to the <a href=\"%s\">journal browser</a>.</body></html>", target
) < 0)
662 return respond_oom(connection
);
664 response
= MHD_create_response_from_buffer(strlen(page
), page
, MHD_RESPMEM_MUST_FREE
);
667 return respond_oom(connection
);
670 MHD_add_response_header(response
, "Content-Type", "text/html");
671 MHD_add_response_header(response
, "Location", target
);
673 ret
= MHD_queue_response(connection
, MHD_HTTP_MOVED_PERMANENTLY
, response
);
674 MHD_destroy_response(response
);
679 static int request_handler_file(
680 struct MHD_Connection
*connection
,
682 const char *mime_type
) {
684 struct MHD_Response
*response
;
686 _cleanup_close_
int fd
= -1;
693 fd
= open(path
, O_RDONLY
|O_CLOEXEC
);
695 return mhd_respondf(connection
, MHD_HTTP_NOT_FOUND
, "Failed to open file %s: %m\n", path
);
697 if (fstat(fd
, &st
) < 0)
698 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to stat file: %m\n");
700 response
= MHD_create_response_from_fd_at_offset(st
.st_size
, fd
, 0);
702 return respond_oom(connection
);
706 MHD_add_response_header(response
, "Content-Type", mime_type
);
708 ret
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
709 MHD_destroy_response(response
);
714 static int get_virtualization(char **v
) {
715 _cleanup_bus_unref_ sd_bus
*bus
= NULL
;
719 r
= sd_bus_default_system(&bus
);
723 r
= sd_bus_get_property_string(
725 "org.freedesktop.systemd1",
726 "/org/freedesktop/systemd1",
727 "org.freedesktop.systemd1.Manager",
744 static int request_handler_machine(
745 struct MHD_Connection
*connection
,
746 void *connection_cls
) {
748 struct MHD_Response
*response
;
749 RequestMeta
*m
= connection_cls
;
751 _cleanup_free_
char* hostname
= NULL
, *os_name
= NULL
;
752 uint64_t cutoff_from
= 0, cutoff_to
= 0, usage
= 0;
755 _cleanup_free_
char *v
= NULL
;
762 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to open journal: %s\n", strerror(-r
));
764 r
= sd_id128_get_machine(&mid
);
766 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine machine ID: %s\n", strerror(-r
));
768 r
= sd_id128_get_boot(&bid
);
770 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine boot ID: %s\n", strerror(-r
));
772 hostname
= gethostname_malloc();
774 return respond_oom(connection
);
776 r
= sd_journal_get_usage(m
->journal
, &usage
);
778 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %s\n", strerror(-r
));
780 r
= sd_journal_get_cutoff_realtime_usec(m
->journal
, &cutoff_from
, &cutoff_to
);
782 return mhd_respondf(connection
, MHD_HTTP_INTERNAL_SERVER_ERROR
, "Failed to determine disk usage: %s\n", strerror(-r
));
784 if (parse_env_file("/etc/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
) == -ENOENT
)
785 (void) parse_env_file("/usr/lib/os-release", NEWLINE
, "PRETTY_NAME", &os_name
, NULL
);
787 get_virtualization(&v
);
790 "{ \"machine_id\" : \"" SD_ID128_FORMAT_STR
"\","
791 "\"boot_id\" : \"" SD_ID128_FORMAT_STR
"\","
792 "\"hostname\" : \"%s\","
793 "\"os_pretty_name\" : \"%s\","
794 "\"virtualization\" : \"%s\","
795 "\"usage\" : \"%"PRIu64
"\","
796 "\"cutoff_from_realtime\" : \"%"PRIu64
"\","
797 "\"cutoff_to_realtime\" : \"%"PRIu64
"\" }\n",
798 SD_ID128_FORMAT_VAL(mid
),
799 SD_ID128_FORMAT_VAL(bid
),
800 hostname_cleanup(hostname
, false),
801 os_name
? os_name
: "Linux",
808 return respond_oom(connection
);
810 response
= MHD_create_response_from_buffer(strlen(json
), json
, MHD_RESPMEM_MUST_FREE
);
813 return respond_oom(connection
);
816 MHD_add_response_header(response
, "Content-Type", "application/json");
817 r
= MHD_queue_response(connection
, MHD_HTTP_OK
, response
);
818 MHD_destroy_response(response
);
823 static int request_handler(
825 struct MHD_Connection
*connection
,
829 const char *upload_data
,
830 size_t *upload_data_size
,
831 void **connection_cls
) {
835 assert(connection_cls
);
839 if (!streq(method
, "GET"))
840 return mhd_respond(connection
, MHD_HTTP_METHOD_NOT_ACCEPTABLE
,
841 "Unsupported method.\n");
844 if (!*connection_cls
) {
845 if (!request_meta(connection_cls
))
846 return respond_oom(connection
);
851 r
= check_permissions(connection
, &code
, NULL
);
857 return request_handler_redirect(connection
, "/browse");
859 if (streq(url
, "/entries"))
860 return request_handler_entries(connection
, *connection_cls
);
862 if (startswith(url
, "/fields/"))
863 return request_handler_fields(connection
, url
+ 8, *connection_cls
);
865 if (streq(url
, "/browse"))
866 return request_handler_file(connection
, DOCUMENT_ROOT
"/browse.html", "text/html");
868 if (streq(url
, "/machine"))
869 return request_handler_machine(connection
, *connection_cls
);
871 return mhd_respond(connection
, MHD_HTTP_NOT_FOUND
, "Not found.\n");
874 static void help(void) {
875 printf("%s [OPTIONS...] ...\n\n"
876 "HTTP server for journal events.\n\n"
877 " -h --help Show this help\n"
878 " --version Show package version\n"
879 " --cert=CERT.PEM Server certificate in PEM format\n"
880 " --key=KEY.PEM Server key in PEM format\n"
881 " --trust=CERT.PEM Certificat authority certificate in PEM format\n",
882 program_invocation_short_name
);
885 static int parse_argv(int argc
, char *argv
[]) {
895 static const struct option options
[] = {
896 { "help", no_argument
, NULL
, 'h' },
897 { "version", no_argument
, NULL
, ARG_VERSION
},
898 { "key", required_argument
, NULL
, ARG_KEY
},
899 { "cert", required_argument
, NULL
, ARG_CERT
},
900 { "trust", required_argument
, NULL
, ARG_TRUST
},
907 while ((c
= getopt_long(argc
, argv
, "h", options
, NULL
)) >= 0)
916 puts(PACKAGE_STRING
);
917 puts(SYSTEMD_FEATURES
);
922 log_error("Key file specified twice");
925 r
= read_full_file(optarg
, &arg_key_pem
, NULL
);
927 return log_error_errno(r
, "Failed to read key file: %m");
933 log_error("Certificate file specified twice");
936 r
= read_full_file(optarg
, &arg_cert_pem
, NULL
);
938 return log_error_errno(r
, "Failed to read certificate file: %m");
939 assert(arg_cert_pem
);
945 log_error("CA certificate file specified twice");
948 r
= read_full_file(optarg
, &arg_trust_pem
, NULL
);
950 return log_error_errno(r
, "Failed to read CA certificate file: %m");
951 assert(arg_trust_pem
);
954 log_error("Option --trust is not available.");
961 assert_not_reached("Unhandled option");
965 log_error("This program does not take arguments.");
969 if (!!arg_key_pem
!= !!arg_cert_pem
) {
970 log_error("Certificate and key files must be specified together");
974 if (arg_trust_pem
&& !arg_key_pem
) {
975 log_error("CA certificate can only be used with certificate file");
982 int main(int argc
, char *argv
[]) {
983 struct MHD_Daemon
*d
= NULL
;
986 log_set_target(LOG_TARGET_AUTO
);
987 log_parse_environment();
990 r
= parse_argv(argc
, argv
);
998 r
= setup_gnutls_logger(NULL
);
1000 return EXIT_FAILURE
;
1002 n
= sd_listen_fds(1);
1004 log_error_errno(n
, "Failed to determine passed sockets: %m");
1007 log_error("Can't listen on more than one socket.");
1010 struct MHD_OptionItem opts
[] = {
1011 { MHD_OPTION_NOTIFY_COMPLETED
,
1012 (intptr_t) request_meta_free
, NULL
},
1013 { MHD_OPTION_EXTERNAL_LOGGER
,
1014 (intptr_t) microhttpd_logger
, NULL
},
1015 { MHD_OPTION_END
, 0, NULL
},
1016 { MHD_OPTION_END
, 0, NULL
},
1017 { MHD_OPTION_END
, 0, NULL
},
1018 { MHD_OPTION_END
, 0, NULL
},
1019 { MHD_OPTION_END
, 0, NULL
}};
1021 int flags
= MHD_USE_THREAD_PER_CONNECTION
|MHD_USE_POLL
|MHD_USE_DEBUG
;
1024 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1025 {MHD_OPTION_LISTEN_SOCKET
, SD_LISTEN_FDS_START
};
1027 assert(arg_cert_pem
);
1028 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1029 {MHD_OPTION_HTTPS_MEM_KEY
, 0, arg_key_pem
};
1030 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1031 {MHD_OPTION_HTTPS_MEM_CERT
, 0, arg_cert_pem
};
1032 flags
|= MHD_USE_SSL
;
1034 if (arg_trust_pem
) {
1035 assert(flags
& MHD_USE_SSL
);
1036 opts
[opts_pos
++] = (struct MHD_OptionItem
)
1037 {MHD_OPTION_HTTPS_MEM_TRUST
, 0, arg_trust_pem
};
1040 d
= MHD_start_daemon(flags
, 19531,
1042 request_handler
, NULL
,
1043 MHD_OPTION_ARRAY
, opts
,
1048 log_error("Failed to start daemon!");