diff --git a/Base/home/anon/NetworkSettings.gml b/Base/home/anon/NetworkSettings.gml new file mode 100644 index 0000000000..b4f7dcfb6e --- /dev/null +++ b/Base/home/anon/NetworkSettings.gml @@ -0,0 +1,83 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [10] + spacing: 5 + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 20 + } + fixed_height: 40 + + @GUI::Label { + fixed_width: 32 + fixed_height: 32 + icon: "/res/icons/32x32/network.png" + } + + @GUI::Label { + text: "Select adapter:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::ComboBox { + model_only: true + name: "adapters_combobox" + } + } + + @GUI::GroupBox { + title: "Network" + shrink_to_fit: true + layout: @GUI::VerticalBoxLayout { + margins: [10] + } + + @GUI::CheckBox {} + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 30 + + @GUI::Label { + text: "IP Address:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::TextBox { + name: "ip_address_textbox" + } + + @GUI::Label { + text: "/" + fixed_width: 10 + } + + @GUI::SpinBox { + name: "cidr_spinbox" + fixed_width: 50 + min: 1 + max: 32 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 30 + + @GUI::Label { + text: "Default gateway:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::TextBox { + name: "default_gateway_textbox" + } + } + } +} diff --git a/Base/res/apps/NetworkSettings.af b/Base/res/apps/NetworkSettings.af new file mode 100644 index 0000000000..ee176e24b1 --- /dev/null +++ b/Base/res/apps/NetworkSettings.af @@ -0,0 +1,5 @@ +[App] +Name=Network Settings +Executable=/bin/NetworkSettings +Category=Settings +Description=Configure network connections diff --git a/Userland/Applications/CMakeLists.txt b/Userland/Applications/CMakeLists.txt index 281212205b..58a21bd8d9 100644 --- a/Userland/Applications/CMakeLists.txt +++ b/Userland/Applications/CMakeLists.txt @@ -22,6 +22,7 @@ add_subdirectory(Magnifier) add_subdirectory(Mail) add_subdirectory(MailSettings) add_subdirectory(MouseSettings) +add_subdirectory(NetworkSettings) add_subdirectory(PDFViewer) add_subdirectory(Piano) add_subdirectory(PixelPaint) diff --git a/Userland/Applications/NetworkSettings/CMakeLists.txt b/Userland/Applications/NetworkSettings/CMakeLists.txt new file mode 100644 index 0000000000..35fe06d0e8 --- /dev/null +++ b/Userland/Applications/NetworkSettings/CMakeLists.txt @@ -0,0 +1,17 @@ +serenity_component( + NetworkSettings + REQUIRED + TARGETS NetworkSettings +) + +compile_gml(NetworkSettings.gml NetworkSettingsGML.h network_settings_gml) + +set(SOURCES + main.cpp + NetworkSettingsGML.h + NetworkSettingsWidget.cpp + NetworkSettingsWidget.h +) + +serenity_app(NetworkSettings ICON network) +target_link_libraries(NetworkSettings LibGUI LibMain) diff --git a/Userland/Applications/NetworkSettings/NetworkSettings.gml b/Userland/Applications/NetworkSettings/NetworkSettings.gml new file mode 100644 index 0000000000..326f2567b8 --- /dev/null +++ b/Userland/Applications/NetworkSettings/NetworkSettings.gml @@ -0,0 +1,91 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [10] + spacing: 5 + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 20 + } + fixed_height: 40 + + @GUI::Label { + fixed_width: 32 + fixed_height: 32 + icon: "/res/icons/32x32/network.png" + } + + @GUI::Label { + text: "Select adapter:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::ComboBox { + model_only: true + name: "adapters_combobox" + } + } + + @GUI::GroupBox { + title: "Network" + shrink_to_fit: true + layout: @GUI::VerticalBoxLayout { + margins: [10] + } + + @GUI::CheckBox { + text: "Enabled" + name: "enabled_checkbox" + } + + @GUI::CheckBox { + text: "Obtain settings automatically (using DHCP)" + name: "dhcp_checkbox" + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + preferred_height: 30 + + @GUI::Label { + text: "IP Address:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::TextBox { + name: "ip_address_textbox" + } + + @GUI::Label { + text: "/" + fixed_width: 10 + } + + @GUI::SpinBox { + name: "cidr_spinbox" + fixed_width: 50 + min: 1 + max: 32 + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + preferred_height: 30 + + @GUI::Label { + text: "Default gateway:" + fixed_width: 100 + text_alignment: "CenterLeft" + } + + @GUI::TextBox { + name: "default_gateway_textbox" + } + } + } +} diff --git a/Userland/Applications/NetworkSettings/NetworkSettingsWidget.cpp b/Userland/Applications/NetworkSettings/NetworkSettingsWidget.cpp new file mode 100644 index 0000000000..ea65051cd7 --- /dev/null +++ b/Userland/Applications/NetworkSettings/NetworkSettingsWidget.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2022, Maciej + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "NetworkSettingsWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NetworkSettings { + +static int netmask_to_cidr(IPv4Address const& address) +{ + auto address_in_host_representation = AK::convert_between_host_and_network_endian(address.to_u32()); + return 32 - count_trailing_zeroes_safe(address_in_host_representation); +} + +NetworkSettingsWidget::NetworkSettingsWidget() +{ + load_from_gml(network_settings_gml); + + m_adapters_combobox = *find_descendant_of_type_named("adapters_combobox"); + m_enabled_checkbox = *find_descendant_of_type_named("enabled_checkbox"); + m_enabled_checkbox->on_checked = [&](bool value) { + m_current_adapter_data->enabled = value; + on_switch_enabled_or_dhcp(); + set_modified(true); + }; + m_dhcp_checkbox = *find_descendant_of_type_named("dhcp_checkbox"); + m_dhcp_checkbox->on_checked = [&](bool value) { + m_current_adapter_data->dhcp = value; + on_switch_enabled_or_dhcp(); + set_modified(true); + }; + m_ip_address_textbox = *find_descendant_of_type_named("ip_address_textbox"); + m_ip_address_textbox->on_change = [&]() { + m_current_adapter_data->ip_address = m_ip_address_textbox->text(); + set_modified(true); + }; + m_cidr_spinbox = *find_descendant_of_type_named("cidr_spinbox"); + m_cidr_spinbox->on_change = [&](int value) { + m_current_adapter_data->cidr = value; + set_modified(true); + }; + m_default_gateway_textbox = *find_descendant_of_type_named("default_gateway_textbox"); + m_default_gateway_textbox->on_change = [&]() { + m_current_adapter_data->default_gateway = m_default_gateway_textbox->text(); + set_modified(true); + }; + + auto config_file = Core::ConfigFile::open_for_system("Network").release_value_but_fixme_should_propagate_errors(); + + auto proc_net_adapters_file = Core::Stream::File::open("/proc/net/adapters", Core::Stream::OpenMode::Read).release_value_but_fixme_should_propagate_errors(); + auto data = proc_net_adapters_file->read_all().release_value_but_fixme_should_propagate_errors(); + JsonParser parser(data); + JsonValue proc_net_adapters_json = parser.parse().release_value_but_fixme_should_propagate_errors(); + + // FIXME: This should be done before creating a window. + if (proc_net_adapters_json.as_array().is_empty()) { + GUI::MessageBox::show_error(window(), "No network adapters found!"); + ::exit(1); + } + + size_t selected_adapter_index = 0; + size_t index = 0; + proc_net_adapters_json.as_array().for_each([&](auto& value) { + auto& if_object = value.as_object(); + auto adapter_name = if_object.get("name").to_string(); + if (adapter_name == "loop") + return; + + bool adapter_exists_in_config = config_file->has_group(adapter_name); + + bool enabled = config_file->read_bool_entry(adapter_name, "Enabled", true); + if (enabled) + selected_adapter_index = index; + + NetworkAdapterData adapter_data; + adapter_data.enabled = enabled; + adapter_data.dhcp = config_file->read_bool_entry(adapter_name, "DHCP", !adapter_exists_in_config); + adapter_data.ip_address = config_file->read_entry(adapter_name, "IPv4Address"); + auto netmask = IPv4Address::from_string(config_file->read_entry(adapter_name, "IPv4Netmask")); + adapter_data.cidr = netmask.has_value() ? netmask_to_cidr(*netmask) : 32; + adapter_data.default_gateway = config_file->read_entry(adapter_name, "IPv4Gateway"); + m_network_adapters.set(adapter_name, move(adapter_data)); + m_adapter_names.append(adapter_name); + index++; + }); + + m_adapters_combobox->set_model(GUI::ItemListModel::create(m_adapter_names)); + m_adapters_combobox->on_change = [this](String const& text, GUI::ModelIndex const&) { + on_switch_adapter(text); + }; + auto const& selected_adapter = selected_adapter_index; + dbgln("{} in {}", selected_adapter, m_adapter_names); + m_adapters_combobox->set_selected_index(selected_adapter); + on_switch_adapter(m_adapter_names[selected_adapter_index]); +} + +void NetworkSettingsWidget::on_switch_adapter(String const& adapter) +{ + auto& adapter_data = m_network_adapters.get(adapter).value(); + m_current_adapter_data = &adapter_data; + on_switch_enabled_or_dhcp(); + + m_enabled_checkbox->set_checked(adapter_data.enabled, GUI::AllowCallback::No); + m_dhcp_checkbox->set_checked(adapter_data.dhcp, GUI::AllowCallback::No); + m_ip_address_textbox->set_text(adapter_data.ip_address, GUI::AllowCallback::No); + m_cidr_spinbox->set_value(adapter_data.cidr, GUI::AllowCallback::No); + m_default_gateway_textbox->set_text(adapter_data.default_gateway, GUI::AllowCallback::No); + + VERIFY(m_current_adapter_data); +} + +void NetworkSettingsWidget::on_switch_enabled_or_dhcp() +{ + m_dhcp_checkbox->set_enabled(m_current_adapter_data->enabled); + m_ip_address_textbox->set_enabled(m_current_adapter_data->enabled && !m_current_adapter_data->dhcp); + m_cidr_spinbox->set_enabled(m_current_adapter_data->enabled && !m_current_adapter_data->dhcp); + m_default_gateway_textbox->set_enabled(m_current_adapter_data->enabled && !m_current_adapter_data->dhcp); +} + +void NetworkSettingsWidget::apply_settings() +{ + auto config_file = Core::ConfigFile::open_for_system("Network", Core::ConfigFile::AllowWriting::Yes).release_value_but_fixme_should_propagate_errors(); + bool may_need_to_reboot = false; + for (auto const& adapter_data : m_network_adapters) { + // FIXME: Setting Enabled=false starts working only after a reboot. Fix this on the NetworkServer side. + if (!m_current_adapter_data->enabled) + may_need_to_reboot = true; + + auto netmask = IPv4Address::netmask_from_cidr(adapter_data.value.cidr).to_string(); + config_file->write_bool_entry(adapter_data.key, "Enabled", adapter_data.value.enabled); + config_file->write_bool_entry(adapter_data.key, "DHCP", adapter_data.value.dhcp); + if (adapter_data.value.enabled && !adapter_data.value.dhcp) { + if (!IPv4Address::from_string(adapter_data.value.ip_address).has_value()) { + GUI::MessageBox::show_error(window(), String::formatted("Invalid IPv4 address for adapter {}", adapter_data.key)); + return; + } + if (!IPv4Address::from_string(adapter_data.value.default_gateway).has_value()) { + GUI::MessageBox::show_error(window(), String::formatted("Invalid IPv4 gateway for adapter {}", adapter_data.key)); + return; + } + } + config_file->write_entry(adapter_data.key, "IPv4Address", adapter_data.value.ip_address); + config_file->write_entry(adapter_data.key, "IPv4Netmask", netmask); + config_file->write_entry(adapter_data.key, "IPv4Gateway", adapter_data.value.default_gateway); + } + + GUI::Process::spawn_or_show_error(window(), "/bin/NetworkServer"); + + if (may_need_to_reboot) + GUI::MessageBox::show(window(), "You may need to reboot for changes to take effect.", "Network Settings", GUI::MessageBox::Type::Warning); +} + +} diff --git a/Userland/Applications/NetworkSettings/NetworkSettingsWidget.h b/Userland/Applications/NetworkSettings/NetworkSettingsWidget.h new file mode 100644 index 0000000000..decf4c13b3 --- /dev/null +++ b/Userland/Applications/NetworkSettings/NetworkSettingsWidget.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, Maciej + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +#include + +namespace NetworkSettings { + +class NetworkSettingsWidget : public GUI::SettingsWindow::Tab { + C_OBJECT(NetworkSettingsWidget) + +public: + virtual void apply_settings() override; + +private: + NetworkSettingsWidget(); + + struct NetworkAdapterData { + bool enabled = false; + bool dhcp = false; + String ip_address; + int cidr = 0; + String default_gateway; + }; + + void on_switch_adapter(String const& adapter); + void on_switch_enabled_or_dhcp(); + + HashMap m_network_adapters; + Vector m_adapter_names; + NetworkAdapterData* m_current_adapter_data = nullptr; + + RefPtr m_enabled_checkbox; + RefPtr m_dhcp_checkbox; + RefPtr m_adapters_combobox; + RefPtr m_ip_address_textbox; + RefPtr m_cidr_spinbox; + RefPtr m_default_gateway_textbox; +}; + +} diff --git a/Userland/Applications/NetworkSettings/main.cpp b/Userland/Applications/NetworkSettings/main.cpp new file mode 100644 index 0000000000..ca56eb3ea4 --- /dev/null +++ b/Userland/Applications/NetworkSettings/main.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022, Maciej + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "NetworkSettingsWidget.h" +#include +#include + +#include +#include +#include +#include +#include +#include + +ErrorOr serenity_main(Main::Arguments args) +{ + TRY(Core::System::pledge("stdio rpath wpath cpath recvfd sendfd unix proc exec")); + + TRY(Core::System::unveil("/bin/NetworkServer", "x")); + TRY(Core::System::unveil("/etc/Network.ini", "rwc")); + TRY(Core::System::unveil("/proc/net/adapters", "r")); + TRY(Core::System::unveil("/res", "r")); + TRY(Core::System::unveil("/tmp/portal/clipboard", "rw")); + TRY(Core::System::unveil("/tmp/portal/window", "rw")); + TRY(Core::System::unveil(nullptr, nullptr)); + + auto app = TRY(GUI::Application::try_create(args)); + + if (getuid() != 0) { + GUI::MessageBox::show_error(nullptr, "You need to be root to run Network Settings!"); + return 1; + } + + TRY(Core::System::pledge("stdio rpath wpath cpath recvfd sendfd proc exec")); + + auto app_icon = GUI::Icon::default_icon("network"); + auto window = TRY(GUI::SettingsWindow::create("Network Settings", GUI::SettingsWindow::ShowDefaultsButton::No)); + (void)TRY(window->add_tab("Network", "network")); + window->set_icon(app_icon.bitmap_for_size(16)); + + window->show(); + return app->exec(); +} diff --git a/Userland/Libraries/LibGUI/Icon.cpp b/Userland/Libraries/LibGUI/Icon.cpp index 1a3af61faf..35b497539a 100644 --- a/Userland/Libraries/LibGUI/Icon.cpp +++ b/Userland/Libraries/LibGUI/Icon.cpp @@ -87,8 +87,10 @@ ErrorOr Icon::try_create_default_icon(StringView name) if (auto bitmap_or_error = Gfx::Bitmap::try_load_from_file(String::formatted("/res/icons/32x32/{}.png", name)); !bitmap_or_error.is_error()) bitmap32 = bitmap_or_error.release_value(); - if (!bitmap16 && !bitmap32) + if (!bitmap16 && !bitmap32) { + dbgln("Default icon not found: {}", name); return Error::from_string_literal("Default icon not found"sv); + } return Icon(move(bitmap16), move(bitmap32)); }