Add 'path' query parameter to console access url

Starting in noVNC v1.1.0, the token query parameter is no longer
forwarded via cookie [1]. We must instead use the 'path' query
parameter to pass the token through to the websocketproxy [2].
This means that if someone deploys noVNC v1.1.0, VNC consoles will
break in nova because the code is relying on the cookie functionality
that v1.1.0 removed.

This modifies the ConsoleAuthToken.access_url property to include the
'path' query parameter as part of the returned access_url that the
client will use to call the console proxy service.

This change is backward compatible with noVNC < v1.1.0. The 'path' query
parameter is a long supported feature in noVNC.

Co-Authored-By: melanie witt <melwittt@gmail.com>

Closes-Bug: #1822676

 Conflicts:
	doc/source/admin/remote-console-access.rst
	nova/tests/unit/console/test_websocketproxy.py

NOTE(melwitt): The conflicts are due to the following changes not being
in Rocky:

  I08991796aaced2abc824f608108c0c786181eb65
  I7f5f08691ca3f73073c66c29dddb996fb2c2b266

[1] 51f9f0098d
[2] https://github.com/novnc/noVNC/pull/1220

Change-Id: I2ddf0f4d768b698e980594dd67206464a9cea37b
(cherry picked from commit 9606c80402)
(cherry picked from commit 186aff98b7)
This commit is contained in:
Mohammed Naser 2019-04-02 11:34:58 -04:00 committed by melanie witt
parent 9c6d900486
commit d72f24569e
17 changed files with 53 additions and 36 deletions

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "rdp-html5",
"url": "http://127.0.0.1:6083/?token=191996c3-7b0f-42f3-95a7-f1839f2da6ed"
"url": "http://127.0.0.1:6083/?path=%3Ftoken%3D21efbb20-b84c-4d1f-807d-4e14f6884b7f"
}
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "serial",
"url":"ws://127.0.0.1:6083/?token=f9906a48-b71e-4f18-baca-c987da3ebdb3"
"url": "ws://127.0.0.1:6083/?path=%3Ftoken%3D6ac46b4c-2705-4d8b-baa3-1b6f1b0c7dd3"
}
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "spice-html5",
"url": "http://127.0.0.1:6082/spice_auto.html?token=a30e5d08-6a20-4043-958f-0852440c6af4"
"url": "http://127.0.0.1:6082/spice_auto.html?path=%3Ftoken%3Da7bd9607-421c-44b9-8689-18e87ada2f78"
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "novnc",
"url": "http://127.0.0.1:6080/vnc_auto.html?token=191996c3-7b0f-42f3-95a7-f1839f2da6ed"
"url": "http://127.0.0.1:6080/vnc_auto.html?path=%3Ftoken%3Ddaae261f-474d-4cae-8f6a-1865278ed8c9"
}
}

View File

@ -2,6 +2,6 @@
"remote_console": {
"protocol": "vnc",
"type": "novnc",
"url": "http://example.com:6080/vnc_auto.html?token=b60bcfc3-5fd4-4d21-986c-e83379107819"
"url": "http://example.com:6080/vnc_auto.html?path=%3Ftoken%3Db60bcfc3-5fd4-4d21-986c-e83379107819"
}
}

View File

@ -467,12 +467,12 @@
</g>
<g id="shape53-111" v:mID="53" v:groupContext="shape" transform="translate(38.8888,-154.814)">
<title>Sheet.53</title>
<desc>Browses the url returned Http://novncip:port/?token=xyz</desc>
<desc>Browses the url returned Http://novncip:port/?path=%3Ftoken%3Dxyz</desc>
<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
<v:textRect cx="58.9065" cy="303.793" width="117.82" height="25.387"/>
<rect x="0" y="291.1" width="117.813" height="25.387" class="st6"/>
<text x="4" y="301.39" class="st7" v:langID="1036"><v:paragraph/><v:tabList/>Browses the url returned<v:newlineChar/><tspan
x="4" dy="1.2em" class="st13">Http</tspan>://novncip:port/?token=xyz</text> </g>
x="4" dy="1.2em" class="st13">Http</tspan>://novncip:port/?path=%3Ftoken%3Dxyz</text> </g>
<g id="group28-115" transform="translate(591.296,-147.811)" v:mID="28" v:groupContext="group">
<title>Sheet.28</title>
<g id="shape29-116" v:mID="29" v:groupContext="shape">

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -76,7 +76,7 @@ access their instances through VNC clients.
The VNC console connection works as follows:
#. A user connects to the API and gets an ``access_url`` such as,
``http://ip:port/?token=xyz``.
``http://ip:port/?path=%3Ftoken%3Dxyz``.
#. The user pastes the URL in a browser or uses it as a client parameter.

View File

@ -84,7 +84,7 @@ Example response::
"remote_console": {
"protocol": "vnc",
"type": "novnc",
"url": "http://example.com:6080/vnc_auto.html?token=XYZ"
"url": "http://example.com:6080/vnc_auto.html?path=%3Ftoken%3DXYZ"
}
}

View File

@ -18,6 +18,7 @@ from oslo_log import log as logging
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six.moves.urllib.parse as urlparse
from nova.db import api as db
from nova import exception
@ -60,7 +61,9 @@ class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
specific to this authorization.
"""
if self.obj_attr_is_set('id'):
return '%s?token=%s' % (self.access_url_base, self.token)
qparams = {'path': '?token=%s' % self.token}
return '%s?%s' % (self.access_url_base,
urlparse.urlencode(qparams))
@staticmethod
def _from_db_object(context, obj, db_obj):

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "rdp-html5",
"url": "http://127.0.0.1:6083/?token=%(uuid)s"
"url": "http://127.0.0.1:6083/?path=%%3Ftoken%%3D%(uuid)s"
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "serial",
"url": "ws://127.0.0.1:6083/?token=%(uuid)s"
"url": "ws://127.0.0.1:6083/?path=%%3Ftoken%%3D%(uuid)s"
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "spice-html5",
"url": "http://127.0.0.1:6082/spice_auto.html?token=%(uuid)s"
"url": "http://127.0.0.1:6082/spice_auto.html?path=%%3Ftoken%%3D%(uuid)s"
}
}

View File

@ -1,6 +1,6 @@
{
"console": {
"type": "novnc",
"url": "http://127.0.0.1:6080/vnc_auto.html?token=%(uuid)s"
"url": "http://127.0.0.1:6080/vnc_auto.html?path=%%3Ftoken%%3D%(uuid)s"
}
}

View File

@ -15,6 +15,7 @@
import re
from oslo_serialization import jsonutils
import six.moves.urllib.parse as urlparse
from nova.tests.functional.api_sample_tests import test_servers
@ -32,7 +33,8 @@ class ConsoleAuthTokensSampleJsonTests(test_servers.ServersSampleBase):
{'action': 'os-getRDPConsole'})
url = self._get_console_url(response.content)
return re.match('.+?token=([^&]+)', url).groups()[0]
path = urlparse.urlencode({'path': '?token='})
return re.match('.+?%s([^&]+)' % path, url).groups()[0]
def test_get_console_connect_info(self):
self.flags(enabled=True, group='rdp')

View File

@ -18,6 +18,7 @@ import copy
import socket
import mock
import six.moves.urllib.parse as urlparse
import nova.conf
from nova.console.securityproxy import base
@ -47,6 +48,7 @@ class NovaProxyRequestHandlerDBTestCase(test.TestCase):
self.wh.msg = mock.MagicMock()
self.wh.do_proxy = mock.MagicMock()
self.wh.headers = mock.MagicMock()
self.path = urlparse.urlencode({'path': '?token=123-456-789'})
def _fake_console_db(self, **updates):
console_db = copy.deepcopy(fake_ca.fake_token_dict)
@ -96,7 +98,7 @@ class NovaProxyRequestHandlerDBTestCase(test.TestCase):
tsock.recv.return_value = "HTTP/1.1 200 OK\r\n\r\n"
self.wh.socket.return_value = tsock
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
if instance_not_found:
@ -143,6 +145,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
self.wh.msg = mock.MagicMock()
self.wh.do_proxy = mock.MagicMock()
self.wh.headers = mock.MagicMock()
self.path = urlparse.urlencode({'path': '?token=123-456-789'})
self.path_invalid = urlparse.urlencode({'path': '?token=XXX'})
fake_header = {
'cookie': 'token="123-456-789"',
@ -228,7 +232,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
'access_url': 'https://example.net:6080'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -261,7 +265,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
validate.return_value = objects.ConsoleAuthToken(**params)
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -287,7 +291,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
validate.return_value = objects.ConsoleAuthToken(**params)
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -312,7 +316,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
validate.return_value = objects.ConsoleAuthToken(**params)
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://[2001:db8::1]/?token=123-456-789"
self.wh.path = "http://[2001:db8::1]/?%s" % self.path
self.wh.headers = self.fake_header_ipv6
self.wh.new_websocket_client()
@ -325,7 +329,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
def test_new_websocket_client_token_invalid(self, validate):
validate.side_effect = exception.InvalidToken(token='XXX')
self.wh.path = "http://127.0.0.1/?token=XXX"
self.wh.path = "http://127.0.0.1/?%s" % self.path_invalid
self.wh.headers = self.fake_header_bad_token
self.assertRaises(exception.InvalidToken,
@ -353,7 +357,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
tsock.recv.return_value = "HTTP/1.1 200 OK\r\n\r\n"
self.wh.socket.return_value = tsock
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -386,7 +390,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
tsock.recv.return_value = "HTTP/1.1 500 Internal Server Error\r\n\r\n"
self.wh.socket.return_value = tsock
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.assertRaises(exception.InvalidConnectionInfo,
@ -418,7 +422,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
HTTP_RESP]
self.wh.socket.return_value = tsock
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -448,7 +452,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
validate.return_value = objects.ConsoleAuthToken(**params)
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.path = "http://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.wh.new_websocket_client()
@ -468,7 +472,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "ws://127.0.0.1/?token=123-456-789"
self.wh.path = "ws://127.0.0.1/?%s" % self.path
self.wh.headers = self.fake_header
self.assertRaises(exception.NovaException,
@ -477,10 +481,9 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
@mock.patch('socket.getfqdn')
def test_address_string_doesnt_do_reverse_dns_lookup(self, getfqdn):
request_mock = mock.MagicMock()
request_mock.makefile().readline.side_effect = [
b'GET /vnc.html?token=123-456-789 HTTP/1.1\r\n',
b''
]
msg = 'GET /vnc.html?%s HTTP/1.1\r\n' % self.path
request_mock.makefile().readline.side_effect = [msg.encode('utf-8'),
b'']
server_mock = mock.MagicMock()
client_address = ('8.8.8.8', 54321)
@ -734,7 +737,8 @@ class NovaWebsocketSecurityProxyTestCase(test.NoDBTestCase):
with mock.patch('websockify.ProxyRequestHandler'):
self.wh = websocketproxy.NovaProxyRequestHandler()
self.wh.server = self.server
self.wh.path = "http://127.0.0.1/?token=123-456-789"
path = urlparse.urlencode({'path': '?token=123-456-789'})
self.wh.path = "http://127.0.0.1/?%s" % path
self.wh.socket = mock.MagicMock()
self.wh.msg = mock.MagicMock()
self.wh.do_proxy = mock.MagicMock()

View File

@ -18,6 +18,7 @@ import mock
from oslo_db.exception import DBDuplicateEntry
from oslo_utils import timeutils
import six.moves.urllib.parse as urlparse
from nova import exception
from nova.objects import console_auth_token as token_obj
@ -70,9 +71,9 @@ class _TestConsoleAuthToken(object):
self.compare_obj(obj, expected)
url = obj.access_url
expected_url = '%s?token=%s' % (
fakes.fake_token_dict['access_url_base'],
fakes.fake_token)
path = urlparse.urlencode({'path': '?token=%s' % fakes.fake_token})
expected_url = '%s?%s' % (
fakes.fake_token_dict['access_url_base'], path)
self.assertEqual(expected_url, url)
@mock.patch('nova.db.api.console_auth_token_create')

View File

@ -0,0 +1,7 @@
---
fixes:
- |
Add support for noVNC >= v1.1.0 for VNC consoles. Prior to this fix, VNC
console token validation always failed regardless of actual token validity
with noVNC >= v1.1.0. See
https://bugs.launchpad.net/nova/+bug/1822676 for more details.