LCOV - code coverage report
Current view: top level - pkg/networkmanager - firewall.jsx Coverage Total Hit
Test: cockpit Lines: 96.5 % 919 887
Test Date: 2026-07-13 10:00:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2018 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6              : import '../lib/patternfly/patternfly-6-cockpit.scss';
       7              : import 'cockpit-dark-theme'; // once per page
       8            7 : import cockpit from "cockpit";
       9            7 : import React from 'react';
      10            7 : import { createRoot } from "react-dom/client";
      11              : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
      12              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      13              : import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
      14              : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
      15              : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      16              : import { DataList, DataListCell, DataListCheck, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
      17              : import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
      18              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      19              : import { Form, FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      20              : import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
      21              : import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
      22              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      23              : import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
      24              : import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
      25              : import { Page, PageBreadcrumb, PageSection, } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      26              : import {
      27              :     Modal, ModalBody, ModalFooter, ModalHeader
      28              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      29              : import { ExclamationCircleIcon } from '@patternfly/react-icons';
      30              : 
      31              : import firewall from "./firewall-client.js";
      32              : import { FormHelper } from "cockpit-components-form-helper";
      33              : import { ListingTable } from 'cockpit-components-table.jsx';
      34              : import { ModalError } from "cockpit-components-inline-notification.jsx";
      35              : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
      36              : import { FirewallSwitch } from "./firewall-switch.jsx";
      37              : 
      38              : import { superuser } from "superuser";
      39              : import { WithDialogs, DialogsContext } from "dialogs.jsx";
      40              : 
      41              : import { KebabDropdown } from "cockpit-components-dropdown";
      42              : 
      43              : import "./networking.scss";
      44              : 
      45            7 : const _ = cockpit.gettext;
      46              : 
      47            7 : superuser.reload_page_on_change();
      48              : 
      49            5 : const upperCaseFirstLetter = text => text[0].toUpperCase() + text.slice(1);
      50              : 
      51            5 : const DeleteDropdown = ({ items }) => {
      52            5 :     const dropdown_items = items.map(item => <DropdownItem key={item.text}
      53            2 :                                                            className={item.danger ? "pf-m-danger" : ""}
      54            5 :                                                            aria-label={item.ariaLabel}
      55            5 :                                                            onClick={item.handleClick}>
      56            5 :         {item.text}
      57            5 :     </DropdownItem>);
      58              : 
      59            5 :     return <KebabDropdown dropdownItems={dropdown_items} />;
      60            5 : };
      61              : 
      62            5 : function serviceRow(props) {
      63            5 :     let tcp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'TCP');
      64            5 :     let udp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'UDP');
      65              : 
      66            2 :     for (const s of props.service.includes) {
      67            2 :         if (firewall.services[s]) {
      68            2 :             tcp = tcp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'TCP'));
      69            2 :             udp = udp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'UDP'));
      70            2 :         }
      71            2 :     }
      72              : 
      73            3 :     function onRemoveService(event) {
      74            3 :         props.onRemoveService(props.service.id);
      75            3 :         event.stopPropagation();
      76            3 :     }
      77              : 
      78            1 :     function onEditService(event) {
      79            1 :         props.onEditService(props.service.id);
      80            1 :         event.stopPropagation();
      81            1 :     }
      82              : 
      83            5 :     const columns = [
      84            5 :         {
      85            5 :             title: props.service.id, header: true
      86            5 :         },
      87            5 :         {
      88            5 :             title: <div key={props.service.id + "tcp"}>
      89            5 :                 { tcp.map(p => p.port).join(', ') }
      90            5 :             </div>
      91            5 :         },
      92            5 :         {
      93            5 :             title: <div key={props.service.id + "udp"}>
      94            5 :                 { udp.map(p => p.port).join(', ') }
      95            5 :             </div>
      96            5 :         },
      97            5 :     ];
      98              : 
      99            5 :     if (!props.readonly) {
     100              :         // Only allow editing manually created services - no name is a decent (and only) indicator
     101            5 :         const items = [];
     102            5 :         if (!props.service.name)
     103            2 :             items.push({ text: _("Edit"), ariaLabel: cockpit.format(_("Edit service $0"), props.service.id), handleClick: onEditService });
     104              : 
     105            5 :         items.push({ text: _("Delete"), danger: true, ariaLabel: cockpit.format(_("Remove service $0"), props.service.id), handleClick: onRemoveService });
     106              : 
     107            5 :         columns.push({
     108            5 :             title: <DeleteDropdown items={items} />
     109            5 :         });
     110            5 :     }
     111              : 
     112            5 :     let description;
     113            5 :     let includes;
     114            5 :     if (props.service.description)
     115            5 :         description = <p>{props.service.description}</p>;
     116              : 
     117            2 :     if (props.service.includes.length > 0) {
     118            2 :         includes = <>
     119            2 :             <h5>{_("Included Services")}</h5>
     120            2 :             <ul>{props.service.includes.map(s => {
     121            2 :                 const service = firewall.services[s];
     122            2 :                 if (service && service.description)
     123            0 :                     return <li key={service.id}><strong>{service.id}</strong>: {service.description}</li>;
     124              :                 else
     125            0 :                     return null;
     126            2 :             })} </ul></>;
     127            2 :     }
     128              : 
     129            5 :     return ({
     130            5 :         props: { key: props.service.id, 'data-row-id': props.service.id },
     131            5 :         columns,
     132            5 :         hasPadding: true,
     133            5 :         expandedContent: <>{description}{includes}</>,
     134            5 :     });
     135            5 : }
     136              : 
     137            2 : function portRow(props) {
     138            1 :     function onRemovePort(event) {
     139            1 :         props.onRemovePort(props.port);
     140            1 :         event.stopPropagation();
     141            1 :     }
     142              : 
     143            2 :     const columns = [
     144            2 :         {
     145            2 :             title: <i key={props.key + "-additional-port"}>{ _("Additional port") }</i>
     146            2 :         },
     147              : 
     148            1 :         { title: props.port.protocol === "tcp" ? props.port.port : "" },
     149            1 :         { title: props.port.protocol === "udp" ? props.port.port : "" },
     150              : 
     151            2 :         { title: <DeleteDropdown items={[{ text: _("Delete"), danger: true, ariaLabel: _("Remove additional port"), handleClick: onRemovePort }]} /> }
     152            2 :     ];
     153            2 :     return ({
     154            2 :         props: { key: props.key + "-port", 'data-row-id': props.key + "-port" },
     155            2 :         columns
     156            2 :     });
     157            2 : }
     158              : 
     159            5 : function ZoneSection(props) {
     160            1 :     function onRemoveZone(event) {
     161            1 :         event.stopPropagation();
     162            1 :         props.onRemoveZone(props.zone.id);
     163            1 :     }
     164              : 
     165            5 :     const deleteButton = (<DeleteDropdown items={[{ text: _("Delete"), danger: true, ariaLabel: cockpit.format(_("Remove zone $0"), props.zone.id), handleClick: onRemoveZone }]} id={`dropdown-${props.zone.id}`} />);
     166            5 :     const addServiceAction = (
     167            3 :         <Button variant="primary" onClick={() => props.openServicesDialog(props.zone.id, props.zone.id)} className="add-services-button" aria-label={cockpit.format(_("Add services to zone $0"), props.zone.id)}>
     168            5 :             {_("Add services")}
     169            5 :         </Button>
     170              :     );
     171              : 
     172            5 :     const actions = !firewall.readonly && <div className="zone-section-buttons">{addServiceAction}{deleteButton}</div>;
     173              : 
     174              :     /** @type {import('cockpit-components-table.jsx').ListingTableColumnProps[]} */
     175            5 :     const listingTableColumns = [
     176            5 :         {
     177            5 :             title: _("Service"),
     178            5 :             props: {
     179            5 :                 width: 40,
     180            5 :             }
     181            5 :         },
     182            5 :         {
     183            5 :             title: _("TCP"),
     184            5 :             props: {
     185            5 :                 width: 30
     186            5 :             }
     187            5 :         },
     188            5 :         {
     189            5 :             title: _("UDP"),
     190            5 :             props: {
     191            5 :                 width: 30
     192            5 :             }
     193            5 :         },
     194            5 :         {
     195            5 :             title: "",
     196            5 :             props: {
     197            5 :                 width: 10,
     198            5 :                 screenReaderText: _("Actions")
     199            5 :             }
     200            5 :         }
     201            5 :     ];
     202              : 
     203            5 :     return <Card isPlain className="zone-section" data-id={props.zone.id}>
     204            5 :         <CardHeader actions={{ actions }} className="zone-section-heading">
     205            5 :             <Flex alignItems={{ default: 'alignSelfBaseline' }} spaceItems={{ default: 'spaceItemsXl' }}>
     206            5 :                 <CardTitle component="h2">
     207            2 :                     { cockpit.format(_("$0 zone"), upperCaseFirstLetter(props.zone.name || props.zone.id)) }
     208            5 :                 </CardTitle>
     209            5 :                 <Flex>
     210            5 :                     { props.zone.interfaces.length > 0 &&
     211            5 :                     <span>
     212            5 :                         <strong>{cockpit.ngettext("Interface", "Interfaces", props.zone.interfaces.length)}</strong> {props.zone.interfaces.join(", ")}
     213            5 :                     </span>
     214              :                     }
     215            5 :                     <span>
     216            2 :                         <strong>{_("Allowed addresses")}</strong> {props.zone.source.length ? props.zone.source.join(", ") : _("Entire subnet")}
     217            5 :                     </span>
     218            5 :                 </Flex>
     219            5 :             </Flex>
     220            5 :         </CardHeader>
     221            3 :         {(props.zone.services.length > 0 || props.zone.ports.length > 0) &&
     222            5 :         <CardBody className="contains-list">
     223            5 :             <ListingTable columns={listingTableColumns}
     224            5 :                           id={props.zone.id}
     225            5 :                           aria-label={props.zone.id}
     226            5 :                           variant="compact"
     227            5 :                           emptyCaption={_("There are no active services in this zone")}
     228            5 :                           rows={
     229            5 :                               props.zone.services.map(s => {
     230            5 :                                   if (s in firewall.services) {
     231            5 :                                       return serviceRow({
     232            5 :                                           key: firewall.services[s].id,
     233            5 :                                           service: firewall.services[s],
     234            3 :                                           onRemoveService: service => props.onRemoveService(props.zone.id, service),
     235            1 :                                           onEditService: service => props.onEditService(props.zone, firewall.services[service]),
     236            5 :                                           readonly: firewall.readonly,
     237            5 :                                       });
     238            0 :                                   } else {
     239            0 :                                       return null;
     240            0 :                                   }
     241            5 :                               }).concat(
     242            5 :                                   props.zone.ports.length > 0
     243            2 :                                       ? props.zone.ports.map(p => {
     244            2 :                                           return portRow({
     245            2 :                                               key: `${props.zone.id}-${p.port}-${p.protocol}`,
     246            2 :                                               zone: props.zone,
     247            2 :                                               port: p,
     248            1 :                                               onRemovePort: port => props.onRemovePort(props.zone.id, port.port, port.protocol),
     249            2 :                                               readonly: firewall.readonly
     250            2 :                                           });
     251            2 :                                       })
     252            5 :                                       : [])
     253            5 :                                       .filter(Boolean)}
     254              : 
     255            5 :             />
     256            5 :         </CardBody>}
     257            5 :     </Card>;
     258            5 : }
     259              : 
     260            7 : class SearchInput extends React.Component {
     261            3 :     constructor(props) {
     262            3 :         super(props);
     263            3 :         this.onValueChanged = this.onValueChanged.bind(this);
     264            3 :         this.state = { value: props.value || "" };
     265            3 :     }
     266              : 
     267            1 :     onValueChanged(value) {
     268            1 :         this.setState({ value });
     269              : 
     270            1 :         if (this.timer)
     271            1 :             window.clearTimeout(this.timer);
     272              : 
     273            1 :         this.timer = window.setTimeout(() => {
     274            1 :             this.props.onChange(value);
     275            1 :             this.timer = null;
     276            1 :         }, 300);
     277            1 :     }
     278              : 
     279            3 :     render() {
     280            3 :         return (
     281            3 :             <Toolbar className="filter-services-toolbar">
     282            3 :                 <ToolbarContent>
     283            3 :                     <ToolbarItem variant="label">
     284            3 :                         {_("Filter services")}
     285            3 :                     </ToolbarItem>
     286            3 :                     <ToolbarItem>
     287            3 :                         <TextInput type="search"
     288            3 :                                    id={this.props.id}
     289            1 :                                    onChange={(_event, value) => this.onValueChanged(value)}
     290            3 :                                    value={this.state.value}
     291            3 :                         />
     292            3 :                     </ToolbarItem>
     293            3 :                 </ToolbarContent>
     294            3 :             </Toolbar>
     295              :         );
     296            3 :     }
     297            7 : }
     298              : 
     299            3 : const renderPorts = service => {
     300            3 :     const tcpPorts = [];
     301            3 :     const udpPorts = [];
     302            3 :     function addPorts(ports) {
     303            3 :         for (const port of ports) {
     304            3 :             if (port.protocol === "tcp")
     305            3 :                 tcpPorts.push(port.port);
     306              :             else
     307            3 :                 udpPorts.push(port.port);
     308            3 :         }
     309            3 :     }
     310            3 :     addPorts(service.ports);
     311            3 :     for (const s of service.includes)
     312            3 :         addPorts(firewall.services[s].ports);
     313              : 
     314            3 :     return (
     315            3 :         <div className="service-list-item-text">
     316            3 :             { tcpPorts.length > 0 && <span className="service-ports tcp"><strong>TCP: </strong>{ tcpPorts.join(', ') }</span> }
     317            3 :             { udpPorts.length > 0 && <span className="service-ports udp"><strong>UDP: </strong>{ udpPorts.join(', ') }</span> }
     318            3 :         </div>
     319              :     );
     320            3 : };
     321              : 
     322            7 : class AddEditServicesModal extends React.Component {
     323            7 :     static contextType = DialogsContext;
     324              : 
     325            3 :     constructor(props) {
     326            3 :         super(props);
     327              : 
     328            3 :         this.state = {
     329            3 :             services: null,
     330            3 :             selected: new Set(),
     331            3 :             filter: "",
     332            3 :             custom: !!props.custom_id,
     333            3 :             generate_custom_id: !props.custom_id,
     334            3 :             tcp_error: "",
     335            3 :             udp_error: "",
     336            3 :             avail_services: null,
     337            3 :             custom_id: props.custom_id || "",
     338            3 :             custom_description: props.custom_description || "",
     339            3 :             custom_tcp_ports: props.custom_tcp_ports || [],
     340            3 :             custom_udp_ports: props.custom_udp_ports || [],
     341            3 :             custom_tcp_value: props.custom_tcp_value || "",
     342            3 :             custom_udp_value: props.custom_udp_value || "",
     343            3 :             dialogError: null,
     344            3 :             dialogErrorDetail: null,
     345            3 :         };
     346            3 :         this.save = this.save.bind(this);
     347            3 :         this.edit = this.edit.bind(this);
     348            3 :         this.checkNullValues = this.checkNullValues.bind(this);
     349            3 :         this.onFilterChanged = this.onFilterChanged.bind(this);
     350            3 :         this.onToggleService = this.onToggleService.bind(this);
     351            3 :         this.setId = this.setId.bind(this);
     352            3 :         this.setDescription = this.setDescription.bind(this);
     353            3 :         this.getName = this.getName.bind(this);
     354            3 :         this.validate = this.validate.bind(this);
     355            3 :         this.createPorts = this.createPorts.bind(this);
     356            3 :         this.parseServices = this.parseServices.bind(this);
     357            3 :         this.onToggleType = this.onToggleType.bind(this);
     358            3 :         this.getCustomId = this.getCustomId.bind(this);
     359            3 :     }
     360              : 
     361            1 :     createPorts() {
     362            1 :         const ret = [];
     363            1 :         this.state.custom_tcp_ports.forEach(port => ret.push([port, 'tcp']));
     364            1 :         this.state.custom_udp_ports.forEach(port => ret.push([port, 'udp']));
     365            1 :         return ret;
     366            1 :     }
     367              : 
     368            1 :     getCustomId() {
     369            1 :         return "custom--" + (
     370            1 :             this.state.custom_tcp_ports.map(port => this.getName(port, "tcp"))
     371            1 :                     .concat(this.state.custom_udp_ports.map(port => this.getName(port, "udp")))
     372            1 :                     .join('-')
     373              :         );
     374            1 :     }
     375              : 
     376            2 :     checkNullValues() {
     377            2 :         return (!this.state.custom_tcp_value && !this.state.custom_udp_value);
     378            2 :     }
     379              : 
     380            1 :     edit(event) {
     381            1 :         const Dialogs = this.context;
     382            1 :         firewall.editService(this.props.custom_id, this.createPorts(), this.state.custom_description)
     383            1 :                 .then(() => Dialogs.close())
     384            0 :                 .catch(error => {
     385            0 :                     this.setState({
     386            0 :                         dialogError: _("Failed to edit service"),
     387            0 :                         dialogErrorDetail: error.name + ": " + error.message,
     388            0 :                     });
     389            0 :                 });
     390              : 
     391            1 :         if (event)
     392            1 :             event.preventDefault();
     393            1 :         return false;
     394            1 :     }
     395              : 
     396            3 :     save(event) {
     397            3 :         const Dialogs = this.context;
     398            3 :         let p;
     399            1 :         if (this.state.custom) {
     400            1 :             const custom_id = this.state.custom_id === "" ? this.getCustomId() : this.state.custom_id;
     401            1 :             p = firewall.createService(custom_id, this.createPorts(), this.props.zoneId, this.state.custom_description);
     402            0 :         } else {
     403            2 :             p = firewall.addServices(this.props.zoneId, [...this.state.selected]);
     404            2 :         }
     405            3 :         p.then(() => Dialogs.close())
     406            1 :                 .catch(error => {
     407            1 :                     this.setState(prevState => ({
     408            0 :                         dialogError: prevState.custom ? _("Failed to add port") : _("Failed to add service"),
     409            1 :                         dialogErrorDetail: error.name + ": " + error.message,
     410            1 :                     }));
     411            1 :                 });
     412              : 
     413            3 :         if (event)
     414            3 :             event.preventDefault();
     415            3 :         return false;
     416            3 :     }
     417              : 
     418            2 :     onToggleService(event, serviceId) {
     419            2 :         const service = serviceId;
     420            2 :         const enabled = event.target.checked;
     421              : 
     422            2 :         this.setState(oldState => {
     423            2 :             const selected = new Set(oldState.selected);
     424              : 
     425            2 :             if (enabled)
     426            0 :                 selected.add(service);
     427              :             else
     428            0 :                 selected.delete(service);
     429              : 
     430            2 :             return {
     431            2 :                 selected
     432            2 :             };
     433            2 :         });
     434            2 :     }
     435              : 
     436              :     /* Create list of services from /etc/services type file
     437              :      *
     438              :      * Return dictionary of services:
     439              :      *  - key => port number or port alias (80/http)
     440              :      *  - item => dictionary with 3 compulsory items:
     441              :      *      - name => port alias (http)
     442              :      *      - port => port number (80)
     443              :      *      - type => list of types (tcp/udp...)
     444              :      *      - description => _may be not present_ (Web Server)
     445              :      */
     446            3 :     parseServices(content) {
     447            0 :         if (!content) {
     448            0 :             console.warn("Couldn't read /etc/services");
     449            0 :             return [];
     450            0 :         }
     451              : 
     452            3 :         const ret = {};
     453            3 :         content.split('\n').forEach(line => {
     454            3 :             if (!line || line.startsWith("#"))
     455            3 :                 return;
     456            3 :             const m = line.match(/^(\S+)\s+(\d+)\/(\S+).*?(#(.*))?$/);
     457            3 :             const new_port = { name: m[1], port: m[2], type: [m[3]] };
     458            3 :             if (m.length > 5 && m[5])
     459            3 :                 new_port.description = m[5].trim();
     460            3 :             if (ret[m[1]])
     461            3 :                 ret[m[1]].type.push(new_port.type[0]);
     462              :             else
     463            3 :                 ret[m[1]] = new_port;
     464            3 :             if (ret[m[2]])
     465            3 :                 ret[m[2]].type.push(new_port.type[0]);
     466              :             else
     467            3 :                 ret[m[2]] = new_port;
     468            3 :         });
     469            3 :         return ret;
     470            3 :     }
     471              : 
     472            1 :     setId(value) {
     473            1 :         this.setState({
     474            1 :             custom_id: value,
     475            1 :             generate_custom_id: value.length === 0,
     476            1 :         });
     477            1 :     }
     478              : 
     479            1 :     setDescription(value) {
     480            1 :         this.setState({
     481            1 :             custom_description: value
     482            1 :         });
     483            1 :     }
     484              : 
     485            1 :     getName(port, type) {
     486            1 :         const known = this.state.avail_services[port];
     487            1 :         if (known && known.type.includes(type))
     488            1 :             return known.name;
     489              :         else
     490            1 :             return port;
     491            1 :     }
     492              : 
     493            1 :     getPortNumber(port, type, avail) {
     494            1 :         if (!avail) {
     495            1 :             const num_p = Number(port);
     496            1 :             if (isNaN(num_p))
     497            1 :                 return [0, _("Unknown service name")];
     498            1 :             else if (num_p <= 0 || num_p > 65535)
     499            1 :                 return [0, _("Invalid port number")];
     500              :             else
     501            1 :                 return [port, ""];
     502            1 :         } else {
     503            1 :             return [avail.port, ""];
     504            1 :         }
     505            1 :     }
     506              : 
     507            1 :     validate(event, value) {
     508            1 :         let error = "";
     509            1 :         let targets = ['tcp', 'custom_tcp_ports', 'tcp_error', 'custom_tcp_value'];
     510            1 :         if (event.target.id === "udp-ports")
     511            1 :             targets = ['udp', 'custom_udp_ports', 'udp_error', 'custom_udp_value'];
     512            1 :         const new_ports = [];
     513            1 :         const event_id = event.target.id;
     514              : 
     515            1 :         this.setState(oldState => {
     516            1 :             const ports = value.split(',');
     517            1 :             ports.forEach((port) => {
     518            1 :                 port = port.trim();
     519            1 :                 if (!port)
     520            1 :                     return;
     521            1 :                 let ports;
     522            1 :                 if (port.indexOf("-") > -1) {
     523            1 :                     ports = port.split("-");
     524            1 :                     if (ports.length != 2) {
     525            1 :                         error = _("Invalid range");
     526            1 :                         return;
     527            1 :                     }
     528            1 :                     [ports[0], error] = this.getPortNumber(ports[0], targets[0], oldState.avail_services[ports[0]]);
     529            1 :                     if (!error) {
     530            1 :                         [ports[1], error] = this.getPortNumber(ports[1], targets[0], oldState.avail_services[ports[1]]);
     531            1 :                         if (!error) {
     532            1 :                             if (Number(ports[0]) >= Number(ports[1]))
     533            1 :                                 error = _("Range must be strictly ordered");
     534              :                             else
     535            1 :                                 new_ports.push(ports[0] + "-" + ports[1]);
     536            1 :                         }
     537            1 :                     }
     538            1 :                 } else {
     539            1 :                     [ports, error] = this.getPortNumber(port, targets[0], oldState.avail_services[port]);
     540            1 :                     if (!error)
     541            1 :                         new_ports.push(ports);
     542            1 :                 }
     543            1 :             });
     544            1 :             const newState = {
     545            1 :                 [targets[1]]: new_ports,
     546            1 :                 [targets[2]]: error,
     547            1 :                 [targets[3]]: value
     548            1 :             };
     549              : 
     550            1 :             let all_ports;
     551            1 :             if (event_id === "udp-ports") {
     552            1 :                 const old_ports = oldState.custom_tcp_ports.map(port => this.getName(port, "tcp"));
     553            1 :                 all_ports = old_ports.concat(new_ports.map(port => this.getName(port, "udp")));
     554            1 :             } else {
     555            1 :                 const old_ports = oldState.custom_udp_ports.map(port => this.getName(port, "udp"));
     556            1 :                 all_ports = new_ports.map(port => this.getName(port, "tcp")).concat(old_ports);
     557            1 :             }
     558              : 
     559            1 :             if (oldState.generate_custom_id) {
     560            1 :                 if (all_ports.length > 0)
     561            1 :                     newState.custom_id = "custom--" + all_ports.join('-');
     562              :                 else
     563            1 :                     newState.custom_id = "";
     564            1 :             }
     565              : 
     566            1 :             return newState;
     567            1 :         });
     568            1 :     }
     569              : 
     570            2 :     onToggleType(event) {
     571            2 :         this.setState({
     572            2 :             custom: event.target.value === "ports"
     573            2 :         });
     574            2 :     }
     575              : 
     576            3 :     componentDidMount() {
     577            3 :         firewall.getAvailableServices()
     578            3 :                 .then(services => this.setState({ services }));
     579            3 :         cockpit.file('/etc/services').read()
     580            3 :                 .then(content => this.setState({
     581            3 :                     avail_services: this.parseServices(content)
     582            3 :                 }));
     583            3 :     }
     584              : 
     585            1 :     onFilterChanged(value) {
     586            1 :         this.setState({ filter: value.toLowerCase() });
     587            1 :     }
     588              : 
     589            3 :     render() {
     590            3 :         const Dialogs = this.context;
     591            3 :         let services;
     592            1 :         if (this.state.filter && this.state.services && !isNaN(this.state.filter))
     593            1 :             services = this.state.services.filter(s => {
     594            1 :                 for (const port of s.ports)
     595            1 :                     if (port.port === this.state.filter)
     596            1 :                         return true;
     597            1 :                 return false;
     598            1 :             });
     599            1 :         else if (this.state.filter && this.state.services)
     600            1 :             services = this.state.services.filter(s => s.id.indexOf(this.state.filter) > -1);
     601              :         else
     602            3 :             services = this.state.services;
     603              : 
     604              :         // hide services which have been enabled in the zone
     605            3 :         if (services)
     606            3 :             services = services.filter(s => firewall.zones[this.props.zoneId].services.indexOf(s.id) === -1);
     607              : 
     608            3 :         let addText = "";
     609            3 :         let titleText = "";
     610            1 :         if (this.props.custom_id) {
     611            1 :             addText = _("Edit service");
     612            1 :             titleText = cockpit.format(_("Edit custom service in $0 zone"), this.props.zoneName);
     613            1 :         } else {
     614            2 :             addText = this.state.custom ? _("Add ports") : _("Add services");
     615            2 :             titleText = this.state.custom ? cockpit.format(_("Add ports to $0 zone"), this.props.zoneName) : cockpit.format(_("Add services to $0 zone"), this.props.zoneName);
     616            3 :         }
     617              : 
     618            3 :         return (
     619            3 :             <Modal id="add-services-dialog" isOpen
     620            3 :                    position="top" variant="medium"
     621            3 :                    onClose={Dialogs.close}
     622              :             >
     623            3 :                 <ModalHeader title={titleText} />
     624            3 :                 <ModalBody>
     625            1 :                     <Form isHorizontal onSubmit={this.props.custom_id ? this.edit : this.save}>
     626              :                         {
     627            1 :                             this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
     628              :                         }
     629            3 :                         { !!this.props.custom_id ||
     630            3 :                             <FormGroup className="add-services-dialog-type" isInline>
     631            3 :                                 <Radio name="type"
     632            3 :                                        id="add-services-dialog--services"
     633            3 :                                        value="services"
     634            3 :                                        isChecked={!this.state.custom}
     635            3 :                                        onChange={this.onToggleType}
     636            3 :                                        label={_("Services")} />
     637            3 :                                 <Radio name="type"
     638            3 :                                        id="add-services-dialog--ports"
     639            3 :                                        value="ports"
     640            3 :                                        isChecked={this.state.custom}
     641            3 :                                        onChange={this.onToggleType}
     642            3 :                                        isDisabled={this.state.avail_services == null}
     643            3 :                                        label={_("Custom ports")} />
     644            3 :                             </FormGroup>
     645              :                         }
     646            3 :                         { this.state.custom ||
     647            3 :                             <div>
     648            3 :                                 { services
     649              :                                     ? (
     650            3 :                                         <>
     651            3 :                                             <SearchInput id="filter-services-input"
     652            3 :                                                      value={this.state.filter}
     653            3 :                                                      onChange={this.onFilterChanged} />
     654            3 :                                             <DataList className="service-list" isCompact>
     655            3 :                                                 {services.map(s => (
     656            3 :                                                     <DataListItem key={s.id} aria-labelledby={s.id}>
     657            3 :                                                         <DataListItemRow>
     658            3 :                                                             <DataListCheck aria-labelledby={s.id}
     659            3 :                                                                        isChecked={this.state.selected.has(s.id)}
     660            2 :                                                                        onChange={(event, value) => this.onToggleService(event, s.id)}
     661            3 :                                                                        id={"firewall-service-" + s.id}
     662            3 :                                                                        name={s.id + "-checkbox"} />
     663            3 :                                                             <DataListItemCells
     664            3 :                                                                 dataListCells={[
     665            3 :                                                                     <DataListCell key="service-list-item">
     666            3 :                                                                         <label htmlFor={"firewall-service-" + s.id}
     667            3 :                                                                                className="service-list-iteam-heading">
     668            3 :                                                                             {s.id}
     669            3 :                                                                         </label>
     670            3 :                                                                         {renderPorts(s)}
     671            3 :                                                                     </DataListCell>,
     672            3 :                                                                 ]} />
     673            3 :                                                         </DataListItemRow>
     674            3 :                                                     </DataListItem>
     675            3 :                                                 ))}
     676            3 :                                             </DataList>
     677            3 :                                         </>
     678              :                                     )
     679              :                                     : (
     680            3 :                                         <EmptyStatePanel loading />
     681              :                                     )}
     682            3 :                             </div>
     683              :                         }
     684            3 :                         { !this.state.custom ||
     685            2 :                             <>
     686            2 :                                 <FormGroup label="TCP">
     687            2 :                                     <TextInput id="tcp-ports" type="text" onChange={this.validate}
     688            1 :                                                validated={this.state.tcp_error ? "error" : "default"}
     689            2 :                                                isDisabled={this.state.avail_services == null}
     690            2 :                                                value={this.state.custom_tcp_value}
     691            2 :                                                placeholder={_("Example: 22,ssh,8080,5900-5910")} />
     692            2 :                                     <FormHelper helperTextInvalid={this.state.tcp_error} helperText={_("Comma-separated ports, ranges, and services are accepted")} />
     693            2 :                                 </FormGroup>
     694              : 
     695            2 :                                 <FormGroup label="UDP">
     696            2 :                                     <TextInput id="udp-ports" type="text" onChange={this.validate}
     697            1 :                                                validated={this.state.udp_error ? "error" : "default"}
     698            2 :                                                isDisabled={this.state.avail_services == null}
     699            2 :                                                value={this.state.custom_udp_value}
     700            2 :                                                placeholder={_("Example: 88,2019,nfs,rsync")} />
     701            2 :                                     <FormHelper helperTextInvalid={this.state.udp_error} helperText={_("Comma-separated ports, ranges, and services are accepted")} />
     702            2 :                                 </FormGroup>
     703              : 
     704            2 :                                 <FormGroup label={_("ID")}>
     705            1 :                                     <TextInput id="service-name" onChange={(_event, value) => this.setId(value)} isDisabled={!!this.props.custom_id || this.state.avail_services == null}
     706            2 :                                                value={this.state.custom_id} />
     707            2 :                                     <FormHelper helperText={_("If left empty, ID will be generated based on associated port services and port numbers")} />
     708            2 :                                 </FormGroup>
     709              : 
     710            2 :                                 <FormGroup label={_("Description")}>
     711            1 :                                     <TextInput id="service-description" onChange={(_event, value) => this.setDescription(value)} isDisabled={this.state.avail_services == null}
     712            2 :                                                value={this.state.custom_description} />
     713            2 :                                 </FormGroup>
     714            2 :                             </>
     715              :                         }
     716            3 :                     </Form>
     717            3 :                 </ModalBody>
     718            3 :                 <ModalFooter>
     719            3 :                     { !this.state.custom ||
     720            2 :                         <Alert variant="warning"
     721            2 :                             isInline
     722            2 :                             title={_("Adding custom ports will reload firewalld. A reload will result in the loss of any runtime-only configuration!")} />
     723              :                     }
     724            1 :                     <Button variant='primary' isDisabled={(this.state.custom && this.checkNullValues()) || (!this.state.custom && !this.state.selected.size)} onClick={this.props.custom_id ? this.edit : this.save} aria-label={titleText}>
     725            3 :                         {addText}
     726            3 :                     </Button>
     727            3 :                     <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
     728            3 :                         {_("Cancel")}
     729            3 :                     </Button>
     730            3 :                 </ModalFooter>
     731            3 :             </Modal>
     732              :         );
     733            3 :     }
     734            7 : }
     735              : 
     736            7 : class ActivateZoneModal extends React.Component {
     737            7 :     static contextType = DialogsContext;
     738              : 
     739            1 :     constructor() {
     740            1 :         super();
     741              : 
     742            1 :         this.state = {
     743            1 :             ipRange: "ip-entire-subnet",
     744            1 :             ipRangeValue: null,
     745            1 :             zone: null,
     746            1 :             interfaces: new Set(),
     747            1 :             dialogError: null,
     748            1 :             dialogErrorDetail: null,
     749            1 :         };
     750            1 :         this.onFirewallChanged = this.onFirewallChanged.bind(this);
     751            1 :         this.onInterfaceChange = this.onInterfaceChange.bind(this);
     752            1 :         this.onChange = this.onChange.bind(this);
     753            1 :         this.save = this.save.bind(this);
     754            1 :     }
     755              : 
     756            1 :     componentDidMount() {
     757            1 :         firewall.addEventListener("changed", this.onFirewallChanged);
     758            1 :     }
     759              : 
     760            1 :     componentWillUnmount() {
     761            1 :         firewall.removeEventListener("changed", this.onFirewallChanged);
     762            1 :     }
     763              : 
     764            1 :     onFirewallChanged() {
     765            1 :         this.setState({});
     766            1 :     }
     767              : 
     768            1 :     onInterfaceChange(event) {
     769            1 :         const int = event.target.value;
     770            1 :         const enabled = event.target.checked;
     771            1 :         this.setState(state => {
     772            1 :             const interfaces = new Set(state.interfaces);
     773            1 :             if (enabled)
     774            0 :                 interfaces.add(int);
     775              :             else
     776            0 :                 interfaces.delete(int);
     777            1 :             return { interfaces };
     778            1 :         });
     779            1 :     }
     780              : 
     781            1 :     onChange(key, value) {
     782            1 :         this.setState({ [key]: value });
     783            1 :     }
     784              : 
     785            1 :     save(event) {
     786            1 :         const Dialogs = this.context;
     787            1 :         let p;
     788            1 :         if (firewall.zones[this.state.zone].services.indexOf("cockpit") === -1)
     789            1 :             p = firewall.addService(this.state.zone, "cockpit");
     790              :         else
     791            1 :             p = Promise.resolve();
     792              : 
     793            1 :         const sources = this.state.ipRange === "ip-range" ? this.state.ipRangeValue.split(",").map(ip => ip.trim()) : [];
     794            1 :         p.then(() =>
     795            1 :             firewall.activateZone(this.state.zone, [...this.state.interfaces], sources)
     796            1 :                     .then(Dialogs.close)
     797            1 :                     .catch(error => {
     798            1 :                         this.setState({
     799            1 :                             dialogError: _("Failed to add zone"),
     800            1 :                             dialogErrorDetail: error.name + ": " + error.message,
     801            1 :                         });
     802            1 :                     }));
     803              : 
     804            1 :         if (event)
     805            1 :             event.preventDefault();
     806            1 :         return false;
     807            1 :     }
     808              : 
     809            1 :     render() {
     810            1 :         const Dialogs = this.context;
     811            1 :         const zones = Object.keys(firewall.zones).filter(z => firewall.zones[z].target === "default" && !firewall.activeZones.has(z));
     812            1 :         const customZones = zones.filter(z => firewall.predefinedZones.indexOf(z) === -1);
     813            1 :         const interfaces = firewall.availableInterfaces.filter(i => {
     814            1 :             let inZone = false;
     815            1 :             firewall.activeZones.forEach(z => {
     816            1 :                 inZone ||= firewall.zones[z].interfaces.indexOf(i.device) !== -1;
     817            1 :             });
     818            1 :             return !inZone;
     819            1 :         });
     820              :         // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceCapabilities
     821            1 :         const NM_DEVICE_CAP_IS_SOFTWARE = 4;
     822            1 :         const virtualDevices = interfaces.filter(i => (i.capabilities & NM_DEVICE_CAP_IS_SOFTWARE) !== 0 && i.device !== "lo").sort((a, b) => a.device.localeCompare(b.device));
     823            0 :         const physicalDevices = interfaces.filter(i => ((i.capabilities & NM_DEVICE_CAP_IS_SOFTWARE) === 0) && i.device !== "lo").sort((a, b) => a.device.localeCompare(b.device));
     824            1 :         return (
     825            1 :             <Modal id="add-zone-dialog" isOpen
     826            1 :                    position="top" variant="medium"
     827            1 :                    onClose={Dialogs.close}
     828              :             >
     829            1 :                 <ModalHeader title={_("Add zone")} />
     830            1 :                 <ModalBody>
     831            1 :                     <Form isHorizontal onSubmit={this.save}>
     832              :                         {
     833            1 :                             this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
     834              :                         }
     835            1 :                         <FormGroup label={ _("Trust level") } className="add-zone-zones">
     836            1 :                             <Flex>
     837            1 :                                 <FlexItem className="add-zone-zones-firewalld">
     838            1 :                                     <legend>{ _("Sorted from least to most trusted") }</legend>
     839            1 :                                     { zones.filter(z => firewall.predefinedZones.indexOf(z) !== -1).sort((a, b) => firewall.predefinedZones.indexOf(a) - firewall.predefinedZones.indexOf(b))
     840            1 :                                             .map(z =>
     841            1 :                                                 <Radio key={z} id={z} name="zone" value={z}
     842            1 :                                                        isChecked={this.state.zone == z}
     843            1 :                                                        onChange={e => this.onChange("zone", e.target.value)}
     844            1 :                                                        label={ firewall.zones[z].id } />
     845            1 :                                             )}
     846            1 :                                 </FlexItem>
     847            1 :                                 <FlexItem className="add-zone-zones-custom">
     848            1 :                                     { customZones.length > 0 && <legend>{ _("Custom zones") }</legend> }
     849            1 :                                     { customZones.map(z =>
     850            1 :                                         <Radio key={z} id={z} name="zone" value={z}
     851            1 :                                                isChecked={this.state.zone == z}
     852            0 :                                                onChange={e => this.onChange("zone", e.target.value)}
     853            1 :                                                label={ firewall.zones[z].id } />
     854            1 :                                     )}
     855            1 :                                 </FlexItem>
     856            1 :                             </Flex>
     857            1 :                         </FormGroup>
     858              : 
     859            1 :                         <FormGroup label={ _("Description") }>
     860            1 :                             <p id="add-zone-description-readonly">
     861            1 :                                 { (this.state.zone && firewall.zones[this.state.zone].description) || _("No description available") }
     862            1 :                             </p>
     863            1 :                         </FormGroup>
     864              : 
     865            1 :                         <FormGroup label={ _("Included services") } hasNoPaddingTop>
     866            1 :                             <div id="add-zone-services-readonly">
     867            1 :                                 { (this.state.zone && firewall.zones[this.state.zone].services.join(", ")) || _("None") }
     868            1 :                             </div>
     869            1 :                             <FormHelper helperText={_("The cockpit service is automatically included")} />
     870            1 :                         </FormGroup>
     871              : 
     872            1 :                         <FormGroup label={ _("Interfaces") } hasNoPaddingTop isInline>
     873            1 :                             { physicalDevices.map(i =>
     874            1 :                                 <Checkbox key={i.device}
     875            1 :                                           id={i.device}
     876            1 :                                           value={i.device}
     877            0 :                                           onChange={(event, value) => this.onInterfaceChange(event)}
     878            1 :                                           isChecked={this.state.interfaces.has(i.device)}
     879            1 :                                           label={i.device} />) }
     880            1 :                             { virtualDevices.map(i =>
     881            1 :                                 <Checkbox key={i.device}
     882            1 :                                           id={i.device}
     883            1 :                                           value={i.device}
     884            1 :                                           onChange={(event, value) => this.onInterfaceChange(event)}
     885            1 :                                           isChecked={this.state.interfaces.has(i.device)}
     886            1 :                                           label={i.device} />) }
     887            1 :                         </FormGroup>
     888              : 
     889            1 :                         <FormGroup label={ _("Allowed addresses") } hasNoPaddingTop isInline>
     890            1 :                             <Radio name="add-zone-ip"
     891            1 :                                    isChecked={this.state.ipRange == "ip-entire-subnet"}
     892            1 :                                    value="ip-entire-subnet"
     893            1 :                                    id="ip-entire-subnet"
     894            0 :                                    onChange={e => this.onChange("ipRange", e.target.value)}
     895            1 :                                    label={ _("Entire subnet") } />
     896            1 :                             <Radio name="add-zone-ip"
     897            1 :                                    isChecked={this.state.ipRange == "ip-range"}
     898            1 :                                    value="ip-range"
     899            1 :                                    id="ip-range"
     900            1 :                                    onChange={e => this.onChange("ipRange", e.target.value)}
     901            1 :                                    label={ _("Range") } />
     902            1 :                             {this.state.ipRange === "ip-range" && (
     903            1 :                                 <>
     904            1 :                                     <TextInput id="add-zone-ip" onChange={(_event, value) => this.onChange("ipRangeValue", value)} />
     905            1 :                                     <FormHelperText>{_("IP address with routing prefix. Separate multiple values with a comma. Example: 192.0.2.0/24, 2001:db8::/32")}</FormHelperText>
     906            1 :                                 </>
     907              :                             )}
     908            1 :                         </FormGroup>
     909            1 :                     </Form>
     910            1 :                 </ModalBody>
     911            1 :                 <ModalFooter>
     912            1 :                     <Button variant="primary" onClick={this.save} isDisabled={this.state.zone === null ||
     913            1 :                                                                             (this.state.interfaces.size === 0 && this.state.ipRange === "ip-entire-subnet") ||
     914            1 :                                                                             (this.state.ipRange === "ip-range" && !this.state.ipRangeValue)}>
     915            1 :                         { _("Add zone") }
     916            1 :                     </Button>
     917            1 :                     <Button variant="link" className="btn-cancel" onClick={Dialogs.close}>
     918            1 :                         { _("Cancel") }
     919            1 :                     </Button>
     920            1 :                 </ModalFooter>
     921            1 :             </Modal>
     922              :         );
     923            1 :     }
     924            7 : }
     925              : 
     926            2 : function DeleteConfirmationModal(props) {
     927            2 :     return (
     928            2 :         <Modal id="delete-confirmation-dialog" isOpen
     929            2 :                position="top" variant="medium"
     930            2 :                onClose={props.onCancel}
     931              :         >
     932            2 :             <ModalHeader title={props.title} />
     933            2 :             {props.body &&
     934            2 :                 <ModalBody>
     935            2 :                     <Alert variant="warning" isInline title={props.body} />
     936            2 :                 </ModalBody>}
     937            2 :             <ModalFooter>
     938            2 :                 <Button variant="danger" onClick={props.onDelete} aria-label={cockpit.format(_("Confirm removal of $0"), props.target)}>
     939            2 :                     { _("Delete") }
     940            2 :                 </Button>
     941            2 :                 <Button variant="link" className="btn-cancel" onClick={props.onCancel}>
     942            2 :                     { _("Cancel") }
     943            2 :                 </Button>
     944            2 :             </ModalFooter>
     945            2 :         </Modal>
     946              :     );
     947            2 : }
     948              : 
     949            7 : export class Firewall extends React.Component {
     950            7 :     static contextType = DialogsContext;
     951              : 
     952            7 :     constructor() {
     953            7 :         super();
     954              : 
     955            7 :         this.state = {
     956            7 :             firewall,
     957            7 :             pendingTarget: null /* `null` for not pending */
     958            7 :         };
     959              : 
     960            7 :         this.onFirewallChanged = this.onFirewallChanged.bind(this);
     961            7 :         this.openServicesDialog = this.openServicesDialog.bind(this);
     962            7 :         this.openAddZoneDialog = this.openAddZoneDialog.bind(this);
     963            7 :         this.onRemoveZone = this.onRemoveZone.bind(this);
     964            7 :         this.onRemoveService = this.onRemoveService.bind(this);
     965            7 :         this.onEditService = this.onEditService.bind(this);
     966            7 :     }
     967              : 
     968            7 :     onFirewallChanged() {
     969            7 :         this.setState((prevState) => {
     970            7 :             if (prevState.pendingTarget === firewall.enabled)
     971            2 :                 return { firewall, pendingTarget: null };
     972              : 
     973            7 :             return { firewall };
     974            7 :         });
     975            7 :     }
     976              : 
     977            1 :     onRemoveZone(zone) {
     978            1 :         const Dialogs = this.context;
     979            1 :         let body;
     980            1 :         if (firewall.zones[zone].services.indexOf("cockpit") !== -1)
     981            0 :             body = _("This zone contains the cockpit service. Make sure that this zone does not apply to your current web console connection.");
     982              :         else
     983            0 :             body = _("Removing the zone will remove all services within it.");
     984            1 :         Dialogs.show(<DeleteConfirmationModal title={ cockpit.format(_("Remove zone $0"), zone) }
     985            1 :                                               body={body}
     986            1 :                                               target={zone}
     987            1 :                                               onCancel={Dialogs.close}
     988            1 :                                               onDelete={ () => {
     989            1 :                                                   firewall.deactiveateZone(zone);
     990            1 :                                                   Dialogs.close();
     991            1 :                                               }} />
     992            1 :         );
     993            1 :     }
     994              : 
     995            3 :     onRemoveService(zone, service) {
     996            3 :         const Dialogs = this.context;
     997            1 :         if (service === 'cockpit') {
     998            1 :             const body = _("Removing the cockpit service might result in the web console becoming unreachable. Make sure that this zone does not apply to your current web console connection.");
     999            1 :             Dialogs.show(<DeleteConfirmationModal title={ cockpit.format(_("Remove $0 service from $1 zone"), service, zone) }
    1000            1 :                                                   body={body}
    1001            1 :                                                   target={service}
    1002            1 :                                                   onCancel={Dialogs.close}
    1003            1 :                                                   onDelete={ () => {
    1004            1 :                                                       firewall.removeService(zone, service);
    1005            1 :                                                       Dialogs.close();
    1006            1 :                                                   }} />
    1007            1 :             );
    1008            1 :         } else {
    1009            3 :             firewall.removeService(zone, service);
    1010            3 :         }
    1011            3 :     }
    1012              : 
    1013            1 :     onRemovePort(zone, port, protocol) {
    1014            1 :         firewall.removePort(zone, port, protocol);
    1015            1 :     }
    1016              : 
    1017            1 :     onEditService(zone, service) {
    1018            1 :         const tcp_ports = [];
    1019            1 :         const udp_ports = [];
    1020            1 :         service.ports.forEach(port => {
    1021            1 :             if (port.protocol === "tcp")
    1022            1 :                 tcp_ports.push(port.port);
    1023              :             else
    1024            1 :                 udp_ports.push(port.port);
    1025            1 :         });
    1026              : 
    1027            0 :         const zone_name = zone.name ? zone.name : upperCaseFirstLetter(zone.id);
    1028              : 
    1029            1 :         const Dialogs = this.context;
    1030            1 :         Dialogs.show(<AddEditServicesModal zoneId={zone.id} zoneName={zone_name} custom_id={service.id}
    1031            1 :                                            custom_tcp_ports={tcp_ports} custom_udp_ports={udp_ports} custom_description={service.description}
    1032            1 :                                            custom_tcp_value={tcp_ports.join(", ")} custom_udp_value={udp_ports.join(", ")} />);
    1033            1 :     }
    1034              : 
    1035            7 :     componentDidMount() {
    1036            7 :         firewall.addEventListener("changed", this.onFirewallChanged);
    1037            7 :     }
    1038              : 
    1039            0 :     componentWillUnmount() {
    1040            0 :         firewall.removeEventListener("changed", this.onFirewallChanged);
    1041            0 :     }
    1042              : 
    1043            3 :     openServicesDialog(zoneId, zoneName) {
    1044            3 :         const Dialogs = this.context;
    1045            3 :         Dialogs.show(<AddEditServicesModal zoneId={zoneId} zoneName={zoneName} />);
    1046            3 :     }
    1047              : 
    1048            1 :     openAddZoneDialog() {
    1049            1 :         const Dialogs = this.context;
    1050            1 :         Dialogs.show(<ActivateZoneModal />);
    1051            1 :     }
    1052              : 
    1053            7 :     render() {
    1054            1 :         function go_up(event) {
    1055            1 :             cockpit.jump("/network", cockpit.transport.host);
    1056            1 :         }
    1057              : 
    1058            2 :         if (!this.state.firewall.installed) {
    1059            2 :             return <EmptyStatePanel title={ _("Firewall is not available") }
    1060            2 :                                     paragraph={ cockpit.format(_("Please install the $0 package"), "firewalld") }
    1061            2 :                                     icon={ ExclamationCircleIcon }
    1062            2 :             />;
    1063            2 :         }
    1064              : 
    1065            7 :         if (!this.state.firewall.ready)
    1066            7 :             return <EmptyStatePanel loading />;
    1067              : 
    1068            7 :         const addZoneAction = (
    1069            7 :             <Button variant="primary" onClick={this.openAddZoneDialog} id="add-zone-button" aria-label={_("Add a new zone")}>
    1070            7 :                 {_("Add new zone")}
    1071            7 :             </Button>
    1072              :         );
    1073              : 
    1074            2 :         const zones = [...this.state.firewall.activeZones].sort((z1, z2) =>
    1075            0 :             z1 === firewall.defaultZone ? -1 : z2 === firewall.defaultZone ? 1 : 0
    1076            5 :         ).map(id => this.state.firewall.zones[id]);
    1077              : 
    1078            7 :         const enabled = this.state.firewall.enabled;
    1079              : 
    1080            7 :         return (
    1081            7 :             <Page className="pf-m-no-sidebar">
    1082            7 :                 <PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
    1083            7 :                     <Breadcrumb>
    1084            7 :                         <BreadcrumbItem onClick={go_up} className="pf-v6-c-breadcrumb__link">{_("Networking")}</BreadcrumbItem>
    1085            7 :                         <BreadcrumbItem isActive>{_("Firewall")}</BreadcrumbItem>
    1086            7 :                     </Breadcrumb>
    1087            7 :                 </PageBreadcrumb>
    1088            7 :                 <PageSection hasBodyWrapper={false} id="firewall-heading" className="firewall-heading">
    1089            7 :                     <Flex alignItems={{ default: 'alignItemsCenter' }} justifyContent={{ default: 'justifyContentSpaceBetween' }}>
    1090            7 :                         <Flex alignItems={{ default: 'alignItemsCenter' }} id="firewall-heading-title-group">
    1091            7 :                             <Title headingLevel="h2" size="3xl">
    1092            7 :                                 {_("Firewall")}
    1093            7 :                             </Title>
    1094            7 :                             <FirewallSwitch firewall={firewall} />
    1095            7 :                             <p>{_("Incoming requests are blocked by default. Outgoing requests are not blocked.")}</p>
    1096            7 :                         </Flex>
    1097            6 :                         { enabled && !firewall.readonly && <span className="btn-group">{addZoneAction}</span> }
    1098            7 :                     </Flex>
    1099            7 :                 </PageSection>
    1100            7 :                 <PageSection hasBodyWrapper={false} id="zones-listing">
    1101            6 :                     { enabled && <Stack hasGutter>
    1102              :                         {
    1103            5 :                             zones.map(z => <ZoneSection key={z.id}
    1104            5 :                                                         zone={z}
    1105            5 :                                                         openServicesDialog={this.openServicesDialog}
    1106            5 :                                                         readonly={this.state.firewall.readonly}
    1107            5 :                                                         onRemoveZone={this.onRemoveZone}
    1108            5 :                                                         onEditService={this.onEditService}
    1109            5 :                                                         onRemoveService={this.onRemoveService}
    1110            5 :                                                         onRemovePort={this.onRemovePort} />
    1111            6 :                             )
    1112              :                         }
    1113            6 :                     </Stack> }
    1114            7 :                 </PageSection>
    1115            7 :             </Page>
    1116              :         );
    1117            7 :     }
    1118            7 : }
    1119              : 
    1120            7 : document.addEventListener("DOMContentLoaded", () => {
    1121            7 :     document.title = cockpit.gettext(document.title);
    1122            7 :     const root = createRoot(document.getElementById("firewall"));
    1123            7 :     root.render(<WithDialogs><Firewall /></WithDialogs>);
    1124            7 : });
        

Generated by: LCOV version 2.0-1