Line data Source code
1 : /*
2 : * Copyright (C) 2019 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 35 : import React, { useState } from 'react';
6 : import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
7 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
8 : import {
9 : Modal, ModalBody, ModalFooter, ModalHeader
10 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
11 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
12 : import { Form, FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
13 : import { HelperText, HelperTextItem, } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
14 : import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
15 : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
16 :
17 : import host_keys_script from "./ssh-list-host-keys.sh";
18 35 : import cockpit from "cockpit";
19 : import { superuser } from "superuser";
20 : import { useObject, useEvent } from "hooks.js";
21 : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
22 : import { ServerTimeConfig } from 'serverTime.js';
23 : import { RealmdClient, RealmButton } from "./realmd.jsx";
24 : import { TunedPerformanceProfile } from './tuned-dialog.jsx';
25 : import { CryptoPolicyRow } from './cryptoPolicies.jsx';
26 : import { useDialogs } from "dialogs.jsx";
27 : import { useInit } from "hooks";
28 :
29 : import "./configurationCard.scss";
30 :
31 35 : const _ = cockpit.gettext;
32 :
33 35 : export const ConfigurationCard = ({ hostname }) => {
34 35 : const Dialogs = useDialogs();
35 35 : const realmd_client = useObject(() => new RealmdClient(), null, []);
36 35 : useEvent(realmd_client, "changed");
37 :
38 28 : const hostname_button = (superuser.allowed && realmd_client.allowHostnameChange())
39 : ? (
40 28 : <Button id="system_information_hostname_button" variant="link"
41 0 : onClick={() => Dialogs.show(<PageSystemInformationChangeHostname />)}
42 28 : isInline aria-label="edit hostname">
43 6 : {hostname !== "" ? _("edit") : _("Set hostname")}
44 28 : </Button>)
45 35 : : null;
46 :
47 35 : return (
48 35 : <Card className="system-configuration">
49 35 : <CardTitle>{_("Configuration")}</CardTitle>
50 35 : <CardBody>
51 35 : <table className="pf-v6-c-table pf-m-grid-md pf-m-compact">
52 35 : <tbody className="pf-v6-c-table__tbody">
53 35 : <tr className="pf-v6-c-table__tr">
54 35 : <th className="pf-v6-c-table__th" scope="row">{_("Hostname")}</th>
55 35 : <td className="pf-v6-c-table__td">
56 32 : {hostname && <span id="system_information_hostname_text">{hostname}</span>}
57 35 : <span>{hostname_button}</span>
58 35 : </td>
59 35 : </tr>
60 :
61 35 : <tr className="pf-v6-c-table__tr">
62 35 : <th className="pf-v6-c-table__th" scope="row">{_("System time")}</th>
63 35 : <td className="pf-v6-c-table__td" data-label="system-time"><ServerTimeConfig /></td>
64 35 : </tr>
65 :
66 35 : <tr className="pf-v6-c-table__tr">
67 35 : <th className="pf-v6-c-table__th" scope="row">{_("Domain")}</th>
68 35 : <td className="pf-v6-c-table__td"><RealmButton realmd_client={realmd_client} /></td>
69 35 : </tr>
70 :
71 35 : <tr className="pf-v6-c-table__tr">
72 35 : <th className="pf-v6-c-table__th" scope="row">{_("Performance profile")}</th>
73 35 : <td className="pf-v6-c-table__td"><TunedPerformanceProfile /></td>
74 35 : </tr>
75 :
76 35 : <CryptoPolicyRow />
77 :
78 35 : <tr className="pf-v6-c-table__tr">
79 35 : <th className="pf-v6-c-table__th" scope="row">{_("Secure shell keys")}</th>
80 35 : <td className="pf-v6-c-table__td">
81 35 : <Button variant="link" isInline id="system-ssh-keys-link"
82 0 : onClick={() => Dialogs.show(<SystemInformationSshKeys />)}>
83 35 : {_("Show fingerprints")}
84 35 : </Button>
85 35 : </td>
86 35 : </tr>
87 35 : </tbody>
88 35 : </table>
89 35 : </CardBody>
90 35 : </Card>
91 : );
92 35 : };
93 :
94 0 : const SystemInformationSshKeys = () => {
95 0 : const Dialogs = useDialogs();
96 0 : const [keys, setKeys] = useState([]);
97 0 : const [error, setError] = useState("");
98 0 : const [loading, setLoading] = useState(true);
99 :
100 0 : function keysUpdate() {
101 0 : cockpit.script(host_keys_script, [], { superuser: "try", err: "message" })
102 0 : .then(data => {
103 0 : const seen = {};
104 0 : const keys = {};
105 :
106 0 : data.trim().split("\n")
107 0 : .forEach(line => {
108 0 : if (!line)
109 0 : return;
110 :
111 0 : const parts = line.trim().split(" ");
112 0 : const fp = parts[1];
113 0 : if (!seen[fp]) {
114 0 : seen[fp] = fp;
115 0 : let title = parts[parts.length - 1];
116 0 : if (title) {
117 0 : const m = title.match(/^\((.*)\)$/);
118 0 : if (m && m[1])
119 0 : title = m[1];
120 0 : }
121 0 : if (!keys[title])
122 0 : keys[title] = [];
123 0 : keys[title].push(fp);
124 0 : }
125 0 : });
126 :
127 0 : let arr = Object.keys(keys);
128 0 : arr.sort();
129 0 : arr = arr.map(function(k) {
130 0 : return { title: k, fps: keys[k] };
131 0 : });
132 :
133 0 : setKeys(arr);
134 0 : setLoading(false);
135 0 : setError("");
136 0 : })
137 0 : .catch(function(ex) {
138 0 : setLoading(false);
139 0 : setError(cockpit.format(_("failed to list ssh host keys: $0"), ex.message));
140 0 : });
141 0 : }
142 :
143 0 : function create_keyUpdater() {
144 : /*
145 : * Yes, we do refresh the keys while the dialog is open.
146 : * It may occur that sshd is not running at the point when
147 : * we try, or in rare cases the keys may change.
148 : */
149 0 : keysUpdate();
150 0 : return window.setInterval(keysUpdate, 10 * 1000);
151 0 : }
152 :
153 0 : function destroy_keyUpdater(interval) {
154 0 : window.clearInterval(interval);
155 0 : }
156 :
157 0 : useObject(create_keyUpdater, destroy_keyUpdater, []);
158 :
159 0 : let body = null;
160 0 : if (error)
161 0 : body = <Alert variant='danger' isInline title={_("Loading of SSH keys failed")}>
162 0 : <p>{_("Error message")}: {error}</p>
163 0 : </Alert>;
164 0 : else if (loading)
165 0 : body = <EmptyStatePanel loading title={ _("Loading keys...") } />;
166 0 : else if (!keys.length)
167 0 : body = <EmptyStatePanel title={ _("No host keys found.") } />;
168 : else
169 0 : body = <List isPlain isBordered>
170 0 : {keys.map(key =>
171 0 : <ListItem key={key.title}>
172 0 : <h4>{key.title}</h4>
173 0 : {key.fps.map((fp, i) => <div key={i}><small>{fp}</small></div>)}
174 0 : </ListItem>
175 0 : )}
176 0 : </List>;
177 :
178 0 : return (
179 0 : <Modal isOpen position="top" variant="medium"
180 0 : onClose={Dialogs.close}
181 0 : id="system_information_ssh_keys"
182 : >
183 0 : <ModalHeader title={_("Machine SSH key fingerprints")} />
184 0 : <ModalBody>
185 0 : {body}
186 0 : </ModalBody>
187 0 : <ModalFooter>
188 0 : <Button variant='secondary' onClick={Dialogs.close}>{_("Close")}</Button>
189 0 : </ModalFooter>
190 0 : </Modal>
191 : );
192 0 : };
193 :
194 0 : const PageSystemInformationChangeHostname = () => {
195 0 : const Dialogs = useDialogs();
196 0 : const [update_from_pretty, set_update_from_pretty] = useState(true);
197 0 : const [init_hostname, set_init_hostname] = useState("");
198 0 : const [hostname, set_hostname] = useState("");
199 0 : const [proxy, set_proxy] = useState(null);
200 0 : const [pretty, set_pretty] = useState("");
201 0 : const [init_pretty, set_init_pretty] = useState("");
202 0 : const [error, set_error] = useState([]);
203 :
204 0 : useInit(() => {
205 0 : const client = cockpit.dbus('org.freedesktop.hostname1', { superuser: "try" });
206 0 : const hostname_proxy = client.proxy();
207 :
208 0 : hostname_proxy.wait()
209 0 : .then(() => {
210 0 : const initial_hostname = hostname_proxy.StaticHostname || "";
211 0 : const initial_pretty_hostname = hostname_proxy.PrettyHostname || "";
212 :
213 0 : set_proxy(hostname_proxy);
214 0 : set_hostname(initial_hostname);
215 0 : set_init_hostname(initial_hostname);
216 0 : set_pretty(initial_pretty_hostname);
217 0 : set_init_pretty(initial_pretty_hostname);
218 0 : });
219 0 : });
220 :
221 0 : function onPrettyChanged(value) {
222 : // Whenever the pretty host name has changed (e.g. the user has edited it), we compute a new
223 : // simple host name (e.g. 7bit ASCII, no special chars/spaces, lower case) from it
224 :
225 0 : set_pretty(value);
226 :
227 0 : if (update_from_pretty) {
228 0 : const old_hostname = hostname;
229 0 : const first_dot = old_hostname.indexOf(".");
230 0 : let new_hostname = value
231 0 : .toLowerCase()
232 0 : .replace(/['".]+/g, "")
233 0 : .replace(/[^a-zA-Z0-9]+/g, "-");
234 0 : new_hostname = new_hostname.substring(0, 64);
235 0 : if (first_dot >= 0)
236 0 : new_hostname = new_hostname + old_hostname.substring(first_dot);
237 0 : set_hostname(new_hostname);
238 0 : }
239 0 : }
240 :
241 0 : function onHostnameChanged(value) {
242 0 : const error = [];
243 0 : if (value.length > 64)
244 0 : error.push(_("Real host name must be 64 characters or less"));
245 0 : if (value.match(/[.a-z0-9-]*/)[0] !== value || value.indexOf("..") !== -1)
246 0 : error.push(_("Real host name can only contain lower-case characters, digits, dashes, and periods (with populated subdomains)"));
247 :
248 0 : set_hostname(value);
249 0 : set_update_from_pretty(false);
250 0 : set_error(error);
251 0 : }
252 :
253 0 : function onSubmit(event) {
254 0 : const one = proxy.call("SetStaticHostname", [hostname, true]);
255 0 : const two = proxy.call("SetPrettyHostname", [pretty, true]);
256 :
257 0 : Promise.all([one, two]).then(Dialogs.close);
258 :
259 0 : if (event)
260 0 : event.preventDefault();
261 0 : return false;
262 0 : }
263 :
264 0 : const disabled = error.length || (init_hostname == hostname && init_pretty == pretty);
265 0 : return (
266 0 : <Modal isOpen position="top" variant="medium"
267 0 : onClose={Dialogs.close}
268 0 : id="system_information_change_hostname"
269 : >
270 0 : <ModalHeader title={_("Change host name")} />
271 0 : <ModalBody>
272 0 : <Form isHorizontal onSubmit={onSubmit}>
273 0 : <FormGroup fieldId="sich-pretty-hostname" label={_("Pretty host name")}>
274 0 : <TextInput id="sich-pretty-hostname" value={pretty} onChange={(_event, value) => onPrettyChanged(value)} />
275 0 : </FormGroup>
276 0 : <FormGroup fieldId="sich-hostname" label={_("Real host name")}>
277 0 : <TextInput id="sich-hostname" value={hostname} onChange={(_event, value) => onHostnameChanged(value)} validated={error.length ? "error" : "default"} />
278 0 : {error.length > 0 && <FormHelperText>
279 0 : <HelperText>
280 0 : {error.map((err, i) =>
281 0 : <HelperTextItem key={i} variant="error">
282 0 : {err}
283 0 : </HelperTextItem>
284 0 : )}
285 0 : </HelperText>
286 0 : </FormHelperText>}
287 0 : </FormGroup>
288 0 : </Form>
289 0 : </ModalBody>
290 0 : <ModalFooter>
291 0 : <Button variant='primary' isDisabled={disabled} onClick={onSubmit}>{_("Change")}</Button>
292 0 : <Button variant='link' onClick={Dialogs.close}>{_("Cancel")}</Button>
293 0 : </ModalFooter>
294 0 : </Modal>
295 : );
296 0 : };
|