Commit 8b2528b2 authored by Jérome Perrin's avatar Jérome Perrin

slaprunner: publish a ssh:// url

Instead of having to copy and paste the "ssh command", having a
clickable link is more user friendly. This integrates seamlessly with
ChromeOS secure shell app (eventhough the app does not do anything with
fingerprint as per version 0.19)
parent 7ee6c9bd
......@@ -18,7 +18,7 @@ md5sum = c44a7481bb85e3258128afe3fcf23f44
[template-runner]
filename = instance-runner.cfg
md5sum = 67bfce2889be79f12da13fe9996b4f4b
md5sum = 0b71f354260ab48615ed62c1c8ffc9d4
[template-runner-import-script]
filename = template/runner-import.sh.jinja2
......
......@@ -326,6 +326,27 @@ recipe = slapos.cookbook:dropbear.add_authorized_key
home = $${buildout:directory}
key = $${slap-parameter:user-authorized-key}
[runner-sshkeys-publickey-fingerprint-cmd]
recipe = plone.recipe.command
command = $${runner-sshkeys-authority:keygen-binary} -lf $${runner-sshkeys-sshd:public-key} | cut -f 2 -d\ | sed 's/+/%2B/g' | sed 's/\//%2F/g' | sed 's/SHA256://'
[runner-sshkeys-publickey-fingerprint-shelloutput]
recipe = collective.recipe.shelloutput
# XXX because collective.recipe.shelloutput ignore errors, we run the same
# command in a plone.recipe.command so that if fails if something goes wrong.
commands =
fingerprint = $${runner-sshkeys-publickey-fingerprint-cmd:command}
[runner-sshkeys-publickey-fingerprint]
# fingerprint for ssh url, see
# https://tools.ietf.org/id/draft-salowey-secsh-uri-00.html#connparam
# https://winscp.net/eng/docs/session_url#hostkey
# format is host-key-alg-fingerprint, but we know that
# $${runner-sshkeys-sshd:public-key} is rsa so for host-key-alg
# we just use use rsa.
fingerprint = ssh-rsa-$${runner-sshkeys-publickey-fingerprint-shelloutput:fingerprint}
#---------------------------
#--
#-- Set nginx frontend
......@@ -612,6 +633,7 @@ backend-url = $${slaprunner:access-url}
init-user = $${runner-htpasswd:user}
init-password = $${runner-htpasswd:password}
ssh-command = ssh $${user-info:pw-name}@$${slap-network-information:global-ipv6} -p $${runner-sshd-port:port}
ssh-url = ssh://$${user-info:pw-name};fingerprint=$${runner-sshkeys-publickey-fingerprint:fingerprint}@[$${slap-network-information:global-ipv6}]:$${runner-sshd-port:port}
git-public-url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}/git-public/
git-private-url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}/git/
monitor-base-url = $${monitor-publish-parameters:monitor-base-url}
......
......@@ -140,6 +140,7 @@ eggs =
erp5.util
lock-file
plone.recipe.command
collective.recipe.shelloutput
slapos.recipe.build
slapos.toolbox[flask_auth]
gunicorn==19.7.1
......@@ -162,6 +163,7 @@ gunicorn = 19.7.1
prettytable = 0.7.2
pycurl = 7.43.0
slapos.recipe.template = 4.3
collective.recipe.shelloutput = 0.1
collective.recipe.environment = 0.2.0
smmap = 0.9.0
lockfile = 0.12.2
......
......@@ -47,6 +47,8 @@ setup(name=name,
'erp5.util',
'supervisor',
'psutil',
'paramiko',
'six',
],
zip_safe=True,
test_suite='test',
......
......@@ -26,6 +26,12 @@
##############################################################################
import os
import paramiko
import contextlib
import base64
import hashlib
from six.moves.urllib.parse import urlparse
from six.moves.urllib.parse import quote
from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
......@@ -40,6 +46,82 @@ class SlaprunnerTestCase(SlapOSInstanceTestCase):
__partition_reference__ = 's'
class TestSSH(SlaprunnerTestCase):
@classmethod
def getInstanceParameterDict(cls):
cls.ssh_key = paramiko.RSAKey.generate(1024)
return {
'user-authorized-key': 'ssh-rsa {}'.format(cls.ssh_key.get_base64())
}
def test_connect(self):
parameter_dict = self.computer_partition.getConnectionParameterDict()
ssh_url = parameter_dict['ssh-url']
parsed = urlparse(ssh_url)
self.assertEqual('ssh', parsed.scheme)
# username contain a fingerprint (only, so we simplify the parsing)
#
# relevant parts of hte grammar defined in
# https://tools.ietf.org/id/draft-salowey-secsh-uri-00.html
#
# ssh-info = [ userinfo ] [";" c-param *("," c-param)]
# c-param = paramname "=" paramvalue
ssh_info = parsed.username
username, fingerprint_from_url = ssh_info.split(';fingerprint=')
client = paramiko.SSHClient()
self.assertTrue(fingerprint_from_url.startswith('ssh-rsa-'), '')
fingerprint_from_url = fingerprint_from_url[len('ssh-rsa-'):]
class KeyPolicy(object):
"""Accept server key and keep it in self.key for inspection
"""
def missing_host_key(self, client, hostname, key):
self.key = key
key_policy = KeyPolicy()
client.set_missing_host_key_policy(key_policy)
with contextlib.closing(client):
client.connect(
username=username,
hostname=parsed.hostname,
port=parsed.port,
pkey=self.ssh_key,
)
# Check fingerprint from server matches the published one.
# Paramiko does not allow to get the fingerprint as SHA256 easily yet
# https://github.com/paramiko/paramiko/pull/1103
self.assertEqual(
fingerprint_from_url,
quote(
# base64 encoded fingerprint adds an extra = at the end
base64.b64encode(
hashlib.sha256(key_policy.key.asbytes()).digest())[:-1],
# also encode /
safe=''))
# Check shell is usable
channel = client.invoke_shell()
channel.settimeout(30)
received = ''
while True:
r = channel.recv(1024)
self.logger.debug("received >%s<", r)
if not r:
break
received += r
if 'slaprunner shell' in received:
break
self.assertIn("Welcome to SlapOS slaprunner shell", received)
# simple commands can also be executed ( this would be like `ssh bash -c 'cd; pwd'` )
self.assertEqual(
self.computer_partition_root_path,
client.exec_command("pwd")[1].read(1000).strip())
class ServicesTestCase(SlaprunnerTestCase):
def test_hashes(self):
hash_files = [
......
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