]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/journal-remote/journal-upload.c
systemd-upload: print paths in help()
[thirdparty/systemd.git] / src / journal-remote / journal-upload.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4 This file is part of systemd.
5
6 Copyright 2014 Zbigniew Jędrzejewski-Szmek
7
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.
12
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.
17
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/>.
20 ***/
21
22 #include <stdio.h>
23 #include <curl/curl.h>
24 #include <sys/stat.h>
25 #include <fcntl.h>
26 #include <getopt.h>
27
28 #include "sd-daemon.h"
29
30 #include "log.h"
31 #include "util.h"
32 #include "build.h"
33 #include "fileio.h"
34 #include "conf-parser.h"
35 #include "journal-upload.h"
36
37 #define PRIV_KEY_FILE CERTIFICATE_ROOT "/private/journal-upload.pem"
38 #define CERT_FILE CERTIFICATE_ROOT "/certs/journal-upload.pem"
39 #define TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem"
40
41 static const char* arg_url;
42
43 static void close_fd_input(Uploader *u);
44
45 static const char *arg_key = NULL;
46 static const char *arg_cert = NULL;
47 static const char *arg_trust = NULL;
48
49 static const char *arg_directory = NULL;
50 static char **arg_file = NULL;
51 static const char *arg_cursor = NULL;
52 static bool arg_after_cursor = false;
53 static int arg_journal_type = 0;
54 static const char *arg_machine = NULL;
55 static bool arg_merge = false;
56 static int arg_follow = -1;
57 static const char *arg_save_state = NULL;
58
59 #define SERVER_ANSWER_KEEP 2048
60
61 #define STATE_FILE "/var/lib/systemd/journal-upload/state"
62
63 #define easy_setopt(curl, opt, value, level, cmd) \
64 { \
65 code = curl_easy_setopt(curl, opt, value); \
66 if (code) { \
67 log_full(level, \
68 "curl_easy_setopt " #opt " failed: %s", \
69 curl_easy_strerror(code)); \
70 cmd; \
71 } \
72 }
73
74 static size_t output_callback(char *buf,
75 size_t size,
76 size_t nmemb,
77 void *userp) {
78 Uploader *u = userp;
79
80 assert(u);
81
82 log_debug("The server answers (%zu bytes): %.*s",
83 size*nmemb, (int)(size*nmemb), buf);
84
85 if (nmemb && !u->answer) {
86 u->answer = strndup(buf, size*nmemb);
87 if (!u->answer)
88 log_warning("Failed to store server answer (%zu bytes): %s",
89 size*nmemb, strerror(ENOMEM));
90 }
91
92 return size * nmemb;
93 }
94
95 static int update_cursor_state(Uploader *u) {
96 _cleanup_free_ char *temp_path = NULL;
97 _cleanup_fclose_ FILE *f = NULL;
98 int r;
99
100 if (!u->state_file || !u->last_cursor)
101 return 0;
102
103 r = fopen_temporary(u->state_file, &f, &temp_path);
104 if (r < 0)
105 goto finish;
106
107 fprintf(f,
108 "# This is private data. Do not parse.\n"
109 "LAST_CURSOR=%s\n",
110 u->last_cursor);
111
112 fflush(f);
113
114 if (ferror(f) || rename(temp_path, u->state_file) < 0) {
115 r = -errno;
116 unlink(u->state_file);
117 unlink(temp_path);
118 }
119
120 finish:
121 if (r < 0)
122 log_error("Failed to save state %s: %s", u->state_file, strerror(-r));
123
124 return r;
125 }
126
127 static int load_cursor_state(Uploader *u) {
128 int r;
129
130 if (!u->state_file)
131 return 0;
132
133 r = parse_env_file(u->state_file, NEWLINE,
134 "LAST_CURSOR", &u->last_cursor,
135 NULL);
136
137 if (r < 0 && r != -ENOENT) {
138 log_error("Failed to read state file %s: %s",
139 u->state_file, strerror(-r));
140 return r;
141 }
142
143 return 0;
144 }
145
146
147
148 int start_upload(Uploader *u,
149 size_t (*input_callback)(void *ptr,
150 size_t size,
151 size_t nmemb,
152 void *userdata),
153 void *data) {
154 CURLcode code;
155
156 assert(u);
157 assert(input_callback);
158
159 if (!u->header) {
160 struct curl_slist *h;
161
162 h = curl_slist_append(NULL, "Content-Type: application/vnd.fdo.journal");
163 if (!h)
164 return log_oom();
165
166 h = curl_slist_append(h, "Transfer-Encoding: chunked");
167 if (!h) {
168 curl_slist_free_all(h);
169 return log_oom();
170 }
171
172 h = curl_slist_append(h, "Accept: text/plain");
173 if (!h) {
174 curl_slist_free_all(h);
175 return log_oom();
176 }
177
178 u->header = h;
179 }
180
181 if (!u->easy) {
182 CURL *curl;
183
184 curl = curl_easy_init();
185 if (!curl) {
186 log_error("Call to curl_easy_init failed.");
187 return -ENOSR;
188 }
189
190 /* tell it to POST to the URL */
191 easy_setopt(curl, CURLOPT_POST, 1L,
192 LOG_ERR, return -EXFULL);
193
194 easy_setopt(curl, CURLOPT_ERRORBUFFER, u->error,
195 LOG_ERR, return -EXFULL);
196
197 /* set where to write to */
198 easy_setopt(curl, CURLOPT_WRITEFUNCTION, output_callback,
199 LOG_ERR, return -EXFULL);
200
201 easy_setopt(curl, CURLOPT_WRITEDATA, data,
202 LOG_ERR, return -EXFULL);
203
204 /* set where to read from */
205 easy_setopt(curl, CURLOPT_READFUNCTION, input_callback,
206 LOG_ERR, return -EXFULL);
207
208 easy_setopt(curl, CURLOPT_READDATA, data,
209 LOG_ERR, return -EXFULL);
210
211 /* use our special own mime type and chunked transfer */
212 easy_setopt(curl, CURLOPT_HTTPHEADER, u->header,
213 LOG_ERR, return -EXFULL);
214
215 /* enable verbose for easier tracing */
216 easy_setopt(curl, CURLOPT_VERBOSE, 1L, LOG_WARNING, );
217
218 easy_setopt(curl, CURLOPT_USERAGENT,
219 "systemd-journal-upload " PACKAGE_STRING,
220 LOG_WARNING, );
221
222 if (arg_key || startswith(u->url, "https://")) {
223 easy_setopt(curl, CURLOPT_SSLKEY, arg_key ?: PRIV_KEY_FILE,
224 LOG_ERR, return -EXFULL);
225 easy_setopt(curl, CURLOPT_SSLCERT, arg_cert ?: CERT_FILE,
226 LOG_ERR, return -EXFULL);
227 }
228
229 if (arg_trust || startswith(u->url, "https://"))
230 easy_setopt(curl, CURLOPT_CAINFO, arg_trust ?: TRUST_FILE,
231 LOG_ERR, return -EXFULL);
232
233 if (arg_key || arg_trust)
234 easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1,
235 LOG_WARNING, );
236
237 u->easy = curl;
238 } else {
239 /* truncate the potential old error message */
240 u->error[0] = '\0';
241
242 free(u->answer);
243 u->answer = 0;
244 }
245
246 /* upload to this place */
247 code = curl_easy_setopt(u->easy, CURLOPT_URL, u->url);
248 if (code) {
249 log_error("curl_easy_setopt CURLOPT_URL failed: %s",
250 curl_easy_strerror(code));
251 return -EXFULL;
252 }
253
254 u->uploading = true;
255
256 return 0;
257 }
258
259 static size_t fd_input_callback(void *buf, size_t size, size_t nmemb, void *userp) {
260 Uploader *u = userp;
261
262 ssize_t r;
263
264 assert(u);
265 assert(nmemb <= SSIZE_MAX / size);
266
267 if (u->input < 0)
268 return 0;
269
270 r = read(u->input, buf, size * nmemb);
271 log_debug("%s: allowed %zu, read %zu", __func__, size*nmemb, r);
272
273 if (r > 0)
274 return r;
275
276 u->uploading = false;
277 if (r == 0) {
278 log_debug("Reached EOF");
279 close_fd_input(u);
280 return 0;
281 } else {
282 log_error("Aborting transfer after read error on input: %m.");
283 return CURL_READFUNC_ABORT;
284 }
285 }
286
287 static void close_fd_input(Uploader *u) {
288 assert(u);
289
290 if (u->input >= 0)
291 close_nointr(u->input);
292 u->input = -1;
293 u->timeout = 0;
294 }
295
296 static int dispatch_fd_input(sd_event_source *event,
297 int fd,
298 uint32_t revents,
299 void *userp) {
300 Uploader *u = userp;
301
302 assert(u);
303 assert(fd >= 0);
304
305 if (revents & EPOLLHUP) {
306 log_debug("Received HUP");
307 close_fd_input(u);
308 return 0;
309 }
310
311 if (!(revents & EPOLLIN)) {
312 log_warning("Unexpected poll event %"PRIu32".", revents);
313 return -EINVAL;
314 }
315
316 if (u->uploading) {
317 log_warning("dispatch_fd_input called when uploading, ignoring.");
318 return 0;
319 }
320
321 return start_upload(u, fd_input_callback, u);
322 }
323
324 static int open_file_for_upload(Uploader *u, const char *filename) {
325 int fd, r = 0;
326
327 if (streq(filename, "-"))
328 fd = STDIN_FILENO;
329 else {
330 fd = open(filename, O_RDONLY|O_CLOEXEC|O_NOCTTY);
331 if (fd < 0) {
332 log_error("Failed to open %s: %m", filename);
333 return -errno;
334 }
335 }
336
337 u->input = fd;
338
339 if (arg_follow) {
340 r = sd_event_add_io(u->events, &u->input_event,
341 fd, EPOLLIN, dispatch_fd_input, u);
342 if (r < 0) {
343 if (r != -EPERM || arg_follow > 0) {
344 log_error("Failed to register input event: %s", strerror(-r));
345 return r;
346 }
347
348 /* Normal files should just be consumed without polling. */
349 r = start_upload(u, fd_input_callback, u);
350 }
351 }
352
353 return r;
354 }
355
356 static int dispatch_sigterm(sd_event_source *event,
357 const struct signalfd_siginfo *si,
358 void *userdata) {
359 Uploader *u = userdata;
360
361 assert(u);
362
363 log_received_signal(LOG_INFO, si);
364
365 close_fd_input(u);
366 close_journal_input(u);
367
368 sd_event_exit(u->events, 0);
369 return 0;
370 }
371
372 static int setup_signals(Uploader *u) {
373 sigset_t mask;
374 int r;
375
376 assert(u);
377
378 assert_se(sigemptyset(&mask) == 0);
379 sigset_add_many(&mask, SIGINT, SIGTERM, -1);
380 assert_se(sigprocmask(SIG_SETMASK, &mask, NULL) == 0);
381
382 r = sd_event_add_signal(u->events, &u->sigterm_event, SIGTERM, dispatch_sigterm, u);
383 if (r < 0)
384 return r;
385
386 r = sd_event_add_signal(u->events, &u->sigint_event, SIGINT, dispatch_sigterm, u);
387 if (r < 0)
388 return r;
389
390 return 0;
391 }
392
393 static int setup_uploader(Uploader *u, const char *url, const char *state_file) {
394 int r;
395
396 assert(u);
397 assert(url);
398
399 memzero(u, sizeof(Uploader));
400 u->input = -1;
401
402 if (!startswith(url, "http://") && !startswith(url, "https://"))
403 url = strappenda("https://", url);
404
405 u->url = strappend(url, "/upload");
406 if (!u->url)
407 return log_oom();
408
409 u->state_file = state_file;
410
411 r = sd_event_default(&u->events);
412 if (r < 0) {
413 log_error("sd_event_default failed: %s", strerror(-r));
414 return r;
415 }
416
417 r = setup_signals(u);
418 if (r < 0) {
419 log_error("Failed to set up signals: %s", strerror(-r));
420 return r;
421 }
422
423 return load_cursor_state(u);
424 }
425
426 static void destroy_uploader(Uploader *u) {
427 assert(u);
428
429 curl_easy_cleanup(u->easy);
430 curl_slist_free_all(u->header);
431 free(u->answer);
432
433 free(u->last_cursor);
434 free(u->current_cursor);
435
436 free(u->url);
437
438 u->input_event = sd_event_source_unref(u->input_event);
439
440 close_fd_input(u);
441 close_journal_input(u);
442
443 sd_event_source_unref(u->sigterm_event);
444 sd_event_source_unref(u->sigint_event);
445 sd_event_unref(u->events);
446 }
447
448 static int perform_upload(Uploader *u) {
449 CURLcode code;
450 long status;
451
452 assert(u);
453
454 code = curl_easy_perform(u->easy);
455 if (code) {
456 log_error("Upload to %s failed: %.*s",
457 u->url,
458 u->error[0] ? (int) sizeof(u->error) : INT_MAX,
459 u->error[0] ? u->error : curl_easy_strerror(code));
460 return -EIO;
461 }
462
463 code = curl_easy_getinfo(u->easy, CURLINFO_RESPONSE_CODE, &status);
464 if (code) {
465 log_error("Failed to retrieve response code: %s",
466 curl_easy_strerror(code));
467 return -EUCLEAN;
468 }
469
470 if (status >= 300) {
471 log_error("Upload to %s failed with code %lu: %s",
472 u->url, status, strna(u->answer));
473 return -EIO;
474 } else if (status < 200) {
475 log_error("Upload to %s finished with unexpected code %lu: %s",
476 u->url, status, strna(u->answer));
477 return -EIO;
478 } else
479 log_debug("Upload finished successfully with code %lu: %s",
480 status, strna(u->answer));
481
482 free(u->last_cursor);
483 u->last_cursor = u->current_cursor;
484 u->current_cursor = NULL;
485
486 return update_cursor_state(u);
487 }
488
489 static int parse_config(void) {
490 const ConfigTableItem items[] = {
491 { "Upload", "URL", config_parse_string, 0, &arg_url },
492 { "Upload", "ServerKeyFile", config_parse_path, 0, &arg_key },
493 { "Upload", "ServerCertificateFile", config_parse_path, 0, &arg_cert },
494 { "Upload", "TrustedCertificateFile", config_parse_path, 0, &arg_trust },
495 {}};
496
497 return config_parse(NULL, PKGSYSCONFDIR "/journal-upload.conf", NULL,
498 "Upload\0",
499 config_item_table_lookup, items,
500 false, false, true, NULL);
501 }
502
503 static void help(void) {
504 printf("%s -u URL {FILE|-}...\n\n"
505 "Upload journal events to a remote server.\n\n"
506 " -h --help Show this help\n"
507 " --version Show package version\n"
508 " -u --url=URL Upload to this address\n"
509 " --key=FILENAME Specify key in PEM format (default:\n"
510 " \"" PRIV_KEY_FILE "\")\n"
511 " --cert=FILENAME Specify certificate in PEM format (default:\n"
512 " \"" CERT_FILE "\")\n"
513 " --trust=FILENAME|all Specify CA certificate or disable checking (default:\n"
514 " \"" TRUST_FILE "\")\n"
515 " --system Use the system journal\n"
516 " --user Use the user journal for the current user\n"
517 " -m --merge Use all available journals\n"
518 " -M --machine=CONTAINER Operate on local container\n"
519 " -D --directory=PATH Use journal files from directory\n"
520 " --file=PATH Use this journal file\n"
521 " --cursor=CURSOR Start at the specified cursor\n"
522 " --after-cursor=CURSOR Start after the specified cursor\n"
523 " --follow[=BOOL] Do [not] wait for input\n"
524 " --save-state[=FILE] Save uploaded cursors (default \n"
525 " " STATE_FILE ")\n"
526 " -h --help Show this help and exit\n"
527 " --version Print version string and exit\n"
528 , program_invocation_short_name);
529 }
530
531 static int parse_argv(int argc, char *argv[]) {
532 enum {
533 ARG_VERSION = 0x100,
534 ARG_KEY,
535 ARG_CERT,
536 ARG_TRUST,
537 ARG_USER,
538 ARG_SYSTEM,
539 ARG_FILE,
540 ARG_CURSOR,
541 ARG_AFTER_CURSOR,
542 ARG_FOLLOW,
543 ARG_SAVE_STATE,
544 };
545
546 static const struct option options[] = {
547 { "help", no_argument, NULL, 'h' },
548 { "version", no_argument, NULL, ARG_VERSION },
549 { "url", required_argument, NULL, 'u' },
550 { "key", required_argument, NULL, ARG_KEY },
551 { "cert", required_argument, NULL, ARG_CERT },
552 { "trust", required_argument, NULL, ARG_TRUST },
553 { "system", no_argument, NULL, ARG_SYSTEM },
554 { "user", no_argument, NULL, ARG_USER },
555 { "merge", no_argument, NULL, 'm' },
556 { "machine", required_argument, NULL, 'M' },
557 { "directory", required_argument, NULL, 'D' },
558 { "file", required_argument, NULL, ARG_FILE },
559 { "cursor", required_argument, NULL, ARG_CURSOR },
560 { "after-cursor", required_argument, NULL, ARG_AFTER_CURSOR },
561 { "follow", optional_argument, NULL, ARG_FOLLOW },
562 { "save-state", optional_argument, NULL, ARG_SAVE_STATE },
563 {}
564 };
565
566 int c, r;
567
568 assert(argc >= 0);
569 assert(argv);
570
571 opterr = 0;
572
573 while ((c = getopt_long(argc, argv, "hu:mM:D:", options, NULL)) >= 0)
574 switch(c) {
575 case 'h':
576 help();
577 return 0 /* done */;
578
579 case ARG_VERSION:
580 puts(PACKAGE_STRING);
581 puts(SYSTEMD_FEATURES);
582 return 0 /* done */;
583
584 case 'u':
585 if (arg_url) {
586 log_error("cannot use more than one --url");
587 return -EINVAL;
588 }
589
590 arg_url = optarg;
591 break;
592
593 case ARG_KEY:
594 if (arg_key) {
595 log_error("cannot use more than one --key");
596 return -EINVAL;
597 }
598
599 arg_key = optarg;
600 break;
601
602 case ARG_CERT:
603 if (arg_cert) {
604 log_error("cannot use more than one --cert");
605 return -EINVAL;
606 }
607
608 arg_cert = optarg;
609 break;
610
611 case ARG_TRUST:
612 if (arg_trust) {
613 log_error("cannot use more than one --trust");
614 return -EINVAL;
615 }
616
617 arg_trust = optarg;
618 break;
619
620 case ARG_SYSTEM:
621 arg_journal_type |= SD_JOURNAL_SYSTEM;
622 break;
623
624 case ARG_USER:
625 arg_journal_type |= SD_JOURNAL_CURRENT_USER;
626 break;
627
628 case 'm':
629 arg_merge = true;
630 break;
631
632 case 'M':
633 if (arg_machine) {
634 log_error("cannot use more than one --machine/-M");
635 return -EINVAL;
636 }
637
638 arg_machine = optarg;
639 break;
640
641 case 'D':
642 if (arg_directory) {
643 log_error("cannot use more than one --directory/-D");
644 return -EINVAL;
645 }
646
647 arg_directory = optarg;
648 break;
649
650 case ARG_FILE:
651 r = glob_extend(&arg_file, optarg);
652 if (r < 0) {
653 log_error("Failed to add paths: %s", strerror(-r));
654 return r;
655 };
656 break;
657
658 case ARG_CURSOR:
659 if (arg_cursor) {
660 log_error("cannot use more than one --cursor/--after-cursor");
661 return -EINVAL;
662 }
663
664 arg_cursor = optarg;
665 break;
666
667 case ARG_AFTER_CURSOR:
668 if (arg_cursor) {
669 log_error("cannot use more than one --cursor/--after-cursor");
670 return -EINVAL;
671 }
672
673 arg_cursor = optarg;
674 arg_after_cursor = true;
675 break;
676
677 case ARG_FOLLOW:
678 if (optarg) {
679 r = parse_boolean(optarg);
680 if (r < 0) {
681 log_error("Failed to parse --follow= parameter.");
682 return -EINVAL;
683 }
684
685 arg_follow = !!r;
686 } else
687 arg_follow = true;
688
689 break;
690
691 case ARG_SAVE_STATE:
692 arg_save_state = optarg ?: STATE_FILE;
693 break;
694
695 case '?':
696 log_error("Unknown option %s.", argv[optind-1]);
697 return -EINVAL;
698
699 case ':':
700 log_error("Missing argument to %s.", argv[optind-1]);
701 return -EINVAL;
702
703 default:
704 assert_not_reached("Unhandled option code.");
705 }
706
707 if (!arg_url) {
708 log_error("Required --url/-u option missing.");
709 return -EINVAL;
710 }
711
712 if (!!arg_key != !!arg_cert) {
713 log_error("Options --key and --cert must be used together.");
714 return -EINVAL;
715 }
716
717 if (optind < argc && (arg_directory || arg_file || arg_machine || arg_journal_type)) {
718 log_error("Input arguments make no sense with journal input.");
719 return -EINVAL;
720 }
721
722 return 1;
723 }
724
725 static int open_journal(sd_journal **j) {
726 int r;
727
728 if (arg_directory)
729 r = sd_journal_open_directory(j, arg_directory, arg_journal_type);
730 else if (arg_file)
731 r = sd_journal_open_files(j, (const char**) arg_file, 0);
732 else if (arg_machine)
733 r = sd_journal_open_container(j, arg_machine, 0);
734 else
735 r = sd_journal_open(j, !arg_merge*SD_JOURNAL_LOCAL_ONLY + arg_journal_type);
736 if (r < 0)
737 log_error("Failed to open %s: %s",
738 arg_directory ? arg_directory : arg_file ? "files" : "journal",
739 strerror(-r));
740 return r;
741 }
742
743 int main(int argc, char **argv) {
744 Uploader u;
745 int r;
746 bool use_journal;
747
748 log_show_color(true);
749 log_parse_environment();
750
751 r = parse_config();
752 if (r < 0)
753 goto finish;
754
755 r = parse_argv(argc, argv);
756 if (r <= 0)
757 goto finish;
758
759 r = setup_uploader(&u, arg_url, arg_save_state);
760 if (r < 0)
761 goto cleanup;
762
763 sd_event_set_watchdog(u.events, true);
764
765 log_debug("%s running as pid "PID_FMT,
766 program_invocation_short_name, getpid());
767
768 use_journal = optind >= argc;
769 if (use_journal) {
770 sd_journal *j;
771 r = open_journal(&j);
772 if (r < 0)
773 goto finish;
774 r = open_journal_for_upload(&u, j,
775 arg_cursor ?: u.last_cursor,
776 arg_cursor ? arg_after_cursor : true,
777 !!arg_follow);
778 if (r < 0)
779 goto finish;
780 }
781
782 sd_notify(false,
783 "READY=1\n"
784 "STATUS=Processing input...");
785
786 while (true) {
787 if (use_journal) {
788 if (!u.journal)
789 break;
790
791 r = check_journal_input(&u);
792 } else if (u.input < 0 && !use_journal) {
793 if (optind >= argc)
794 break;
795
796 log_debug("Using %s as input.", argv[optind]);
797 r = open_file_for_upload(&u, argv[optind++]);
798 }
799 if (r < 0)
800 goto cleanup;
801
802 r = sd_event_get_state(u.events);
803 if (r < 0)
804 break;
805 if (r == SD_EVENT_FINISHED)
806 break;
807
808 if (u.uploading) {
809 r = perform_upload(&u);
810 if (r < 0)
811 break;
812 }
813
814 r = sd_event_run(u.events, u.timeout);
815 if (r < 0) {
816 log_error("Failed to run event loop: %s", strerror(-r));
817 break;
818 }
819 }
820
821 cleanup:
822 sd_notify(false,
823 "STOPPING=1\n"
824 "STATUS=Shutting down...");
825
826 destroy_uploader(&u);
827
828 finish:
829 return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
830 }