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 ...@@ -18,7 +18,7 @@ md5sum = c44a7481bb85e3258128afe3fcf23f44
[template-runner] [template-runner]
filename = instance-runner.cfg filename = instance-runner.cfg
md5sum = 67bfce2889be79f12da13fe9996b4f4b md5sum = 0b71f354260ab48615ed62c1c8ffc9d4
[template-runner-import-script] [template-runner-import-script]
filename = template/runner-import.sh.jinja2 filename = template/runner-import.sh.jinja2
......
...@@ -326,6 +326,27 @@ recipe = slapos.cookbook:dropbear.add_authorized_key ...@@ -326,6 +326,27 @@ recipe = slapos.cookbook:dropbear.add_authorized_key
home = $${buildout:directory} home = $${buildout:directory}
key = $${slap-parameter:user-authorized-key} 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 #-- Set nginx frontend
...@@ -612,6 +633,7 @@ backend-url = $${slaprunner:access-url} ...@@ -612,6 +633,7 @@ backend-url = $${slaprunner:access-url}
init-user = $${runner-htpasswd:user} init-user = $${runner-htpasswd:user}
init-password = $${runner-htpasswd:password} init-password = $${runner-htpasswd:password}
ssh-command = ssh $${user-info:pw-name}@$${slap-network-information:global-ipv6} -p $${runner-sshd-port:port} 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-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/ git-private-url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}/git/
monitor-base-url = $${monitor-publish-parameters:monitor-base-url} monitor-base-url = $${monitor-publish-parameters:monitor-base-url}
......
...@@ -140,6 +140,7 @@ eggs = ...@@ -140,6 +140,7 @@ eggs =
erp5.util erp5.util
lock-file lock-file
plone.recipe.command plone.recipe.command
collective.recipe.shelloutput
slapos.recipe.build slapos.recipe.build
slapos.toolbox[flask_auth] slapos.toolbox[flask_auth]
gunicorn==19.7.1 gunicorn==19.7.1
...@@ -162,6 +163,7 @@ gunicorn = 19.7.1 ...@@ -162,6 +163,7 @@ gunicorn = 19.7.1
prettytable = 0.7.2 prettytable = 0.7.2
pycurl = 7.43.0 pycurl = 7.43.0
slapos.recipe.template = 4.3 slapos.recipe.template = 4.3
collective.recipe.shelloutput = 0.1
collective.recipe.environment = 0.2.0 collective.recipe.environment = 0.2.0
smmap = 0.9.0 smmap = 0.9.0
lockfile = 0.12.2 lockfile = 0.12.2
......
...@@ -47,6 +47,8 @@ setup(name=name, ...@@ -47,6 +47,8 @@ setup(name=name,
'erp5.util', 'erp5.util',
'supervisor', 'supervisor',
'psutil', 'psutil',
'paramiko',
'six',
], ],
zip_safe=True, zip_safe=True,
test_suite='test', test_suite='test',
......
...@@ -26,6 +26,12 @@ ...@@ -26,6 +26,12 @@
############################################################################## ##############################################################################
import os 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.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
...@@ -40,6 +46,82 @@ class SlaprunnerTestCase(SlapOSInstanceTestCase): ...@@ -40,6 +46,82 @@ class SlaprunnerTestCase(SlapOSInstanceTestCase):
__partition_reference__ = 's' __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): class ServicesTestCase(SlaprunnerTestCase):
def test_hashes(self): def test_hashes(self):
hash_files = [ 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