cockpit/test/verify/check-connection

505 lines
23 KiB
Python
Executable File

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
import base64
import time
import parent
from testlib import *
class TestConnection(MachineCase):
def setUp(self):
super().setUp()
self.ws_executable = "/usr/libexec/cockpit-ws"
if "debian" in self.machine.image or "ubuntu" in self.machine.image:
self.ws_executable = "/usr/lib/cockpit/cockpit-ws"
def testBasic(self):
b = self.browser
m = self.machine
m.start_cockpit()
# take cockpit-ws down on the login page
b.open("/system")
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
m.stop_cockpit()
b.click('#login-button')
b.wait_text_not('#login-fatal-message', "")
m.start_cockpit()
b.reload()
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
# cookie should not be marked as secure, it's not https
cookie = b.cookie("cockpit")
self.assertTrue(cookie["httpOnly"])
self.assertFalse(cookie["secure"])
# take cockpit-ws down on the server page
m.stop_cockpit()
b.switch_to_top()
b.wait_in_text(".curtains-ct h1", "Disconnected")
m.start_cockpit()
b.click("#machine-reconnect")
b.expect_load()
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
# sever the connection on the login page
m.execute("iptables -w -I INPUT -p tcp --dport 9090 -j REJECT --reject-with tcp-reset")
b.click('#login-button')
with b.wait_timeout(20):
b.wait_text_not('#login-fatal-message', "")
m.execute("iptables -w -D INPUT -p tcp --dport 9090 -j REJECT --reject-with tcp-reset")
b.reload()
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
# sever the connection on the server page
m.execute("iptables -w -I INPUT -p tcp --dport 9090 -j REJECT")
b.switch_to_top()
with b.wait_timeout(60):
b.wait_visible(".curtains-ct")
b.wait_in_text(".curtains-ct h1", "Disconnected")
b.wait_in_text('.curtains-ct p', "Connection has timed out.")
m.execute("iptables -w -D INPUT -p tcp --dport 9090 -j REJECT")
b.click("#machine-reconnect")
b.expect_load()
b.enter_page("/system")
b.logout()
# deleted cookie after logout should not be marked as secure, it's not https
cookie = b.cookie("cockpit")
self.assertEqual(cookie["value"], "deleted")
if m.image != "rhel-7-6-distropkg":
# fixed in PR #10766
self.assertTrue(cookie["httpOnly"])
self.assertFalse(cookie["secure"])
if not m.atomic_image: # cannot write to /usr on Atomics, and cockpit-session is in a container
# damage cockpit-session permissions (Fedora-ish and Debian-ish path), expect generic error message
m.execute("chmod g-x /usr/libexec/cockpit-session 2>/dev/null || chmod g-x /usr/lib/cockpit/cockpit-session")
b.open("/system")
b.wait_in_text('#login-fatal-message', "Internal error in login process")
m.execute("chmod g+x /usr/libexec/cockpit-session 2>/dev/null || chmod g+x /usr/lib/cockpit/cockpit-session")
self.allow_journal_messages(".*cockpit-session: bridge program failed.*")
# pretend cockpit-bridge is not installed, expect specific error message
m.execute("mv /usr/bin/cockpit-bridge /usr/bin/cockpit-bridge.disabled")
b.open("/system")
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
b.click('#login-button')
b.wait_visible('#login-fatal-message')
if m.image not in ["rhel-7-6-distropkg"]:
b.wait_text('#login-fatal-message', "The cockpit package is not installed")
else:
# cockpit-ws < 184 had an unspecific error message
b.wait_in_text('#login-fatal-message', "Internal error")
m.execute("mv /usr/bin/cockpit-bridge.disabled /usr/bin/cockpit-bridge")
# Reauthorization can fail due to disconnects above
self.allow_authorize_journal_messages()
self.allow_restart_journal_messages()
# Lets crash a systemd-crontrolled process and see if we get a proper backtrace in the logs
# This helps with debugging failures in the tests elsewhere
m.execute("systemctl start systemd-hostnamed; pkill -e -SEGV systemd-hostnam")
wait(lambda: m.execute("journalctl -b | grep 'Process.*systemd-hostnam.*of user.*dumped core.'"))
# Make sure the core dumps exist in the directory, so we can download them
cores = m.execute("find /var/lib/systemd/coredump -type f")
self.assertNotEqual(cores, "")
self.allow_core_dumps = True
self.allow_journal_messages(".*org.freedesktop.hostname1.*DBus.Error.NoReply.*")
def testTls(self):
m = self.machine
b = self.browser
# Start Cockpit with TLS
m.start_cockpit(tls=True)
# A normal TLS connection works
output = m.execute('openssl s_client -connect 172.27.0.15:9090 2>&1')
m.message(output)
self.assertIn("DONE", output)
# SSLv3 should not work
output = m.execute('openssl s_client -connect 172.27.0.15:9090 -ssl3 2>&1 || true')
self.assertNotIn("DONE", output)
# Some operating systems fail SSL3 on the server side
self.assertRegex(output, "Secure Renegotiation IS NOT supported|"
"ssl handshake failure|"
"Option unknown option -ssl3|"
"null ssl method passed|"
"wrong version number")
# RC4 should not work
output = m.execute('! openssl s_client -connect 172.27.0.15:9090 -tls1_2 -cipher RC4 2>&1')
self.assertNotIn("DONE", output)
# HACK: (NONE) cipher is a regression in Fedora 29 (https://bugzilla.redhat.com/show_bug.cgi?id=1629732)
self.assertRegex(
output, "no cipher match|no ciphers available|ssl handshake failure|Cipher is \(NONE\)")
# Install a certificate chain, and give it an arbitrary bad file context
m.upload(["verify/files/cert-chain.cert"], "/etc/cockpit/ws-certs.d")
m.execute("! selinuxenabled || chcon --type svirt_sandbox_file_t /etc/cockpit/ws-certs.d/cert-chain.cert")
# This should also reset the file context
m.restart_cockpit()
# Should use the new certificates and entire chain should show up
output = m.execute('openssl s_client -connect 172.27.0.15:9090 2>&1')
self.assertIn("DONE", output)
self.assertRegex(output, "s:/?CN *= *localhost")
self.assertRegex(output, "1 s:/?OU *= *Intermediate")
# login handler: correct password
m.execute("curl -k -c cockpit.jar -s --head --header 'Authorization: Basic {}' https://127.0.0.1:9090/cockpit/login".format(
base64.b64encode(b"admin:foobar").decode(), ))
headers = m.execute("curl -k --head -b cockpit.jar -s https://127.0.0.1:9090/")
self.assertIn(
"default-src 'self' https://127.0.0.1:9090; connect-src 'self' https://127.0.0.1:9090 wss://127.0.0.1:9090", headers)
self.assertIn("Access-Control-Allow-Origin: https://127.0.0.1:9090", headers)
self.allow_journal_messages(
".*Peer failed to perform TLS handshake",
".*Peer sent fatal TLS alert:.*",
".*invalid base64 data in Basic header",
".*Error performing TLS handshake: No supported cipher suites have been found.",
".*Error performing TLS handshake: Could not negotiate a supported cipher suite.")
# check the Debian smoke test
m.upload(["../tools/debian/tests/smoke"], "/tmp")
m.execute("/tmp/smoke")
b.ignore_ssl_certificate_errors(True)
self.login_and_go("/system", tls=True)
cookie = b.cookie("cockpit")
# cookie should be marked as secure
self.assertTrue(cookie["httpOnly"])
self.assertTrue(cookie["secure"])
# same after logout
b.logout()
cookie = b.cookie("cockpit")
self.assertEqual(cookie["value"], "deleted")
if m.image not in ["rhel-7-6-distropkg", "rhel-8-0-distropkg"]:
# fixed in PR #10766
self.assertTrue(cookie["httpOnly"])
# fixed in PR #11279
self.assertTrue(cookie["secure"])
def testConfigOrigins(self):
m = self.machine
m.execute(
'mkdir -p /etc/cockpit/ && echo "[WebService]\nOrigins = http://other-origin:9090 http://localhost:9090" > /etc/cockpit/cockpit.conf')
m.start_cockpit()
output = m.execute('curl -s -f -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Origin: http://other-origin:9090" -H "Host: localhost:9090" -H "Sec-Websocket-Key: 3sc2c9IzwRUc3BlSIYwtSA==" -H "Sec-Websocket-Version: 13" http://localhost:9090/cockpit/socket')
self.assertIn('"no-session"', output)
# The socket should also answer at /socket
output = m.execute('curl -s -f -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Origin: http://other-origin:9090" -H "Host: localhost:9090" -H "Sec-Websocket-Key: 3sc2c9IzwRUc3BlSIYwtSA==" -H "Sec-Websocket-Version: 13" http://localhost:9090/socket')
self.assertIn('"no-session"', output)
self.allow_journal_messages('peer did not close io when expected')
@skipImage("Atomic doesn't use socket", "fedora-atomic", "rhel-atomic", "continuous-atomic")
def testSocket(self):
m = self.machine
if m.image not in ["rhel-7-6-distropkg"]:
self.assertIn("systemctl", m.execute("cat /etc/issue.d/cockpit.issue"))
self.assertIn("systemctl", m.execute("cat /etc/motd.d/cockpit"))
self.assertNotIn("9090", m.execute("cat /etc/motd.d/cockpit"))
m.start_cockpit()
if m.image not in ["rhel-7-6-distropkg"]:
self.assertNotIn("systemctl", m.execute("cat /etc/motd.d/cockpit"))
self.assertIn("9090", m.execute("cat /etc/issue.d/cockpit.issue"))
self.assertIn("9090", m.execute("cat /etc/motd.d/cockpit"))
m.execute("systemctl stop cockpit.socket")
# Change port according to documentation: https://cockpit-project.org/guide/latest/listen.html
m.execute('! selinuxenabled || semanage port -m -t websm_port_t -p tcp 443')
m.execute(
'mkdir -p /etc/systemd/system/cockpit.socket.d/ && printf "[Socket]\nListenStream=\nListenStream=443" > /etc/systemd/system/cockpit.socket.d/listen.conf')
# cockpit-ws from base package
if m.image not in ["rhel-7-6-distropkg"]:
self.assertIn("systemctl", m.execute("cat /etc/issue.d/cockpit.issue"))
self.assertIn("systemctl", m.execute("cat /etc/motd.d/cockpit"))
self.assertNotIn("9090", m.execute("cat /etc/motd.d/cockpit"))
self.assertNotIn("443", m.execute("cat /etc/motd.d/cockpit"))
m.start_cockpit(tls=True)
# cockpit-ws from base package
if m.image not in ["rhel-7-6-distropkg"]:
self.assertNotIn("systemctl", m.execute("cat /etc/motd.d/cockpit"))
self.assertNotIn("9090", m.execute("cat /etc/motd.d/cockpit"))
self.assertIn("443", m.execute("cat /etc/issue.d/cockpit.issue"))
self.assertIn("443", m.execute("cat /etc/motd.d/cockpit"))
output = m.execute('curl -k https://localhost 2>&1 || true')
self.assertIn('Loading...', output)
output = m.execute('curl -k https://localhost:9090 2>&1 || true')
self.assertIn('Connection refused', output)
self.allow_journal_messages(".*Peer failed to perform TLS handshake")
@skipImage("Atomic doesn't have cockpit-ws", "fedora-atomic", "rhel-atomic", "continuous-atomic")
def testCommandline(self):
m = self.machine
m.execute(
'mkdir -p /test/cockpit/ws-certs.d && echo "[WebService]\nLoginTitle = A Custom Title" > /test/cockpit/cockpit.conf')
m.execute('mkdir -p /test/cockpit/static/ && echo "<!DOCTYPE html><html><head></head><body><p>Custom Default Root</p></body></html>" > /test/cockpit/static/login.html')
m.execute("XDG_CONFIG_DIRS=/test XDG_DATA_DIRS=/test remotectl certificate --ensure")
self.assertTrue(m.execute("ls /test/cockpit/ws-certs.d/*"))
self.assertFalse(m.execute("ls /etc/cockpit/ws-certs.d/* || true"))
m.execute("XDG_CONFIG_DIRS=/test XDG_DATA_DIRS=/test {} --port 9000 --address 127.0.0.1 0<&- &>/dev/null &".format(self.ws_executable))
# The port may not be available immediately, so wait for it
wait(lambda: 'A Custom Title' in m.execute('curl -s -k https://localhost:9000/'))
output = m.execute('curl -s -S -k https://172.27.0.15:9000/ 2>&1 || true')
self.assertIn('Connection refused', output)
def testHeadRequest(self):
m = self.machine
m.start_cockpit()
# static handler
headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit/static/login.min.html")
self.assertIn("HTTP/1.1 200 OK\r\n", headers)
self.assertIn("Content-Type: text/html\r\n", headers)
# login.html is not always accessible as a file (e. g. in Atomic), so just assert a reasonable content length
self.assertIn("Content-Length: ", headers)
length = int(headers.split('Content-Length: ', 1)[1].split()[0])
self.assertGreater(length, 10000)
self.assertLess(length, 100000)
# login handler: wrong password
headers = m.execute("curl -s --head --header 'Authorization: Basic {}' http://172.27.0.15:9090/cockpit/login".format(
base64.b64encode(b"admin:hahawrong").decode()))
self.assertIn("HTTP/1.1 401 Authentication failed\r\n", headers)
self.assertNotIn("Set-Cookie:", headers)
# login handler: correct password
headers = m.execute("curl -s --head --header 'Authorization: Basic {}' http://172.27.0.15:9090/cockpit/login".format(
base64.b64encode(b"admin:foobar").decode()))
self.assertIn("HTTP/1.1 200 OK\r\n", headers)
self.assertIn("Set-Cookie: cockpit", headers)
# socket handler; this should refuse HEAD (as it makes little sense on sockets), so 404
headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit/socket")
self.assertIn("HTTP/1.1 404 Not Found\r\n", headers)
# external channel handler; unauthenticated, thus 404
headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit+123/channel/foo")
self.assertIn("HTTP/1.1 404 Not Found\r\n", headers)
def testFlowControl(self):
m = self.machine
b = self.browser
self.login_and_go("/playground/speed", user="root")
# Check the speed playground page
b.switch_to_top()
b.go("/playground/speed")
b.enter_page("/playground/speed")
b.wait_text_not("#pid", "")
pid = b.text("#pid")
b.set_val("#read-path", "/dev/vda")
b.click("#read-sideband")
b.wait_text_not("#speed", "")
time.sleep(20)
output = m.execute("cat /proc/{}/statm".format(pid))
rss = int(output.split(" ")[0])
# This fails when flow control is not present
self.assertLess(rss, 200000)
@skipImage("Atomic doesn't have cockpit-ws installed", "fedora-atomic", "rhel-atomic", "continuous-atomic")
@skipImage("Added in 184", "rhel-7-6-distropkg")
def testLocalSession(self):
m = self.machine
# start ws with --local-session, let it spawn bridge; ensure that this works without /etc/cockpit/
m.spawn("su - -c 'G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 "
"--local-session=cockpit-bridge' admin" % self.ws_executable,
"cockpit-ws-local")
m.wait_for_cockpit_running('127.0.0.90', 9999)
# System frame should work directly, no login page
out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
# shut it down, wait until it is gone
m.execute("pkill -ef cockpit-ws")
# start ws with --local-session and existing running bridge
script = '''#!/bin/bash -eu
coproc env G_MESSAGES_DEBUG=all cockpit-bridge
G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local-session=- <&${COPROC[0]} >&${COPROC[1]}
''' % self.ws_executable
m.execute(["tee", "/tmp/local.sh"], input=script)
m.execute("chmod a+x /tmp/local.sh")
m.spawn("su - -c /tmp/local.sh admin", "local.sh")
m.wait_for_cockpit_running('127.0.0.90', 9999)
# System frame should work directly, no login page
out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
self.allow_journal_messages("couldn't register polkit authentication agent.*")
@skipImage("Atomic doesn't have cockpit-ws installed", "fedora-atomic", "rhel-atomic", "continuous-atomic")
@skipImage("Kernel does not allow user namespaces", "centos-7", "rhel-7-6", "rhel-7-7", "debian-stable", "debian-testing")
@skipImage("Simple paths added in PR #11701", "rhel-7-6-distropkg")
def testCockpitDesktop(self):
cases = [(['/cockpit/@localhost/system/index.html', 'system', 'system/index', 'system/'],
['id="system_machine_id"', 'data-action="shutdown"']
),
(['/cockpit/@localhost/network/firewall.html', 'network/firewall'],
['div id="firewall"', 'script src="firewall.js"']
),
(['/cockpit/@localhost/playground/react-patterns.html', 'playground/react-patterns'],
['script src="react-patterns.js"']
),
]
m = self.machine
if "debian" in m.image or "ubuntu" in m.image:
cockpit_desktop = "/usr/lib/cockpit/cockpit-desktop"
else:
cockpit_desktop = "/usr/libexec/cockpit-desktop"
for (pages, asserts) in cases:
if m.image == "rhel-8-0-distropkg": # rhel-8-0-distropkg can handle only full paths, see #11701
pages = [pages[0]]
for page in pages:
m.execute('''su - -c 'BROWSER="curl --silent --compressed -o /tmp/out.html" %s %s' admin''' %
(cockpit_desktop, page))
out = m.execute("cat /tmp/out.html")
for a in asserts:
self.assertIn(a, out)
# should clean up processes
self.assertEqual(m.execute("! pgrep -a cockpit-ws && ! pgrep -a cockpit-bridge"), "")
self.allow_journal_messages("couldn't register polkit authentication agent.*")
@skipImage("missing socat", "fedora-atomic", "rhel-atomic", "continuous-atomic", "centos-7")
@skipImage("Added in PR #11813", "rhel-7-6-distropkg", "rhel-8-0-distropkg")
def testReverseTlsProxy(self):
m = self.machine
b = self.browser
# set up a poor man's reverse TLS proxy with socat
m.upload(["../src/bridge/mock-server.crt", "../src/bridge/mock-server.key"], "/tmp")
m.spawn("socat OPENSSL-LISTEN:9090,reuseaddr,fork,cert=/tmp/mock-server.crt,"
"key=/tmp/mock-server.key,verify=0 TCP:localhost:9099",
"socat.log")
# ws with plain --no-tls should fail after login with mismatching Origin (expected http, got https)
m.spawn("su -s /bin/sh -c '%s --no-tls -p 9099 -a 127.0.0.1' cockpit-ws" % self.ws_executable,
"ws-notls.log")
m.wait_for_cockpit_running(tls=True)
b.ignore_ssl_certificate_errors(True)
b.open("https://%s:%s/system" % (b.address, b.port))
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
b.click('#login-button')
b.expect_load()
def check_wss_log():
for log in self.browser.get_js_log():
if 'Error during WebSocket handshake: Unexpected response code: 403' in log:
return True
return False
wait(check_wss_log)
wait(lambda: "received request from bad Origin" in m.execute("journalctl -b -t cockpit-ws"))
# sanity check: HTTP does not work
m.execute("! curl http://localhost:9090")
m.execute("pkill -e cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
# this page failure is reeally noisy
self.allow_authorize_journal_messages()
self.allow_journal_messages(".*No authentication agent found.*")
self.allow_journal_messages("couldn't register polkit authentication agent.*")
self.allow_journal_messages("received request from bad Origin.*")
self.allow_journal_messages(".*invalid handshake.*")
self.allow_browser_errors(".*received unsupported version in init message.*")
self.allow_browser_errors(".*received message before init.*")
self.allow_browser_errors("Error reading machine id")
# ws with --for-tls-proxy accepts only https origins, thus should work
m.spawn("su -s /bin/sh -c '%s --for-tls-proxy -p 9099 -a 127.0.0.1' cockpit-ws" % self.ws_executable,
"ws-fortlsproxy.log")
m.wait_for_cockpit_running(tls=True)
b.open("https://%s:%s/system" % (b.address, b.port))
b.wait_visible("#login")
b.set_val("#login-user-input", "admin")
b.set_val("#login-password-input", "foobar")
b.click('#login-button')
b.expect_load()
b.wait_visible('#content')
b.enter_page("/system")
b.logout()
if __name__ == '__main__':
test_main()