LCOV - code coverage report
Current view: top level - pkg/networkmanager - firewall.jsx Coverage Total Hit
Test: cockpit Lines: 22.4 % 919 206
Test Date: 2026-06-17 06:28:00

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

Generated by: LCOV version 2.0-1