Skip to content

Commit

Permalink
WIP - put warning in dialog flow
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Sep 2, 2024
1 parent ab47b79 commit 40ef9ad
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 155 deletions.
74 changes: 60 additions & 14 deletions pkg/shell/hosts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";

import 'polyfills';
import { CockpitNav, CockpitNavItem } from "./nav.jsx";
import { HostModal } from "./hosts_dialog.jsx";
import { HostModal, try2Connect, codes } from "./hosts_dialog.jsx";
import { useLoggedInUser } from "hooks";

const _ = cockpit.gettext;
Expand Down Expand Up @@ -75,20 +75,33 @@ export class CockpitHosts extends React.Component {
current_key: props.machine.key,
show_modal: false,
edit_machine: null,
switch_machine: null,
error_options: null,
warn_before_connecting: true,
};

this.toggleMenu = this.toggleMenu.bind(this);
this.filterHosts = this.filterHosts.bind(this);
this.onAddNewHost = this.onAddNewHost.bind(this);
this.onEditHosts = this.onEditHosts.bind(this);
this.onHostEdit = this.onHostEdit.bind(this);
this.onHostSwitch = this.onHostSwitch.bind(this);
this.onRemove = this.onRemove.bind(this);
}

componentDidMount() {
cockpit.user().then(user => {
this.setState({ current_user: user.name || "" });
}).catch(exc => console.log(exc));

window.trigger_connection_flow = machine => {
if (!this.state.show_modal)
this.onHostSwitch(machine);
};
}

componentWillUnmount() {
window.trigger_connection_flow = null;
}

static getDerivedStateFromProps(nextProps, prevState) {
Expand Down Expand Up @@ -124,6 +137,26 @@ export class CockpitHosts extends React.Component {
this.setState({ show_modal: true, edit_machine: machine });
}

onHostSwitch(machine) {
if (machine.state == "connected" || machine.address == "localhost") {
const addr = this.props.hostAddr({ host: machine.address }, true);
this.props.jump(addr);
} else if (machine.state != "connecting") {
if (this.state.warn_before_connecting)
this.setState({ show_modal: true, switch_machine: machine });
else {
try2Connect(this.props.machines, machine.connection_string)
.then(() => {
const addr = this.props.hostAddr({ host: machine.address }, true);
this.props.jump(addr);
})
.catch(err => {
this.setState({ show_modal: true, switch_machine: machine, error_options: err });
});
}
}
}

onEditHosts() {
this.setState(s => { return { editing: !s.editing } });
}
Expand Down Expand Up @@ -180,7 +213,7 @@ export class CockpitHosts extends React.Component {
header={(m.user ? m.user : this.state.current_user) + " @"}
status={m.state === "failed" ? { type: "error", title: _("Connection error") } : null}
className={m.state}
jump={this.props.jump}
jump={() => this.onHostSwitch(m)}
actions={<>
<Tooltip content={_("Edit")} position="right">
<Button isDisabled={m.address === "localhost"} className="nav-action" hidden={!editing} onClick={e => this.onHostEdit(e, m)} key={m.label + "edit"} variant="secondary"><EditIcon /></Button>
Expand Down Expand Up @@ -242,22 +275,35 @@ export class CockpitHosts extends React.Component {
</div>
{this.state.show_modal &&
<HostModal machines_ins={this.props.machines}
onClose={() => this.setState({ show_modal: false, edit_machine: null })}
address={this.state.edit_machine ? this.state.edit_machine.address : null}
caller_callback={this.state.edit_machine
? (new_connection_string) => {
const parts = this.props.machines.split_connection_string(new_connection_string);
if (this.state.edit_machine == this.props.machine && parts.address != this.state.edit_machine.address) {
const addr = this.props.hostAddr({ host: parts.address }, true);
onClose={() => this.setState({ show_modal: false,
edit_machine: null,
switch_machine: null,
error_options: null,
})}
address={this.state.edit_machine?.address || this.state.switch_machine?.address}
template={this.state.switch_machine
? (this.state.error_options
? codes[this.state.error_options.problem] || "change-port"
: "connect")
: null}
error_options={this.state.error_options}
troubleshoot_error={this.state.connection_error}
caller_callback={(new_connection_string) => {
const parts = this.props.machines.split_connection_string(new_connection_string);
const addr = this.props.hostAddr({ host: parts.address }, true);
if (this.state.edit_machine) {
if (this.state.edit_machine == this.props.machine &&
parts.address != this.state.edit_machine.address) {
this.props.jump(addr);
}
return Promise.resolve();
}
: (new_connection_string) => {
const parts = this.props.machines.split_connection_string(new_connection_string);
} else {
this.props.loader.connect(parts.address);
if (this.state.switch_machine)
this.props.jump(addr);
return Promise.resolve();
}} />
}
}}
/>
}
</>
);
Expand Down
159 changes: 113 additions & 46 deletions pkg/shell/hosts_dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/in
import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { Modal, ModalVariant } from "@patternfly/react-core/dist/esm/components/Modal/index.js";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import ModalVariant.
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons";
import { OutlinedQuestionCircleIcon, ExternalLinkAltIcon } from "@patternfly/react-icons";
import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";

import { FormHelper } from "cockpit-components-form-helper";
import { ModalError } from "cockpit-components-inline-notification.jsx";
import { fmt_to_fragments } from "utils.js";

const _ = cockpit.gettext;

export const codes = {
"danger": "connect",
"no-cockpit": "not-supported",
"not-supported": "not-supported",
"protocol-error": "not-supported",
Expand Down Expand Up @@ -108,6 +111,66 @@ export const CrossMachineWarning = () => {
title={_("Malicious pages on a remote machine may affect other connected hosts")} />;
};

class Connect extends React.Component {
constructor(props) {
super(props);

this.state = {
inProgress: false,
};
}

onConnect() {
this.setState({ inProgress: true });
this.props.run(this.props.try2Connect(this.props.full_address), ex => {
if (ex.problem === "no-host") {
let host_id_port = address;
let port = "22";
const port_index = host_id_port.lastIndexOf(":");
if (port_index === -1)
host_id_port = address + ":22";
else
port = host_id_port.substr(port_index + 1);

ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
ex.problem = "not-found";
}
this.setState({ inProgress: false });
this.props.setError(ex);
});
}

render() {
return (
<Modal id="hosts_connect_server_dialog" isOpen
position="top" variant="small"
onClose={this.props.onClose}
title={fmt_to_fragments(_("Connect to $0?"), <b>{this.props.host}</b>)}
titleIconVariant="warning"
footer={<>
<HelperText>
<HelperTextItem>{_("You will be reminded once per session")}</HelperTextItem>
</HelperText>
<Button variant="warning" isLoading={this.state.inProgress}
onClick={() => this.onConnect()}>
{_("Connect")}
</Button>
<Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
{ _("Cancel") }
</Button>
</>}
>
<p>{_("Remote hosts have the ability to run JavaScript on all connected hosts. Only connect to machines that you trust.")}</p>
<p>
<a href="https://cockpit-project.org" target="blank" rel="noopener noreferrer">
<ExternalLinkAltIcon /> {_("Read more")}
</a>
</p>
</Modal>
);
}
}

class AddMachine extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -229,22 +292,24 @@ class AddMachine extends React.Component {
});
});

this.props.run(this.props.try2Connect(address), ex => {
if (ex.problem === "no-host") {
let host_id_port = address;
let port = "22";
const port_index = host_id_port.lastIndexOf(":");
if (port_index === -1)
host_id_port = address + ":22";
else
port = host_id_port.substr(port_index + 1);

ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
ex.problem = "not-found";
}
this.setState({ inProgress: false });
this.props.setError(ex);
});
this.props.setError({ problem: "danger", command: "close" });

// this.props.run(this.props.try2Connect(address), ex => {
// if (ex.problem === "no-host") {
// let host_id_port = address;
// let port = "22";
// const port_index = host_id_port.lastIndexOf(":");
// if (port_index === -1)
// host_id_port = address + ":22";
// else
// port = host_id_port.substr(port_index + 1);

// ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
// ex.problem = "not-found";
// }
// this.setState({ inProgress: false });
// this.props.setError(ex);
// });
}

render() {
Expand Down Expand Up @@ -295,7 +360,6 @@ class AddMachine extends React.Component {
<Stack hasGutter>
{ this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
{body}
<CrossMachineWarning />
</Stack>
</Modal>
);
Expand Down Expand Up @@ -393,7 +457,6 @@ class MachinePort extends React.Component {
<Stack hasGutter>
{ this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
{body}
<CrossMachineWarning />
</Stack>
</Modal>
);
Expand Down Expand Up @@ -527,7 +590,6 @@ class HostKey extends React.Component {
<Stack hasGutter>
{ this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
{body}
<CrossMachineWarning />
</Stack>
</Modal>
);
Expand Down Expand Up @@ -905,13 +967,37 @@ class ChangeAuth extends React.Component {
<Stack hasGutter>
{ this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
{body}
<CrossMachineWarning />
</Stack>
</Modal>
);
}
}

export function try2Connect(machines_ins, address, options) {
return new Promise((resolve, reject) => {
const conn_options = { ...options, payload: "echo", host: address };

conn_options["init-superuser"] = get_init_superuser_for_options(conn_options);

const machine = machines_ins.lookup(address);
if (machine && machine.host_key && !machine.on_disk) {
conn_options['temp-session'] = false; // Compatibility option
conn_options.session = 'shared';
conn_options['host-key'] = machine.host_key;
}

const client = cockpit.channel(conn_options);
client.send("x");
client.addEventListener("message", () => {
resolve();
client.close();
});
client.addEventListener("close", (event, options) => {
reject(options);
});
});
}

export class HostModal extends React.Component {
constructor(props) {
super(props);
Expand All @@ -920,7 +1006,7 @@ export class HostModal extends React.Component {
current_template: this.props.template || "add-machine",
address: full_address(props.machines_ins, props.address),
old_address: full_address(props.machines_ins, props.address),
error_options: null,
error_options: this.props.error_options,
dialogError: "", // Error to be shown in the modal
};

Expand Down Expand Up @@ -950,28 +1036,7 @@ export class HostModal extends React.Component {
}

try2Connect(address, options) {
return new Promise((resolve, reject) => {
const conn_options = { ...options, payload: "echo", host: address };

conn_options["init-superuser"] = get_init_superuser_for_options(conn_options);

const machine = this.props.machines_ins.lookup(address);
if (machine && machine.host_key && !machine.on_disk) {
conn_options['temp-session'] = false; // Compatibility option
conn_options.session = 'shared';
conn_options['host-key'] = machine.host_key;
}

const client = cockpit.channel(conn_options);
client.send("x");
client.addEventListener("message", () => {
resolve();
client.close();
});
client.addEventListener("close", (event, options) => {
reject(options);
});
});
return try2Connect(this.props.machines_ins, address, options);
}

complete() {
Expand Down Expand Up @@ -1060,7 +1125,9 @@ export class HostModal extends React.Component {
complete: this.complete,
};

if (template === "add-machine")
if (template === "connect")
return <Connect {...props} />;
else if (template === "add-machine")
return <AddMachine {...props} />;
else if (template === "unknown-hostkey" || template === "unknown-host" || template === "invalid-hostkey")
return <HostKey {...props} />;
Expand Down
Loading

0 comments on commit 40ef9ad

Please sign in to comment.