Commit 2cf64a19 authored by danielng's avatar danielng Committed by Commit Bot

crostini: Prevent adding duplicate ports in the port forwarding UI

Signalling to the user that they can't add a port that's already being
stored in the port forwarding settings for Crostini. Also, touched up
some of the browser tests.

Tests: Added browser tests and tested changes locally
Bug: 848127
Change-Id: Ia001380f6a08970f20f43029b6dc78acc47ccadc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2210339
Commit-Queue: Daniel Ng <danielng@google.com>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarJulian Watson <juwa@google.com>
Cr-Commit-Position: refs/heads/master@{#772580}
parent d19dce76
...@@ -992,6 +992,9 @@ ...@@ -992,6 +992,9 @@
<message name="IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_ERROR" desc="Description for the error when adding an invalid port."> <message name="IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_ERROR" desc="Description for the error when adding an invalid port.">
Error forwarding port Error forwarding port
</message> </message>
<message name="IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_EXISTING" desc="Description for the error when trying to add a port that already exists.">
Port already exists in settings
</message>
<message name="IDS_SETTINGS_CROSTINI_PORT_FORWARDING_REMOVE_ALL_PORTS" desc="Label for the button to deallocate all ports currently being forwarded in Crostini and clear the list of ports in the Crostini port forwarding settings page."> <message name="IDS_SETTINGS_CROSTINI_PORT_FORWARDING_REMOVE_ALL_PORTS" desc="Label for the button to deallocate all ports currently being forwarded in Crostini and clear the list of ports in the Crostini port forwarding settings page.">
Remove all ports Remove all ports
</message> </message>
......
2e164b48a1a46e227d870ade182f3cde916ff2b6
\ No newline at end of file
...@@ -241,6 +241,7 @@ void CrostiniPortForwarder::TryDeactivatePort( ...@@ -241,6 +241,7 @@ void CrostiniPortForwarder::TryDeactivatePort(
case Protocol::UDP: case Protocol::UDP:
client->ReleaseUdpPortForward(key.port_number, current_interface_, client->ReleaseUdpPortForward(key.port_number, current_interface_,
std::move(result_callback)); std::move(result_callback));
break;
} }
} }
......
...@@ -12,6 +12,7 @@ js_type_check("closure_compile") { ...@@ -12,6 +12,7 @@ js_type_check("closure_compile") {
":crostini_export_import", ":crostini_export_import",
":crostini_page", ":crostini_page",
":crostini_port_forwarding", ":crostini_port_forwarding",
":crostini_port_forwarding_add_port_dialog",
":crostini_shared_paths", ":crostini_shared_paths",
":crostini_shared_usb_devices", ":crostini_shared_usb_devices",
":crostini_subpage", ":crostini_subpage",
...@@ -92,6 +93,14 @@ js_library("crostini_port_forwarding") { ...@@ -92,6 +93,14 @@ js_library("crostini_port_forwarding") {
] ]
} }
js_library("crostini_port_forwarding_add_port_dialog") {
deps = [
":crostini_browser_proxy",
"..:metrics_recorder",
"//ui/webui/resources/js:cr",
]
}
js_library("crostini_subpage") { js_library("crostini_subpage") {
deps = [ deps = [
":crostini_browser_proxy", ":crostini_browser_proxy",
......
...@@ -121,7 +121,9 @@ ...@@ -121,7 +121,9 @@
</div> </div>
</template> </template>
<template is="dom-if" if="[[showAddPortDialog_]]" restamp> <template is="dom-if" if="[[showAddPortDialog_]]" restamp>
<settings-crostini-add-port-dialog on-close="onAddPortDialogClose_"> <settings-crostini-add-port-dialog
on-close="onAddPortDialogClose_"
all-ports="[[allPorts_]]">
</settings-crostini-add-port-dialog> </settings-crostini-add-port-dialog>
</template> </template>
<cr-lazy-render id="removeAllPortsMenu"> <cr-lazy-render id="removeAllPortsMenu">
......
...@@ -11,6 +11,11 @@ ...@@ -11,6 +11,11 @@
<dom-module id="settings-crostini-add-port-dialog"> <dom-module id="settings-crostini-add-port-dialog">
<template> <template>
<style include="settings-shared md-select"> <style include="settings-shared md-select">
#errorText {
color: var(--settings-error-color);
float: left;
}
#portNumberInput { #portNumberInput {
padding-inline-end: 20px; padding-inline-end: 20px;
width: 376px; width: 376px;
...@@ -25,11 +30,6 @@ ...@@ -25,11 +30,6 @@
max-width: 96px; max-width: 96px;
} }
#errorText {
color: var(--settings-error-color);
float: left;
}
.custom-body { .custom-body {
padding-bottom: 20px; padding-bottom: 20px;
} }
...@@ -83,11 +83,9 @@ ...@@ -83,11 +83,9 @@
</div> </div>
</div> </div>
<div slot="body" class="custom-body"> <div slot="body" class="custom-body">
<template is="dom-if" if="[[invalidPort_]]"> <span id="errorText">
<span id="errorText"> [[portState_]]
$i18n{crostiniPortForwardingAddError} </span>
</span>
</template>
<div slot="button-container" class="custom-button-container"> <div slot="button-container" class="custom-button-container">
<cr-button id="cancel" class="cancel-button" <cr-button id="cancel" class="cancel-button"
on-click="onCancelTap_">$i18n{cancel}</cr-button> on-click="onCancelTap_">$i18n{cancel}</cr-button>
......
...@@ -7,6 +7,16 @@ ...@@ -7,6 +7,16 @@
* user to start forwarding a different port by filling in the appropriate * user to start forwarding a different port by filling in the appropriate
* fields and clicking add. * fields and clicking add.
*/ */
/**
* @enum {string}
*/
const PortState = {
VALID: '',
INVALID: loadTimeData.getString('crostiniPortForwardingAddError'),
DUPLICATE: loadTimeData.getString('crostiniPortForwardingAddExisting'),
};
const MIN_VALID_PORT_NUMBER = 1024; // Minimum 16-bit integer value. const MIN_VALID_PORT_NUMBER = 1024; // Minimum 16-bit integer value.
const MAX_VALID_PORT_NUMBER = 65535; // Maximum 16-bit integer value. const MAX_VALID_PORT_NUMBER = 65535; // Maximum 16-bit integer value.
...@@ -39,11 +49,22 @@ Polymer({ ...@@ -39,11 +49,22 @@ Polymer({
}, },
/** /**
* @private {boolean} * @private {!PortState}
*/
portState_: {
type: String,
value: PortState.VALID,
},
/**
* List of ports that are already stored in the settings.
* @type {!Array<!CrostiniPortSetting>}
*/ */
invalidPort_: { allPorts: {
type: Boolean, type: Array,
value: false, value() {
return [];
},
}, },
}, },
...@@ -51,7 +72,7 @@ Polymer({ ...@@ -51,7 +72,7 @@ Polymer({
attached: function() { attached: function() {
this.$.dialog.showModal(); this.$.dialog.showModal();
this.async(() => { this.async(() => {
this.portNumberInput.focus(); this.$.portNumberInput.focus();
}, 1); }, 1);
}, },
...@@ -59,25 +80,26 @@ Polymer({ ...@@ -59,25 +80,26 @@ Polymer({
this.inputPortLabel_ = ''; this.inputPortLabel_ = '';
this.inputPortNumber_ = null; this.inputPortNumber_ = null;
this.inputProtocolIndex_ = 0; this.inputProtocolIndex_ = 0;
this.portState_ = PortState.VALID;
}, },
/** /**
* @returns {string} input for the port number. * @return {string} input for the port number.
*/ */
get portNumberInput() { get portNumberInput() {
return this.$.portNumberInput; return this.$.portNumberInput;
}, },
/** /**
* @returns {string} input for the optional port label. * @return {string} input for the optional port label.
*/ */
get portLabelInput() { get portLabelInput() {
return this.$.portLabelInput; return this.$.portLabelInput;
}, },
/** /**
* @param {string} to verify. * @param {string} input The port input to verify.
* @returns {boolean} if the input string is a valid port number. * @return {?boolean} if the input string is a valid port number.
*/ */
isValidPortNumber: function(input) { isValidPortNumber: function(input) {
const numberRegex = /^[0-9]+$/; const numberRegex = /^[0-9]+$/;
...@@ -85,6 +107,23 @@ Polymer({ ...@@ -85,6 +107,23 @@ Polymer({
Number(input) <= MAX_VALID_PORT_NUMBER; Number(input) <= MAX_VALID_PORT_NUMBER;
}, },
/**
* @return {!PortState}
* @private
*/
computePortState_: function() {
if (!this.isValidPortNumber(this.$.portNumberInput.value)) {
return PortState.INVALID;
}
if (this.allPorts.find(
portSetting =>
portSetting.port_number == this.$.portNumberInput.value &&
portSetting.protocol_type == this.inputProtocolIndex_)) {
return PortState.DUPLICATE;
}
return PortState.VALID;
},
/** /**
* @param {!Event} e * @param {!Event} e
* @private * @private
...@@ -101,17 +140,18 @@ Polymer({ ...@@ -101,17 +140,18 @@ Polymer({
/** @private */ /** @private */
onAddTap_: function() { onAddTap_: function() {
if (!this.isValidPortNumber(this.portNumberInput.value)) { this.portState_ = this.computePortState_();
this.invalidPort_ = true; if (!this.portState_ == PortState.VALID) {
return; return;
} }
const portNumber = +this.portNumberInput.value; const portNumber = +this.$.portNumberInput.value;
const portLabel = this.portLabelInput.value; const portLabel = this.$.portLabelInput.value;
this.invalidPort_ = false; this.invalidPort_ = false;
settings.CrostiniBrowserProxyImpl.getInstance() settings.CrostiniBrowserProxyImpl.getInstance()
.addCrostiniPortForward( .addCrostiniPortForward(
DEFAULT_CROSTINI_VM, DEFAULT_CROSTINI_CONTAINER, portNumber, DEFAULT_CROSTINI_VM, DEFAULT_CROSTINI_CONTAINER, portNumber,
this.inputProtocolIndex_, portLabel) /** @type {!CrostiniPortProtocol} */ (this.inputProtocolIndex_),
portLabel)
.then(result => { .then(result => {
// TODO(crbug.com/848127): Error handling for result // TODO(crbug.com/848127): Error handling for result
this.$.dialog.close(); this.$.dialog.close();
......
...@@ -326,6 +326,8 @@ void CrostiniSection::AddLoadTimeData(content::WebUIDataSource* html_source) { ...@@ -326,6 +326,8 @@ void CrostiniSection::AddLoadTimeData(content::WebUIDataSource* html_source) {
{"crostiniPortForwardingUDP", IDS_SETTINGS_CROSTINI_PORT_FORWARDING_UDP}, {"crostiniPortForwardingUDP", IDS_SETTINGS_CROSTINI_PORT_FORWARDING_UDP},
{"crostiniPortForwardingAddError", {"crostiniPortForwardingAddError",
IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_ERROR}, IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_ERROR},
{"crostiniPortForwardingAddExisting",
IDS_SETTINGS_CROSTINI_PORT_FORWARDING_ADD_EXISTING},
{"crostiniPortForwardingRemoveAllPorts", {"crostiniPortForwardingRemoveAllPorts",
IDS_SETTINGS_CROSTINI_PORT_FORWARDING_REMOVE_ALL_PORTS}, IDS_SETTINGS_CROSTINI_PORT_FORWARDING_REMOVE_ALL_PORTS},
{"crostiniPortForwardingRemovePort", {"crostiniPortForwardingRemovePort",
......
...@@ -418,7 +418,7 @@ suite('CrostiniPageTests', function() { ...@@ -418,7 +418,7 @@ suite('CrostiniPageTests', function() {
assertTrue(!!subpage); assertTrue(!!subpage);
}); });
test('Forwarded ports are shown', function() { test('DisplayPorts', function() {
// Extra list item for the titles. // Extra list item for the titles.
assertEquals( assertEquals(
3, subpage.shadowRoot.querySelectorAll('.list-item').length); 3, subpage.shadowRoot.querySelectorAll('.list-item').length);
...@@ -432,7 +432,7 @@ suite('CrostiniPageTests', function() { ...@@ -432,7 +432,7 @@ suite('CrostiniPageTests', function() {
await flushAsync(); await flushAsync();
subpage = subpage.$$('settings-crostini-add-port-dialog'); subpage = subpage.$$('settings-crostini-add-port-dialog');
const portNumberInput = subpage.$$('#portNumberInput'); const portNumberInput = subpage.$$('#portNumberInput');
portNumberInput.value = '5000'; portNumberInput.value = '5002';
const portLabelInput = subpage.$$('#portLabelInput'); const portLabelInput = subpage.$$('#portLabelInput');
portLabelInput.value = 'Some Label'; portLabelInput.value = 'Some Label';
subpage.$$('cr-dialog cr-button[id="continue"]').click(); subpage.$$('cr-dialog cr-button[id="continue"]').click();
...@@ -447,6 +447,8 @@ suite('CrostiniPageTests', function() { ...@@ -447,6 +447,8 @@ suite('CrostiniPageTests', function() {
await flushAsync(); await flushAsync();
subpage = subpage.$$('settings-crostini-add-port-dialog'); subpage = subpage.$$('settings-crostini-add-port-dialog');
const errorText = subpage.$$('#errorText');
assertEquals(errorText.innerText, '');
const portNumberInput = subpage.$$('#portNumberInput'); const portNumberInput = subpage.$$('#portNumberInput');
portNumberInput.value = 'INVALID_PORT_NUMBER'; portNumberInput.value = 'INVALID_PORT_NUMBER';
const portLabelInput = subpage.$$('#portLabelInput'); const portLabelInput = subpage.$$('#portLabelInput');
...@@ -454,14 +456,23 @@ suite('CrostiniPageTests', function() { ...@@ -454,14 +456,23 @@ suite('CrostiniPageTests', function() {
subpage.$$('cr-dialog cr-button[id="continue"]').click(); subpage.$$('cr-dialog cr-button[id="continue"]').click();
assertEquals( assertEquals(
0, crostiniBrowserProxy.getCallCount('addCrostiniPortForward')); 0, crostiniBrowserProxy.getCallCount('addCrostiniPortForward'));
assertEquals(
errorText.innerText,
loadTimeData.getString('crostiniPortForwardingAddError'));
portNumberInput.value = '65536'; portNumberInput.value = '65536';
subpage.$$('cr-dialog cr-button[id="continue"]').click(); subpage.$$('cr-dialog cr-button[id="continue"]').click();
assertEquals( assertEquals(
0, crostiniBrowserProxy.getCallCount('addCrostiniPortForward')); 0, crostiniBrowserProxy.getCallCount('addCrostiniPortForward'));
portNumberInput.value = '65535'; assertEquals(
errorText.innerText,
loadTimeData.getString('crostiniPortForwardingAddError'));
portNumberInput.value = '5000';
subpage.$$('cr-dialog cr-button[id="continue"]').click(); subpage.$$('cr-dialog cr-button[id="continue"]').click();
assertEquals( assertEquals(
1, crostiniBrowserProxy.getCallCount('addCrostiniPortForward')); 0, crostiniBrowserProxy.getCallCount('addCrostiniPortForward'));
assertEquals(
errorText.innerText,
loadTimeData.getString('crostiniPortForwardingAddExisting'));
}); });
test('AddPortCancel', async function() { test('AddPortCancel', async function() {
...@@ -568,6 +579,49 @@ suite('CrostiniPageTests', function() { ...@@ -568,6 +579,49 @@ suite('CrostiniPageTests', function() {
assertFalse(crToggle.checked); assertFalse(crToggle.checked);
}); });
test('PortPrefsChange', async function() {
setCrostiniPrefs(true, {
forwardedPorts: [
{
port_number: 5000,
protocol_type: 0,
label: 'Label1',
},
{
port_number: 5001,
protocol_type: 0,
label: 'Label1',
},
]
});
assertEquals(
3, subpage.shadowRoot.querySelectorAll('.list-item').length);
setCrostiniPrefs(true, {
forwardedPorts: [
{
port_number: 5000,
protocol_type: 0,
label: 'Label1',
},
{
port_number: 5001,
protocol_type: 0,
label: 'Label1',
},
{
port_number: 5002,
protocol_type: 0,
label: 'Label1',
},
]
});
assertEquals(
4, subpage.shadowRoot.querySelectorAll('.list-item').length);
setCrostiniPrefs(true, {forwardedPorts: []});
assertEquals(
0, subpage.shadowRoot.querySelectorAll('.list-item').length);
});
test('CrostiniStopAndStart', function() { test('CrostiniStopAndStart', function() {
const crToggle = subpage.$$('#toggleActivationButton0'); const crToggle = subpage.$$('#toggleActivationButton0');
assertFalse(crToggle.disabled); assertFalse(crToggle.disabled);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment