diff --git a/WebminCore.pm b/WebminCore.pm index 70ce9524f..66ab88aed 100644 --- a/WebminCore.pm +++ b/WebminCore.pm @@ -23,7 +23,7 @@ $main::export_to_caller = 1; # Add functions in web-lib-funcs.pl # Generated with : # grep -h "^sub " web-lib-funcs.pl ui-lib.pl | sed -e 's/sub //' | xargs echo -@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys webmin_temp_dir_name webmin_temp_dir_path tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper ui_form_field_state_javascript js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir generate_miniserv_websocket_token allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); +@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys webmin_temp_dir_name webmin_temp_dir_path tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata copydata_len ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper ui_form_field_state_javascript js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir generate_miniserv_websocket_token allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); # Add global variables in web-lib.pl push(@EXPORT, qw(&can_read_file_under_global_acl &global_acl_file_unix_user diff --git a/servers/link.cgi b/servers/link.cgi index 99657184c..82f4e72c8 100755 --- a/servers/link.cgi +++ b/servers/link.cgi @@ -141,10 +141,12 @@ if (($ENV{'HTTP_X_REQUESTED_WITH_PROXY'} || '') eq 'XMLHttpRequest') { &write_http_connection($con, "X-Requested-With: XMLHttpRequest\r\n"); } &write_http_connection($con, "\r\n"); -my $post; if ($cl) { - &read_fully(\*STDIN, \$post, $cl); - &write_http_connection($con, $post); + my $got = ©data_len(\*STDIN, + sub { &write_http_connection($con, $_[0]) }, + $cl, &get_buffer_size_binary()); + defined($got) || &error("Failed to forward request body"); + $got == $cl || &error("Failed to read complete request body"); } # read back the headers diff --git a/t/web-lib-funcs-io.t b/t/web-lib-funcs-io.t new file mode 100644 index 000000000..e5064082d --- /dev/null +++ b/t/web-lib-funcs-io.t @@ -0,0 +1,109 @@ +#!/usr/bin/perl +# Unit tests for byte-copying helpers in web-lib-funcs.pl. + +use strict; +use warnings; +use Test::More; +use File::Basename qw(dirname); +use File::Spec; +use bytes (); + +my $script = File::Spec->rel2abs( + File::Spec->catfile(dirname(__FILE__), '..', 'web-lib-funcs.pl')); +require $script; + +subtest 'copydata_len to file handle' => sub { + my $source = "abcdef"; + my $dest = ""; + open(my $in, '<', \$source) or die "open input scalar: $!"; + open(my $out, '>', \$dest) or die "open output scalar: $!"; + + is(main::copydata_len($in, $out, 4, 2), 4, + 'reports copied byte count'); + is($dest, 'abcd', 'copies only the requested bytes'); + + my $left = <$in>; + is($left, 'ef', 'leaves bytes past the requested length unread'); +}; + +subtest 'copydata_len to writer callback' => sub { + my $source = "0123456789"; + my @chunks; + open(my $in, '<', \$source) or die "open input scalar: $!"; + + is(main::copydata_len($in, sub { push(@chunks, $_[0]); 1 }, 7, 3), + 7, 'reports bytes copied through callback'); + is(join('', @chunks), '0123456', 'callback receives requested data'); + is_deeply([ map { length($_) } @chunks ], [ 3, 3, 1 ], + 'callback receives bounded chunks'); +}; + +subtest 'copydata_len short input and write failure' => sub { + my $short = "abc"; + my $dest = ""; + open(my $in, '<', \$short) or die "open input scalar: $!"; + open(my $out, '>', \$dest) or die "open output scalar: $!"; + + is(main::copydata_len($in, $out, 5, 2), 3, + 'short input returns actual copied byte count'); + is($dest, 'abc', 'short input copies available bytes'); + + my $source = "abcdef"; + open(my $fail_in, '<', \$source) or die "open input scalar: $!"; + is(main::copydata_len($fail_in, sub { 0 }, 4, 2), undef, + 'writer callback failure returns undef'); +}; + +subtest 'write_http_connection retries partial SSL writes' => sub { + my @writes; + { + no warnings 'redefine'; + local *Net::SSLeay::write = sub { + my ($ssl, $data) = @_; + push(@writes, $data); + return length($data) > 2 ? 2 : length($data); + }; + + ok(main::write_http_connection( + { ssl_ctx => 1, ssl_con => 'ssl' }, 'abcde'), + 'partial SSL writes eventually succeed'); + } + is_deeply(\@writes, [ 'abcde', 'cde', 'e' ], + 'remaining bytes are retried after each partial write'); +}; + +subtest 'write_http_connection uses byte offsets for UTF-8 strings' => sub { + my $source = "\x{100}\x{101}\x{102}"; + my @accepted; + { + no warnings 'redefine'; + local *Net::SSLeay::write = sub { + my ($ssl, $data) = @_; + my $len = bytes::length($data); + my $wrote = $len > 3 ? 3 : $len; + push(@accepted, + unpack('H*', bytes::substr($data, 0, $wrote))); + return $wrote; + }; + + ok(main::write_http_connection( + { ssl_ctx => 1, ssl_con => 'ssl' }, $source), + 'partial SSL writes preserve a UTF-8-flagged scalar'); + } + is(join('', @accepted), + unpack('H*', bytes::substr($source, 0, bytes::length($source))), + 'byte-count return values are applied as byte offsets'); +}; + +subtest 'write_http_connection fails on SSL write error' => sub { + { + no warnings 'redefine'; + local *Net::SSLeay::write = sub { return 0; }; + + ok(!main::write_http_connection( + { ssl_ctx => 1, ssl_con => 'ssl' }, 'abc'), + 'zero-byte SSL write is reported as failure'); + } +}; + +done_testing(); diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index a6ae1b039..2ee470af2 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -16,6 +16,7 @@ Example code: use Socket; use POSIX; use IO::Handle; +use bytes (); use feature 'state'; eval "use Socket6"; $ipv6_module_error = $@; @@ -928,6 +929,38 @@ while(read($in, $buf, $bs) > 0) { return 1; } +=head2 copydata_len(in-handle, out-handle|&writer, length, [buffer-size]) + +Read a fixed number of bytes from one file handle and write them to another, +or pass each chunk to a writer function. The writer must synchronously consume +the entire chunk and return true on success. Returns the number of bytes +copied, or undef if a read or write fails. + +=cut +sub copydata_len +{ +my ($in, $out, $len, $bs) = @_; +$in = &callers_package($in); +$out = &callers_package($out) if (ref($out) ne 'CODE'); +$bs ||= &get_buffer_size(); +my $got = 0; +while($got < $len) { + my $want = $len - $got; + $want = $bs if ($want > $bs); + my $buf; + my $r = read($in, $buf, $want); + if (!defined($r)) { + next if ($!{'EINTR'}); + return undef; + } + last if (!$r); + my $ok = ref($out) eq 'CODE' ? &$out($buf) : print $out $buf; + return undef if (!$ok); + $got += $r; + } +return $got; +} + =head2 ReadParseMime([maximum], [&cbfunc, &cbargs], [array-mode], [&direct-write]) Read data submitted via a POST request using the multipart/form-data coding, @@ -9893,9 +9926,21 @@ my $h = shift(@_); my $fh = $h->{'fh'}; my $allok = 1; if ($h->{'ssl_ctx'}) { + # Net::SSLeay::write returns a byte count, but regular length/substr may + # count characters for UTF-8-flagged strings. Mixing those units could + # end the retry loop early or skip data, so track lengths and offsets in + # bytes. foreach my $s (@_) { - my $ok = Net::SSLeay::write($h->{'ssl_con'}, $s); - $allok = 0 if (!$ok); + my $len = bytes::length($s); + my $got = 0; + while($got < $len) { + my $ok = Net::SSLeay::write( + $h->{'ssl_con'}, bytes::substr($s, $got)); + if (!defined($ok) || $ok <= 0) { + return 0; + } + $got += $ok; + } } } else {