RAHHH
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"conch tests"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,671 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_keys -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# pylint: disable=I0011,C0103,W9401,W9402
|
||||
|
||||
"""
|
||||
Data used by test_keys as well as others.
|
||||
"""
|
||||
|
||||
|
||||
from base64 import decodebytes
|
||||
|
||||
RSAData = {
|
||||
"n": int(
|
||||
"269413617238113438198661010376758399219880277968382122687862697"
|
||||
"296942471209955603071120391975773283844560230371884389952067978"
|
||||
"789684135947515341209478065209455427327369102356204259106807047"
|
||||
"964139525310539133073743116175821417513079706301100600025815509"
|
||||
"786721808719302671068052414466483676821987505720384645561708425"
|
||||
"794379383191274856941628512616355437197560712892001107828247792"
|
||||
"561858327085521991407807015047750218508971611590850575870321007"
|
||||
"991909043252470730134547038841839367764074379439843108550888709"
|
||||
"430958143271417044750314742880542002948053835745429446485015316"
|
||||
"60749404403945254975473896534482849256068133525751"
|
||||
),
|
||||
"e": 65537,
|
||||
"d": int(
|
||||
"420335724286999695680502438485489819800002417295071059780489811"
|
||||
"840828351636754206234982682752076205397047218449504537476523960"
|
||||
"987613148307573487322720481066677105211155388802079519869249746"
|
||||
"774085882219244493290663802569201213676433159425782937159766786"
|
||||
"329742053214957933941260042101377175565683849732354700525628975"
|
||||
"239000548651346620826136200952740446562751690924335365940810658"
|
||||
"931238410612521441739702170503547025018016868116037053013935451"
|
||||
"477930426013703886193016416453215950072147440344656137718959053"
|
||||
"897268663969428680144841987624962928576808352739627262941675617"
|
||||
"7724661940425316604626522633351193810751757014073"
|
||||
),
|
||||
"p": int(
|
||||
"152689878451107675391723141129365667732639179427453246378763774"
|
||||
"448531436802867910180261906924087589684175595016060014593521649"
|
||||
"964959248408388984465569934780790357826811592229318702991401054"
|
||||
"226302790395714901636384511513449977061729214247279176398290513"
|
||||
"085108930550446985490864812445551198848562639933888780317"
|
||||
),
|
||||
"q": int(
|
||||
"176444974592327996338888725079951900172097062203378367409936859"
|
||||
"072670162290963119826394224277287608693818012745872307600855894"
|
||||
"647300295516866118620024751601329775653542084052616260193174546"
|
||||
"400544176890518564317596334518015173606460860373958663673307503"
|
||||
"231977779632583864454001476729233959405710696795574874403"
|
||||
),
|
||||
"u": int(
|
||||
"936018002388095842969518498561007090965136403384715613439364803"
|
||||
"229386793506402222847415019772053080458257034241832795210460612"
|
||||
"924445085372678524176842007912276654532773301546269997020970818"
|
||||
"155956828553418266110329867222673040098885651348225673298948529"
|
||||
"93885224775891490070400861134282266967852120152546563278"
|
||||
),
|
||||
}
|
||||
|
||||
DSAData = {
|
||||
"g": int(
|
||||
"10253261326864117157640690761723586967382334319435778695"
|
||||
"29171533815411392477819921538350732400350395446211982054"
|
||||
"96512489289702949127531056893725702005035043292195216541"
|
||||
"11525058911428414042792836395195432445511200566318251789"
|
||||
"10575695836669396181746841141924498545494149998282951407"
|
||||
"18645344764026044855941864175"
|
||||
),
|
||||
"p": int(
|
||||
"10292031726231756443208850082191198787792966516790381991"
|
||||
"77502076899763751166291092085666022362525614129374702633"
|
||||
"26262930887668422949051881895212412718444016917144560705"
|
||||
"45675251775747156453237145919794089496168502517202869160"
|
||||
"78674893099371444940800865897607102159386345313384716752"
|
||||
"18590012064772045092956919481"
|
||||
),
|
||||
"q": 1393384845225358996250882900535419012502712821577,
|
||||
"x": 1220877188542930584999385210465204342686893855021,
|
||||
"y": int(
|
||||
"14604423062661947579790240720337570315008549983452208015"
|
||||
"39426429789435409684914513123700756086453120500041882809"
|
||||
"10283610277194188071619191739512379408443695946763554493"
|
||||
"86398594314468629823767964702559709430618263927529765769"
|
||||
"10270265745700231533660131769648708944711006508965764877"
|
||||
"684264272082256183140297951"
|
||||
),
|
||||
}
|
||||
|
||||
ECDatanistp256 = {
|
||||
"x": int(
|
||||
"762825130203920963171185031449647317742997734817505505433829043"
|
||||
"45687059013883"
|
||||
),
|
||||
"y": int(
|
||||
"815431978646028526322656647694416475343443758943143196810611371"
|
||||
"59310646683104"
|
||||
),
|
||||
"privateValue": int(
|
||||
"3463874347721034170096400845565569825355565567882605"
|
||||
"9678074967909361042656500"
|
||||
),
|
||||
"curve": b"ecdsa-sha2-nistp256",
|
||||
}
|
||||
|
||||
SKECDatanistp256 = {
|
||||
"x": int(
|
||||
"239399367768747020111880335553299826848360860410053166887934464"
|
||||
"83115637049597"
|
||||
),
|
||||
"y": int(
|
||||
"114119006635761413192818806701564910719235784173643448381780025"
|
||||
"223832906554748"
|
||||
),
|
||||
"curve": b"sk-ecdsa-sha2-nistp256@openssh.com",
|
||||
}
|
||||
|
||||
ECDatanistp384 = {
|
||||
"privateValue": int(
|
||||
"280814107134858470598753916394807521398239633534281"
|
||||
"633982576099083357871098966021020900021966162732114"
|
||||
"95718603965098"
|
||||
),
|
||||
"x": int(
|
||||
"10036914308591746758780165503819213553101287571902957054148542"
|
||||
"504671046744460374996612408381962208627004841444205030"
|
||||
),
|
||||
"y": int(
|
||||
"17337335659928075994560513699823544906448896792102247714689323"
|
||||
"575406618073069185107088229463828921069465902299522926"
|
||||
),
|
||||
"curve": b"ecdsa-sha2-nistp384",
|
||||
}
|
||||
|
||||
ECDatanistp521 = {
|
||||
"x": int(
|
||||
"12944742826257420846659527752683763193401384271391513286022917"
|
||||
"29910013082920512632908350502247952686156279140016049549948975"
|
||||
"670668730618745449113644014505462"
|
||||
),
|
||||
"y": int(
|
||||
"10784108810271976186737587749436295782985563640368689081052886"
|
||||
"16296815984553198866894145509329328086635278430266482551941240"
|
||||
"591605833440825557820439734509311"
|
||||
),
|
||||
"privateValue": int(
|
||||
"662751235215460886290293902658128847495347691199214"
|
||||
"706697089140769672273950767961331442265530524063943"
|
||||
"548846724348048614239791498442599782310681891569896"
|
||||
"0565"
|
||||
),
|
||||
"curve": b"ecdsa-sha2-nistp521",
|
||||
}
|
||||
|
||||
Ed25519Data = {
|
||||
"a": (
|
||||
b"\xf1\x16\xd1\x15J\x1e\x15\x0e\x19^\x19F\xb5\xf2D\r\xb2R\xa0\xae*k"
|
||||
b"#\x13sE\xfd@\xd9W{\x8b"
|
||||
),
|
||||
"k": (
|
||||
b"7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U"
|
||||
b"\xbb\xc9DX"
|
||||
),
|
||||
}
|
||||
|
||||
SKEd25519Data = {
|
||||
"a": (
|
||||
b"\x08}'U\xd2i\x04\x11\xea\x01~+\x165iRM\xdd\xe6R\x7f\xd3\xaf\\\xa8p"
|
||||
b"\xa0LL\xe5\x8a\xa0"
|
||||
),
|
||||
"k": (
|
||||
b"7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U"
|
||||
b"\xbb\xc9DX"
|
||||
),
|
||||
}
|
||||
|
||||
privateECDSA_openssh521 = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MIHcAgEBBEIAjn0lSVF6QweS4bjOGP9RHwqxUiTastSE0MVuLtFvkxygZqQ712oZ
|
||||
ewMvqKkxthMQgxzSpGtRBcmkL7RqZ94+18qgBwYFK4EEACOhgYkDgYYABAFpX/6B
|
||||
mxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB
|
||||
j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8X
|
||||
f09ETdku/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw==
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh521_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaV/+gZscYJcA/laRL8NIXMsVcy8T
|
||||
ZzBs8WRhe8ZjY/J5RezPk0HLXZUdl7/yfgICHlRpgY9qfOG5FfYwc61DSRAArCY7bWcPDW
|
||||
1K4cY5C0/96zDbbsIxOLy42tD+wzExNBFfF39PRE3ZLv8/9bTkkq0r0cJlHDPZ0FCbRxwG
|
||||
Za+UNH8AAAEAeRISlnkSEpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||
AAAIUEAWlf/oGbHGCXAP5WkS/DSFzLFXMvE2cwbPFkYXvGY2PyeUXsz5NBy12VHZe/8n4C
|
||||
Ah5UaYGPanzhuRX2MHOtQ0kQAKwmO21nDw1tSuHGOQtP/esw227CMTi8uNrQ/sMxMTQRXx
|
||||
d/T0RN2S7/P/W05JKtK9HCZRwz2dBQm0ccBmWvlDR/AAAAQgCOfSVJUXpDB5LhuM4Y/1Ef
|
||||
CrFSJNqy1ITQxW4u0W+THKBmpDvXahl7Ay+oqTG2ExCDHNKka1EFyaQvtGpn3j7XygAAAA
|
||||
ABAg==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
publicECDSA_openssh521 = (
|
||||
b"ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACF"
|
||||
b"BAFpX/6BmxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB"
|
||||
b"j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8Xf09ETdku"
|
||||
b"/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== comment"
|
||||
)
|
||||
|
||||
privateECDSA_openssh384 = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDAtAi7I8j73WCX20qUM5hhHwHuFzYWYYILs2Sh8UZ+awNkARZ/Fu2LU
|
||||
LLl5RtOQpbWgBwYFK4EEACKhZANiAATU17sA9P5FRwSknKcFsjjsk0+E3CeXPYX0
|
||||
Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCW
|
||||
G0RqpQ+np31aKmeJshkcYALEchnU+tQ=
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh384_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTU17sA9P5FRwSknKcFsjjsk0+E3CeX
|
||||
PYX0Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCWG0
|
||||
RqpQ+np31aKmeJshkcYALEchnU+tQAAADIiktpWIpLaVgAAAATZWNkc2Etc2hhMi1uaXN0
|
||||
cDM4NAAAAAhuaXN0cDM4NAAAAGEE1Ne7APT+RUcEpJynBbI47JNPhNwnlz2F9E5PzNBytz
|
||||
6VkFoKzvCXURz/XhRPTv9tz/AbpsBbexfwLz5Qs1Xd+VX62hU9KOHAlhtEaqUPp6d9Wipn
|
||||
ibIZHGACxHIZ1PrUAAAAMC0CLsjyPvdYJfbSpQzmGEfAe4XNhZhgguzZKHxRn5rA2QBFn8
|
||||
W7YtQsuXlG05CltQAAAAA=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
publicECDSA_openssh384 = (
|
||||
b"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABh"
|
||||
b"BNTXuwD0/kVHBKScpwWyOOyTT4TcJ5c9hfROT8zQcrc+lZBaCs7wl1Ec/14UT07/bc/wG6bA"
|
||||
b"W3sX8C8+ULNV3flV+toVPSjhwJYbRGqlD6enfVoqZ4myGRxgAsRyGdT61A== comment"
|
||||
)
|
||||
|
||||
publicECDSA_openssh = (
|
||||
b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABB"
|
||||
b"BKimX1DZ7+Qj0SpfePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBY"
|
||||
b"gkN/34n42F4vpeA= comment"
|
||||
)
|
||||
|
||||
publicSKECDSA_openssh = (
|
||||
b"sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3"
|
||||
b"BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDTthidmBSzlQiO8aZPfLmUDOS2TSRevW8IrHPK"
|
||||
b"IhYj9/E0RnTyvPIB1eWQx4rQl5iO1mihuBz+u4LkjwVEU3XwAAAAUc3NoOmVjZHNhLWZpZG8y"
|
||||
b"LXRlc3Q= comment"
|
||||
)
|
||||
|
||||
publicSKEd25519_openssh = (
|
||||
b"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA"
|
||||
b"h9J1XSaQQR6gF+KxY1aVJN3eZSf9OvXKhwoExM5YqgAAAABHNzaDo= comment"
|
||||
)
|
||||
|
||||
publicSKECDSA_cert_openssh = (
|
||||
b"sk-ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3"
|
||||
b"BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBDTthidmBSzlQiO8aZPfLmUDOS2TSRevW8IrHPK"
|
||||
b"IhYj9/E0RnTyvPIB1eWQx4rQl5iO1mihuBz+u4LkjwVEU3XwAAAAUc3NoOmVjZHNhLWZpZG8y"
|
||||
b"LXRlc3Q= comment"
|
||||
)
|
||||
|
||||
publicSKEd25519_cert_openssh = (
|
||||
b"sk-ssh-ed25519-cert-v01@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIA"
|
||||
b"h9J1XSaQQR6gF+KxY1aVJN3eZSf9OvXKhwoExM5YqgAAAABHNzaDo= comment"
|
||||
)
|
||||
|
||||
privateECDSA_openssh = b"""-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIEyU1YOT2JxxofwbJXIjGftdNcJK55aQdNrhIt2xYQz0oAoGCCqGSM49
|
||||
AwEHoUQDQgAEqKZfUNnv5CPRKl948xujWlvrIaQBvmXt24LWXznnIPu0R9B+qTtt
|
||||
zu/jpZ7WEszLPo5tQFiCQ3/fifjYXi+l4A==
|
||||
-----END EC PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateECDSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSopl9Q2e/kI9EqX3jzG6NaW+shpAG+
|
||||
Ze3bgtZfOecg+7RH0H6pO23O7+OlntYSzMs+jm1AWIJDf9+J+NheL6XgAAAAmCKU4hcilO
|
||||
IXAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKimX1DZ7+Qj0Spf
|
||||
ePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBYgkN/34n42F4vpe
|
||||
AAAAAgTJTVg5PYnHGh/BslciMZ+101wkrnlpB02uEi3bFhDPQAAAAA
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
publicEd25519_openssh = (
|
||||
b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPEW0RVKHhUOGV4ZRrXyRA2yUqCuKmsjE3NF"
|
||||
b"/UDZV3uL comment"
|
||||
)
|
||||
|
||||
# OpenSSH has only ever supported the "new" (v1) format for Ed25519.
|
||||
privateEd25519_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iwAAAJA61eMLOtXj
|
||||
CwAAAAtzc2gtZWQyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iw
|
||||
AAAEA3LyXajdSomnh8YfCYAcb0Xm1nBWkxN0xpDQVVu8lEWPEW0RVKHhUOGV4ZRrXyRA2y
|
||||
UqCuKmsjE3NF/UDZV3uLAAAAB2NvbW1lbnQBAgMEBQY=
|
||||
-----END OPENSSH PRIVATE KEY-----"""
|
||||
|
||||
publicRSA_openssh = (
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bWG+wloVDEd2NQhEUBVUIUKirg"
|
||||
b"0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgYwBGTJAkMgUyP"
|
||||
b"95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm/9NNN9b0b/h9qp"
|
||||
b"KSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6RKXCpCnd1bqcPUWz"
|
||||
b"xiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1"
|
||||
b"foNfICZgptyti8ZseZj3 comment"
|
||||
)
|
||||
|
||||
privateRSA_openssh = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkG
|
||||
XoRVdV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMW
|
||||
aqQE6Ul3w+RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4fa
|
||||
qSknqv+t9YXmPhq4eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp
|
||||
3dW6nD1Fs8YsGGTVuj3fq3/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28
|
||||
OyluAgtX33ToE7Q3NX6DXyAmYKbcrYvGbHmY9wIDAQABAoIBACFMCGaiKNW0+44P
|
||||
chuFCQC58k438BxXS+NRf54jp+Q6mFUb6ot6mB682Lqx+YkSGGCs6MwLTglaQGq6
|
||||
L5n4syRghLnOaZWa+eL8H1FNJxXbKyet77RprL59EOuGR3BztACHlRU7N/nnFOeA
|
||||
u2geG+bdu3NjuWfmsid/z88wm8KY/dkYNi82LvE9gXqf4QMtR9s0UWI53U/prKiL
|
||||
2dbzhMQXuXGdBghCeE27xSr0w1jNVSvtvjNfBOp75gQkY/It1z0bbNWcY0MvkoiN
|
||||
Pm7aGDfYDyVniR25RjReyc7Ei+2SWjMHD9+GCPmS6dvrOAg2yc3NCgFIWzk+esrG
|
||||
gKnc1DkCgYEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx
|
||||
dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6D
|
||||
MaIVokQ9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0CgYEA+0QX
|
||||
i6Q2vh43Haf2YWwExKrdeD4HjB4zAq4DFIeDeuWefQhnqPKqvxJwz3Kpp8cLHYjV
|
||||
IP2cY8pHMFVOi8TP9H8WpJISdKEJwsRunIwz76Xl9+ArrU9cEaoahDdb/Xrqw818
|
||||
sMjkH1Rjtcev3/QJp/zHJfxc6ZHXksWYHlbTsSMCgYBRr+mSn5QLSoRlPpSzO5IQ
|
||||
tXS4jMnvyQ4BMvovaBKhAyauz1FoFEwmmyikAjMIX+GncJgBNHleUo7Ezza8H0tV
|
||||
rOvBU4TH4WGoStSi/0ANgB8SqVDAKhh1lAwGmxZQqEvsQc177/dLyXUCaMSYuIaI
|
||||
GFpD5wIzlyJkk4MMRSp87QKBgGlmN8ZA3SHFBPOwuD5HlHx2/C3rPzk8lcNDAVHE
|
||||
Qpfz6Bakxu7s1EkQUDgE7jvN19DMzDJpkAegG1qf/jHNHjp+cR4ZlBpOTwzfX1LV
|
||||
0Rdu7NectlWd244hX7wkiLb8r6vw76QssNyfhrADEriL4t0PwO4jPUpQ/i+4KUZY
|
||||
v7YnAoGAZhb5IDTQVCW8YTGsgvvvnDUefkpVAmiVDQqTvh6/4UD6kKdUcDHpePzg
|
||||
Zrcid5rr3dXSMEbK4tdeQZvPtUg1Uaol3N7bNClIIdvWdPx+5S9T95wJcLnkoHam
|
||||
rXp0IjScTxfLP+Cq5V6lJ94/pX8Ppoj1FdZfNxeS4NYFSRA7kvY=
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
# New format introduced in OpenSSH 6.5
|
||||
privateRSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkGXoRV
|
||||
dV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMWaqQE6Ul3w+
|
||||
RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4faqSknqv+t9YXmPhq4
|
||||
eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp3dW6nD1Fs8YsGGTVuj3fq3
|
||||
/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28OyluAgtX33ToE7Q3NX6DXyAmYKbc
|
||||
rYvGbHmY9wAAA7gXkBoMF5AaDAAAAAdzc2gtcnNhAAABAQDVaqx4I9bWG+wloVDEd2NQhE
|
||||
UBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgY
|
||||
wBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm
|
||||
/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6
|
||||
RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvb
|
||||
w7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAAAAwEAAQAAAQAhTAhmoijVtPuOD3Ib
|
||||
hQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z+LMkYI
|
||||
S5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln
|
||||
5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8
|
||||
Uq9MNYzVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvt
|
||||
klozBw/fhgj5kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AAAAgQCFSxt6mxIQN54frV7a/s
|
||||
aW/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3R
|
||||
TFy8PdImJv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnR
|
||||
zrg0x4PXlUzgAAAIEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx
|
||||
dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6DMaIVok
|
||||
Q9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0AAACBAPtEF4ukNr4eNx2n
|
||||
9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/
|
||||
R/FqSSEnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8
|
||||
XOmR15LFmB5W07EjAAAAAAEC
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
# Encrypted with the passphrase 'encrypted'
|
||||
privateRSA_openssh_encrypted = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF
|
||||
|
||||
p2A1YsHLXkpMVcsEqhh/nCYb5AqL0uMzfEIqc8hpZ/Ub8PtLsypilMkqzYTnZIGS
|
||||
ouyPjU/WgtR4VaDnutPWdgYaKdixSEmGhKghCtXFySZqCTJ4O8NCczsktYjUK3D4
|
||||
Jtl90zL6O81WBY6xP76PBQo9lrI/heAetATeyqutc18bwQIGU+gKk32qvfo15DfS
|
||||
VYiY0Ds4D7F7fd9pz+f5+UbFUCgU+tfDvBrqodYrUgmH7jKoW/CRDCHHyeEIZDbF
|
||||
mcMwdcKOyw1sRLaPdihRSVx3kOMvIotHKVTkIDMp+0RTNeXzQnp5U2qzsxzTcG/M
|
||||
UyJN38XXkuvq5VMj2zmmjHzx34w3NK3ZxpZcoaFUqUBlNp2C8hkCLrAa/DWobKqN
|
||||
5xA1ElrQvli9XXkT/RIuy4Gc10bbGEoJjuxNRibtSxxWd5Bd1E40ocOd4l1ebI8+
|
||||
w69XvMTnsmHvkBEADGF2zfRszKnMelg+W5NER1UDuNT03i+1cuhp+2AZg8z7niTO
|
||||
M17XP3ScGVxrQAEYgtxPrPeIpFJvOx2j5Yt78U9Y2WlaAG6DrubbYv2RsMIibhOG
|
||||
yk139vMdD8FwCey6yMkkhFAJwnBtC22MAWgjmC5c6AF3SRQSjjQXepPsJcLgpOjy
|
||||
YwjhnL8w56x9kVDUNPw9A9Cqgxo2sty34ATnKrh4h59PsP83LOL6OC5WjbASgZRd
|
||||
OIBD8RloQPISo+RUF7X0i4kdaHVNPlR0KyapR+3M5BwhQuvEO99IArDV2LNKGzfc
|
||||
W4ssugm8iyAJlmwmb2yRXIDHXabInWY7XCdGk8J2qPFbDTvnPbiagJBimjVjgpWw
|
||||
tV3sVlJYqmOqmCDP78J6he04l0vaHtiOWTDEmNCrK7oFMXIIp3XWjOZGPSOJFdPs
|
||||
6Go3YB+EGWfOQxqkFM28gcqmYfVPF2sa1FbZLz0ffO11Ma/rliZxZu7WdrAXe/tc
|
||||
BgIQ8etp2PwAK4jCwwVwjIO8FzqQGpS23Y9NY3rfi97ckgYXKESFtXPsMMA+drZd
|
||||
ThbXvccfh4EPmaqQXKf4WghHiVJ+/yuY1kUIDEl/O0jRZWT7STgBim/Aha1m6qRs
|
||||
zl1H7hkDbU4solb1GM5oPzbgGTzyBc+z0XxM9iFRM+fMzPB8+yYHTr4kPbVmKBjy
|
||||
SCovjQQVsHE4YeUGTq6k/NF5cVIRKTW/RlHvzxsky1Zj31MC736jrxGw4KG7VSLZ
|
||||
fP6F5jj+mXwS7m0v5to42JBZmRJdKUD88QaGE3ncyQ4yleW5bn9Lf9SuzQg1Dhao
|
||||
3rSA1RuexsHlIAHvGxx/17X+pyygl8DJbt6TBfbLQk9wc707DJTfh5M/bnk9wwIX
|
||||
l/Hsa1WtylAMW/2MzgiVy83MbYz4+Ss6GQ5W66okWji+NxrnrYEy6q+WgVQanp7X
|
||||
D+D7oKykqE1Cdvvulvtfl5fh8wlAs8mrUnKPBBUru348u++2lfacLkxRXyT1ooqY
|
||||
uSNE5nlwFt08N2Ou/bl7yq6QNRMYrRkn+UEfHWCNYDoGMHln2/i6Z1RapQzNarik
|
||||
tJf7radBz5nBwBjP08YAEACNSQvpsUgdqiuYjLwX7efFXQva2RzqaQ==
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
# Encrypted with the passphrase 'encrypted', and using the new format
|
||||
# introduced in OpenSSH 6.5
|
||||
privateRSA_openssh_encrypted_new = b"""-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD0f9WAof
|
||||
DTbmwztb8pdrSeAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bW
|
||||
G+wloVDEd2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n
|
||||
3WmM06QHjVyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9T
|
||||
MA2l5bs9auIJNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQN
|
||||
UZdy03w17snaY6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASf
|
||||
NaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAADwPQaac8s1xX3af
|
||||
hQTQexj0vEAWDQsLYzDHN9G7W+UP5WHUu7igeu2GqAC/TOnjUXDP73I+EN3n7T3JFeDRfs
|
||||
U1Z6Zqb0NKHSRVYwDIdIi8qVohFv85g6+xQ01OpaoOzz+vI34OUvCRHQGTgR6L9fQShZyC
|
||||
McopYMYfbIse6KcqkfxX3KSdG1Pao6Njx/ShFRbgvmALpR/z0EaGCzHCDxpfUyAdnxm621
|
||||
Jzaf+LverWdN7sfrfMptaS9//9iJb70sL67K+YIB64qhDnA/w9UOQfXGQFL+AEtdM0BPv8
|
||||
thP1bs7T0yucBl+ZXdrDKVLZfaS3S/w85Jlgfu+a1DG73pOBOuag435iEJ9EnspjXiiydx
|
||||
GrfSRk2C+/c4fBDZVGFscK5bfQuUUZyU1qOagekxX7WLHFKk9xajnud+nrAN070SeNwlX8
|
||||
FZ2CI4KGlQfDvVUpKanYn8Kkj3fZ+YBGyx4M+19clF65FKSM0x1Rrh5tAmNT/SNDbSc28m
|
||||
ASxrBhztzxUFTrIn3tp+uqkJniFLmFsUtiAUmj8fNyE9blykU7dqq+CqpLA872nQ9bOHHA
|
||||
JsS1oBYmQ0n6AJz8WrYMdcepqWVld6Q8QSD1zdrY/sAWUovuBA1s4oIEXZhpXSS4ZJiMfh
|
||||
PVktKBwj5bmoG/mmwYLbo0JHntK8N3TGTzTGLq5TpSBBdVvWSWo7tnfEkrFObmhi1uJSrQ
|
||||
3zfPVP6BguboxBv+oxhaUBK8UOANe6ZwM4vfiu+QN+sZqWymHIfAktz7eWzwlToe4cKpdG
|
||||
Uv+e3/7Lo2dyMl3nke5HsSUrlsMGPREuGkBih8+o85ii6D+cuCiVtus3f5c78Cir80zLIr
|
||||
Z0wWvEAjciEvml00DWaA+JIaOrWwvXySaOzFGpCqC9SQjao379bvn9P3b7kVZsy6zBfHqm
|
||||
bNEJUOuhBZaY8Okz36chh1xqh4sz7m3nsZ3GYGcvM+3mvRY72QnqsQEG0Sp1XYIn2bHa29
|
||||
tqp7CG9X8J6dqMcPeoPRDWIX9gw7EPl/M0LP6xgewGJ9bgxwle6Mnr9kNITIswjAJqrLec
|
||||
zx7dfixjAPc42ADqrw/tEdFQcSqxigcfJNKO1LbDBjh+Hk/cSBou2PoxbIcl0qfQfbGcqI
|
||||
Dbpd695IEuiW9pYR22txNoIi+7cbMsuFHxQ/OqbrX/jCsprGNNJLAjgGsVEI1JnHWDH0db
|
||||
3UbqbOHAeY3ufoYXNY1utVOIACpW3r9wBw3FjRi04d70VcKr16OXvOAHGN2G++Y+kMya84
|
||||
Hl/Kt/gA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
# Encrypted with the passphrase 'testxp'. NB: this key was generated by
|
||||
# OpenSSH, so it doesn't use the same key data as the other keys here.
|
||||
privateRSA_openssh_encrypted_aes = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,0673309A6ACCAB4B77DEE1C1E536AC26
|
||||
|
||||
4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n
|
||||
T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H
|
||||
g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB
|
||||
sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5
|
||||
9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV
|
||||
gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW
|
||||
0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE
|
||||
vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS
|
||||
hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk
|
||||
2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf
|
||||
qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk
|
||||
4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY
|
||||
EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n
|
||||
8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0
|
||||
fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P
|
||||
V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+
|
||||
0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5
|
||||
xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI
|
||||
dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup
|
||||
VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk
|
||||
gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c
|
||||
8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw
|
||||
SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
|
||||
CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
|
||||
xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
publicRSA_lsh = (
|
||||
b"{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD"
|
||||
b"Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj"
|
||||
b"VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI"
|
||||
b"JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY"
|
||||
b"6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw"
|
||||
b"7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}"
|
||||
)
|
||||
|
||||
privateRSA_lsh = (
|
||||
b"(11:private-key(9:rsa-pkcs1(1:n257:\x00\xd5j\xacx#\xd6\xd6\x1b\xec%\xa1P"
|
||||
b"\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ\xfa9\x06^\x84Uu_"
|
||||
b"\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98\xcd:@x\xd5\xca"
|
||||
b"\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j\xa4\x04\xe9Iw"
|
||||
b"\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e\xefS0\r\xa5\xe5"
|
||||
b"\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad\xf5\x85\xe6>"
|
||||
b"\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?\x91I\x96@\xd5"
|
||||
b"\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E\xb3\xc6,\x18d"
|
||||
b"\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b\x0b\xa4*nC\xc3"
|
||||
b"\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdft\xe8\x13\xb475"
|
||||
b"~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7)(1:e3:\x01\x00\x01)(1:d256:!L"
|
||||
b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q"
|
||||
b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12"
|
||||
b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9"
|
||||
b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00"
|
||||
b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6"
|
||||
b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G"
|
||||
b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx"
|
||||
b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl"
|
||||
b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4"
|
||||
b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd"
|
||||
b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49)(1:p129:\x00\xfbD\x17\x8b\xa46\xbe"
|
||||
b"\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03\x14\x87"
|
||||
b"\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b\x1d\x88"
|
||||
b"\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1\t\xc2"
|
||||
b"\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz\xea"
|
||||
b"\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\\xe9"
|
||||
b"\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#)(1:q129:\x00\xd9p\x06\xd8\xe2\xbc\xd4"
|
||||
b"x\x91P\x94\xd4\xc1\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}"
|
||||
b"\x1a\xb1e\xe7qu9\xe02\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca"
|
||||
b"\xe6MC\xb3\x9c\xf4k}\xe6\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$"
|
||||
b'n\x831\xa2\x15\xa2D="\xa9b&"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff'
|
||||
b"\x19\x18\x8e\xd8\xab\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d)(1:a128:if7"
|
||||
b"\xc6@\xdd!\xc5\x04\xf3\xb0\xb8>G\x94|v\xfc-\xeb?9<\x95\xc3C\x01Q\xc4B"
|
||||
b"\x97\xf3\xe8\x16\xa4\xc6\xee\xec\xd4I\x10P8\x04\xee;\xcd\xd7\xd0\xcc\xcc"
|
||||
b"2i\x90\x07\xa0\x1bZ\x9f\xfe1\xcd\x1e:~q\x1e\x19\x94\x1aNO\x0c\xdf_R\xd5"
|
||||
b"\xd1\x17n\xec\xd7\x9c\xb6U\x9d\xdb\x8e!_\xbc$\x88\xb6\xfc\xaf\xab\xf0"
|
||||
b"\xef\xa4,\xb0\xdc\x9f\x86\xb0\x03\x12\xb8\x8b\xe2\xdd\x0f\xc0\xee#=JP"
|
||||
b"\xfe/\xb8)FX\xbf\xb6')(1:b128:Q\xaf\xe9\x92\x9f\x94\x0bJ\x84e>\x94\xb3;"
|
||||
b"\x92\x10\xb5t\xb8\x8c\xc9\xef\xc9\x0e\x012\xfa/h\x12\xa1\x03&\xae\xcfQh"
|
||||
b"\x14L&\x9b(\xa4\x023\x08_\xe1\xa7p\x98\x014y^R\x8e\xc4\xcf6\xbc\x1fKU"
|
||||
b"\xac\xeb\xc1S\x84\xc7\xe1a\xa8J\xd4\xa2\xff@\r\x80\x1f\x12\xa9P\xc0*\x18"
|
||||
b"u\x94\x0c\x06\x9b\x16P\xa8K\xecA\xcd{\xef\xf7K\xc9u\x02h\xc4\x98\xb8\x86"
|
||||
b'\x88\x18ZC\xe7\x023\x97"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%'
|
||||
b"\xbca1\xac\x82\xfb\xef\x9c5\x1e~JU\x02h\x95\r\n\x93\xbe\x1e\xbf\xe1@\xfa"
|
||||
b'\x90\xa7Tp1\xe9x\xfc\xe0f\xb7"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b'
|
||||
b"\xcf\xb5H5Q\xaa%\xdc\xde\xdb4)H!\xdb\xd6t\xfc~\xe5/S\xf7\x9c\tp\xb9\xe4"
|
||||
b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5'\xde?\xa5\x7f\x0f\xa6"
|
||||
b"\x88\xf5\x15\xd6_7\x17\x92\xe0\xd6\x05I\x10;\x92\xf6)))"
|
||||
)
|
||||
|
||||
privateRSA_agentv3 = (
|
||||
b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x00!L"
|
||||
b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q"
|
||||
b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12"
|
||||
b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9"
|
||||
b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00"
|
||||
b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6"
|
||||
b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G"
|
||||
b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx"
|
||||
b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl"
|
||||
b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4"
|
||||
b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd"
|
||||
b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49\x00\x00\x01\x01\x00\xd5j\xacx#\xd6"
|
||||
b"\xd6\x1b\xec%\xa1P\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ"
|
||||
b"\xfa9\x06^\x84Uu_\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98"
|
||||
b"\xcd:@x\xd5\xca\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j"
|
||||
b"\xa4\x04\xe9Iw\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e"
|
||||
b"\xefS0\r\xa5\xe5\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad"
|
||||
b"\xf5\x85\xe6>\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?"
|
||||
b"\x91I\x96@\xd5\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E"
|
||||
b"\xb3\xc6,\x18d\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b"
|
||||
b"\x0b\xa4*nC\xc3\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdf"
|
||||
b"t\xe8\x13\xb475~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7\x00\x00\x00\x81"
|
||||
b"\x00\x85K\x1bz\x9b\x12\x107\x9e\x1f\xad^\xda\xfe\xc6\x96\xfe\xdf5k\xb94"
|
||||
b"\xe2\x16\x97\x92&\t\xbd\xbdp \x03\xa75\xbd-\x1b\xa0\xd2\x07G+\xd4\xde"
|
||||
b"\xa8\xa8\x07\x07\x1b\xb8\x04 \xa7'A<l99\xe9A\xce\xe7\x17\x1d\xd1L\\\xbc="
|
||||
b"\xd2&&\xfej\xd6\xfdHr\xaeF\xfa{\xc3\xd3\x19`D\x1d\xa5\x13\xa7\x80\xf5c)"
|
||||
b"\xd4z]\x06\x07\x16]\xf6\x8b=\xcbd:\xe2\x84ZM\x8c\x06--\x9d\x1c\xeb\x83Lx"
|
||||
b"=yT\xce\x00\x00\x00\x81\x00\xd9p\x06\xd8\xe2\xbc\xd4x\x91P\x94\xd4\xc1"
|
||||
b"\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}\x1a\xb1e\xe7qu9\xe02"
|
||||
b"\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca\xe6MC\xb3\x9c\xf4k}\xe6"
|
||||
b'\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$n\x831\xa2\x15\xa2D="'
|
||||
b'\xa9b&"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff\x19\x18\x8e\xd8\xab'
|
||||
b"\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d\x00\x00\x00\x81\x00\xfbD\x17\x8b"
|
||||
b"\xa46\xbe\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03"
|
||||
b"\x14\x87\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b"
|
||||
b"\x1d\x88\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1"
|
||||
b"\t\xc2\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz"
|
||||
b"\xea\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\"
|
||||
b"\xe9\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#"
|
||||
)
|
||||
|
||||
publicDSA_openssh = b"""\
|
||||
ssh-dss AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9\
|
||||
LvFYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G\
|
||||
+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0\
|
||||
EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB\
|
||||
7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nD\
|
||||
ioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQT\
|
||||
NEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2G\
|
||||
gdgMQWC7S6WFIXePGGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8= \
|
||||
comment\
|
||||
"""
|
||||
|
||||
privateDSA_openssh = b"""\
|
||||
-----BEGIN DSA PRIVATE KEY-----
|
||||
MIIBvAIBAAKBgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyD
|
||||
JKsvnLLCDTP5Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VV
|
||||
Ir6LPzJmFSeuqk/fYbXGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQIVAPQR
|
||||
iZM1oUnwJLT68VIXidhzISdJAoGBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2M
|
||||
zB3tZXTMRAe8zcOWjp8Y4aGC7Yh3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4
|
||||
Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDKJ0fDdwBDub/UnwaktkPUejga+pX9OEb8
|
||||
KR2dFBrvAoGAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNICl
|
||||
GlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXeP
|
||||
GGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8CFQDV2gbL
|
||||
czUdxCus0pfEP1bddaXRLQ==
|
||||
-----END DSA PRIVATE KEY-----\
|
||||
"""
|
||||
|
||||
privateDSA_openssh_new = b"""\
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH
|
||||
NzAAAAgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyDJKsvnLLCDTP5
|
||||
Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VVIr6LPzJmFSeuqk/fYb
|
||||
XGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQAAABUA9BGJkzWhSfAktPrxUheJ2HMh
|
||||
J0kAAACBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2MzB3tZXTMRAe8zcOWjp8Y4aGC7Y
|
||||
h3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDK
|
||||
J0fDdwBDub/UnwaktkPUejga+pX9OEb8KR2dFBrvAAAAgAIUacRjCFhMmhIfGJ44ms0EzR
|
||||
KZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPxh2pFuWFh
|
||||
OHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQhJ
|
||||
lz6CzfAAAB2MVcBjzFXAY8AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9Lv
|
||||
FYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+
|
||||
Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+S
|
||||
x9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jA
|
||||
GmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmiz
|
||||
kvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ
|
||||
0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi35
|
||||
9efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDdWxlX8u
|
||||
mhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11
|
||||
pdEtAAAAAAE=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
publicDSA_lsh = decodebytes(
|
||||
b"""\
|
||||
e0tERXdPbkIxWW14cFl5MXJaWGtvTXpwa2MyRW9NVHB3TVRJNU9nQ1NrRHJGUkVWUTBDS1FEUngv
|
||||
aVFBTXBhUFM3eFdKaFJWVENMaHhScFdhU0MrN0lkeURKS3N2bkxMQ0RUUDVaeHc5MzVyQU1pNVZG
|
||||
MmJiZWp3L1M0R1VXczdEem9LYmJoL2hydVBCdnNoYmhQSmRIMVZWSXI2TFB6Sm1GU2V1cWsvZlli
|
||||
WEdTbXJhMDFtWjZWVnU0QlFBYUtzUG9YdGkyZElKSGxOUGtzZld1U2tvTVRweE1qRTZBUFFSaVpN
|
||||
MW9VbndKTFQ2OFZJWGlkaHpJU2RKS1NneE9tY3hNams2QUpJQzQ4UnlRQk82aFZuVTZuK3hHWjRM
|
||||
dTR3QnBuUWZaTjJNekIzdFpYVE1SQWU4emNPV2pwOFk0YUdDN1loM3FCSERwTmx6c3I1eFBsWDRT
|
||||
RnNGZ0hUb2lrVXhHWlpvczVMNFJtNnRBd250aER6RjRxNDZsZ0FpT1p3NHFFRjlhQkRLSjBmRGR3
|
||||
QkR1Yi9Vbndha3RrUFVlamdhK3BYOU9FYjhLUjJkRkJydktTZ3hPbmt4TWpnNkFoUnB4R01JV0V5
|
||||
YUVoOFluamlhelFUTkVwa2xSWnFlQkdvMWdvdEpnZ05tVmFJUU5JQ2xHbEx5Q2kzNTllZkVVdVFj
|
||||
WjlTWHhNNTlQK2hlY2MvR1UvR0hha1c1WVdFNGRQMkdnZGdNUVdDN1M2V0ZJWGVQR0dYcU5RRGRX
|
||||
eGxYOHVtaGVudlFxYTFQbktyRlJoRHJKdzhaN0dqZEh4ZmxzeENFbVhQb0xOOHBLU2s9fQ==
|
||||
"""
|
||||
)
|
||||
|
||||
privateDSA_lsh = decodebytes(
|
||||
b"""\
|
||||
KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToAkpA6xURFUNAikA0cf4kADKWj0u8ViYUVUwi4
|
||||
cUaVmkgvuyHcgySrL5yywg0z+WccPd+awDIuVRdm23o8P0uBlFrOw86Cm24f4a7jwb7IW4TyXR9V
|
||||
VSK+iz8yZhUnrqpP32G1xkpq2tNZmelVbuAUAGirD6F7YtnSCR5TT5LH1rkpKDE6cTIxOgD0EYmT
|
||||
NaFJ8CS0+vFSF4nYcyEnSSkoMTpnMTI5OgCSAuPEckATuoVZ1Op/sRmeC7uMAaZ0H2TdjMwd7WV0
|
||||
zEQHvM3Dlo6fGOGhgu2Id6gRw6TZc7K+cT5V+EhbBYB06IpFMRmWaLOS+EZurQMJ7YQ8xeKuOpYA
|
||||
IjmcOKhBfWgQyidHw3cAQ7m/1J8GpLZD1Ho4GvqV/ThG/CkdnRQa7ykoMTp5MTI4OgIUacRjCFhM
|
||||
mhIfGJ44ms0EzRKZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPx
|
||||
h2pFuWFhOHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQ
|
||||
hJlz6CzfKSgxOngyMToA1doGy3M1HcQrrNKXxD9W3XWl0S0pKSk=
|
||||
"""
|
||||
)
|
||||
|
||||
privateDSA_agentv3 = decodebytes(
|
||||
b"""\
|
||||
AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9LvFYmFFVMIuHFGlZpIL7sh3IMkqy+c
|
||||
ssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99h
|
||||
tcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAA
|
||||
AIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOy
|
||||
vnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2
|
||||
Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQ
|
||||
NIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDd
|
||||
WxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11
|
||||
pdEt
|
||||
"""
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DSAData",
|
||||
"RSAData",
|
||||
"privateDSA_agentv3",
|
||||
"privateDSA_lsh",
|
||||
"privateDSA_openssh",
|
||||
"privateRSA_agentv3",
|
||||
"privateRSA_lsh",
|
||||
"privateRSA_openssh",
|
||||
"publicDSA_lsh",
|
||||
"publicDSA_openssh",
|
||||
"publicRSA_lsh",
|
||||
"publicRSA_openssh",
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Loopback helper used in test_ssh and test_recvline
|
||||
"""
|
||||
|
||||
|
||||
from twisted.protocols import loopback
|
||||
|
||||
|
||||
class LoopbackRelay(loopback.LoopbackRelay):
|
||||
clearCall = None
|
||||
|
||||
def logPrefix(self):
|
||||
return f"LoopbackRelay({self.target.__class__.__name__!r})"
|
||||
|
||||
def write(self, data):
|
||||
loopback.LoopbackRelay.write(self, data)
|
||||
if self.clearCall is not None:
|
||||
self.clearCall.cancel()
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
self.clearCall = reactor.callLater(0, self._clearBuffer)
|
||||
|
||||
def _clearBuffer(self):
|
||||
self.clearCall = None
|
||||
loopback.LoopbackRelay.clearBuffer(self)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{SSHTransportAddrress} in ssh/address.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from twisted.conch.ssh.address import SSHTransportAddress
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.test.test_address import AddressTestCaseMixin
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class SSHTransportAddressTests(unittest.TestCase, AddressTestCaseMixin):
|
||||
"""
|
||||
L{twisted.conch.ssh.address.SSHTransportAddress} is what Conch transports
|
||||
use to represent the other side of the SSH connection. This tests the
|
||||
basic functionality of that class (string representation, comparison, &c).
|
||||
"""
|
||||
|
||||
def _stringRepresentation(self, stringFunction: Callable[[object], str]) -> None:
|
||||
"""
|
||||
The string representation of C{SSHTransportAddress} should be
|
||||
"SSHTransportAddress(<stringFunction on address>)".
|
||||
"""
|
||||
addr = self.buildAddress()
|
||||
stringValue = stringFunction(addr)
|
||||
addressValue = stringFunction(addr.address)
|
||||
self.assertEqual(stringValue, "SSHTransportAddress(%s)" % addressValue)
|
||||
|
||||
def buildAddress(self) -> SSHTransportAddress:
|
||||
"""
|
||||
Create an arbitrary new C{SSHTransportAddress}. A new instance is
|
||||
created for each call, but always for the same address.
|
||||
"""
|
||||
return SSHTransportAddress(IPv4Address("TCP", "127.0.0.1", 22))
|
||||
|
||||
def buildDifferentAddress(self) -> SSHTransportAddress:
|
||||
"""
|
||||
Like C{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return SSHTransportAddress(IPv4Address("TCP", "127.0.0.2", 22))
|
||||
@@ -0,0 +1,398 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.agent}.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.test import iosim
|
||||
from twisted.trial import unittest
|
||||
|
||||
try:
|
||||
import cryptography as _cryptography
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
else:
|
||||
cryptography = _cryptography
|
||||
|
||||
try:
|
||||
from twisted.conch.ssh import agent as _agent, keys as _keys
|
||||
except ImportError:
|
||||
keys = agent = None
|
||||
else:
|
||||
keys, agent = _keys, _agent
|
||||
|
||||
from twisted.conch.error import ConchError, MissingKeyStoreError
|
||||
from twisted.conch.test import keydata
|
||||
|
||||
|
||||
class StubFactory:
|
||||
"""
|
||||
Mock factory that provides the keys attribute required by the
|
||||
SSHAgentServerProtocol
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.keys = {}
|
||||
|
||||
|
||||
class AgentTestBase(unittest.TestCase):
|
||||
"""
|
||||
Tests for SSHAgentServer/Client.
|
||||
"""
|
||||
|
||||
if agent is None or keys is None:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def setUp(self):
|
||||
# wire up our client <-> server
|
||||
self.client, self.server, self.pump = iosim.connectedServerAndClient(
|
||||
agent.SSHAgentServer, agent.SSHAgentClient
|
||||
)
|
||||
|
||||
# the server's end of the protocol is stateful and we store it on the
|
||||
# factory, for which we only need a mock
|
||||
self.server.factory = StubFactory()
|
||||
|
||||
# pub/priv keys of each kind
|
||||
self.rsaPrivate = keys.Key.fromString(keydata.privateRSA_openssh)
|
||||
self.dsaPrivate = keys.Key.fromString(keydata.privateDSA_openssh)
|
||||
|
||||
self.rsaPublic = keys.Key.fromString(keydata.publicRSA_openssh)
|
||||
self.dsaPublic = keys.Key.fromString(keydata.publicDSA_openssh)
|
||||
|
||||
|
||||
class ServerProtocolContractWithFactoryTests(AgentTestBase):
|
||||
"""
|
||||
The server protocol is stateful and so uses its factory to track state
|
||||
across requests. This test asserts that the protocol raises if its factory
|
||||
doesn't provide the necessary storage for that state.
|
||||
"""
|
||||
|
||||
def test_factorySuppliesKeyStorageForServerProtocol(self):
|
||||
# need a message to send into the server
|
||||
msg = struct.pack("!LB", 1, agent.AGENTC_REQUEST_IDENTITIES)
|
||||
del self.server.factory.__dict__["keys"]
|
||||
self.assertRaises(MissingKeyStoreError, self.server.dataReceived, msg)
|
||||
|
||||
|
||||
class UnimplementedVersionOneServerTests(AgentTestBase):
|
||||
"""
|
||||
Tests for methods with no-op implementations on the server. We need these
|
||||
for clients, such as openssh, that try v1 methods before going to v2.
|
||||
|
||||
Because the client doesn't expose these operations with nice method names,
|
||||
we invoke sendRequest directly with an op code.
|
||||
"""
|
||||
|
||||
def test_agentc_REQUEST_RSA_IDENTITIES(self):
|
||||
"""
|
||||
assert that we get the correct op code for an RSA identities request
|
||||
"""
|
||||
d = self.client.sendRequest(agent.AGENTC_REQUEST_RSA_IDENTITIES, b"")
|
||||
self.pump.flush()
|
||||
|
||||
def _cb(packet):
|
||||
self.assertEqual(agent.AGENT_RSA_IDENTITIES_ANSWER, ord(packet[0:1]))
|
||||
|
||||
return d.addCallback(_cb)
|
||||
|
||||
def test_agentc_REMOVE_RSA_IDENTITY(self):
|
||||
"""
|
||||
assert that we get the correct op code for an RSA remove identity request
|
||||
"""
|
||||
d = self.client.sendRequest(agent.AGENTC_REMOVE_RSA_IDENTITY, b"")
|
||||
self.pump.flush()
|
||||
return d.addCallback(self.assertEqual, b"")
|
||||
|
||||
def test_agentc_REMOVE_ALL_RSA_IDENTITIES(self):
|
||||
"""
|
||||
assert that we get the correct op code for an RSA remove all identities
|
||||
request.
|
||||
"""
|
||||
d = self.client.sendRequest(agent.AGENTC_REMOVE_ALL_RSA_IDENTITIES, b"")
|
||||
self.pump.flush()
|
||||
return d.addCallback(self.assertEqual, b"")
|
||||
|
||||
|
||||
if agent is not None:
|
||||
|
||||
class CorruptServer(agent.SSHAgentServer): # type: ignore[name-defined]
|
||||
"""
|
||||
A misbehaving server that returns bogus response op codes so that we can
|
||||
verify that our callbacks that deal with these op codes handle such
|
||||
miscreants.
|
||||
"""
|
||||
|
||||
def agentc_REQUEST_IDENTITIES(self, data):
|
||||
self.sendResponse(254, b"")
|
||||
|
||||
def agentc_SIGN_REQUEST(self, data):
|
||||
self.sendResponse(254, b"")
|
||||
|
||||
|
||||
class ClientWithBrokenServerTests(AgentTestBase):
|
||||
"""
|
||||
verify error handling code in the client using a misbehaving server
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
AgentTestBase.setUp(self)
|
||||
self.client, self.server, self.pump = iosim.connectedServerAndClient(
|
||||
CorruptServer, agent.SSHAgentClient
|
||||
)
|
||||
# the server's end of the protocol is stateful and we store it on the
|
||||
# factory, for which we only need a mock
|
||||
self.server.factory = StubFactory()
|
||||
|
||||
def test_signDataCallbackErrorHandling(self):
|
||||
"""
|
||||
Assert that L{SSHAgentClient.signData} raises a ConchError
|
||||
if we get a response from the server whose opcode doesn't match
|
||||
the protocol for data signing requests.
|
||||
"""
|
||||
d = self.client.signData(self.rsaPublic.blob(), b"John Hancock")
|
||||
self.pump.flush()
|
||||
return self.assertFailure(d, ConchError)
|
||||
|
||||
def test_requestIdentitiesCallbackErrorHandling(self):
|
||||
"""
|
||||
Assert that L{SSHAgentClient.requestIdentities} raises a ConchError
|
||||
if we get a response from the server whose opcode doesn't match
|
||||
the protocol for identity requests.
|
||||
"""
|
||||
d = self.client.requestIdentities()
|
||||
self.pump.flush()
|
||||
return self.assertFailure(d, ConchError)
|
||||
|
||||
|
||||
class AgentKeyAdditionTests(AgentTestBase):
|
||||
"""
|
||||
Test adding different flavors of keys to an agent.
|
||||
"""
|
||||
|
||||
def test_addRSAIdentityNoComment(self):
|
||||
"""
|
||||
L{SSHAgentClient.addIdentity} adds the private key it is called
|
||||
with to the SSH agent server to which it is connected, associating
|
||||
it with the comment it is called with.
|
||||
|
||||
This test asserts that omitting the comment produces an
|
||||
empty string for the comment on the server.
|
||||
"""
|
||||
d = self.client.addIdentity(self.rsaPrivate.privateBlob())
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
serverKey = self.server.factory.keys[self.rsaPrivate.blob()]
|
||||
self.assertEqual(self.rsaPrivate, serverKey[0])
|
||||
self.assertEqual(b"", serverKey[1])
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_addDSAIdentityNoComment(self):
|
||||
"""
|
||||
L{SSHAgentClient.addIdentity} adds the private key it is called
|
||||
with to the SSH agent server to which it is connected, associating
|
||||
it with the comment it is called with.
|
||||
|
||||
This test asserts that omitting the comment produces an
|
||||
empty string for the comment on the server.
|
||||
"""
|
||||
d = self.client.addIdentity(self.dsaPrivate.privateBlob())
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
serverKey = self.server.factory.keys[self.dsaPrivate.blob()]
|
||||
self.assertEqual(self.dsaPrivate, serverKey[0])
|
||||
self.assertEqual(b"", serverKey[1])
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_addRSAIdentityWithComment(self):
|
||||
"""
|
||||
L{SSHAgentClient.addIdentity} adds the private key it is called
|
||||
with to the SSH agent server to which it is connected, associating
|
||||
it with the comment it is called with.
|
||||
|
||||
This test asserts that the server receives/stores the comment
|
||||
as sent by the client.
|
||||
"""
|
||||
d = self.client.addIdentity(
|
||||
self.rsaPrivate.privateBlob(), comment=b"My special key"
|
||||
)
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
serverKey = self.server.factory.keys[self.rsaPrivate.blob()]
|
||||
self.assertEqual(self.rsaPrivate, serverKey[0])
|
||||
self.assertEqual(b"My special key", serverKey[1])
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_addDSAIdentityWithComment(self):
|
||||
"""
|
||||
L{SSHAgentClient.addIdentity} adds the private key it is called
|
||||
with to the SSH agent server to which it is connected, associating
|
||||
it with the comment it is called with.
|
||||
|
||||
This test asserts that the server receives/stores the comment
|
||||
as sent by the client.
|
||||
"""
|
||||
d = self.client.addIdentity(
|
||||
self.dsaPrivate.privateBlob(), comment=b"My special key"
|
||||
)
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
serverKey = self.server.factory.keys[self.dsaPrivate.blob()]
|
||||
self.assertEqual(self.dsaPrivate, serverKey[0])
|
||||
self.assertEqual(b"My special key", serverKey[1])
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
|
||||
class AgentClientFailureTests(AgentTestBase):
|
||||
def test_agentFailure(self):
|
||||
"""
|
||||
verify that the client raises ConchError on AGENT_FAILURE
|
||||
"""
|
||||
d = self.client.sendRequest(254, b"")
|
||||
self.pump.flush()
|
||||
return self.assertFailure(d, ConchError)
|
||||
|
||||
|
||||
class AgentIdentityRequestsTests(AgentTestBase):
|
||||
"""
|
||||
Test operations against a server with identities already loaded.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
AgentTestBase.setUp(self)
|
||||
self.server.factory.keys[self.dsaPrivate.blob()] = (
|
||||
self.dsaPrivate,
|
||||
b"a comment",
|
||||
)
|
||||
self.server.factory.keys[self.rsaPrivate.blob()] = (
|
||||
self.rsaPrivate,
|
||||
b"another comment",
|
||||
)
|
||||
|
||||
def test_signDataRSA(self):
|
||||
"""
|
||||
Sign data with an RSA private key and then verify it with the public
|
||||
key.
|
||||
"""
|
||||
d = self.client.signData(self.rsaPublic.blob(), b"John Hancock")
|
||||
self.pump.flush()
|
||||
signature = self.successResultOf(d)
|
||||
|
||||
expected = self.rsaPrivate.sign(b"John Hancock")
|
||||
self.assertEqual(expected, signature)
|
||||
self.assertTrue(self.rsaPublic.verify(signature, b"John Hancock"))
|
||||
|
||||
def test_signDataDSA(self):
|
||||
"""
|
||||
Sign data with a DSA private key and then verify it with the public
|
||||
key.
|
||||
"""
|
||||
d = self.client.signData(self.dsaPublic.blob(), b"John Hancock")
|
||||
self.pump.flush()
|
||||
|
||||
def _check(sig):
|
||||
# Cannot do this b/c DSA uses random numbers when signing
|
||||
# expected = self.dsaPrivate.sign("John Hancock")
|
||||
# self.assertEqual(expected, sig)
|
||||
self.assertTrue(self.dsaPublic.verify(sig, b"John Hancock"))
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_signDataRSAErrbackOnUnknownBlob(self):
|
||||
"""
|
||||
Assert that we get an errback if we try to sign data using a key that
|
||||
wasn't added.
|
||||
"""
|
||||
del self.server.factory.keys[self.rsaPublic.blob()]
|
||||
d = self.client.signData(self.rsaPublic.blob(), b"John Hancock")
|
||||
self.pump.flush()
|
||||
return self.assertFailure(d, ConchError)
|
||||
|
||||
def test_requestIdentities(self):
|
||||
"""
|
||||
Assert that we get all of the keys/comments that we add when we issue a
|
||||
request for all identities.
|
||||
"""
|
||||
d = self.client.requestIdentities()
|
||||
self.pump.flush()
|
||||
|
||||
def _check(keyt):
|
||||
expected = {}
|
||||
expected[self.dsaPublic.blob()] = b"a comment"
|
||||
expected[self.rsaPublic.blob()] = b"another comment"
|
||||
|
||||
received = {}
|
||||
for k in keyt:
|
||||
received[keys.Key.fromString(k[0], type="blob").blob()] = k[1]
|
||||
self.assertEqual(expected, received)
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
|
||||
class AgentKeyRemovalTests(AgentTestBase):
|
||||
"""
|
||||
Test support for removing keys in a remote server.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
AgentTestBase.setUp(self)
|
||||
self.server.factory.keys[self.dsaPrivate.blob()] = (
|
||||
self.dsaPrivate,
|
||||
b"a comment",
|
||||
)
|
||||
self.server.factory.keys[self.rsaPrivate.blob()] = (
|
||||
self.rsaPrivate,
|
||||
b"another comment",
|
||||
)
|
||||
|
||||
def test_removeRSAIdentity(self):
|
||||
"""
|
||||
Assert that we can remove an RSA identity.
|
||||
"""
|
||||
# only need public key for this
|
||||
d = self.client.removeIdentity(self.rsaPrivate.blob())
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
self.assertEqual(1, len(self.server.factory.keys))
|
||||
self.assertIn(self.dsaPrivate.blob(), self.server.factory.keys)
|
||||
self.assertNotIn(self.rsaPrivate.blob(), self.server.factory.keys)
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_removeDSAIdentity(self):
|
||||
"""
|
||||
Assert that we can remove a DSA identity.
|
||||
"""
|
||||
# only need public key for this
|
||||
d = self.client.removeIdentity(self.dsaPrivate.blob())
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
self.assertEqual(1, len(self.server.factory.keys))
|
||||
self.assertIn(self.rsaPrivate.blob(), self.server.factory.keys)
|
||||
|
||||
return d.addCallback(_check)
|
||||
|
||||
def test_removeAllIdentities(self):
|
||||
"""
|
||||
Assert that we can remove all identities.
|
||||
"""
|
||||
d = self.client.removeAllIdentities()
|
||||
self.pump.flush()
|
||||
|
||||
def _check(ignored):
|
||||
self.assertEqual(0, len(self.server.factory.keys))
|
||||
|
||||
return d.addCallback(_check)
|
||||
1510
backend/lib/python3.12/site-packages/twisted/conch/test/test_cftp.py
Normal file
1510
backend/lib/python3.12/site-packages/twisted/conch/test/test_cftp.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,358 @@
|
||||
# Copyright Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test ssh/channel.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
try:
|
||||
from twisted.conch.ssh import channel
|
||||
from twisted.conch.ssh.address import SSHTransportAddress
|
||||
from twisted.conch.ssh.service import SSHService
|
||||
from twisted.conch.ssh.transport import SSHServerTransport
|
||||
from twisted.internet import interfaces
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.testing import StringTransport
|
||||
|
||||
skipTest = ""
|
||||
except ImportError:
|
||||
skipTest = "Conch SSH not supported."
|
||||
SSHService = object # type: ignore[assignment,misc]
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class MockConnection(SSHService):
|
||||
"""
|
||||
A mock for twisted.conch.ssh.connection.SSHConnection. Record the data
|
||||
that channels send, and when they try to close the connection.
|
||||
|
||||
@ivar data: a L{dict} mapping channel id #s to lists of data sent by that
|
||||
channel.
|
||||
@ivar extData: a L{dict} mapping channel id #s to lists of 2-tuples
|
||||
(extended data type, data) sent by that channel.
|
||||
@ivar closes: a L{dict} mapping channel id #s to True if that channel sent
|
||||
a close message.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.data: dict[channel.SSHChannel, list[bytes]] = {}
|
||||
self.extData: dict[channel.SSHChannel, list[tuple[int, bytes]]] = {}
|
||||
self.closes: dict[channel.SSHChannel, bool] = {}
|
||||
|
||||
def logPrefix(self) -> str:
|
||||
"""
|
||||
Return our logging prefix.
|
||||
"""
|
||||
return "MockConnection"
|
||||
|
||||
def sendData(self, channel: channel.SSHChannel, data: bytes) -> None:
|
||||
"""
|
||||
Record the sent data.
|
||||
"""
|
||||
self.data.setdefault(channel, []).append(data)
|
||||
|
||||
def sendExtendedData(
|
||||
self, channel: channel.SSHChannel, type: int, data: bytes
|
||||
) -> None:
|
||||
"""
|
||||
Record the sent extended data.
|
||||
"""
|
||||
self.extData.setdefault(channel, []).append((type, data))
|
||||
|
||||
def sendClose(self, channel: channel.SSHChannel) -> None:
|
||||
"""
|
||||
Record that the channel sent a close message.
|
||||
"""
|
||||
self.closes[channel] = True
|
||||
|
||||
|
||||
def connectSSHTransport(
|
||||
service: SSHService,
|
||||
hostAddress: interfaces.IAddress | None = None,
|
||||
peerAddress: interfaces.IAddress | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Connect a SSHTransport which is already connected to a remote peer to
|
||||
the channel under test.
|
||||
|
||||
@param service: Service used over the connected transport.
|
||||
@type service: L{SSHService}
|
||||
|
||||
@param hostAddress: Local address of the connected transport.
|
||||
@type hostAddress: L{interfaces.IAddress}
|
||||
|
||||
@param peerAddress: Remote address of the connected transport.
|
||||
@type peerAddress: L{interfaces.IAddress}
|
||||
"""
|
||||
transport = SSHServerTransport()
|
||||
transport.makeConnection(
|
||||
StringTransport(hostAddress=hostAddress, peerAddress=peerAddress)
|
||||
)
|
||||
transport.setService(service)
|
||||
|
||||
|
||||
@skipIf(skipTest, skipTest)
|
||||
class ChannelTests(TestCase):
|
||||
"""
|
||||
Tests for L{SSHChannel}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Initialize the channel. remoteMaxPacket is 10 so that data is able
|
||||
to be sent (the default of 0 means no data is sent because no packets
|
||||
are made).
|
||||
"""
|
||||
self.conn = MockConnection()
|
||||
self.channel = channel.SSHChannel(conn=self.conn, remoteMaxPacket=10)
|
||||
self.channel.name = b"channel"
|
||||
|
||||
def test_interface(self) -> None:
|
||||
"""
|
||||
L{SSHChannel} instances provide L{interfaces.ITransport}.
|
||||
"""
|
||||
self.assertTrue(verifyObject(interfaces.ITransport, self.channel))
|
||||
|
||||
def test_init(self) -> None:
|
||||
"""
|
||||
Test that SSHChannel initializes correctly. localWindowSize defaults
|
||||
to 131072 (2**17) and localMaxPacket to 32768 (2**15) as reasonable
|
||||
defaults (what OpenSSH uses for those variables).
|
||||
|
||||
The values in the second set of assertions are meaningless; they serve
|
||||
only to verify that the instance variables are assigned in the correct
|
||||
order.
|
||||
"""
|
||||
c = channel.SSHChannel(conn=self.conn)
|
||||
self.assertEqual(c.localWindowSize, 131072)
|
||||
self.assertEqual(c.localWindowLeft, 131072)
|
||||
self.assertEqual(c.localMaxPacket, 32768)
|
||||
self.assertEqual(c.remoteWindowLeft, 0)
|
||||
self.assertEqual(c.remoteMaxPacket, 0)
|
||||
self.assertEqual(c.conn, self.conn)
|
||||
self.assertIsNone(c.data)
|
||||
self.assertIsNone(c.avatar)
|
||||
|
||||
c2 = channel.SSHChannel(1, 2, 3, 4, 5, 6, 7)
|
||||
self.assertEqual(c2.localWindowSize, 1)
|
||||
self.assertEqual(c2.localWindowLeft, 1)
|
||||
self.assertEqual(c2.localMaxPacket, 2)
|
||||
self.assertEqual(c2.remoteWindowLeft, 3)
|
||||
self.assertEqual(c2.remoteMaxPacket, 4)
|
||||
self.assertEqual(c2.conn, 5)
|
||||
self.assertEqual(c2.data, 6)
|
||||
self.assertEqual(c2.avatar, 7)
|
||||
|
||||
def test_str(self) -> None:
|
||||
"""
|
||||
Test that str(SSHChannel) works gives the channel name and local and
|
||||
remote windows at a glance..
|
||||
"""
|
||||
self.assertEqual(str(self.channel), "<SSHChannel channel (lw 131072 rw 0)>")
|
||||
self.assertEqual(
|
||||
str(channel.SSHChannel(localWindow=1)), "<SSHChannel None (lw 1 rw 0)>"
|
||||
)
|
||||
|
||||
def test_bytes(self) -> None:
|
||||
"""
|
||||
Test that bytes(SSHChannel) works, gives the channel name and
|
||||
local and remote windows at a glance..
|
||||
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.channel.__bytes__(), b"<SSHChannel channel (lw 131072 rw 0)>"
|
||||
)
|
||||
self.assertEqual(
|
||||
channel.SSHChannel(localWindow=1).__bytes__(),
|
||||
b"<SSHChannel None (lw 1 rw 0)>",
|
||||
)
|
||||
|
||||
def test_logPrefix(self) -> None:
|
||||
"""
|
||||
Test that SSHChannel.logPrefix gives the name of the channel, the
|
||||
local channel ID and the underlying connection.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.channel.logPrefix(), "SSHChannel channel (unknown) on MockConnection"
|
||||
)
|
||||
|
||||
def test_addWindowBytes(self) -> None:
|
||||
"""
|
||||
Test that addWindowBytes adds bytes to the window and resumes writing
|
||||
if it was paused.
|
||||
"""
|
||||
cb = [False]
|
||||
|
||||
def stubStartWriting() -> None:
|
||||
cb[0] = True
|
||||
|
||||
self.channel.startWriting = stubStartWriting # type: ignore[method-assign]
|
||||
self.channel.write(b"test")
|
||||
self.channel.writeExtended(1, b"test")
|
||||
self.channel.addWindowBytes(50)
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 50 - 4 - 4)
|
||||
self.assertTrue(self.channel.areWriting)
|
||||
self.assertTrue(cb[0])
|
||||
self.assertEqual(self.channel.buf, b"")
|
||||
self.assertEqual(self.conn.data[self.channel], [b"test"])
|
||||
self.assertEqual(self.channel.extBuf, [])
|
||||
self.assertEqual(self.conn.extData[self.channel], [(1, b"test")])
|
||||
|
||||
cb[0] = False
|
||||
self.channel.addWindowBytes(20)
|
||||
self.assertFalse(cb[0])
|
||||
|
||||
self.channel.write(b"a" * 80)
|
||||
self.channel.loseConnection()
|
||||
self.channel.addWindowBytes(20)
|
||||
self.assertFalse(cb[0])
|
||||
|
||||
def test_requestReceived(self) -> None:
|
||||
"""
|
||||
Test that requestReceived handles requests by dispatching them to
|
||||
request_* methods.
|
||||
"""
|
||||
self.channel.request_test_method = lambda data: data == b"" # type: ignore[attr-defined]
|
||||
self.assertTrue(self.channel.requestReceived(b"test-method", b""))
|
||||
self.assertFalse(self.channel.requestReceived(b"test-method", b"a"))
|
||||
self.assertFalse(self.channel.requestReceived(b"bad-method", b""))
|
||||
|
||||
def test_closeReceieved(self) -> None:
|
||||
"""
|
||||
Test that the default closeReceieved closes the connection.
|
||||
"""
|
||||
self.assertFalse(self.channel.closing)
|
||||
self.channel.closeReceived()
|
||||
self.assertTrue(self.channel.closing)
|
||||
|
||||
def test_write(self) -> None:
|
||||
"""
|
||||
Test that write handles data correctly. Send data up to the size
|
||||
of the remote window, splitting the data into packets of length
|
||||
remoteMaxPacket.
|
||||
"""
|
||||
cb = [False]
|
||||
|
||||
def stubStopWriting() -> None:
|
||||
cb[0] = True
|
||||
|
||||
# no window to start with
|
||||
self.channel.stopWriting = stubStopWriting # type: ignore[method-assign]
|
||||
self.channel.write(b"d")
|
||||
self.channel.write(b"a")
|
||||
self.assertFalse(self.channel.areWriting)
|
||||
self.assertTrue(cb[0])
|
||||
# regular write
|
||||
self.channel.addWindowBytes(20)
|
||||
self.channel.write(b"ta")
|
||||
data = self.conn.data[self.channel]
|
||||
self.assertEqual(data, [b"da", b"ta"])
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 16)
|
||||
# larger than max packet
|
||||
self.channel.write(b"12345678901")
|
||||
self.assertEqual(data, [b"da", b"ta", b"1234567890", b"1"])
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 5)
|
||||
# running out of window
|
||||
cb[0] = False
|
||||
self.channel.write(b"123456")
|
||||
self.assertFalse(self.channel.areWriting)
|
||||
self.assertTrue(cb[0])
|
||||
self.assertEqual(data, [b"da", b"ta", b"1234567890", b"1", b"12345"])
|
||||
self.assertEqual(self.channel.buf, b"6")
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 0)
|
||||
|
||||
def test_writeExtended(self) -> None:
|
||||
"""
|
||||
Test that writeExtended handles data correctly. Send extended data
|
||||
up to the size of the window, splitting the extended data into packets
|
||||
of length remoteMaxPacket.
|
||||
"""
|
||||
cb = [False]
|
||||
|
||||
def stubStopWriting() -> None:
|
||||
cb[0] = True
|
||||
|
||||
# no window to start with
|
||||
self.channel.stopWriting = stubStopWriting # type: ignore[method-assign]
|
||||
self.channel.writeExtended(1, b"d")
|
||||
self.channel.writeExtended(1, b"a")
|
||||
self.channel.writeExtended(2, b"t")
|
||||
self.assertFalse(self.channel.areWriting)
|
||||
self.assertTrue(cb[0])
|
||||
# regular write
|
||||
self.channel.addWindowBytes(20)
|
||||
self.channel.writeExtended(2, b"a")
|
||||
data = self.conn.extData[self.channel]
|
||||
self.assertEqual(data, [(1, b"da"), (2, b"t"), (2, b"a")])
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 16)
|
||||
# larger than max packet
|
||||
self.channel.writeExtended(3, b"12345678901")
|
||||
self.assertEqual(
|
||||
data, [(1, b"da"), (2, b"t"), (2, b"a"), (3, b"1234567890"), (3, b"1")]
|
||||
)
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 5)
|
||||
# running out of window
|
||||
cb[0] = False
|
||||
self.channel.writeExtended(4, b"123456")
|
||||
self.assertFalse(self.channel.areWriting)
|
||||
self.assertTrue(cb[0])
|
||||
self.assertEqual(
|
||||
data,
|
||||
[
|
||||
(1, b"da"),
|
||||
(2, b"t"),
|
||||
(2, b"a"),
|
||||
(3, b"1234567890"),
|
||||
(3, b"1"),
|
||||
(4, b"12345"),
|
||||
],
|
||||
)
|
||||
self.assertEqual(self.channel.extBuf, [[4, b"6"]])
|
||||
self.assertEqual(self.channel.remoteWindowLeft, 0)
|
||||
|
||||
def test_writeSequence(self) -> None:
|
||||
"""
|
||||
Test that writeSequence is equivalent to write(''.join(sequece)).
|
||||
"""
|
||||
self.channel.addWindowBytes(20)
|
||||
self.channel.writeSequence(b"%d" % (i,) for i in range(10))
|
||||
self.assertEqual(self.conn.data[self.channel], [b"0123456789"])
|
||||
|
||||
def test_loseConnection(self) -> None:
|
||||
"""
|
||||
Tesyt that loseConnection() doesn't close the channel until all
|
||||
the data is sent.
|
||||
"""
|
||||
self.channel.write(b"data")
|
||||
self.channel.writeExtended(1, b"datadata")
|
||||
self.channel.loseConnection()
|
||||
self.assertIsNone(self.conn.closes.get(self.channel))
|
||||
self.channel.addWindowBytes(4) # send regular data
|
||||
self.assertIsNone(self.conn.closes.get(self.channel))
|
||||
self.channel.addWindowBytes(8) # send extended data
|
||||
self.assertTrue(self.conn.closes.get(self.channel))
|
||||
|
||||
def test_getPeer(self) -> None:
|
||||
"""
|
||||
L{SSHChannel.getPeer} returns the same object as the underlying
|
||||
transport's C{getPeer} method returns.
|
||||
"""
|
||||
peer = IPv4Address("TCP", "192.168.0.1", 54321)
|
||||
connectSSHTransport(service=self.channel.conn, peerAddress=peer)
|
||||
|
||||
self.assertEqual(SSHTransportAddress(peer), self.channel.getPeer())
|
||||
|
||||
def test_getHost(self) -> None:
|
||||
"""
|
||||
L{SSHChannel.getHost} returns the same object as the underlying
|
||||
transport's C{getHost} method returns.
|
||||
"""
|
||||
host = IPv4Address("TCP", "127.0.0.1", 12345)
|
||||
connectSSHTransport(service=self.channel.conn, hostAddress=host)
|
||||
|
||||
self.assertEqual(SSHTransportAddress(host), self.channel.getHost())
|
||||
@@ -0,0 +1,886 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.checkers}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from base64 import encodebytes
|
||||
from collections import namedtuple
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
cryptSkip: Optional[str]
|
||||
try:
|
||||
import crypt
|
||||
except ImportError:
|
||||
cryptSkip = "cannot run without crypt module"
|
||||
else:
|
||||
cryptSkip = None
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
|
||||
from twisted.cred.credentials import (
|
||||
ISSHPrivateKey,
|
||||
IUsernamePassword,
|
||||
SSHPrivateKey,
|
||||
UsernamePassword,
|
||||
)
|
||||
from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.python import util
|
||||
from twisted.python.fakepwd import ShadowDatabase, UserDatabase
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.test.test_process import MockOS
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if requireModule("cryptography"):
|
||||
dependencySkip = None
|
||||
from twisted.conch import checkers
|
||||
from twisted.conch.error import NotEnoughAuthentication, ValidPublicKey
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.conch.test import keydata
|
||||
else:
|
||||
dependencySkip = "can't run without cryptography"
|
||||
|
||||
if getattr(os, "geteuid", None) is not None:
|
||||
euidSkip = None
|
||||
else:
|
||||
euidSkip = "Cannot run without effective UIDs (questionable)"
|
||||
|
||||
|
||||
class HelperTests(TestCase):
|
||||
"""
|
||||
Tests for helper functions L{verifyCryptedPassword}, L{_pwdGetByName} and
|
||||
L{_shadowGetByName}.
|
||||
"""
|
||||
|
||||
skip = cryptSkip or dependencySkip
|
||||
|
||||
def setUp(self):
|
||||
self.mockos = MockOS()
|
||||
|
||||
def test_verifyCryptedPassword(self):
|
||||
"""
|
||||
L{verifyCryptedPassword} returns C{True} if the plaintext password
|
||||
passed to it matches the encrypted password passed to it.
|
||||
"""
|
||||
password = "secret string"
|
||||
salt = "salty"
|
||||
crypted = crypt.crypt(password, salt)
|
||||
self.assertTrue(
|
||||
checkers.verifyCryptedPassword(crypted, password),
|
||||
"{!r} supposed to be valid encrypted password for {!r}".format(
|
||||
crypted, password
|
||||
),
|
||||
)
|
||||
|
||||
def test_verifyCryptedPasswordMD5(self):
|
||||
"""
|
||||
L{verifyCryptedPassword} returns True if the provided cleartext password
|
||||
matches the provided MD5 password hash.
|
||||
"""
|
||||
password = "password"
|
||||
salt = "$1$salt"
|
||||
crypted = crypt.crypt(password, salt)
|
||||
self.assertTrue(
|
||||
checkers.verifyCryptedPassword(crypted, password),
|
||||
"{!r} supposed to be valid encrypted password for {}".format(
|
||||
crypted, password
|
||||
),
|
||||
)
|
||||
|
||||
def test_refuteCryptedPassword(self):
|
||||
"""
|
||||
L{verifyCryptedPassword} returns C{False} if the plaintext password
|
||||
passed to it does not match the encrypted password passed to it.
|
||||
"""
|
||||
password = "string secret"
|
||||
wrong = "secret string"
|
||||
crypted = crypt.crypt(password, password)
|
||||
self.assertFalse(
|
||||
checkers.verifyCryptedPassword(crypted, wrong),
|
||||
"{!r} not supposed to be valid encrypted password for {}".format(
|
||||
crypted, wrong
|
||||
),
|
||||
)
|
||||
|
||||
def test_pwdGetByName(self):
|
||||
"""
|
||||
L{_pwdGetByName} returns a tuple of items from the UNIX /etc/passwd
|
||||
database if the L{pwd} module is present.
|
||||
"""
|
||||
userdb = UserDatabase()
|
||||
userdb.addUser("alice", "secrit", 1, 2, "first last", "/foo", "/bin/sh")
|
||||
self.patch(checkers, "pwd", userdb)
|
||||
self.assertEqual(checkers._pwdGetByName("alice"), userdb.getpwnam("alice"))
|
||||
|
||||
def test_pwdGetByNameWithoutPwd(self):
|
||||
"""
|
||||
If the C{pwd} module isn't present, L{_pwdGetByName} returns L{None}.
|
||||
"""
|
||||
self.patch(checkers, "pwd", None)
|
||||
self.assertIsNone(checkers._pwdGetByName("alice"))
|
||||
|
||||
def test_shadowGetByName(self):
|
||||
"""
|
||||
L{_shadowGetByName} returns a tuple of items from the UNIX /etc/shadow
|
||||
database if the L{spwd} is present.
|
||||
"""
|
||||
userdb = ShadowDatabase()
|
||||
userdb.addUser("bob", "passphrase", 1, 2, 3, 4, 5, 6, 7)
|
||||
self.patch(checkers, "spwd", userdb)
|
||||
|
||||
self.mockos.euid = 2345
|
||||
self.mockos.egid = 1234
|
||||
self.patch(util, "os", self.mockos)
|
||||
|
||||
self.assertEqual(checkers._shadowGetByName("bob"), userdb.getspnam("bob"))
|
||||
self.assertEqual(self.mockos.seteuidCalls, [0, 2345])
|
||||
self.assertEqual(self.mockos.setegidCalls, [0, 1234])
|
||||
|
||||
def test_shadowGetByNameWithoutSpwd(self):
|
||||
"""
|
||||
L{_shadowGetByName} returns L{None} if C{spwd} is not present.
|
||||
"""
|
||||
self.patch(checkers, "spwd", None)
|
||||
|
||||
self.assertIsNone(checkers._shadowGetByName("bob"))
|
||||
self.assertEqual(self.mockos.seteuidCalls, [])
|
||||
self.assertEqual(self.mockos.setegidCalls, [])
|
||||
|
||||
|
||||
class SSHPublicKeyDatabaseTests(TestCase):
|
||||
"""
|
||||
Tests for L{SSHPublicKeyDatabase}.
|
||||
"""
|
||||
|
||||
skip = euidSkip or dependencySkip
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.checker = checkers.SSHPublicKeyDatabase()
|
||||
self.key1 = encodebytes(b"foobar")
|
||||
self.key2 = encodebytes(b"eggspam")
|
||||
self.content = b"t1 " + self.key1 + b" foo\nt2 " + self.key2 + b" egg\n"
|
||||
|
||||
self.mockos = MockOS()
|
||||
self.patch(util, "os", self.mockos)
|
||||
|
||||
self.path = FilePath(self.mktemp())
|
||||
assert isinstance(self.path.path, str) # text mode
|
||||
self.sshDir = self.path.child(".ssh")
|
||||
self.sshDir.makedirs()
|
||||
|
||||
userdb = UserDatabase()
|
||||
userdb.addUser(
|
||||
"user",
|
||||
"password",
|
||||
1,
|
||||
2,
|
||||
"first last",
|
||||
self.path.path,
|
||||
"/bin/shell",
|
||||
)
|
||||
self.checker._userdb = userdb # type: ignore
|
||||
|
||||
def test_deprecated(self):
|
||||
"""
|
||||
L{SSHPublicKeyDatabase} is deprecated as of version 15.0
|
||||
"""
|
||||
warningsShown = self.flushWarnings(offendingFunctions=[self.setUp])
|
||||
self.assertEqual(warningsShown[0]["category"], DeprecationWarning)
|
||||
self.assertEqual(
|
||||
warningsShown[0]["message"],
|
||||
"twisted.conch.checkers.SSHPublicKeyDatabase "
|
||||
"was deprecated in Twisted 15.0.0: Please use "
|
||||
"twisted.conch.checkers.SSHPublicKeyChecker, "
|
||||
"initialized with an instance of "
|
||||
"twisted.conch.checkers.UNIXAuthorizedKeysFiles instead.",
|
||||
)
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
|
||||
def _testCheckKey(self, filename):
|
||||
self.sshDir.child(filename).setContent(self.content)
|
||||
user = UsernamePassword(b"user", b"password")
|
||||
user.blob = b"foobar"
|
||||
self.assertTrue(self.checker.checkKey(user))
|
||||
user.blob = b"eggspam"
|
||||
self.assertTrue(self.checker.checkKey(user))
|
||||
user.blob = b"notallowed"
|
||||
self.assertFalse(self.checker.checkKey(user))
|
||||
|
||||
def test_checkKey(self):
|
||||
"""
|
||||
L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the
|
||||
authorized_keys file and check the keys against that file.
|
||||
"""
|
||||
self._testCheckKey("authorized_keys")
|
||||
self.assertEqual(self.mockos.seteuidCalls, [])
|
||||
self.assertEqual(self.mockos.setegidCalls, [])
|
||||
|
||||
def test_checkKey2(self):
|
||||
"""
|
||||
L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the
|
||||
authorized_keys2 file and check the keys against that file.
|
||||
"""
|
||||
self._testCheckKey("authorized_keys2")
|
||||
self.assertEqual(self.mockos.seteuidCalls, [])
|
||||
self.assertEqual(self.mockos.setegidCalls, [])
|
||||
|
||||
def test_checkKeyAsRoot(self):
|
||||
"""
|
||||
If the key file is readable, L{SSHPublicKeyDatabase.checkKey} should
|
||||
switch its uid/gid to the ones of the authenticated user.
|
||||
"""
|
||||
keyFile = self.sshDir.child("authorized_keys")
|
||||
keyFile.setContent(self.content)
|
||||
# Fake permission error by changing the mode
|
||||
keyFile.chmod(0o000)
|
||||
self.addCleanup(keyFile.chmod, 0o777)
|
||||
# And restore the right mode when seteuid is called
|
||||
savedSeteuid = self.mockos.seteuid
|
||||
|
||||
def seteuid(euid):
|
||||
keyFile.chmod(0o777)
|
||||
return savedSeteuid(euid)
|
||||
|
||||
self.mockos.euid = 2345
|
||||
self.mockos.egid = 1234
|
||||
self.patch(self.mockos, "seteuid", seteuid)
|
||||
self.patch(util, "os", self.mockos)
|
||||
user = UsernamePassword(b"user", b"password")
|
||||
user.blob = b"foobar"
|
||||
self.assertTrue(self.checker.checkKey(user))
|
||||
self.assertEqual(self.mockos.seteuidCalls, [0, 1, 0, 2345])
|
||||
self.assertEqual(self.mockos.setegidCalls, [2, 1234])
|
||||
|
||||
def test_requestAvatarId(self):
|
||||
"""
|
||||
L{SSHPublicKeyDatabase.requestAvatarId} should return the avatar id
|
||||
passed in if its C{_checkKey} method returns True.
|
||||
"""
|
||||
|
||||
def _checkKey(ignored):
|
||||
return True
|
||||
|
||||
self.patch(self.checker, "checkKey", _checkKey)
|
||||
credentials = SSHPrivateKey(
|
||||
b"test",
|
||||
b"ssh-rsa",
|
||||
keydata.publicRSA_openssh,
|
||||
b"foo",
|
||||
keys.Key.fromString(keydata.privateRSA_openssh).sign(b"foo"),
|
||||
)
|
||||
d = self.checker.requestAvatarId(credentials)
|
||||
|
||||
def _verify(avatarId):
|
||||
self.assertEqual(avatarId, b"test")
|
||||
|
||||
return d.addCallback(_verify)
|
||||
|
||||
def test_requestAvatarIdWithoutSignature(self):
|
||||
"""
|
||||
L{SSHPublicKeyDatabase.requestAvatarId} should raise L{ValidPublicKey}
|
||||
if the credentials represent a valid key without a signature. This
|
||||
tells the user that the key is valid for login, but does not actually
|
||||
allow that user to do so without a signature.
|
||||
"""
|
||||
|
||||
def _checkKey(ignored):
|
||||
return True
|
||||
|
||||
self.patch(self.checker, "checkKey", _checkKey)
|
||||
credentials = SSHPrivateKey(
|
||||
b"test", b"ssh-rsa", keydata.publicRSA_openssh, None, None
|
||||
)
|
||||
d = self.checker.requestAvatarId(credentials)
|
||||
return self.assertFailure(d, ValidPublicKey)
|
||||
|
||||
def test_requestAvatarIdInvalidKey(self):
|
||||
"""
|
||||
If L{SSHPublicKeyDatabase.checkKey} returns False,
|
||||
C{_cbRequestAvatarId} should raise L{UnauthorizedLogin}.
|
||||
"""
|
||||
|
||||
def _checkKey(ignored):
|
||||
return False
|
||||
|
||||
self.patch(self.checker, "checkKey", _checkKey)
|
||||
d = self.checker.requestAvatarId(None)
|
||||
return self.assertFailure(d, UnauthorizedLogin)
|
||||
|
||||
def test_requestAvatarIdInvalidSignature(self):
|
||||
"""
|
||||
Valid keys with invalid signatures should cause
|
||||
L{SSHPublicKeyDatabase.requestAvatarId} to return a {UnauthorizedLogin}
|
||||
failure
|
||||
"""
|
||||
|
||||
def _checkKey(ignored):
|
||||
return True
|
||||
|
||||
self.patch(self.checker, "checkKey", _checkKey)
|
||||
credentials = SSHPrivateKey(
|
||||
b"test",
|
||||
b"ssh-rsa",
|
||||
keydata.publicRSA_openssh,
|
||||
b"foo",
|
||||
keys.Key.fromString(keydata.privateDSA_openssh).sign(b"foo"),
|
||||
)
|
||||
d = self.checker.requestAvatarId(credentials)
|
||||
return self.assertFailure(d, UnauthorizedLogin)
|
||||
|
||||
def test_requestAvatarIdNormalizeException(self):
|
||||
"""
|
||||
Exceptions raised while verifying the key should be normalized into an
|
||||
C{UnauthorizedLogin} failure.
|
||||
"""
|
||||
|
||||
def _checkKey(ignored):
|
||||
return True
|
||||
|
||||
self.patch(self.checker, "checkKey", _checkKey)
|
||||
credentials = SSHPrivateKey(b"test", None, b"blob", b"sigData", b"sig")
|
||||
d = self.checker.requestAvatarId(credentials)
|
||||
|
||||
def _verifyLoggedException(failure):
|
||||
errors = self.flushLoggedErrors(keys.BadKeyError)
|
||||
self.assertEqual(len(errors), 1)
|
||||
return failure
|
||||
|
||||
d.addErrback(_verifyLoggedException)
|
||||
return self.assertFailure(d, UnauthorizedLogin)
|
||||
|
||||
|
||||
class SSHProtocolCheckerTests(TestCase):
|
||||
"""
|
||||
Tests for L{SSHProtocolChecker}.
|
||||
"""
|
||||
|
||||
skip = dependencySkip
|
||||
|
||||
def test_registerChecker(self):
|
||||
"""
|
||||
L{SSHProcotolChecker.registerChecker} should add the given checker to
|
||||
the list of registered checkers.
|
||||
"""
|
||||
checker = checkers.SSHProtocolChecker()
|
||||
self.assertEqual(checker.credentialInterfaces, [])
|
||||
checker.registerChecker(
|
||||
checkers.SSHPublicKeyDatabase(),
|
||||
)
|
||||
self.assertEqual(checker.credentialInterfaces, [ISSHPrivateKey])
|
||||
self.assertIsInstance(
|
||||
checker.checkers[ISSHPrivateKey], checkers.SSHPublicKeyDatabase
|
||||
)
|
||||
|
||||
def test_registerCheckerWithInterface(self):
|
||||
"""
|
||||
If a specific interface is passed into
|
||||
L{SSHProtocolChecker.registerChecker}, that interface should be
|
||||
registered instead of what the checker specifies in
|
||||
credentialIntefaces.
|
||||
"""
|
||||
checker = checkers.SSHProtocolChecker()
|
||||
self.assertEqual(checker.credentialInterfaces, [])
|
||||
checker.registerChecker(checkers.SSHPublicKeyDatabase(), IUsernamePassword)
|
||||
self.assertEqual(checker.credentialInterfaces, [IUsernamePassword])
|
||||
self.assertIsInstance(
|
||||
checker.checkers[IUsernamePassword], checkers.SSHPublicKeyDatabase
|
||||
)
|
||||
|
||||
def test_requestAvatarId(self):
|
||||
"""
|
||||
L{SSHProtocolChecker.requestAvatarId} should defer to one if its
|
||||
registered checkers to authenticate a user.
|
||||
"""
|
||||
checker = checkers.SSHProtocolChecker()
|
||||
passwordDatabase = InMemoryUsernamePasswordDatabaseDontUse()
|
||||
passwordDatabase.addUser(b"test", b"test")
|
||||
checker.registerChecker(passwordDatabase)
|
||||
d = checker.requestAvatarId(UsernamePassword(b"test", b"test"))
|
||||
|
||||
def _callback(avatarId):
|
||||
self.assertEqual(avatarId, b"test")
|
||||
|
||||
return d.addCallback(_callback)
|
||||
|
||||
def test_requestAvatarIdWithNotEnoughAuthentication(self):
|
||||
"""
|
||||
If the client indicates that it is never satisfied, by always returning
|
||||
False from _areDone, then L{SSHProtocolChecker} should raise
|
||||
L{NotEnoughAuthentication}.
|
||||
"""
|
||||
checker = checkers.SSHProtocolChecker()
|
||||
|
||||
def _areDone(avatarId):
|
||||
return False
|
||||
|
||||
self.patch(checker, "areDone", _areDone)
|
||||
|
||||
passwordDatabase = InMemoryUsernamePasswordDatabaseDontUse()
|
||||
passwordDatabase.addUser(b"test", b"test")
|
||||
checker.registerChecker(passwordDatabase)
|
||||
d = checker.requestAvatarId(UsernamePassword(b"test", b"test"))
|
||||
return self.assertFailure(d, NotEnoughAuthentication)
|
||||
|
||||
def test_requestAvatarIdInvalidCredential(self):
|
||||
"""
|
||||
If the passed credentials aren't handled by any registered checker,
|
||||
L{SSHProtocolChecker} should raise L{UnhandledCredentials}.
|
||||
"""
|
||||
checker = checkers.SSHProtocolChecker()
|
||||
d = checker.requestAvatarId(UsernamePassword(b"test", b"test"))
|
||||
return self.assertFailure(d, UnhandledCredentials)
|
||||
|
||||
def test_areDone(self):
|
||||
"""
|
||||
The default L{SSHProcotolChecker.areDone} should simply return True.
|
||||
"""
|
||||
self.assertTrue(checkers.SSHProtocolChecker().areDone(None))
|
||||
|
||||
|
||||
class UNIXPasswordDatabaseTests(TestCase):
|
||||
"""
|
||||
Tests for L{UNIXPasswordDatabase}.
|
||||
"""
|
||||
|
||||
skip = cryptSkip or dependencySkip
|
||||
|
||||
def assertLoggedIn(self, d: Deferred[bytes], username: bytes) -> None:
|
||||
"""
|
||||
Assert that the L{Deferred} passed in is called back with the value
|
||||
'username'. This represents a valid login for this TestCase.
|
||||
|
||||
@param d: a L{Deferred} from an L{IChecker.requestAvatarId} method.
|
||||
"""
|
||||
self.assertEqual(self.successResultOf(d), username)
|
||||
|
||||
def test_defaultCheckers(self):
|
||||
"""
|
||||
L{UNIXPasswordDatabase} with no arguments has checks the C{pwd} database
|
||||
and then the C{spwd} database.
|
||||
"""
|
||||
checker = checkers.UNIXPasswordDatabase()
|
||||
|
||||
def crypted(username, password):
|
||||
salt = crypt.crypt(password, username)
|
||||
crypted = crypt.crypt(password, "$1$" + salt)
|
||||
return crypted
|
||||
|
||||
pwd = UserDatabase()
|
||||
pwd.addUser(
|
||||
"alice", crypted("alice", "password"), 1, 2, "foo", "/foo", "/bin/sh"
|
||||
)
|
||||
# x and * are convention for "look elsewhere for the password"
|
||||
pwd.addUser("bob", "x", 1, 2, "bar", "/bar", "/bin/sh")
|
||||
spwd = ShadowDatabase()
|
||||
spwd.addUser("alice", "wrong", 1, 2, 3, 4, 5, 6, 7)
|
||||
spwd.addUser("bob", crypted("bob", "password"), 8, 9, 10, 11, 12, 13, 14)
|
||||
|
||||
self.patch(checkers, "pwd", pwd)
|
||||
self.patch(checkers, "spwd", spwd)
|
||||
|
||||
mockos = MockOS()
|
||||
self.patch(util, "os", mockos)
|
||||
|
||||
mockos.euid = 2345
|
||||
mockos.egid = 1234
|
||||
|
||||
cred = UsernamePassword(b"alice", b"password")
|
||||
self.assertLoggedIn(checker.requestAvatarId(cred), b"alice")
|
||||
self.assertEqual(mockos.seteuidCalls, [])
|
||||
self.assertEqual(mockos.setegidCalls, [])
|
||||
cred.username = b"bob"
|
||||
self.assertLoggedIn(checker.requestAvatarId(cred), b"bob")
|
||||
self.assertEqual(mockos.seteuidCalls, [0, 2345])
|
||||
self.assertEqual(mockos.setegidCalls, [0, 1234])
|
||||
|
||||
def assertUnauthorizedLogin(self, d):
|
||||
"""
|
||||
Asserts that the L{Deferred} passed in is erred back with an
|
||||
L{UnauthorizedLogin} L{Failure}. This reprsents an invalid login for
|
||||
this TestCase.
|
||||
|
||||
NOTE: To work, this method's return value must be returned from the
|
||||
test method, or otherwise hooked up to the test machinery.
|
||||
|
||||
@param d: a L{Deferred} from an L{IChecker.requestAvatarId} method.
|
||||
@type d: L{Deferred}
|
||||
@rtype: L{None}
|
||||
"""
|
||||
self.failureResultOf(d, checkers.UnauthorizedLogin)
|
||||
|
||||
def test_passInCheckers(self):
|
||||
"""
|
||||
L{UNIXPasswordDatabase} takes a list of functions to check for UNIX
|
||||
user information.
|
||||
"""
|
||||
password = crypt.crypt("secret", "secret")
|
||||
userdb = UserDatabase()
|
||||
userdb.addUser("anybody", password, 1, 2, "foo", "/bar", "/bin/sh")
|
||||
checker = checkers.UNIXPasswordDatabase([userdb.getpwnam])
|
||||
self.assertLoggedIn(
|
||||
checker.requestAvatarId(UsernamePassword(b"anybody", b"secret")), b"anybody"
|
||||
)
|
||||
|
||||
def test_verifyPassword(self):
|
||||
"""
|
||||
If the encrypted password provided by the getpwnam function is valid
|
||||
(verified by the L{verifyCryptedPassword} function), we callback the
|
||||
C{requestAvatarId} L{Deferred} with the username.
|
||||
"""
|
||||
|
||||
def verifyCryptedPassword(crypted, pw):
|
||||
return crypted == pw
|
||||
|
||||
def getpwnam(username):
|
||||
return [username, username]
|
||||
|
||||
self.patch(checkers, "verifyCryptedPassword", verifyCryptedPassword)
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam])
|
||||
credential = UsernamePassword(b"username", b"username")
|
||||
self.assertLoggedIn(checker.requestAvatarId(credential), b"username")
|
||||
|
||||
def test_failOnKeyError(self):
|
||||
"""
|
||||
If the getpwnam function raises a KeyError, the login fails with an
|
||||
L{UnauthorizedLogin} exception.
|
||||
"""
|
||||
|
||||
def getpwnam(username):
|
||||
raise KeyError(username)
|
||||
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam])
|
||||
credential = UsernamePassword(b"username", b"password")
|
||||
self.assertUnauthorizedLogin(checker.requestAvatarId(credential))
|
||||
|
||||
def test_failOnBadPassword(self):
|
||||
"""
|
||||
If the verifyCryptedPassword function doesn't verify the password, the
|
||||
login fails with an L{UnauthorizedLogin} exception.
|
||||
"""
|
||||
|
||||
def verifyCryptedPassword(crypted, pw):
|
||||
return False
|
||||
|
||||
def getpwnam(username):
|
||||
return [username, b"password"]
|
||||
|
||||
self.patch(checkers, "verifyCryptedPassword", verifyCryptedPassword)
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam])
|
||||
credential = UsernamePassword(b"username", b"password")
|
||||
self.assertUnauthorizedLogin(checker.requestAvatarId(credential))
|
||||
|
||||
def test_loopThroughFunctions(self):
|
||||
"""
|
||||
UNIXPasswordDatabase.requestAvatarId loops through each getpwnam
|
||||
function associated with it and returns a L{Deferred} which fires with
|
||||
the result of the first one which returns a value other than None.
|
||||
ones do not verify the password.
|
||||
"""
|
||||
|
||||
def verifyCryptedPassword(crypted, pw):
|
||||
return crypted == pw
|
||||
|
||||
def getpwnam1(username):
|
||||
return [username, "not the password"]
|
||||
|
||||
def getpwnam2(username):
|
||||
return [username, "password"]
|
||||
|
||||
self.patch(checkers, "verifyCryptedPassword", verifyCryptedPassword)
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam1, getpwnam2])
|
||||
credential = UsernamePassword(b"username", b"password")
|
||||
self.assertLoggedIn(checker.requestAvatarId(credential), b"username")
|
||||
|
||||
def test_failOnSpecial(self):
|
||||
"""
|
||||
If the password returned by any function is C{""}, C{"x"}, or C{"*"} it
|
||||
is not compared against the supplied password. Instead it is skipped.
|
||||
"""
|
||||
pwd = UserDatabase()
|
||||
pwd.addUser("alice", "", 1, 2, "", "foo", "bar")
|
||||
pwd.addUser("bob", "x", 1, 2, "", "foo", "bar")
|
||||
pwd.addUser("carol", "*", 1, 2, "", "foo", "bar")
|
||||
self.patch(checkers, "pwd", pwd)
|
||||
|
||||
checker = checkers.UNIXPasswordDatabase([checkers._pwdGetByName])
|
||||
cred = UsernamePassword(b"alice", b"")
|
||||
self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
|
||||
|
||||
cred = UsernamePassword(b"bob", b"x")
|
||||
self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
|
||||
|
||||
cred = UsernamePassword(b"carol", b"*")
|
||||
self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
|
||||
|
||||
|
||||
class AuthorizedKeyFileReaderTests(TestCase):
|
||||
"""
|
||||
Tests for L{checkers.readAuthorizedKeyFile}
|
||||
"""
|
||||
|
||||
skip = dependencySkip
|
||||
|
||||
def test_ignoresComments(self):
|
||||
"""
|
||||
L{checkers.readAuthorizedKeyFile} does not attempt to turn comments
|
||||
into keys
|
||||
"""
|
||||
fileobj = BytesIO(
|
||||
b"# this comment is ignored\n"
|
||||
b"this is not\n"
|
||||
b"# this is again\n"
|
||||
b"and this is not"
|
||||
)
|
||||
result = checkers.readAuthorizedKeyFile(fileobj, lambda x: x)
|
||||
self.assertEqual([b"this is not", b"and this is not"], list(result))
|
||||
|
||||
def test_ignoresLeadingWhitespaceAndEmptyLines(self):
|
||||
"""
|
||||
L{checkers.readAuthorizedKeyFile} ignores leading whitespace in
|
||||
lines, as well as empty lines
|
||||
"""
|
||||
fileobj = BytesIO(
|
||||
b"""
|
||||
# ignore
|
||||
not ignored
|
||||
"""
|
||||
)
|
||||
result = checkers.readAuthorizedKeyFile(fileobj, parseKey=lambda x: x)
|
||||
self.assertEqual([b"not ignored"], list(result))
|
||||
|
||||
def test_ignoresUnparsableKeys(self):
|
||||
"""
|
||||
L{checkers.readAuthorizedKeyFile} does not raise an exception
|
||||
when a key fails to parse (raises a
|
||||
L{twisted.conch.ssh.keys.BadKeyError}), but rather just keeps going
|
||||
"""
|
||||
|
||||
def failOnSome(line):
|
||||
if line.startswith(b"f"):
|
||||
raise keys.BadKeyError("failed to parse")
|
||||
return line
|
||||
|
||||
fileobj = BytesIO(b"failed key\ngood key")
|
||||
result = checkers.readAuthorizedKeyFile(fileobj, parseKey=failOnSome)
|
||||
self.assertEqual([b"good key"], list(result))
|
||||
|
||||
|
||||
class InMemorySSHKeyDBTests(TestCase):
|
||||
"""
|
||||
Tests for L{checkers.InMemorySSHKeyDB}
|
||||
"""
|
||||
|
||||
skip = dependencySkip
|
||||
|
||||
def test_implementsInterface(self):
|
||||
"""
|
||||
L{checkers.InMemorySSHKeyDB} implements
|
||||
L{checkers.IAuthorizedKeysDB}
|
||||
"""
|
||||
keydb = checkers.InMemorySSHKeyDB({b"alice": [b"key"]})
|
||||
verifyObject(checkers.IAuthorizedKeysDB, keydb)
|
||||
|
||||
def test_noKeysForUnauthorizedUser(self):
|
||||
"""
|
||||
If the user is not in the mapping provided to
|
||||
L{checkers.InMemorySSHKeyDB}, an empty iterator is returned
|
||||
by L{checkers.InMemorySSHKeyDB.getAuthorizedKeys}
|
||||
"""
|
||||
keydb = checkers.InMemorySSHKeyDB({b"alice": [b"keys"]})
|
||||
self.assertEqual([], list(keydb.getAuthorizedKeys(b"bob")))
|
||||
|
||||
def test_allKeysForAuthorizedUser(self):
|
||||
"""
|
||||
If the user is in the mapping provided to
|
||||
L{checkers.InMemorySSHKeyDB}, an iterator with all the keys
|
||||
is returned by L{checkers.InMemorySSHKeyDB.getAuthorizedKeys}
|
||||
"""
|
||||
keydb = checkers.InMemorySSHKeyDB({b"alice": [b"a", b"b"]})
|
||||
self.assertEqual([b"a", b"b"], list(keydb.getAuthorizedKeys(b"alice")))
|
||||
|
||||
|
||||
class UNIXAuthorizedKeysFilesTests(TestCase):
|
||||
"""
|
||||
Tests for L{checkers.UNIXAuthorizedKeysFiles}.
|
||||
"""
|
||||
|
||||
skip = dependencySkip
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.path = FilePath(self.mktemp())
|
||||
assert isinstance(self.path.path, str)
|
||||
self.path.makedirs()
|
||||
|
||||
self.userdb = UserDatabase()
|
||||
self.userdb.addUser(
|
||||
"alice",
|
||||
"password",
|
||||
1,
|
||||
2,
|
||||
"alice lastname",
|
||||
self.path.path,
|
||||
"/bin/shell",
|
||||
)
|
||||
|
||||
self.sshDir = self.path.child(".ssh")
|
||||
self.sshDir.makedirs()
|
||||
authorizedKeys = self.sshDir.child("authorized_keys")
|
||||
authorizedKeys.setContent(b"key 1\nkey 2")
|
||||
|
||||
self.expectedKeys = [b"key 1", b"key 2"]
|
||||
|
||||
def test_implementsInterface(self):
|
||||
"""
|
||||
L{checkers.UNIXAuthorizedKeysFiles} implements
|
||||
L{checkers.IAuthorizedKeysDB}.
|
||||
"""
|
||||
keydb = checkers.UNIXAuthorizedKeysFiles(self.userdb)
|
||||
verifyObject(checkers.IAuthorizedKeysDB, keydb)
|
||||
|
||||
def test_noKeysForUnauthorizedUser(self):
|
||||
"""
|
||||
If the user is not in the user database provided to
|
||||
L{checkers.UNIXAuthorizedKeysFiles}, an empty iterator is returned
|
||||
by L{checkers.UNIXAuthorizedKeysFiles.getAuthorizedKeys}.
|
||||
"""
|
||||
keydb = checkers.UNIXAuthorizedKeysFiles(self.userdb, parseKey=lambda x: x)
|
||||
self.assertEqual([], list(keydb.getAuthorizedKeys(b"bob")))
|
||||
|
||||
def test_allKeysInAllAuthorizedFilesForAuthorizedUser(self):
|
||||
"""
|
||||
If the user is in the user database provided to
|
||||
L{checkers.UNIXAuthorizedKeysFiles}, an iterator with all the keys in
|
||||
C{~/.ssh/authorized_keys} and C{~/.ssh/authorized_keys2} is returned
|
||||
by L{checkers.UNIXAuthorizedKeysFiles.getAuthorizedKeys}.
|
||||
"""
|
||||
self.sshDir.child("authorized_keys2").setContent(b"key 3")
|
||||
keydb = checkers.UNIXAuthorizedKeysFiles(self.userdb, parseKey=lambda x: x)
|
||||
self.assertEqual(
|
||||
self.expectedKeys + [b"key 3"], list(keydb.getAuthorizedKeys(b"alice"))
|
||||
)
|
||||
|
||||
def test_ignoresNonexistantFile(self):
|
||||
"""
|
||||
L{checkers.UNIXAuthorizedKeysFiles.getAuthorizedKeys} returns only
|
||||
the keys in C{~/.ssh/authorized_keys} and C{~/.ssh/authorized_keys2}
|
||||
if they exist.
|
||||
"""
|
||||
keydb = checkers.UNIXAuthorizedKeysFiles(self.userdb, parseKey=lambda x: x)
|
||||
self.assertEqual(self.expectedKeys, list(keydb.getAuthorizedKeys(b"alice")))
|
||||
|
||||
def test_ignoresUnreadableFile(self):
|
||||
"""
|
||||
L{checkers.UNIXAuthorizedKeysFiles.getAuthorizedKeys} returns only
|
||||
the keys in C{~/.ssh/authorized_keys} and C{~/.ssh/authorized_keys2}
|
||||
if they are readable.
|
||||
"""
|
||||
self.sshDir.child("authorized_keys2").makedirs()
|
||||
keydb = checkers.UNIXAuthorizedKeysFiles(self.userdb, parseKey=lambda x: x)
|
||||
self.assertEqual(self.expectedKeys, list(keydb.getAuthorizedKeys(b"alice")))
|
||||
|
||||
|
||||
_KeyDB = namedtuple("_KeyDB", ["getAuthorizedKeys"])
|
||||
|
||||
|
||||
class _DummyException(Exception):
|
||||
"""
|
||||
Fake exception to be used for testing.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SSHPublicKeyCheckerTests(TestCase):
|
||||
"""
|
||||
Tests for L{checkers.SSHPublicKeyChecker}.
|
||||
"""
|
||||
|
||||
skip = dependencySkip
|
||||
|
||||
def setUp(self):
|
||||
self.credentials = SSHPrivateKey(
|
||||
b"alice",
|
||||
b"ssh-rsa",
|
||||
keydata.publicRSA_openssh,
|
||||
b"foo",
|
||||
keys.Key.fromString(keydata.privateRSA_openssh).sign(b"foo"),
|
||||
)
|
||||
self.keydb = _KeyDB(lambda _: [keys.Key.fromString(keydata.publicRSA_openssh)])
|
||||
self.checker = checkers.SSHPublicKeyChecker(self.keydb)
|
||||
|
||||
def test_credentialsWithoutSignature(self):
|
||||
"""
|
||||
Calling L{checkers.SSHPublicKeyChecker.requestAvatarId} with
|
||||
credentials that do not have a signature fails with L{ValidPublicKey}.
|
||||
"""
|
||||
self.credentials.signature = None
|
||||
self.failureResultOf(
|
||||
self.checker.requestAvatarId(self.credentials), ValidPublicKey
|
||||
)
|
||||
|
||||
def test_credentialsWithBadKey(self):
|
||||
"""
|
||||
Calling L{checkers.SSHPublicKeyChecker.requestAvatarId} with
|
||||
credentials that have a bad key fails with L{keys.BadKeyError}.
|
||||
"""
|
||||
self.credentials.blob = b""
|
||||
self.failureResultOf(
|
||||
self.checker.requestAvatarId(self.credentials), keys.BadKeyError
|
||||
)
|
||||
|
||||
def test_credentialsNoMatchingKey(self):
|
||||
"""
|
||||
If L{checkers.IAuthorizedKeysDB.getAuthorizedKeys} returns no keys
|
||||
that match the credentials,
|
||||
L{checkers.SSHPublicKeyChecker.requestAvatarId} fails with
|
||||
L{UnauthorizedLogin}.
|
||||
"""
|
||||
self.credentials.blob = keydata.publicDSA_openssh
|
||||
self.failureResultOf(
|
||||
self.checker.requestAvatarId(self.credentials), UnauthorizedLogin
|
||||
)
|
||||
|
||||
def test_credentialsInvalidSignature(self):
|
||||
"""
|
||||
Calling L{checkers.SSHPublicKeyChecker.requestAvatarId} with
|
||||
credentials that are incorrectly signed fails with
|
||||
L{UnauthorizedLogin}.
|
||||
"""
|
||||
self.credentials.signature = keys.Key.fromString(
|
||||
keydata.privateDSA_openssh
|
||||
).sign(b"foo")
|
||||
self.failureResultOf(
|
||||
self.checker.requestAvatarId(self.credentials), UnauthorizedLogin
|
||||
)
|
||||
|
||||
def test_failureVerifyingKey(self):
|
||||
"""
|
||||
If L{keys.Key.verify} raises an exception,
|
||||
L{checkers.SSHPublicKeyChecker.requestAvatarId} fails with
|
||||
L{UnauthorizedLogin}.
|
||||
"""
|
||||
|
||||
def fail(*args, **kwargs):
|
||||
raise _DummyException()
|
||||
|
||||
self.patch(keys.Key, "verify", fail)
|
||||
|
||||
self.failureResultOf(
|
||||
self.checker.requestAvatarId(self.credentials), UnauthorizedLogin
|
||||
)
|
||||
self.flushLoggedErrors(_DummyException)
|
||||
|
||||
def test_usernameReturnedOnSuccess(self):
|
||||
"""
|
||||
L{checker.SSHPublicKeyChecker.requestAvatarId}, if successful,
|
||||
callbacks with the username.
|
||||
"""
|
||||
d = self.checker.requestAvatarId(self.credentials)
|
||||
self.assertEqual(b"alice", self.successResultOf(d))
|
||||
@@ -0,0 +1,725 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.scripts.ckeygen}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from io import StringIO
|
||||
from typing import Callable
|
||||
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
from twisted.conch.test.keydata import (
|
||||
privateECDSA_openssh,
|
||||
privateEd25519_openssh_new,
|
||||
privateRSA_openssh,
|
||||
privateRSA_openssh_encrypted,
|
||||
publicRSA_openssh,
|
||||
)
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if requireModule("cryptography"):
|
||||
from twisted.conch.scripts.ckeygen import (
|
||||
_getKeyOrDefault,
|
||||
_saveKey,
|
||||
changePassPhrase,
|
||||
displayPublicKey,
|
||||
enumrepresentation,
|
||||
printFingerprint,
|
||||
)
|
||||
from twisted.conch.ssh.keys import (
|
||||
BadFingerPrintFormat,
|
||||
BadKeyError,
|
||||
FingerprintFormats,
|
||||
Key,
|
||||
)
|
||||
else:
|
||||
skip = "cryptography required for twisted.conch.scripts.ckeygen"
|
||||
|
||||
|
||||
def makeGetpass(*passphrases: str) -> Callable[[object], str]:
|
||||
"""
|
||||
Return a callable to patch C{getpass.getpass}. Yields a passphrase each
|
||||
time called. Use case is to provide an old, then new passphrase(s) as if
|
||||
requested interactively.
|
||||
|
||||
@param passphrases: The list of passphrases returned, one per each call.
|
||||
|
||||
@return: A callable to patch C{getpass.getpass}.
|
||||
"""
|
||||
passphrasesIter = iter(passphrases)
|
||||
|
||||
def fakeGetpass(_: object) -> str:
|
||||
return next(passphrasesIter)
|
||||
|
||||
return fakeGetpass
|
||||
|
||||
|
||||
class KeyGenTests(TestCase):
|
||||
"""
|
||||
Tests for various functions used to implement the I{ckeygen} script.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Patch C{sys.stdout} so tests can make assertions about what's printed.
|
||||
"""
|
||||
self.stdout = StringIO()
|
||||
self.patch(sys, "stdout", self.stdout)
|
||||
|
||||
def _testrun(
|
||||
self,
|
||||
keyType: str,
|
||||
keySize: str | None = None,
|
||||
privateKeySubtype: str | None = None,
|
||||
) -> None:
|
||||
filename = self.mktemp()
|
||||
args = ["ckeygen", "-t", keyType, "-f", filename, "--no-passphrase"]
|
||||
if keySize is not None:
|
||||
args.extend(["-b", keySize])
|
||||
if privateKeySubtype is not None:
|
||||
args.extend(["--private-key-subtype", privateKeySubtype])
|
||||
subprocess.call(args)
|
||||
privKey = Key.fromFile(filename)
|
||||
pubKey = Key.fromFile(filename + ".pub")
|
||||
if keyType == "ecdsa":
|
||||
self.assertEqual(privKey.type(), "EC")
|
||||
elif keyType == "ed25519":
|
||||
self.assertEqual(privKey.type(), "Ed25519")
|
||||
else:
|
||||
self.assertEqual(privKey.type(), keyType.upper())
|
||||
self.assertTrue(pubKey.isPublic())
|
||||
|
||||
def test_keygeneration(self) -> None:
|
||||
self._testrun("ecdsa", "384")
|
||||
self._testrun("ecdsa", "384", privateKeySubtype="v1")
|
||||
self._testrun("ecdsa")
|
||||
self._testrun("ecdsa", privateKeySubtype="v1")
|
||||
self._testrun("ed25519")
|
||||
self._testrun("dsa", "2048")
|
||||
self._testrun("dsa", "2048", privateKeySubtype="v1")
|
||||
self._testrun("dsa")
|
||||
self._testrun("dsa", privateKeySubtype="v1")
|
||||
self._testrun("rsa", "2048")
|
||||
self._testrun("rsa", "2048", privateKeySubtype="v1")
|
||||
self._testrun("rsa")
|
||||
self._testrun("rsa", privateKeySubtype="v1")
|
||||
|
||||
def test_runBadKeytype(self) -> None:
|
||||
filename = self.mktemp()
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
subprocess.check_call(["ckeygen", "-t", "foo", "-f", filename])
|
||||
|
||||
def test_enumrepresentation(self) -> None:
|
||||
"""
|
||||
L{enumrepresentation} takes a dictionary as input and returns a
|
||||
dictionary with its attributes changed to enum representation.
|
||||
"""
|
||||
options = enumrepresentation({"format": "md5-hex"})
|
||||
self.assertIs(options["format"], FingerprintFormats.MD5_HEX)
|
||||
|
||||
def test_enumrepresentationsha256(self) -> None:
|
||||
"""
|
||||
Test for format L{FingerprintFormats.SHA256-BASE64}.
|
||||
"""
|
||||
options = enumrepresentation({"format": "sha256-base64"})
|
||||
self.assertIs(options["format"], FingerprintFormats.SHA256_BASE64)
|
||||
|
||||
def test_enumrepresentationBadFormat(self) -> None:
|
||||
"""
|
||||
Test for unsupported fingerprint format
|
||||
"""
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
enumrepresentation({"format": "sha-base64"})
|
||||
self.assertEqual(
|
||||
"Unsupported fingerprint format: sha-base64", em.exception.args[0]
|
||||
)
|
||||
|
||||
def test_printFingerprint(self) -> None:
|
||||
"""
|
||||
L{printFingerprint} writes a line to standard out giving the number of
|
||||
bits of the key, its fingerprint, and the basename of the file from it
|
||||
was read.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
printFingerprint({"filename": filename, "format": "md5-hex"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"2048 85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da temp\n",
|
||||
)
|
||||
|
||||
def test_printFingerprintsha256(self) -> None:
|
||||
"""
|
||||
L{printFigerprint} will print key fingerprint in
|
||||
L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
printFingerprint({"filename": filename, "format": "sha256-base64"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"2048 FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI= temp\n",
|
||||
)
|
||||
|
||||
def test_printFingerprintBadFingerPrintFormat(self) -> None:
|
||||
"""
|
||||
L{printFigerprint} raises C{keys.BadFingerprintFormat} when unsupported
|
||||
formats are requested.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
printFingerprint({"filename": filename, "format": "sha-base64"})
|
||||
self.assertEqual(
|
||||
"Unsupported fingerprint format: sha-base64", em.exception.args[0]
|
||||
)
|
||||
|
||||
def test_printFingerprintSuffixAppended(self) -> None:
|
||||
"""
|
||||
L{printFingerprint} checks if the filename with the '.pub' suffix
|
||||
exists in ~/.ssh.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename + ".pub").setContent(publicRSA_openssh)
|
||||
printFingerprint({"filename": filename, "format": "md5-hex"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"2048 85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da temp.pub\n",
|
||||
)
|
||||
|
||||
def test_saveKey(self) -> None:
|
||||
"""
|
||||
L{_saveKey} writes the private and public parts of a key to two
|
||||
different files and writes a report of this to standard out.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_rsa").path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {"filename": filename, "pass": "passphrase", "format": "md5-hex"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da\n" % (filename, filename),
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_rsa").getContent(), None, "passphrase"), key
|
||||
)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child("id_rsa.pub").getContent()), key.public()
|
||||
)
|
||||
|
||||
def test_saveKeyECDSA(self) -> None:
|
||||
"""
|
||||
L{_saveKey} writes the private and public parts of a key to two
|
||||
different files and writes a report of this to standard out.
|
||||
Test with ECDSA key.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_ecdsa").path
|
||||
key = Key.fromString(privateECDSA_openssh)
|
||||
_saveKey(key, {"filename": filename, "pass": "passphrase", "format": "md5-hex"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"1e:ab:83:a6:f2:04:22:99:7c:64:14:d2:ab:fa:f5:16\n" % (filename, filename),
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_ecdsa").getContent(), None, "passphrase"), key
|
||||
)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child("id_ecdsa.pub").getContent()), key.public()
|
||||
)
|
||||
|
||||
def test_saveKeyEd25519(self) -> None:
|
||||
"""
|
||||
L{_saveKey} writes the private and public parts of a key to two
|
||||
different files and writes a report of this to standard out.
|
||||
Test with Ed25519 key.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_ed25519").path
|
||||
key = Key.fromString(privateEd25519_openssh_new)
|
||||
_saveKey(key, {"filename": filename, "pass": "passphrase", "format": "md5-hex"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"ab:ee:c8:ed:e5:01:1b:45:b7:8d:b2:f0:8f:61:1c:14\n" % (filename, filename),
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_ed25519").getContent(), None, "passphrase"),
|
||||
key,
|
||||
)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child("id_ed25519.pub").getContent()), key.public()
|
||||
)
|
||||
|
||||
def test_saveKeysha256(self) -> None:
|
||||
"""
|
||||
L{_saveKey} will generate key fingerprint in
|
||||
L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_rsa").path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(
|
||||
key, {"filename": filename, "pass": "passphrase", "format": "sha256-base64"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=SHA256_BASE64> is:\n"
|
||||
"FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=\n" % (filename, filename),
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_rsa").getContent(), None, "passphrase"), key
|
||||
)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child("id_rsa.pub").getContent()), key.public()
|
||||
)
|
||||
|
||||
def test_saveKeyBadFingerPrintformat(self) -> None:
|
||||
"""
|
||||
L{_saveKey} raises C{keys.BadFingerprintFormat} when unsupported
|
||||
formats are requested.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_rsa").path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
with self.assertRaises(BadFingerPrintFormat) as em:
|
||||
_saveKey(
|
||||
key,
|
||||
{"filename": filename, "pass": "passphrase", "format": "sha-base64"},
|
||||
)
|
||||
self.assertEqual(
|
||||
"Unsupported fingerprint format: sha-base64", em.exception.args[0]
|
||||
)
|
||||
|
||||
def test_saveKeyEmptyPassphrase(self) -> None:
|
||||
"""
|
||||
L{_saveKey} will choose an empty string for the passphrase if
|
||||
no-passphrase is C{True}.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_rsa").path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(
|
||||
key, {"filename": filename, "no-passphrase": True, "format": "md5-hex"}
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_rsa").getContent(), None, b""), key
|
||||
)
|
||||
|
||||
def test_saveKeyECDSAEmptyPassphrase(self) -> None:
|
||||
"""
|
||||
L{_saveKey} will choose an empty string for the passphrase if
|
||||
no-passphrase is C{True}.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_ecdsa").path
|
||||
key = Key.fromString(privateECDSA_openssh)
|
||||
_saveKey(
|
||||
key, {"filename": filename, "no-passphrase": True, "format": "md5-hex"}
|
||||
)
|
||||
self.assertEqual(key.fromString(base.child("id_ecdsa").getContent(), None), key)
|
||||
|
||||
def test_saveKeyEd25519EmptyPassphrase(self) -> None:
|
||||
"""
|
||||
L{_saveKey} will choose an empty string for the passphrase if
|
||||
no-passphrase is C{True}.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_ed25519").path
|
||||
key = Key.fromString(privateEd25519_openssh_new)
|
||||
_saveKey(
|
||||
key, {"filename": filename, "no-passphrase": True, "format": "md5-hex"}
|
||||
)
|
||||
self.assertEqual(
|
||||
key.fromString(base.child("id_ed25519").getContent(), None), key
|
||||
)
|
||||
|
||||
def test_saveKeyNoFilename(self) -> None:
|
||||
"""
|
||||
When no path is specified, it will ask for the path used to store the
|
||||
key.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
keyPath = base.child("custom_key").path
|
||||
input_prompts: list[str] = []
|
||||
|
||||
import twisted.conch.scripts.ckeygen
|
||||
|
||||
def mock_input(*args: object) -> str:
|
||||
input_prompts.append("")
|
||||
return ""
|
||||
|
||||
self.patch(twisted.conch.scripts.ckeygen, "_inputSaveFile", lambda _: keyPath)
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(
|
||||
key,
|
||||
{"filename": None, "no-passphrase": True, "format": "md5-hex"},
|
||||
mock_input,
|
||||
)
|
||||
|
||||
persistedKeyContent = base.child("custom_key").getContent()
|
||||
persistedKey = key.fromString(persistedKeyContent, None, b"")
|
||||
self.assertEqual(key, persistedKey)
|
||||
|
||||
def test_saveKeyFileExists(self) -> None:
|
||||
"""
|
||||
When the specified file exists, it will ask the user for confirmation
|
||||
before overwriting.
|
||||
"""
|
||||
|
||||
def mock_input(*args: object) -> list[str]:
|
||||
return ["n"]
|
||||
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
keyPath = base.child("custom_key").path
|
||||
|
||||
self.patch(os.path, "exists", lambda _: True)
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
options = {"filename": keyPath, "no-passphrase": True, "format": "md5-hex"}
|
||||
self.assertRaises(SystemExit, _saveKey, key, options, mock_input)
|
||||
|
||||
def test_saveKeySubtypeV1(self) -> None:
|
||||
"""
|
||||
L{_saveKey} can be told to write the new private key file in OpenSSH
|
||||
v1 format.
|
||||
"""
|
||||
base = FilePath(self.mktemp())
|
||||
base.makedirs()
|
||||
filename = base.child("id_rsa").path
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(
|
||||
key,
|
||||
{
|
||||
"filename": filename,
|
||||
"pass": "passphrase",
|
||||
"format": "md5-hex",
|
||||
"private-key-subtype": "v1",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue(),
|
||||
"Your identification has been saved in %s\n"
|
||||
"Your public key has been saved in %s.pub\n"
|
||||
"The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
|
||||
"85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da\n" % (filename, filename),
|
||||
)
|
||||
privateKeyContent = base.child("id_rsa").getContent()
|
||||
self.assertEqual(key.fromString(privateKeyContent, None, "passphrase"), key)
|
||||
self.assertTrue(
|
||||
privateKeyContent.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----\n")
|
||||
)
|
||||
self.assertEqual(
|
||||
Key.fromString(base.child("id_rsa.pub").getContent()), key.public()
|
||||
)
|
||||
|
||||
def test_displayPublicKey(self) -> None:
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
displayPublicKey({"filename": filename})
|
||||
displayed = self.stdout.getvalue().strip("\n").encode("ascii")
|
||||
self.assertEqual(displayed, pubKey.toString("openssh"))
|
||||
|
||||
def test_displayPublicKeyEncrypted(self) -> None:
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key using the given passphrase when it's encrypted.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
displayPublicKey({"filename": filename, "pass": "encrypted"})
|
||||
displayed = self.stdout.getvalue().strip("\n").encode("ascii")
|
||||
self.assertEqual(displayed, pubKey.toString("openssh"))
|
||||
|
||||
def test_displayPublicKeyEncryptedPassphrasePrompt(self) -> None:
|
||||
"""
|
||||
L{displayPublicKey} prints out the public key associated with a given
|
||||
private key, asking for the passphrase when it's encrypted.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
pubKey = Key.fromString(publicRSA_openssh)
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
self.patch(getpass, "getpass", lambda x: "encrypted")
|
||||
displayPublicKey({"filename": filename})
|
||||
displayed = self.stdout.getvalue().strip("\n").encode("ascii")
|
||||
self.assertEqual(displayed, pubKey.toString("openssh"))
|
||||
|
||||
def test_displayPublicKeyWrongPassphrase(self) -> None:
|
||||
"""
|
||||
L{displayPublicKey} fails with a L{BadKeyError} when trying to decrypt
|
||||
an encrypted key with the wrong password.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
self.assertRaises(
|
||||
BadKeyError, displayPublicKey, {"filename": filename, "pass": "wrong"}
|
||||
)
|
||||
|
||||
def test_changePassphrase(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a
|
||||
private key interactively.
|
||||
"""
|
||||
oldNewConfirm = makeGetpass("encrypted", "newpass", "newpass")
|
||||
self.patch(getpass, "getpass", oldNewConfirm)
|
||||
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase({"filename": filename})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip("\n"),
|
||||
"Your identification has been saved with the new passphrase.",
|
||||
)
|
||||
self.assertNotEqual(
|
||||
privateRSA_openssh_encrypted, FilePath(filename).getContent()
|
||||
)
|
||||
|
||||
def test_changePassphraseWithOld(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a
|
||||
private key, providing the old passphrase and prompting for new one.
|
||||
"""
|
||||
newConfirm = makeGetpass("newpass", "newpass")
|
||||
self.patch(getpass, "getpass", newConfirm)
|
||||
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase({"filename": filename, "pass": "encrypted"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip("\n"),
|
||||
"Your identification has been saved with the new passphrase.",
|
||||
)
|
||||
self.assertNotEqual(
|
||||
privateRSA_openssh_encrypted, FilePath(filename).getContent()
|
||||
)
|
||||
|
||||
def test_changePassphraseWithBoth(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} allows a user to change the passphrase of a private
|
||||
key by providing both old and new passphrases without prompting.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase(
|
||||
{"filename": filename, "pass": "encrypted", "newpass": "newencrypt"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip("\n"),
|
||||
"Your identification has been saved with the new passphrase.",
|
||||
)
|
||||
self.assertNotEqual(
|
||||
privateRSA_openssh_encrypted, FilePath(filename).getContent()
|
||||
)
|
||||
|
||||
def test_changePassphraseWrongPassphrase(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} exits if passed an invalid old passphrase when
|
||||
trying to change the passphrase of a private key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase, {"filename": filename, "pass": "wrong"}
|
||||
)
|
||||
self.assertEqual(
|
||||
"Could not change passphrase: old passphrase error", str(error)
|
||||
)
|
||||
self.assertEqual(privateRSA_openssh_encrypted, FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphraseEmptyGetPass(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} exits if no passphrase is specified for the
|
||||
C{getpass} call and the key is encrypted.
|
||||
"""
|
||||
self.patch(getpass, "getpass", makeGetpass(""))
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
error = self.assertRaises(SystemExit, changePassPhrase, {"filename": filename})
|
||||
self.assertEqual(
|
||||
"Could not change passphrase: Passphrase must be provided "
|
||||
"for an encrypted key",
|
||||
str(error),
|
||||
)
|
||||
self.assertEqual(privateRSA_openssh_encrypted, FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphraseBadKey(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} exits if the file specified points to an invalid
|
||||
key.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(b"foobar")
|
||||
error = self.assertRaises(SystemExit, changePassPhrase, {"filename": filename})
|
||||
|
||||
expected = "Could not change passphrase: cannot " "guess the type of b'foobar'"
|
||||
self.assertEqual(expected, str(error))
|
||||
self.assertEqual(b"foobar", FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphraseCreateError(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} doesn't modify the key file if an unexpected error
|
||||
happens when trying to create the key with the new passphrase.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
|
||||
def toString(*args: object, **kwargs: object) -> NoReturn:
|
||||
raise RuntimeError("oops")
|
||||
|
||||
self.patch(Key, "toString", toString)
|
||||
|
||||
error = self.assertRaises(
|
||||
SystemExit,
|
||||
changePassPhrase,
|
||||
{"filename": filename, "newpass": "newencrypt"},
|
||||
)
|
||||
|
||||
self.assertEqual("Could not change passphrase: oops", str(error))
|
||||
|
||||
self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphraseEmptyStringError(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} doesn't modify the key file if C{toString} returns
|
||||
an empty string.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh)
|
||||
|
||||
def toString(*args: object, **kwargs: object) -> str:
|
||||
return ""
|
||||
|
||||
self.patch(Key, "toString", toString)
|
||||
|
||||
error = self.assertRaises(
|
||||
SystemExit,
|
||||
changePassPhrase,
|
||||
{"filename": filename, "newpass": "newencrypt"},
|
||||
)
|
||||
|
||||
expected = "Could not change passphrase: cannot guess the type of b''"
|
||||
self.assertEqual(expected, str(error))
|
||||
|
||||
self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphrasePublicKey(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} exits when trying to change the passphrase on a
|
||||
public key, and doesn't change the file.
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(publicRSA_openssh)
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase, {"filename": filename, "newpass": "pass"}
|
||||
)
|
||||
self.assertEqual("Could not change passphrase: key not encrypted", str(error))
|
||||
self.assertEqual(publicRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
def test_changePassphraseSubtypeV1(self) -> None:
|
||||
"""
|
||||
L{changePassPhrase} can be told to write the new private key file in
|
||||
OpenSSH v1 format.
|
||||
"""
|
||||
oldNewConfirm = makeGetpass("encrypted", "newpass", "newpass")
|
||||
self.patch(getpass, "getpass", oldNewConfirm)
|
||||
|
||||
filename = self.mktemp()
|
||||
FilePath(filename).setContent(privateRSA_openssh_encrypted)
|
||||
|
||||
changePassPhrase({"filename": filename, "private-key-subtype": "v1"})
|
||||
self.assertEqual(
|
||||
self.stdout.getvalue().strip("\n"),
|
||||
"Your identification has been saved with the new passphrase.",
|
||||
)
|
||||
privateKeyContent = FilePath(filename).getContent()
|
||||
self.assertNotEqual(privateRSA_openssh_encrypted, privateKeyContent)
|
||||
self.assertTrue(
|
||||
privateKeyContent.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----\n")
|
||||
)
|
||||
|
||||
def test_useDefaultForKey(self) -> None:
|
||||
"""
|
||||
L{options} will default to "~/.ssh/id_rsa" if the user doesn't
|
||||
specify a key.
|
||||
"""
|
||||
input_prompts: list[str] = []
|
||||
|
||||
def mock_input(*args: object) -> str:
|
||||
input_prompts.append("")
|
||||
return ""
|
||||
|
||||
options = {"filename": ""}
|
||||
|
||||
filename = _getKeyOrDefault(options, mock_input)
|
||||
self.assertEqual(
|
||||
options["filename"],
|
||||
"",
|
||||
)
|
||||
# Resolved path is an RSA key inside .ssh dir.
|
||||
self.assertTrue(filename.endswith(os.path.join(".ssh", "id_rsa")))
|
||||
# The user is prompted once to enter the path, since no path was
|
||||
# provided via CLI.
|
||||
self.assertEqual(1, len(input_prompts))
|
||||
self.assertEqual([""], input_prompts)
|
||||
|
||||
def test_displayPublicKeyHandleFileNotFound(self) -> None:
|
||||
"""
|
||||
Ensure FileNotFoundError is handled, whether the user has supplied
|
||||
a bad path, or has no key at the default path.
|
||||
"""
|
||||
options = {"filename": "/foo/bar"}
|
||||
exc = self.assertRaises(SystemExit, displayPublicKey, options)
|
||||
self.assertIn("could not be opened, please specify a file.", exc.args[0])
|
||||
|
||||
def test_changePassPhraseHandleFileNotFound(self) -> None:
|
||||
"""
|
||||
Ensure FileNotFoundError is handled for an invalid filename.
|
||||
"""
|
||||
options = {"filename": "/foo/bar"}
|
||||
exc = self.assertRaises(SystemExit, changePassPhrase, options)
|
||||
self.assertIn("could not be opened, please specify a file.", exc.args[0])
|
||||
|
||||
def test_printFingerprintHandleFileNotFound(self) -> None:
|
||||
"""
|
||||
Ensure FileNotFoundError is handled for an invalid filename.
|
||||
"""
|
||||
options = {"filename": "/foo/bar", "format": "md5-hex"}
|
||||
exc = self.assertRaises(SystemExit, printFingerprint, options)
|
||||
self.assertIn("could not be opened, please specify a file.", exc.args[0])
|
||||
@@ -0,0 +1,766 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_conch -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from itertools import count
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.test.keydata import privateRSA_openssh, publicRSA_openssh
|
||||
from twisted.conch.test.test_ssh import ConchTestRealm
|
||||
from twisted.cred import portal
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.error import ProcessExitedAlready
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.python import filepath, log, runtime
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.procutils import which
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.test.testutils import HAS_IPV6, skipWithoutIPv6
|
||||
from twisted.trial.unittest import SkipTest, TestCase
|
||||
|
||||
try:
|
||||
from twisted.conch.test.test_ssh import (
|
||||
ConchTestServerFactory,
|
||||
conchTestPublicKeyChecker,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch.avatar import ConchUser
|
||||
from twisted.conch.ssh.session import ISession, SSHSession, wrapProtocol
|
||||
else:
|
||||
from twisted.conch.interfaces import ISession
|
||||
|
||||
class ConchUser: # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
from twisted.conch.scripts.conch import SSHSession as _StdioInteractingSession
|
||||
except ImportError as e:
|
||||
StdioInteractingSession = None
|
||||
_reason = str(e)
|
||||
del e
|
||||
else:
|
||||
StdioInteractingSession = _StdioInteractingSession
|
||||
|
||||
|
||||
class FakeStdio:
|
||||
"""
|
||||
A fake for testing L{twisted.conch.scripts.conch.SSHSession.eofReceived} and
|
||||
L{twisted.conch.scripts.cftp.SSHSession.eofReceived}.
|
||||
|
||||
@ivar writeConnLost: A flag which records whether L{loserWriteConnection}
|
||||
has been called.
|
||||
"""
|
||||
|
||||
writeConnLost = False
|
||||
|
||||
def loseWriteConnection(self):
|
||||
"""
|
||||
Record the call to loseWriteConnection.
|
||||
"""
|
||||
self.writeConnLost = True
|
||||
|
||||
|
||||
class StdioInteractingSessionTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.conch.scripts.conch.SSHSession}.
|
||||
"""
|
||||
|
||||
if StdioInteractingSession is None:
|
||||
skip = _reason
|
||||
|
||||
def test_eofReceived(self):
|
||||
"""
|
||||
L{twisted.conch.scripts.conch.SSHSession.eofReceived} loses the
|
||||
write half of its stdio connection.
|
||||
"""
|
||||
stdio = FakeStdio()
|
||||
channel = StdioInteractingSession()
|
||||
channel.stdio = stdio
|
||||
channel.eofReceived()
|
||||
self.assertTrue(stdio.writeConnLost)
|
||||
|
||||
|
||||
class Echo(protocol.Protocol):
|
||||
def connectionMade(self):
|
||||
log.msg("ECHO CONNECTION MADE")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
log.msg("ECHO CONNECTION DONE")
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.transport.write(data)
|
||||
if b"\n" in data:
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
||||
class EchoFactory(protocol.Factory):
|
||||
protocol = Echo
|
||||
|
||||
|
||||
class ConchTestOpenSSHProcess(protocol.ProcessProtocol):
|
||||
"""
|
||||
Test protocol for launching an OpenSSH client process.
|
||||
|
||||
@ivar deferred: Set by whatever uses this object. Accessed using
|
||||
L{_getDeferred}, which destroys the value so the Deferred is not
|
||||
fired twice. Fires when the process is terminated.
|
||||
"""
|
||||
|
||||
deferred = None
|
||||
buf = b""
|
||||
problems = b""
|
||||
|
||||
def _getDeferred(self):
|
||||
d, self.deferred = self.deferred, None
|
||||
return d
|
||||
|
||||
def outReceived(self, data):
|
||||
self.buf += data
|
||||
|
||||
def errReceived(self, data):
|
||||
self.problems += data
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
Called when the process has ended.
|
||||
|
||||
@param reason: a Failure giving the reason for the process' end.
|
||||
"""
|
||||
if reason.value.exitCode != 0:
|
||||
self._getDeferred().errback(
|
||||
ConchError(
|
||||
"exit code was not 0: {} ({})".format(
|
||||
reason.value.exitCode,
|
||||
self.problems.decode("charmap"),
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
buf = self.buf.replace(b"\r\n", b"\n")
|
||||
self._getDeferred().callback(buf)
|
||||
|
||||
|
||||
class ConchTestForwardingProcess(protocol.ProcessProtocol):
|
||||
"""
|
||||
Manages a third-party process which launches a server.
|
||||
|
||||
Uses L{ConchTestForwardingPort} to connect to the third-party server.
|
||||
Once L{ConchTestForwardingPort} has disconnected, kill the process and fire
|
||||
a Deferred with the data received by the L{ConchTestForwardingPort}.
|
||||
|
||||
@ivar deferred: Set by whatever uses this object. Accessed using
|
||||
L{_getDeferred}, which destroys the value so the Deferred is not
|
||||
fired twice. Fires when the process is terminated.
|
||||
"""
|
||||
|
||||
deferred = None
|
||||
|
||||
def __init__(self, port, data):
|
||||
"""
|
||||
@type port: L{int}
|
||||
@param port: The port on which the third-party server is listening.
|
||||
(it is assumed that the server is running on localhost).
|
||||
|
||||
@type data: L{str}
|
||||
@param data: This is sent to the third-party server. Must end with '\n'
|
||||
in order to trigger a disconnect.
|
||||
"""
|
||||
self.port = port
|
||||
self.buffer = None
|
||||
self.data = data
|
||||
|
||||
def _getDeferred(self):
|
||||
d, self.deferred = self.deferred, None
|
||||
return d
|
||||
|
||||
def connectionMade(self):
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""
|
||||
Connect to the server, which is often a third-party process.
|
||||
Tries to reconnect if it fails because we have no way of determining
|
||||
exactly when the port becomes available for listening -- we can only
|
||||
know when the process starts.
|
||||
"""
|
||||
cc = protocol.ClientCreator(reactor, ConchTestForwardingPort, self, self.data)
|
||||
d = cc.connectTCP("127.0.0.1", self.port)
|
||||
d.addErrback(self._ebConnect)
|
||||
return d
|
||||
|
||||
def _ebConnect(self, f):
|
||||
reactor.callLater(0.1, self._connect)
|
||||
|
||||
def forwardingPortDisconnected(self, buffer):
|
||||
"""
|
||||
The network connection has died; save the buffer of output
|
||||
from the network and attempt to quit the process gracefully,
|
||||
and then (after the reactor has spun) send it a KILL signal.
|
||||
"""
|
||||
self.buffer = buffer
|
||||
self.transport.write(b"\x03")
|
||||
self.transport.loseConnection()
|
||||
reactor.callLater(0, self._reallyDie)
|
||||
|
||||
def _reallyDie(self):
|
||||
try:
|
||||
self.transport.signalProcess("KILL")
|
||||
except ProcessExitedAlready:
|
||||
pass
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
Fire the Deferred at self.deferred with the data collected
|
||||
from the L{ConchTestForwardingPort} connection, if any.
|
||||
"""
|
||||
self._getDeferred().callback(self.buffer)
|
||||
|
||||
|
||||
class ConchTestForwardingPort(protocol.Protocol):
|
||||
"""
|
||||
Connects to server launched by a third-party process (managed by
|
||||
L{ConchTestForwardingProcess}) sends data, then reports whatever it
|
||||
received back to the L{ConchTestForwardingProcess} once the connection
|
||||
is ended.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol, data):
|
||||
"""
|
||||
@type protocol: L{ConchTestForwardingProcess}
|
||||
@param protocol: The L{ProcessProtocol} which made this connection.
|
||||
|
||||
@type data: str
|
||||
@param data: The data to be sent to the third-party server.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.data = data
|
||||
|
||||
def connectionMade(self):
|
||||
self.buffer = b""
|
||||
self.transport.write(self.data)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buffer += data
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.protocol.forwardingPortDisconnected(self.buffer)
|
||||
|
||||
|
||||
def _makeArgs(args, mod="conch"):
|
||||
start = [
|
||||
sys.executable,
|
||||
"-c"
|
||||
"""
|
||||
### Twisted Preamble
|
||||
import sys, os
|
||||
path = os.path.abspath(sys.argv[0])
|
||||
while os.path.dirname(path) != path:
|
||||
if os.path.basename(path).startswith('Twisted'):
|
||||
sys.path.insert(0, path)
|
||||
break
|
||||
path = os.path.dirname(path)
|
||||
|
||||
from twisted.conch.scripts.%s import run
|
||||
run()"""
|
||||
% mod,
|
||||
]
|
||||
madeArgs = []
|
||||
for arg in start + list(args):
|
||||
if isinstance(arg, str):
|
||||
arg = arg.encode("utf-8")
|
||||
madeArgs.append(arg)
|
||||
return madeArgs
|
||||
|
||||
|
||||
class ConchServerSetupMixin:
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
@staticmethod
|
||||
def realmFactory():
|
||||
return ConchTestRealm(b"testuser")
|
||||
|
||||
def _createFiles(self):
|
||||
for f in ["rsa_test", "rsa_test.pub", "kh_test"]:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
with open("rsa_test", "wb") as f:
|
||||
f.write(privateRSA_openssh)
|
||||
with open("rsa_test.pub", "wb") as f:
|
||||
f.write(publicRSA_openssh)
|
||||
os.chmod("rsa_test", 0o600)
|
||||
permissions = FilePath("rsa_test").getPermissions()
|
||||
if permissions.group.read or permissions.other.read:
|
||||
raise SkipTest(
|
||||
"private key readable by others despite chmod;"
|
||||
" possible windows permission issue?"
|
||||
" see https://tm.tl/9767"
|
||||
)
|
||||
with open("kh_test", "wb") as f:
|
||||
f.write(b"127.0.0.1 " + publicRSA_openssh)
|
||||
|
||||
def _getFreePort(self):
|
||||
s = socket.socket()
|
||||
s.bind(("", 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
def _makeConchFactory(self):
|
||||
"""
|
||||
Make a L{ConchTestServerFactory}, which allows us to start a
|
||||
L{ConchTestServer} -- i.e. an actually listening conch.
|
||||
"""
|
||||
realm = self.realmFactory()
|
||||
p = portal.Portal(realm)
|
||||
p.registerChecker(conchTestPublicKeyChecker())
|
||||
factory = ConchTestServerFactory()
|
||||
factory.portal = p
|
||||
return factory
|
||||
|
||||
def setUp(self):
|
||||
self._createFiles()
|
||||
self.conchFactory = self._makeConchFactory()
|
||||
self.conchFactory.expectedLoseConnection = 1
|
||||
self.conchServer = reactor.listenTCP(
|
||||
0, self.conchFactory, interface="127.0.0.1"
|
||||
)
|
||||
self.echoServer = reactor.listenTCP(0, EchoFactory())
|
||||
self.echoPort = self.echoServer.getHost().port
|
||||
if HAS_IPV6:
|
||||
self.echoServerV6 = reactor.listenTCP(0, EchoFactory(), interface="::1")
|
||||
self.echoPortV6 = self.echoServerV6.getHost().port
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.conchFactory.proto.done = 1
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.conchFactory.proto.transport.loseConnection()
|
||||
deferreds = [
|
||||
defer.maybeDeferred(self.conchServer.stopListening),
|
||||
defer.maybeDeferred(self.echoServer.stopListening),
|
||||
]
|
||||
if HAS_IPV6:
|
||||
deferreds.append(defer.maybeDeferred(self.echoServerV6.stopListening))
|
||||
return defer.gatherResults(deferreds)
|
||||
|
||||
|
||||
class ForwardingMixin(ConchServerSetupMixin):
|
||||
"""
|
||||
Template class for tests of the Conch server's ability to forward arbitrary
|
||||
protocols over SSH.
|
||||
|
||||
These tests are integration tests, not unit tests. They launch a Conch
|
||||
server, a custom TCP server (just an L{EchoProtocol}) and then call
|
||||
L{execute}.
|
||||
|
||||
L{execute} is implemented by subclasses of L{ForwardingMixin}. It should
|
||||
cause an SSH client to connect to the Conch server, asking it to forward
|
||||
data to the custom TCP server.
|
||||
"""
|
||||
|
||||
def test_exec(self):
|
||||
"""
|
||||
Test that we can use whatever client to send the command "echo goodbye"
|
||||
to the Conch server. Make sure we receive "goodbye" back from the
|
||||
server.
|
||||
"""
|
||||
d = self.execute("echo goodbye", ConchTestOpenSSHProcess())
|
||||
return d.addCallback(self.assertEqual, b"goodbye\n")
|
||||
|
||||
def test_localToRemoteForwarding(self):
|
||||
"""
|
||||
Test that we can use whatever client to forward a local port to a
|
||||
specified port on the server.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b"test\n")
|
||||
d = self.execute(
|
||||
"", process, sshArgs="-N -L%i:127.0.0.1:%i" % (localPort, self.echoPort)
|
||||
)
|
||||
d.addCallback(self.assertEqual, b"test\n")
|
||||
return d
|
||||
|
||||
def test_remoteToLocalForwarding(self):
|
||||
"""
|
||||
Test that we can use whatever client to forward a port from the server
|
||||
to a port locally.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b"test\n")
|
||||
d = self.execute(
|
||||
"", process, sshArgs="-N -R %i:127.0.0.1:%i" % (localPort, self.echoPort)
|
||||
)
|
||||
d.addCallback(self.assertEqual, b"test\n")
|
||||
return d
|
||||
|
||||
|
||||
# Conventionally there is a separate adapter object which provides ISession for
|
||||
# the user, but making the user provide ISession directly works too. This isn't
|
||||
# a full implementation of ISession though, just enough to make these tests
|
||||
# pass.
|
||||
@implementer(ISession)
|
||||
class RekeyAvatar(ConchUser):
|
||||
"""
|
||||
This avatar implements a shell which sends 60 numbered lines to whatever
|
||||
connects to it, then closes the session with a 0 exit status.
|
||||
|
||||
60 lines is selected as being enough to send more than 2kB of traffic, the
|
||||
amount the client is configured to initiate a rekey after.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
ConchUser.__init__(self)
|
||||
self.channelLookup[b"session"] = SSHSession
|
||||
|
||||
def openShell(self, transport):
|
||||
"""
|
||||
Write 60 lines of data to the transport, then exit.
|
||||
"""
|
||||
proto = protocol.Protocol()
|
||||
proto.makeConnection(transport)
|
||||
transport.makeConnection(wrapProtocol(proto))
|
||||
|
||||
# Send enough bytes to the connection so that a rekey is triggered in
|
||||
# the client.
|
||||
def write(counter):
|
||||
i = next(counter)
|
||||
if i == 60:
|
||||
call.stop()
|
||||
transport.session.conn.sendRequest(
|
||||
transport.session, b"exit-status", b"\x00\x00\x00\x00"
|
||||
)
|
||||
transport.loseConnection()
|
||||
else:
|
||||
line = "line #%02d\n" % (i,)
|
||||
line = line.encode("utf-8")
|
||||
transport.write(line)
|
||||
|
||||
# The timing for this loop is an educated guess (and/or the result of
|
||||
# experimentation) to exercise the case where a packet is generated
|
||||
# mid-rekey. Since the other side of the connection is (so far) the
|
||||
# OpenSSH command line client, there's no easy way to determine when the
|
||||
# rekey has been initiated. If there were, then generating a packet
|
||||
# immediately at that time would be a better way to test the
|
||||
# functionality being tested here.
|
||||
call = LoopingCall(write, count())
|
||||
call.start(0.01)
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
Ignore the close of the session.
|
||||
"""
|
||||
|
||||
def eofReceived(self):
|
||||
# ISession.eofReceived
|
||||
pass
|
||||
|
||||
def execCommand(self, proto, command):
|
||||
# ISession.execCommand
|
||||
pass
|
||||
|
||||
def getPty(self, term, windowSize, modes):
|
||||
# ISession.getPty
|
||||
pass
|
||||
|
||||
def windowChanged(self, newWindowSize):
|
||||
# ISession.windowChanged
|
||||
pass
|
||||
|
||||
|
||||
class RekeyRealm:
|
||||
"""
|
||||
This realm gives out new L{RekeyAvatar} instances for any avatar request.
|
||||
"""
|
||||
|
||||
def requestAvatar(self, avatarID, mind, *interfaces):
|
||||
return interfaces[0], RekeyAvatar(), lambda: None
|
||||
|
||||
|
||||
class RekeyTestsMixin(ConchServerSetupMixin):
|
||||
"""
|
||||
TestCase mixin which defines tests exercising L{SSHTransportBase}'s handling
|
||||
of rekeying messages.
|
||||
"""
|
||||
|
||||
realmFactory = RekeyRealm
|
||||
|
||||
def test_clientRekey(self):
|
||||
"""
|
||||
After a client-initiated rekey is completed, application data continues
|
||||
to be passed over the SSH connection.
|
||||
"""
|
||||
process = ConchTestOpenSSHProcess()
|
||||
d = self.execute("", process, "-o RekeyLimit=2K")
|
||||
|
||||
def finished(result):
|
||||
expectedResult = "\n".join(["line #%02d" % (i,) for i in range(60)]) + "\n"
|
||||
expectedResult = expectedResult.encode("utf-8")
|
||||
self.assertEqual(result, expectedResult)
|
||||
|
||||
d.addCallback(finished)
|
||||
return d
|
||||
|
||||
|
||||
class OpenSSHClientMixin:
|
||||
if not which("ssh"):
|
||||
skip = "no ssh command-line client available"
|
||||
|
||||
def execute(self, remoteCommand, process, sshArgs=""):
|
||||
"""
|
||||
Connects to the SSH server started in L{ConchServerSetupMixin.setUp} by
|
||||
running the 'ssh' command line tool.
|
||||
|
||||
@type remoteCommand: str
|
||||
@param remoteCommand: The command (with arguments) to run on the
|
||||
remote end.
|
||||
|
||||
@type process: L{ConchTestOpenSSHProcess}
|
||||
|
||||
@type sshArgs: str
|
||||
@param sshArgs: Arguments to pass to the 'ssh' process.
|
||||
|
||||
@return: L{defer.Deferred}
|
||||
"""
|
||||
process.deferred = defer.Deferred()
|
||||
# Pass -F /dev/null to avoid the user's configuration file from
|
||||
# being loaded, as it may contain settings that cause our tests to
|
||||
# fail or hang.
|
||||
cmdline = (
|
||||
(
|
||||
"ssh -2 -l testuser -p %i "
|
||||
"-F /dev/null "
|
||||
"-oIdentitiesOnly=yes "
|
||||
"-oUserKnownHostsFile=kh_test "
|
||||
"-oPasswordAuthentication=no "
|
||||
# Always use the RSA key, since that's the one in kh_test.
|
||||
"-oHostKeyAlgorithms=ssh-rsa "
|
||||
"-a "
|
||||
"-i rsa_test "
|
||||
)
|
||||
+ sshArgs
|
||||
+ " 127.0.0.1 "
|
||||
+ remoteCommand
|
||||
)
|
||||
port = self.conchServer.getHost().port
|
||||
cmds = (cmdline % port).split()
|
||||
encodedCmds = []
|
||||
for cmd in cmds:
|
||||
encodedCmds.append(cmd.encode("utf-8"))
|
||||
reactor.spawnProcess(process, which("ssh")[0], encodedCmds)
|
||||
return process.deferred
|
||||
|
||||
|
||||
class OpenSSHKeyExchangeTests(ConchServerSetupMixin, OpenSSHClientMixin, TestCase):
|
||||
"""
|
||||
Tests L{SSHTransportBase}'s key exchange algorithm compatibility with
|
||||
OpenSSH.
|
||||
"""
|
||||
|
||||
def assertExecuteWithKexAlgorithm(self, keyExchangeAlgo):
|
||||
"""
|
||||
Call execute() method of L{OpenSSHClientMixin} with an ssh option that
|
||||
forces the exclusive use of the key exchange algorithm specified by
|
||||
keyExchangeAlgo
|
||||
|
||||
@type keyExchangeAlgo: L{str}
|
||||
@param keyExchangeAlgo: The key exchange algorithm to use
|
||||
|
||||
@return: L{defer.Deferred}
|
||||
"""
|
||||
kexAlgorithms = []
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[which("ssh")[0], "-Q", "kex"], stderr=subprocess.STDOUT
|
||||
)
|
||||
if not isinstance(output, str):
|
||||
output = output.decode("utf-8")
|
||||
kexAlgorithms = output.split()
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
if keyExchangeAlgo not in kexAlgorithms:
|
||||
raise SkipTest(f"{keyExchangeAlgo} not supported by ssh client")
|
||||
|
||||
d = self.execute(
|
||||
"echo hello",
|
||||
ConchTestOpenSSHProcess(),
|
||||
"-oKexAlgorithms=" + keyExchangeAlgo,
|
||||
)
|
||||
return d.addCallback(self.assertEqual, b"hello\n")
|
||||
|
||||
def test_ECDHSHA256(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp256 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm("ecdh-sha2-nistp256")
|
||||
|
||||
def test_ECDHSHA384(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp384 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm("ecdh-sha2-nistp384")
|
||||
|
||||
def test_ECDHSHA521(self):
|
||||
"""
|
||||
The ecdh-sha2-nistp521 key exchange algorithm is compatible with
|
||||
OpenSSH
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm("ecdh-sha2-nistp521")
|
||||
|
||||
def test_DH_GROUP14(self):
|
||||
"""
|
||||
The diffie-hellman-group14-sha1 key exchange algorithm is compatible
|
||||
with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm("diffie-hellman-group14-sha1")
|
||||
|
||||
def test_DH_GROUP_EXCHANGE_SHA1(self):
|
||||
"""
|
||||
The diffie-hellman-group-exchange-sha1 key exchange algorithm is
|
||||
compatible with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm("diffie-hellman-group-exchange-sha1")
|
||||
|
||||
def test_DH_GROUP_EXCHANGE_SHA256(self):
|
||||
"""
|
||||
The diffie-hellman-group-exchange-sha256 key exchange algorithm is
|
||||
compatible with OpenSSH.
|
||||
"""
|
||||
return self.assertExecuteWithKexAlgorithm(
|
||||
"diffie-hellman-group-exchange-sha256"
|
||||
)
|
||||
|
||||
def test_unsupported_algorithm(self):
|
||||
"""
|
||||
The list of key exchange algorithms supported
|
||||
by OpenSSH client is obtained with C{ssh -Q kex}.
|
||||
"""
|
||||
self.assertRaises(
|
||||
SkipTest, self.assertExecuteWithKexAlgorithm, "unsupported-algorithm"
|
||||
)
|
||||
|
||||
|
||||
class OpenSSHClientForwardingTests(ForwardingMixin, OpenSSHClientMixin, TestCase):
|
||||
"""
|
||||
Connection forwarding tests run against the OpenSSL command line client.
|
||||
"""
|
||||
|
||||
@skipWithoutIPv6
|
||||
def test_localToRemoteForwardingV6(self):
|
||||
"""
|
||||
Forwarding of arbitrary IPv6 TCP connections via SSH.
|
||||
"""
|
||||
localPort = self._getFreePort()
|
||||
process = ConchTestForwardingProcess(localPort, b"test\n")
|
||||
d = self.execute(
|
||||
"", process, sshArgs="-N -L%i:[::1]:%i" % (localPort, self.echoPortV6)
|
||||
)
|
||||
d.addCallback(self.assertEqual, b"test\n")
|
||||
return d
|
||||
|
||||
|
||||
class OpenSSHClientRekeyTests(RekeyTestsMixin, OpenSSHClientMixin, TestCase):
|
||||
"""
|
||||
Rekeying tests run against the OpenSSL command line client.
|
||||
"""
|
||||
|
||||
|
||||
class CmdLineClientTests(ForwardingMixin, TestCase):
|
||||
"""
|
||||
Connection forwarding tests run against the Conch command line client.
|
||||
"""
|
||||
|
||||
if runtime.platformType == "win32":
|
||||
skip = "can't run cmdline client on win32"
|
||||
|
||||
def execute(self, remoteCommand, process, sshArgs="", conchArgs=None):
|
||||
"""
|
||||
As for L{OpenSSHClientTestCase.execute}, except it runs the 'conch'
|
||||
command line tool, not 'ssh'.
|
||||
"""
|
||||
if conchArgs is None:
|
||||
conchArgs = []
|
||||
|
||||
process.deferred = defer.Deferred()
|
||||
port = self.conchServer.getHost().port
|
||||
cmd = (
|
||||
"-p {} -l testuser "
|
||||
"--known-hosts kh_test "
|
||||
"--user-authentications publickey "
|
||||
"-a "
|
||||
"-i rsa_test "
|
||||
"-v ".format(port) + sshArgs + " 127.0.0.1 " + remoteCommand
|
||||
)
|
||||
cmds = _makeArgs(conchArgs + cmd.split())
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = os.pathsep.join(sys.path)
|
||||
encodedCmds = []
|
||||
encodedEnv = {}
|
||||
for cmd in cmds:
|
||||
if isinstance(cmd, str):
|
||||
cmd = cmd.encode("utf-8")
|
||||
encodedCmds.append(cmd)
|
||||
for var in env:
|
||||
val = env[var]
|
||||
if isinstance(var, str):
|
||||
var = var.encode("utf-8")
|
||||
if isinstance(val, str):
|
||||
val = val.encode("utf-8")
|
||||
encodedEnv[var] = val
|
||||
reactor.spawnProcess(process, sys.executable, encodedCmds, env=encodedEnv)
|
||||
return process.deferred
|
||||
|
||||
def test_runWithLogFile(self):
|
||||
"""
|
||||
It can store logs to a local file.
|
||||
"""
|
||||
|
||||
def cb_check_log(result):
|
||||
logContent = logPath.getContent()
|
||||
self.assertIn(b"Log opened.", logContent)
|
||||
|
||||
logPath = filepath.FilePath(self.mktemp())
|
||||
|
||||
d = self.execute(
|
||||
remoteCommand="echo goodbye",
|
||||
process=ConchTestOpenSSHProcess(),
|
||||
conchArgs=[
|
||||
"--log",
|
||||
"--logfile",
|
||||
logPath.path,
|
||||
"--host-key-algorithms",
|
||||
"ssh-rsa",
|
||||
],
|
||||
)
|
||||
|
||||
d.addCallback(self.assertEqual, b"goodbye\n")
|
||||
d.addCallback(cb_check_log)
|
||||
return d
|
||||
|
||||
def test_runWithNoHostAlgorithmsSpecified(self):
|
||||
"""
|
||||
Do not use --host-key-algorithms flag on command line.
|
||||
"""
|
||||
d = self.execute(
|
||||
remoteCommand="echo goodbye", process=ConchTestOpenSSHProcess()
|
||||
)
|
||||
|
||||
d.addCallback(self.assertEqual, b"goodbye\n")
|
||||
return d
|
||||
@@ -0,0 +1,846 @@
|
||||
# Copyright (c) 2007-2010 Twisted Matrix Laboratories.
|
||||
# See LICENSE for details
|
||||
|
||||
"""
|
||||
This module tests twisted.conch.ssh.connection.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.ssh import channel
|
||||
from twisted.conch.test import test_userauth
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
|
||||
from twisted.conch import error
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import common, connection
|
||||
else:
|
||||
|
||||
class connection: # type: ignore[no-redef]
|
||||
class SSHConnection:
|
||||
pass
|
||||
|
||||
|
||||
class TestChannel(channel.SSHChannel):
|
||||
"""
|
||||
A mocked-up version of twisted.conch.ssh.channel.SSHChannel.
|
||||
|
||||
@ivar gotOpen: True if channelOpen has been called.
|
||||
@type gotOpen: L{bool}
|
||||
@ivar specificData: the specific channel open data passed to channelOpen.
|
||||
@type specificData: L{bytes}
|
||||
@ivar openFailureReason: the reason passed to openFailed.
|
||||
@type openFailed: C{error.ConchError}
|
||||
@ivar inBuffer: a C{list} of strings received by the channel.
|
||||
@type inBuffer: C{list}
|
||||
@ivar extBuffer: a C{list} of 2-tuples (type, extended data) of received by
|
||||
the channel.
|
||||
@type extBuffer: C{list}
|
||||
@ivar numberRequests: the number of requests that have been made to this
|
||||
channel.
|
||||
@type numberRequests: L{int}
|
||||
@ivar gotEOF: True if the other side sent EOF.
|
||||
@type gotEOF: L{bool}
|
||||
@ivar gotOneClose: True if the other side closed the connection.
|
||||
@type gotOneClose: L{bool}
|
||||
@ivar gotClosed: True if the channel is closed.
|
||||
@type gotClosed: L{bool}
|
||||
"""
|
||||
|
||||
name = b"TestChannel"
|
||||
gotOpen = False
|
||||
gotClosed = False
|
||||
|
||||
def logPrefix(self):
|
||||
return "TestChannel %i" % self.id
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
The channel is open. Set up the instance variables.
|
||||
"""
|
||||
self.gotOpen = True
|
||||
self.specificData = specificData
|
||||
self.inBuffer = []
|
||||
self.extBuffer = []
|
||||
self.numberRequests = 0
|
||||
self.gotEOF = False
|
||||
self.gotOneClose = False
|
||||
self.gotClosed = False
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
Opening the channel failed. Store the reason why.
|
||||
"""
|
||||
self.openFailureReason = reason
|
||||
|
||||
def request_test(self, data):
|
||||
"""
|
||||
A test request. Return True if data is 'data'.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self.numberRequests += 1
|
||||
return data == b"data"
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Data was received. Store it in the buffer.
|
||||
"""
|
||||
self.inBuffer.append(data)
|
||||
|
||||
def extReceived(self, code, data):
|
||||
"""
|
||||
Extended data was received. Store it in the buffer.
|
||||
"""
|
||||
self.extBuffer.append((code, data))
|
||||
|
||||
def eofReceived(self):
|
||||
"""
|
||||
EOF was received. Remember it.
|
||||
"""
|
||||
self.gotEOF = True
|
||||
|
||||
def closeReceived(self):
|
||||
"""
|
||||
Close was received. Remember it.
|
||||
"""
|
||||
self.gotOneClose = True
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
The channel is closed. Rembember it.
|
||||
"""
|
||||
self.gotClosed = True
|
||||
|
||||
|
||||
class TestAvatar:
|
||||
"""
|
||||
A mocked-up version of twisted.conch.avatar.ConchUser
|
||||
"""
|
||||
|
||||
_ARGS_ERROR_CODE = 123
|
||||
|
||||
def lookupChannel(self, channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The server wants us to return a channel. If the requested channel is
|
||||
our TestChannel, return it, otherwise return None.
|
||||
"""
|
||||
if channelType == TestChannel.name:
|
||||
return TestChannel(
|
||||
remoteWindow=windowSize,
|
||||
remoteMaxPacket=maxPacket,
|
||||
data=data,
|
||||
avatar=self,
|
||||
)
|
||||
elif channelType == b"conch-error-args":
|
||||
# Raise a ConchError with backwards arguments to make sure the
|
||||
# connection fixes it for us. This case should be deprecated and
|
||||
# deleted eventually, but only after all of Conch gets the argument
|
||||
# order right.
|
||||
raise error.ConchError(self._ARGS_ERROR_CODE, "error args in wrong order")
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
"""
|
||||
The client has made a global request. If the global request is
|
||||
'TestGlobal', return True. If the global request is 'TestData',
|
||||
return True and the request-specific data we received. Otherwise,
|
||||
return False.
|
||||
"""
|
||||
if requestType == b"TestGlobal":
|
||||
return True
|
||||
elif requestType == b"TestData":
|
||||
return True, data
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class TestConnection(connection.SSHConnection):
|
||||
"""
|
||||
A subclass of SSHConnection for testing.
|
||||
|
||||
@ivar channel: the current channel.
|
||||
@type channel. C{TestChannel}
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def logPrefix(self):
|
||||
return "TestConnection"
|
||||
|
||||
def global_TestGlobal(self, data):
|
||||
"""
|
||||
The other side made the 'TestGlobal' global request. Return True.
|
||||
"""
|
||||
return True
|
||||
|
||||
def global_Test_Data(self, data):
|
||||
"""
|
||||
The other side made the 'Test-Data' global request. Return True and
|
||||
the data we received.
|
||||
"""
|
||||
return True, data
|
||||
|
||||
def channel_TestChannel(self, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side is requesting the TestChannel. Create a C{TestChannel}
|
||||
instance, store it, and return it.
|
||||
"""
|
||||
self.channel = TestChannel(
|
||||
remoteWindow=windowSize, remoteMaxPacket=maxPacket, data=data
|
||||
)
|
||||
return self.channel
|
||||
|
||||
def channel_ErrorChannel(self, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side is requesting the ErrorChannel. Raise an exception.
|
||||
"""
|
||||
raise AssertionError("no such thing")
|
||||
|
||||
|
||||
class ConnectionTests(unittest.TestCase):
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def setUp(self):
|
||||
self.transport = test_userauth.FakeTransport(None)
|
||||
self.transport.avatar = TestAvatar()
|
||||
self.conn = TestConnection()
|
||||
self.conn.transport = self.transport
|
||||
self.conn.serviceStarted()
|
||||
|
||||
def _openChannel(self, channel):
|
||||
"""
|
||||
Open the channel with the default connection.
|
||||
"""
|
||||
self.conn.openChannel(channel)
|
||||
self.transport.packets = self.transport.packets[:-1]
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(
|
||||
struct.pack(">2L", channel.id, 255) + b"\x00\x02\x00\x00\x00\x00\x80\x00"
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.conn.serviceStopped()
|
||||
|
||||
def test_linkAvatar(self):
|
||||
"""
|
||||
Test that the connection links itself to the avatar in the
|
||||
transport.
|
||||
"""
|
||||
self.assertIs(self.transport.avatar.conn, self.conn)
|
||||
|
||||
def test_serviceStopped(self):
|
||||
"""
|
||||
Test that serviceStopped() closes any open channels.
|
||||
"""
|
||||
channel1 = TestChannel()
|
||||
channel2 = TestChannel()
|
||||
self.conn.openChannel(channel1)
|
||||
self.conn.openChannel(channel2)
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(b"\x00\x00\x00\x00" * 4)
|
||||
self.assertTrue(channel1.gotOpen)
|
||||
self.assertFalse(channel1.gotClosed)
|
||||
self.assertFalse(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
self.conn.serviceStopped()
|
||||
self.assertTrue(channel1.gotClosed)
|
||||
self.assertFalse(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
from twisted.internet.error import ConnectionLost
|
||||
|
||||
self.assertIsInstance(channel2.openFailureReason, ConnectionLost)
|
||||
|
||||
def test_GLOBAL_REQUEST(self):
|
||||
"""
|
||||
Test that global request packets are dispatched to the global_*
|
||||
methods and the return values are translated into success or failure
|
||||
messages.
|
||||
"""
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b"TestGlobal") + b"\xff")
|
||||
self.assertEqual(
|
||||
self.transport.packets, [(connection.MSG_REQUEST_SUCCESS, b"")]
|
||||
)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b"TestData") + b"\xff" + b"test data")
|
||||
self.assertEqual(
|
||||
self.transport.packets, [(connection.MSG_REQUEST_SUCCESS, b"test data")]
|
||||
)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b"TestBad") + b"\xff")
|
||||
self.assertEqual(
|
||||
self.transport.packets, [(connection.MSG_REQUEST_FAILURE, b"")]
|
||||
)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_GLOBAL_REQUEST(common.NS(b"TestGlobal") + b"\x00")
|
||||
self.assertEqual(self.transport.packets, [])
|
||||
|
||||
def test_REQUEST_SUCCESS(self):
|
||||
"""
|
||||
Test that global request success packets cause the Deferred to be
|
||||
called back.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b"request", b"data", True)
|
||||
self.conn.ssh_REQUEST_SUCCESS(b"data")
|
||||
|
||||
def check(data):
|
||||
self.assertEqual(data, b"data")
|
||||
|
||||
d.addCallback(check)
|
||||
d.addErrback(self.fail)
|
||||
return d
|
||||
|
||||
def test_REQUEST_FAILURE(self):
|
||||
"""
|
||||
Test that global request failure packets cause the Deferred to be
|
||||
erred back.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b"request", b"data", True)
|
||||
self.conn.ssh_REQUEST_FAILURE(b"data")
|
||||
|
||||
def check(f):
|
||||
self.assertEqual(f.value.data, b"data")
|
||||
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_OPEN(self):
|
||||
"""
|
||||
Test that open channel packets cause a channel to be created and
|
||||
opened or a failure message to be returned.
|
||||
"""
|
||||
del self.transport.avatar
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b"TestChannel") + b"\x00\x00\x00\x01" * 4)
|
||||
self.assertTrue(self.conn.channel.gotOpen)
|
||||
self.assertEqual(self.conn.channel.conn, self.conn)
|
||||
self.assertEqual(self.conn.channel.data, b"\x00\x00\x00\x01")
|
||||
self.assertEqual(self.conn.channel.specificData, b"\x00\x00\x00\x01")
|
||||
self.assertEqual(self.conn.channel.remoteWindowLeft, 1)
|
||||
self.assertEqual(self.conn.channel.remoteMaxPacket, 1)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_OPEN_CONFIRMATION,
|
||||
b"\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x00"
|
||||
b"\x00\x00\x80\x00",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b"BadChannel") + b"\x00\x00\x00\x02" * 4)
|
||||
self.flushLoggedErrors()
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
b"\x00\x00\x00\x02\x00\x00\x00\x03"
|
||||
+ common.NS(b"unknown channel")
|
||||
+ common.NS(b""),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_OPEN(common.NS(b"ErrorChannel") + b"\x00\x00\x00\x02" * 4)
|
||||
self.flushLoggedErrors()
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
b"\x00\x00\x00\x02\x00\x00\x00\x02"
|
||||
+ common.NS(b"unknown failure")
|
||||
+ common.NS(b""),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def _lookupChannelErrorTest(self, code):
|
||||
"""
|
||||
Deliver a request for a channel open which will result in an exception
|
||||
being raised during channel lookup. Assert that an error response is
|
||||
delivered as a result.
|
||||
"""
|
||||
self.transport.avatar._ARGS_ERROR_CODE = code
|
||||
self.conn.ssh_CHANNEL_OPEN(
|
||||
common.NS(b"conch-error-args") + b"\x00\x00\x00\x01" * 4
|
||||
)
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(len(errors), 1, f"Expected one error, got: {errors!r}")
|
||||
self.assertEqual(errors[0].value.args, (123, "error args in wrong order"))
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_OPEN_FAILURE,
|
||||
# The response includes some bytes which identifying the
|
||||
# associated request, as well as the error code (7b in hex) and
|
||||
# the error message.
|
||||
b"\x00\x00\x00\x01\x00\x00\x00\x7b"
|
||||
+ common.NS(b"error args in wrong order")
|
||||
+ common.NS(b""),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_lookupChannelError(self):
|
||||
"""
|
||||
If a C{lookupChannel} implementation raises L{error.ConchError} with the
|
||||
arguments in the wrong order, a C{MSG_CHANNEL_OPEN} failure is still
|
||||
sent in response to the message.
|
||||
|
||||
This is a temporary work-around until L{error.ConchError} is given
|
||||
better attributes and all of the Conch code starts constructing
|
||||
instances of it properly. Eventually this functionality should be
|
||||
deprecated and then removed.
|
||||
"""
|
||||
self._lookupChannelErrorTest(123)
|
||||
|
||||
def test_CHANNEL_OPEN_CONFIRMATION(self):
|
||||
"""
|
||||
Test that channel open confirmation packets cause the channel to be
|
||||
notified that it's open.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(b"\x00\x00\x00\x00" * 5)
|
||||
self.assertEqual(channel.remoteWindowLeft, 0)
|
||||
self.assertEqual(channel.remoteMaxPacket, 0)
|
||||
self.assertEqual(channel.specificData, b"\x00\x00\x00\x00")
|
||||
self.assertEqual(self.conn.channelsToRemoteChannel[channel], 0)
|
||||
self.assertEqual(self.conn.localToRemoteChannel[0], 0)
|
||||
|
||||
def test_CHANNEL_OPEN_FAILURE(self):
|
||||
"""
|
||||
Test that channel open failure packets cause the channel to be
|
||||
notified that its opening failed.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_OPEN_FAILURE(
|
||||
b"\x00\x00\x00\x00\x00\x00\x00" b"\x01" + common.NS(b"failure!")
|
||||
)
|
||||
self.assertEqual(channel.openFailureReason.args, (b"failure!", 1))
|
||||
self.assertIsNone(self.conn.channels.get(channel))
|
||||
|
||||
def test_CHANNEL_WINDOW_ADJUST(self):
|
||||
"""
|
||||
Test that channel window adjust messages add bytes to the channel
|
||||
window.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
oldWindowSize = channel.remoteWindowLeft
|
||||
self.conn.ssh_CHANNEL_WINDOW_ADJUST(b"\x00\x00\x00\x00\x00\x00\x00" b"\x01")
|
||||
self.assertEqual(channel.remoteWindowLeft, oldWindowSize + 1)
|
||||
|
||||
def test_CHANNEL_DATA(self):
|
||||
"""
|
||||
Test that channel data messages are passed up to the channel, or
|
||||
cause the channel to be closed if the data is too large.
|
||||
"""
|
||||
channel = TestChannel(localWindow=6, localMaxPacket=5)
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_DATA(b"\x00\x00\x00\x00" + common.NS(b"data"))
|
||||
self.assertEqual(channel.inBuffer, [b"data"])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_WINDOW_ADJUST,
|
||||
b"\x00\x00\x00\xff" b"\x00\x00\x00\x04",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.transport.packets = []
|
||||
longData = b"a" * (channel.localWindowLeft + 1)
|
||||
self.conn.ssh_CHANNEL_DATA(b"\x00\x00\x00\x00" + common.NS(longData))
|
||||
self.assertEqual(channel.inBuffer, [b"data"])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
bigData = b"a" * (channel.localMaxPacket + 1)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_DATA(b"\x00\x00\x00\x01" + common.NS(bigData))
|
||||
self.assertEqual(channel.inBuffer, [])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
|
||||
def test_CHANNEL_EXTENDED_DATA(self):
|
||||
"""
|
||||
Test that channel extended data messages are passed up to the channel,
|
||||
or cause the channel to be closed if they're too big.
|
||||
"""
|
||||
channel = TestChannel(localWindow=6, localMaxPacket=5)
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(
|
||||
b"\x00\x00\x00\x00\x00\x00\x00" b"\x00" + common.NS(b"data")
|
||||
)
|
||||
self.assertEqual(channel.extBuffer, [(0, b"data")])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_WINDOW_ADJUST,
|
||||
b"\x00\x00\x00\xff" b"\x00\x00\x00\x04",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.transport.packets = []
|
||||
longData = b"a" * (channel.localWindowLeft + 1)
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(
|
||||
b"\x00\x00\x00\x00\x00\x00\x00" b"\x00" + common.NS(longData)
|
||||
)
|
||||
self.assertEqual(channel.extBuffer, [(0, b"data")])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
bigData = b"a" * (channel.localMaxPacket + 1)
|
||||
self.transport.packets = []
|
||||
self.conn.ssh_CHANNEL_EXTENDED_DATA(
|
||||
b"\x00\x00\x00\x01\x00\x00\x00" b"\x00" + common.NS(bigData)
|
||||
)
|
||||
self.assertEqual(channel.extBuffer, [])
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
|
||||
def test_CHANNEL_EOF(self):
|
||||
"""
|
||||
Test that channel eof messages are passed up to the channel.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_EOF(b"\x00\x00\x00\x00")
|
||||
self.assertTrue(channel.gotEOF)
|
||||
|
||||
def test_CHANNEL_CLOSE(self):
|
||||
"""
|
||||
Test that channel close messages are passed up to the channel. Also,
|
||||
test that channel.close() is called if both sides are closed when this
|
||||
message is received.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.assertTrue(channel.gotOpen)
|
||||
self.assertFalse(channel.gotOneClose)
|
||||
self.assertFalse(channel.gotClosed)
|
||||
self.conn.sendClose(channel)
|
||||
self.conn.ssh_CHANNEL_CLOSE(b"\x00\x00\x00\x00")
|
||||
self.assertTrue(channel.gotOneClose)
|
||||
self.assertTrue(channel.gotClosed)
|
||||
|
||||
def test_CHANNEL_REQUEST_success(self):
|
||||
"""
|
||||
Test that channel requests that succeed send MSG_CHANNEL_SUCCESS.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.ssh_CHANNEL_REQUEST(
|
||||
b"\x00\x00\x00\x00" + common.NS(b"test") + b"\x00"
|
||||
)
|
||||
self.assertEqual(channel.numberRequests, 1)
|
||||
d = self.conn.ssh_CHANNEL_REQUEST(
|
||||
b"\x00\x00\x00\x00" + common.NS(b"test") + b"\xff" + b"data"
|
||||
)
|
||||
|
||||
def check(result):
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_SUCCESS, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
|
||||
d.addCallback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_failure(self):
|
||||
"""
|
||||
Test that channel requests that fail send MSG_CHANNEL_FAILURE.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.ssh_CHANNEL_REQUEST(
|
||||
b"\x00\x00\x00\x00" + common.NS(b"test") + b"\xff"
|
||||
)
|
||||
|
||||
def check(result):
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_FAILURE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_SUCCESS(self):
|
||||
"""
|
||||
Test that channel request success messages cause the Deferred to be
|
||||
called back.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b"test", b"data", True)
|
||||
self.conn.ssh_CHANNEL_SUCCESS(b"\x00\x00\x00\x00")
|
||||
|
||||
def check(result):
|
||||
self.assertTrue(result)
|
||||
|
||||
return d
|
||||
|
||||
def test_CHANNEL_REQUEST_FAILURE(self):
|
||||
"""
|
||||
Test that channel request failure messages cause the Deferred to be
|
||||
erred back.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b"test", b"", True)
|
||||
self.conn.ssh_CHANNEL_FAILURE(b"\x00\x00\x00\x00")
|
||||
|
||||
def check(result):
|
||||
self.assertEqual(result.value.value, "channel request failed")
|
||||
|
||||
d.addCallback(self.fail)
|
||||
d.addErrback(check)
|
||||
return d
|
||||
|
||||
def test_sendGlobalRequest(self):
|
||||
"""
|
||||
Test that global request messages are sent in the right format.
|
||||
"""
|
||||
d = self.conn.sendGlobalRequest(b"wantReply", b"data", True)
|
||||
# must be added to prevent errbacking during teardown
|
||||
d.addErrback(lambda failure: None)
|
||||
self.conn.sendGlobalRequest(b"noReply", b"", False)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(connection.MSG_GLOBAL_REQUEST, common.NS(b"wantReply") + b"\xffdata"),
|
||||
(connection.MSG_GLOBAL_REQUEST, common.NS(b"noReply") + b"\x00"),
|
||||
],
|
||||
)
|
||||
self.assertEqual(self.conn.deferreds, {"global": [d]})
|
||||
|
||||
def test_openChannel(self):
|
||||
"""
|
||||
Test that open channel messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self.conn.openChannel(channel, b"aaaa")
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_OPEN,
|
||||
common.NS(b"TestChannel")
|
||||
+ b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x80\x00aaaa",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(channel.id, 0)
|
||||
self.assertEqual(self.conn.localChannelID, 1)
|
||||
|
||||
def test_sendRequest(self):
|
||||
"""
|
||||
Test that channel request messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
d = self.conn.sendRequest(channel, b"test", b"test", True)
|
||||
# needed to prevent errbacks during teardown.
|
||||
d.addErrback(lambda failure: None)
|
||||
self.conn.sendRequest(channel, b"test2", b"", False)
|
||||
channel.localClosed = True # emulate sending a close message
|
||||
self.conn.sendRequest(channel, b"test3", b"", True)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_REQUEST,
|
||||
b"\x00\x00\x00\xff" + common.NS(b"test") + b"\x01test",
|
||||
),
|
||||
(
|
||||
connection.MSG_CHANNEL_REQUEST,
|
||||
b"\x00\x00\x00\xff" + common.NS(b"test2") + b"\x00",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.assertEqual(self.conn.deferreds[0], [d])
|
||||
|
||||
def test_adjustWindow(self):
|
||||
"""
|
||||
Test that channel window adjust messages cause bytes to be added
|
||||
to the window.
|
||||
"""
|
||||
channel = TestChannel(localWindow=5)
|
||||
self._openChannel(channel)
|
||||
channel.localWindowLeft = 0
|
||||
self.conn.adjustWindow(channel, 1)
|
||||
self.assertEqual(channel.localWindowLeft, 1)
|
||||
channel.localClosed = True
|
||||
self.conn.adjustWindow(channel, 2)
|
||||
self.assertEqual(channel.localWindowLeft, 1)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_WINDOW_ADJUST,
|
||||
b"\x00\x00\x00\xff" b"\x00\x00\x00\x01",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_sendData(self):
|
||||
"""
|
||||
Test that channel data messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendData(channel, b"a")
|
||||
channel.localClosed = True
|
||||
self.conn.sendData(channel, b"b")
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_DATA, b"\x00\x00\x00\xff" + common.NS(b"a"))],
|
||||
)
|
||||
|
||||
def test_sendExtendedData(self):
|
||||
"""
|
||||
Test that channel extended data messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendExtendedData(channel, 1, b"test")
|
||||
channel.localClosed = True
|
||||
self.conn.sendExtendedData(channel, 2, b"test2")
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[
|
||||
(
|
||||
connection.MSG_CHANNEL_EXTENDED_DATA,
|
||||
b"\x00\x00\x00\xff" + b"\x00\x00\x00\x01" + common.NS(b"test"),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_sendEOF(self):
|
||||
"""
|
||||
Test that channel EOF messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendEOF(channel)
|
||||
self.assertEqual(
|
||||
self.transport.packets, [(connection.MSG_CHANNEL_EOF, b"\x00\x00\x00\xff")]
|
||||
)
|
||||
channel.localClosed = True
|
||||
self.conn.sendEOF(channel)
|
||||
self.assertEqual(
|
||||
self.transport.packets, [(connection.MSG_CHANNEL_EOF, b"\x00\x00\x00\xff")]
|
||||
)
|
||||
|
||||
def test_sendClose(self):
|
||||
"""
|
||||
Test that channel close messages are sent in the right format.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
self.conn.sendClose(channel)
|
||||
self.assertTrue(channel.localClosed)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
self.conn.sendClose(channel)
|
||||
self.assertEqual(
|
||||
self.transport.packets,
|
||||
[(connection.MSG_CHANNEL_CLOSE, b"\x00\x00\x00\xff")],
|
||||
)
|
||||
|
||||
channel2 = TestChannel()
|
||||
self._openChannel(channel2)
|
||||
self.assertTrue(channel2.gotOpen)
|
||||
self.assertFalse(channel2.gotClosed)
|
||||
channel2.remoteClosed = True
|
||||
self.conn.sendClose(channel2)
|
||||
self.assertTrue(channel2.gotClosed)
|
||||
|
||||
def test_getChannelWithAvatar(self):
|
||||
"""
|
||||
Test that getChannel dispatches to the avatar when an avatar is
|
||||
present. Correct functioning without the avatar is verified in
|
||||
test_CHANNEL_OPEN.
|
||||
"""
|
||||
channel = self.conn.getChannel(b"TestChannel", 50, 30, b"data")
|
||||
self.assertEqual(channel.data, b"data")
|
||||
self.assertEqual(channel.remoteWindowLeft, 50)
|
||||
self.assertEqual(channel.remoteMaxPacket, 30)
|
||||
self.assertRaises(
|
||||
error.ConchError, self.conn.getChannel, b"BadChannel", 50, 30, b"data"
|
||||
)
|
||||
|
||||
def test_gotGlobalRequestWithoutAvatar(self):
|
||||
"""
|
||||
Test that gotGlobalRequests dispatches to global_* without an avatar.
|
||||
"""
|
||||
del self.transport.avatar
|
||||
self.assertTrue(self.conn.gotGlobalRequest(b"TestGlobal", b"data"))
|
||||
self.assertEqual(
|
||||
self.conn.gotGlobalRequest(b"Test-Data", b"data"), (True, b"data")
|
||||
)
|
||||
self.assertFalse(self.conn.gotGlobalRequest(b"BadGlobal", b"data"))
|
||||
|
||||
def test_channelClosedCausesLeftoverChannelDeferredsToErrback(self):
|
||||
"""
|
||||
Whenever an SSH channel gets closed any Deferred that was returned by a
|
||||
sendRequest() on its parent connection must be errbacked.
|
||||
"""
|
||||
channel = TestChannel()
|
||||
self._openChannel(channel)
|
||||
|
||||
d = self.conn.sendRequest(channel, b"dummyrequest", b"dummydata", wantReply=1)
|
||||
d = self.assertFailure(d, error.ConchError)
|
||||
self.conn.channelClosed(channel)
|
||||
return d
|
||||
|
||||
|
||||
class CleanConnectionShutdownTests(unittest.TestCase):
|
||||
"""
|
||||
Check whether correct cleanup is performed on connection shutdown.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def setUp(self):
|
||||
self.transport = test_userauth.FakeTransport(None)
|
||||
self.transport.avatar = TestAvatar()
|
||||
self.conn = TestConnection()
|
||||
self.conn.transport = self.transport
|
||||
|
||||
def test_serviceStoppedCausesLeftoverGlobalDeferredsToErrback(self):
|
||||
"""
|
||||
Once the service is stopped any leftover global deferred returned by
|
||||
a sendGlobalRequest() call must be errbacked.
|
||||
"""
|
||||
self.conn.serviceStarted()
|
||||
|
||||
d = self.conn.sendGlobalRequest(b"dummyrequest", b"dummydata", wantReply=1)
|
||||
d = self.assertFailure(d, error.ConchError)
|
||||
self.conn.serviceStopped()
|
||||
return d
|
||||
@@ -0,0 +1,326 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.client.default}.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
doSkip = False
|
||||
skipReason = ""
|
||||
|
||||
if requireModule("cryptography"):
|
||||
from twisted.conch.client import default
|
||||
from twisted.conch.client.agent import SSHAgentClient
|
||||
from twisted.conch.client.default import SSHUserAuthClient
|
||||
from twisted.conch.client.options import ConchOptions
|
||||
from twisted.conch.ssh.keys import Key
|
||||
else:
|
||||
doSkip = True
|
||||
skipReason = "cryptography required for twisted.conch.client.default."
|
||||
skip = skipReason # no SSL available, skip the entire module
|
||||
|
||||
if platform.isWindows():
|
||||
doSkip = True
|
||||
skipReason = (
|
||||
"genericAnswers and getPassword does not work on Windows."
|
||||
" Should be fixed as part of fixing bug 6409 and 6410"
|
||||
)
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
doSkip = True
|
||||
skipReason = "sys.stdin is not an interactive tty"
|
||||
if not sys.stdout.isatty():
|
||||
doSkip = True
|
||||
skipReason = "sys.stdout is not an interactive tty"
|
||||
|
||||
|
||||
class SSHUserAuthClientTests(TestCase):
|
||||
"""
|
||||
Tests for L{SSHUserAuthClient}.
|
||||
|
||||
@type rsaPublic: L{Key}
|
||||
@ivar rsaPublic: A public RSA key.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.rsaPublic = Key.fromString(keydata.publicRSA_openssh)
|
||||
self.tmpdir = FilePath(self.mktemp())
|
||||
self.tmpdir.makedirs()
|
||||
self.rsaFile = self.tmpdir.child("id_rsa")
|
||||
self.rsaFile.setContent(keydata.privateRSA_openssh)
|
||||
self.tmpdir.child("id_rsa.pub").setContent(keydata.publicRSA_openssh)
|
||||
|
||||
def test_signDataWithAgent(self):
|
||||
"""
|
||||
When connected to an agent, L{SSHUserAuthClient} can use it to
|
||||
request signatures of particular data with a particular L{Key}.
|
||||
"""
|
||||
client = SSHUserAuthClient(b"user", ConchOptions(), None)
|
||||
agent = SSHAgentClient()
|
||||
transport = StringTransport()
|
||||
agent.makeConnection(transport)
|
||||
client.keyAgent = agent
|
||||
cleartext = b"Sign here"
|
||||
client.signData(self.rsaPublic, cleartext)
|
||||
self.assertEqual(
|
||||
transport.value(),
|
||||
b"\x00\x00\x01\x2d\r\x00\x00\x01\x17"
|
||||
+ self.rsaPublic.blob()
|
||||
+ b"\x00\x00\x00\t"
|
||||
+ cleartext
|
||||
+ b"\x00\x00\x00\x00",
|
||||
)
|
||||
|
||||
def test_agentGetPublicKey(self):
|
||||
"""
|
||||
L{SSHUserAuthClient} looks up public keys from the agent using the
|
||||
L{SSHAgentClient} class. That L{SSHAgentClient.getPublicKey} returns a
|
||||
L{Key} object with one of the public keys in the agent. If no more
|
||||
keys are present, it returns L{None}.
|
||||
"""
|
||||
agent = SSHAgentClient()
|
||||
agent.blobs = [self.rsaPublic.blob()]
|
||||
key = agent.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
self.assertIsNone(agent.getPublicKey())
|
||||
|
||||
def test_getPublicKeyFromFile(self):
|
||||
"""
|
||||
L{SSHUserAuthClient.getPublicKey()} is able to get a public key from
|
||||
the first file described by its options' C{identitys} list, and return
|
||||
the corresponding public L{Key} object.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
|
||||
def test_getPublicKeyAgentFallback(self):
|
||||
"""
|
||||
If an agent is present, but doesn't return a key,
|
||||
L{SSHUserAuthClient.getPublicKey} continue with the normal key lookup.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
agent = SSHAgentClient()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
client.keyAgent = agent
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, self.rsaPublic)
|
||||
|
||||
def test_getPublicKeyBadKeyError(self):
|
||||
"""
|
||||
If L{keys.Key.fromFile} raises a L{keys.BadKeyError}, the
|
||||
L{SSHUserAuthClient.getPublicKey} tries again to get a public key by
|
||||
calling itself recursively.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
self.tmpdir.child("id_dsa.pub").setContent(keydata.publicDSA_openssh)
|
||||
dsaFile = self.tmpdir.child("id_dsa")
|
||||
dsaFile.setContent(keydata.privateDSA_openssh)
|
||||
options.identitys = [self.rsaFile.path, dsaFile.path]
|
||||
self.tmpdir.child("id_rsa.pub").setContent(b"not a key!")
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
key = client.getPublicKey()
|
||||
self.assertTrue(key.isPublic())
|
||||
self.assertEqual(key, Key.fromString(keydata.publicDSA_openssh))
|
||||
self.assertEqual(client.usedFiles, [self.rsaFile.path, dsaFile.path])
|
||||
|
||||
def test_getPrivateKey(self):
|
||||
"""
|
||||
L{SSHUserAuthClient.getPrivateKey} will load a private key from the
|
||||
last used file populated by L{SSHUserAuthClient.getPublicKey}, and
|
||||
return a L{Deferred} which fires with the corresponding private L{Key}.
|
||||
"""
|
||||
rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
# Populate the list of used files
|
||||
client.getPublicKey()
|
||||
|
||||
def _cbGetPrivateKey(key):
|
||||
self.assertFalse(key.isPublic())
|
||||
self.assertEqual(key, rsaPrivate)
|
||||
|
||||
return client.getPrivateKey().addCallback(_cbGetPrivateKey)
|
||||
|
||||
def test_getPrivateKeyPassphrase(self):
|
||||
"""
|
||||
L{SSHUserAuthClient} can get a private key from a file, and return a
|
||||
Deferred called back with a private L{Key} object, even if the key is
|
||||
encrypted.
|
||||
"""
|
||||
rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
|
||||
passphrase = b"this is the passphrase"
|
||||
self.rsaFile.setContent(rsaPrivate.toString("openssh", passphrase=passphrase))
|
||||
options = ConchOptions()
|
||||
options.identitys = [self.rsaFile.path]
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
# Populate the list of used files
|
||||
client.getPublicKey()
|
||||
|
||||
def _getPassword(prompt):
|
||||
self.assertEqual(
|
||||
prompt, f"Enter passphrase for key '{self.rsaFile.path}': "
|
||||
)
|
||||
return nativeString(passphrase)
|
||||
|
||||
def _cbGetPrivateKey(key):
|
||||
self.assertFalse(key.isPublic())
|
||||
self.assertEqual(key, rsaPrivate)
|
||||
|
||||
self.patch(client, "_getPassword", _getPassword)
|
||||
return client.getPrivateKey().addCallback(_cbGetPrivateKey)
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
def test_getPassword(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
"""
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self, host):
|
||||
self.transport = self
|
||||
self.host = host
|
||||
|
||||
def getPeer(self):
|
||||
return self
|
||||
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
client.transport = FakeTransport("127.0.0.1")
|
||||
|
||||
def getpass(prompt):
|
||||
self.assertEqual(prompt, "user@127.0.0.1's password: ")
|
||||
return "bad password"
|
||||
|
||||
self.patch(default.getpass, "getpass", getpass)
|
||||
d = client.getPassword()
|
||||
d.addCallback(self.assertEqual, b"bad password")
|
||||
return d
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
def test_getPasswordPrompt(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
using a different prompt.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
prompt = b"Give up your password"
|
||||
|
||||
def getpass(p):
|
||||
self.assertEqual(p, nativeString(prompt))
|
||||
return "bad password"
|
||||
|
||||
self.patch(default.getpass, "getpass", getpass)
|
||||
d = client.getPassword(prompt)
|
||||
d.addCallback(self.assertEqual, b"bad password")
|
||||
return d
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
def test_getPasswordConchError(self):
|
||||
"""
|
||||
Get the password using
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getPassword}
|
||||
and trigger a {twisted.conch.error import ConchError}.
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
|
||||
def getpass(prompt):
|
||||
raise KeyboardInterrupt("User pressed CTRL-C")
|
||||
|
||||
self.patch(default.getpass, "getpass", getpass)
|
||||
stdout, stdin = sys.stdout, sys.stdin
|
||||
d = client.getPassword(b"?")
|
||||
|
||||
@d.addErrback
|
||||
def check_sys(fail):
|
||||
self.assertEqual([stdout, stdin], [sys.stdout, sys.stdin])
|
||||
return fail
|
||||
|
||||
self.assertFailure(d, ConchError)
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
def test_getGenericAnswers(self):
|
||||
"""
|
||||
L{twisted.conch.client.default.SSHUserAuthClient.getGenericAnswers}
|
||||
"""
|
||||
options = ConchOptions()
|
||||
client = SSHUserAuthClient(b"user", options, None)
|
||||
|
||||
def getpass(prompt):
|
||||
self.assertEqual(prompt, "pass prompt")
|
||||
return "getpass"
|
||||
|
||||
self.patch(default.getpass, "getpass", getpass)
|
||||
|
||||
def raw_input(prompt):
|
||||
self.assertEqual(prompt, "raw_input prompt")
|
||||
return "raw_input"
|
||||
|
||||
self.patch(default, "_input", raw_input)
|
||||
d = client.getGenericAnswers(
|
||||
b"Name",
|
||||
b"Instruction",
|
||||
[(b"pass prompt", False), (b"raw_input prompt", True)],
|
||||
)
|
||||
d.addCallback(self.assertListEqual, ["getpass", "raw_input"])
|
||||
return d
|
||||
|
||||
|
||||
class ConchOptionsParsing(TestCase):
|
||||
"""
|
||||
Options parsing.
|
||||
"""
|
||||
|
||||
def test_macs(self):
|
||||
"""
|
||||
Specify MAC algorithms.
|
||||
"""
|
||||
opts = ConchOptions()
|
||||
e = self.assertRaises(SystemExit, opts.opt_macs, "invalid-mac")
|
||||
self.assertIn("Unknown mac type", e.code)
|
||||
opts = ConchOptions()
|
||||
opts.opt_macs("hmac-sha2-512")
|
||||
self.assertEqual(opts["macs"], [b"hmac-sha2-512"])
|
||||
opts.opt_macs(b"hmac-sha2-512")
|
||||
self.assertEqual(opts["macs"], [b"hmac-sha2-512"])
|
||||
opts.opt_macs("hmac-sha2-256,hmac-sha1,hmac-md5")
|
||||
self.assertEqual(opts["macs"], [b"hmac-sha2-256", b"hmac-sha1", b"hmac-md5"])
|
||||
|
||||
def test_host_key_algorithms(self):
|
||||
"""
|
||||
Specify host key algorithms.
|
||||
"""
|
||||
opts = ConchOptions()
|
||||
e = self.assertRaises(SystemExit, opts.opt_host_key_algorithms, "invalid-key")
|
||||
self.assertIn("Unknown host key type", e.code)
|
||||
opts = ConchOptions()
|
||||
opts.opt_host_key_algorithms("ssh-rsa")
|
||||
self.assertEqual(opts["host-key-algorithms"], [b"ssh-rsa"])
|
||||
opts.opt_host_key_algorithms(b"ssh-dss")
|
||||
self.assertEqual(opts["host-key-algorithms"], [b"ssh-dss"])
|
||||
opts.opt_host_key_algorithms("ssh-rsa,ssh-dss")
|
||||
self.assertEqual(opts["host-key-algorithms"], [b"ssh-rsa", b"ssh-dss"])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,964 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_filetransfer -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE file for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.filetransfer}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from unittest import skipIf
|
||||
|
||||
from hamcrest import assert_that, equal_to
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.error import ConnectionLost
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.protocols import loopback
|
||||
from twisted.python import components
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.conch import unix as _unix
|
||||
except ImportError:
|
||||
unix = None
|
||||
else:
|
||||
unix = _unix
|
||||
|
||||
try:
|
||||
from twisted.conch.unix import (
|
||||
SFTPServerForUnixConchUser as _SFTPServerForUnixConchUser,
|
||||
)
|
||||
except ImportError:
|
||||
SFTPServerForUnixConchUser = None
|
||||
else:
|
||||
SFTPServerForUnixConchUser = _SFTPServerForUnixConchUser
|
||||
|
||||
try:
|
||||
import cryptography as _cryptography
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
else:
|
||||
cryptography = _cryptography
|
||||
|
||||
try:
|
||||
from twisted.conch.avatar import ConchUser as _ConchUser
|
||||
except ImportError:
|
||||
ConchUser = object
|
||||
else:
|
||||
ConchUser = _ConchUser # type: ignore[misc]
|
||||
|
||||
try:
|
||||
from twisted.conch.ssh import common, connection, filetransfer, session
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class TestAvatar(ConchUser):
|
||||
def __init__(self):
|
||||
ConchUser.__init__(self)
|
||||
self.channelLookup[b"session"] = session.SSHSession
|
||||
self.subsystemLookup[b"sftp"] = filetransfer.FileTransferServer
|
||||
|
||||
def _runAsUser(self, f, *args, **kw):
|
||||
try:
|
||||
f = iter(f)
|
||||
except TypeError:
|
||||
f = [(f, args, kw)]
|
||||
for i in f:
|
||||
func = i[0]
|
||||
args = len(i) > 1 and i[1] or ()
|
||||
kw = len(i) > 2 and i[2] or {}
|
||||
r = func(*args, **kw)
|
||||
return r
|
||||
|
||||
|
||||
class FileTransferTestAvatar(TestAvatar):
|
||||
def __init__(self, homeDir):
|
||||
TestAvatar.__init__(self)
|
||||
self.homeDir = homeDir
|
||||
|
||||
def getHomeDir(self):
|
||||
return FilePath(os.getcwd()).preauthChild(self.homeDir.path)
|
||||
|
||||
|
||||
class ConchSessionForTestAvatar:
|
||||
def __init__(self, avatar):
|
||||
self.avatar = avatar
|
||||
|
||||
|
||||
if SFTPServerForUnixConchUser is None:
|
||||
# unix should either be a fully working module, or None. I'm not sure
|
||||
# how this happens, but on win32 it does. Try to cope. --spiv.
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
"twisted.conch.unix imported %r, "
|
||||
"but doesn't define SFTPServerForUnixConchUser'"
|
||||
)
|
||||
% (unix,)
|
||||
)
|
||||
else:
|
||||
|
||||
class FileTransferForTestAvatar(SFTPServerForUnixConchUser): # type: ignore[misc,valid-type]
|
||||
def gotVersion(self, version, otherExt):
|
||||
return {b"conchTest": b"ext data"}
|
||||
|
||||
def extendedRequest(self, extName, extData):
|
||||
if extName == b"testExtendedRequest":
|
||||
return b"bar"
|
||||
raise NotImplementedError
|
||||
|
||||
components.registerAdapter(
|
||||
FileTransferForTestAvatar, TestAvatar, filetransfer.ISFTPServer
|
||||
)
|
||||
|
||||
|
||||
class SFTPTestBase(TestCase):
|
||||
def setUp(self):
|
||||
self.testDir = FilePath(self.mktemp())
|
||||
# Give the testDir another level so we can safely "cd .." from it in
|
||||
# tests.
|
||||
self.testDir = self.testDir.child("extra")
|
||||
self.testDir.child("testDirectory").makedirs(True)
|
||||
|
||||
with self.testDir.child("testfile1").open(mode="wb") as f:
|
||||
f.write(b"a" * 10 + b"b" * 10)
|
||||
with open("/dev/urandom", "rb") as f2:
|
||||
f.write(f2.read(1024 * 64)) # random data
|
||||
self.testDir.child("testfile1").chmod(0o644)
|
||||
with self.testDir.child("testRemoveFile").open(mode="wb") as f:
|
||||
f.write(b"a")
|
||||
with self.testDir.child("testRenameFile").open(mode="wb") as f:
|
||||
f.write(b"a")
|
||||
with self.testDir.child(".testHiddenFile").open(mode="wb") as f:
|
||||
f.write(b"a")
|
||||
|
||||
|
||||
@skipIf(not unix, "can't run on non-posix computers")
|
||||
class OurServerOurClientTests(SFTPTestBase):
|
||||
def setUp(self):
|
||||
SFTPTestBase.setUp(self)
|
||||
|
||||
self.avatar = FileTransferTestAvatar(self.testDir)
|
||||
self.server = filetransfer.FileTransferServer(avatar=self.avatar)
|
||||
clientTransport = loopback.LoopbackRelay(self.server)
|
||||
|
||||
self.client = filetransfer.FileTransferClient()
|
||||
self._serverVersion = None
|
||||
self._extData = None
|
||||
|
||||
def _(serverVersion, extData):
|
||||
self._serverVersion = serverVersion
|
||||
self._extData = extData
|
||||
|
||||
self.client.gotServerVersion = _
|
||||
serverTransport = loopback.LoopbackRelay(self.client)
|
||||
self.client.makeConnection(clientTransport)
|
||||
self.server.makeConnection(serverTransport)
|
||||
|
||||
self.clientTransport = clientTransport
|
||||
self.serverTransport = serverTransport
|
||||
|
||||
self._emptyBuffers()
|
||||
|
||||
def _emptyBuffers(self):
|
||||
while self.serverTransport.buffer or self.clientTransport.buffer:
|
||||
self.serverTransport.clearBuffer()
|
||||
self.clientTransport.clearBuffer()
|
||||
|
||||
def tearDown(self):
|
||||
self.serverTransport.loseConnection()
|
||||
self.clientTransport.loseConnection()
|
||||
self.serverTransport.clearBuffer()
|
||||
self.clientTransport.clearBuffer()
|
||||
|
||||
def test_serverVersion(self):
|
||||
self.assertEqual(self._serverVersion, 3)
|
||||
self.assertEqual(self._extData, {b"conchTest": b"ext data"})
|
||||
|
||||
def test_interface_implementation(self):
|
||||
"""
|
||||
It implements the ISFTPServer interface.
|
||||
"""
|
||||
self.assertTrue(
|
||||
filetransfer.ISFTPServer.providedBy(self.server.client),
|
||||
f"ISFTPServer not provided by {self.server.client!r}",
|
||||
)
|
||||
|
||||
def test_openedFileClosedWithConnection(self):
|
||||
"""
|
||||
A file opened with C{openFile} is closed when the connection is lost.
|
||||
"""
|
||||
d = self.client.openFile(
|
||||
b"testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
oldClose = os.close
|
||||
closed = []
|
||||
|
||||
def close(fd):
|
||||
closed.append(fd)
|
||||
oldClose(fd)
|
||||
|
||||
self.patch(os, "close", close)
|
||||
|
||||
def _fileOpened(openFile):
|
||||
fd = self.server.openFiles[openFile.handle[4:]].fd
|
||||
self.serverTransport.loseConnection()
|
||||
self.clientTransport.loseConnection()
|
||||
self.serverTransport.clearBuffer()
|
||||
self.clientTransport.clearBuffer()
|
||||
self.assertEqual(self.server.openFiles, {})
|
||||
self.assertIn(fd, closed)
|
||||
|
||||
d.addCallback(_fileOpened)
|
||||
return d
|
||||
|
||||
def test_openedDirectoryClosedWithConnection(self):
|
||||
"""
|
||||
A directory opened with C{openDirectory} is close when the connection
|
||||
is lost.
|
||||
"""
|
||||
d = self.client.openDirectory("")
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getFiles(openDir):
|
||||
self.serverTransport.loseConnection()
|
||||
self.clientTransport.loseConnection()
|
||||
self.serverTransport.clearBuffer()
|
||||
self.clientTransport.clearBuffer()
|
||||
self.assertEqual(self.server.openDirs, {})
|
||||
|
||||
d.addCallback(_getFiles)
|
||||
return d
|
||||
|
||||
def test_openFileIO(self):
|
||||
d = self.client.openFile(
|
||||
b"testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
def _fileOpened(openFile):
|
||||
self.assertEqual(openFile, filetransfer.ISFTPFile(openFile))
|
||||
d = _readChunk(openFile)
|
||||
d.addCallback(_writeChunk, openFile)
|
||||
return d
|
||||
|
||||
def _readChunk(openFile):
|
||||
d = openFile.readChunk(0, 20)
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, b"a" * 10 + b"b" * 10)
|
||||
return d
|
||||
|
||||
def _writeChunk(_, openFile):
|
||||
d = openFile.writeChunk(20, b"c" * 10)
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_readChunk2, openFile)
|
||||
return d
|
||||
|
||||
def _readChunk2(_, openFile):
|
||||
d = openFile.readChunk(0, 30)
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, b"a" * 10 + b"b" * 10 + b"c" * 10)
|
||||
return d
|
||||
|
||||
d.addCallback(_fileOpened)
|
||||
return d
|
||||
|
||||
def test_closedFileGetAttrs(self):
|
||||
d = self.client.openFile(
|
||||
b"testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getAttrs(_, openFile):
|
||||
d = openFile.getAttrs()
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
def _err(f):
|
||||
self.flushLoggedErrors()
|
||||
return f
|
||||
|
||||
def _close(openFile):
|
||||
d = openFile.close()
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_getAttrs, openFile)
|
||||
d.addErrback(_err)
|
||||
return self.assertFailure(d, filetransfer.SFTPError)
|
||||
|
||||
d.addCallback(_close)
|
||||
return d
|
||||
|
||||
def test_openFileAttributes(self):
|
||||
d = self.client.openFile(
|
||||
b"testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getAttrs(openFile):
|
||||
d = openFile.getAttrs()
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_getAttrs2)
|
||||
return d
|
||||
|
||||
def _getAttrs2(attrs1):
|
||||
d = self.client.getAttrs(b"testfile1")
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, attrs1)
|
||||
return d
|
||||
|
||||
return d.addCallback(_getAttrs)
|
||||
|
||||
def test_openFileSetAttrs(self):
|
||||
# XXX test setAttrs
|
||||
# Ok, how about this for a start? It caught a bug :) -- spiv.
|
||||
d = self.client.openFile(
|
||||
b"testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getAttrs(openFile):
|
||||
d = openFile.getAttrs()
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_setAttrs)
|
||||
return d
|
||||
|
||||
def _setAttrs(attrs):
|
||||
attrs["atime"] = 0
|
||||
d = self.client.setAttrs(b"testfile1", attrs)
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_getAttrs2)
|
||||
d.addCallback(self.assertEqual, attrs)
|
||||
return d
|
||||
|
||||
def _getAttrs2(_):
|
||||
d = self.client.getAttrs(b"testfile1")
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
d.addCallback(_getAttrs)
|
||||
return d
|
||||
|
||||
def test_openFileExtendedAttributes(self):
|
||||
"""
|
||||
Check that L{filetransfer.FileTransferClient.openFile} can send
|
||||
extended attributes, that should be extracted server side. By default,
|
||||
they are ignored, so we just verify they are correctly parsed.
|
||||
"""
|
||||
savedAttributes = {}
|
||||
oldOpenFile = self.server.client.openFile
|
||||
|
||||
def openFile(filename, flags, attrs):
|
||||
savedAttributes.update(attrs)
|
||||
return oldOpenFile(filename, flags, attrs)
|
||||
|
||||
self.server.client.openFile = openFile
|
||||
|
||||
d = self.client.openFile(
|
||||
b"testfile1",
|
||||
filetransfer.FXF_READ | filetransfer.FXF_WRITE,
|
||||
{"ext_foo": b"bar"},
|
||||
)
|
||||
self._emptyBuffers()
|
||||
|
||||
def check(ign):
|
||||
self.assertEqual(savedAttributes, {"ext_foo": b"bar"})
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_removeFile(self):
|
||||
d = self.client.getAttrs(b"testRemoveFile")
|
||||
self._emptyBuffers()
|
||||
|
||||
def _removeFile(ignored):
|
||||
d = self.client.removeFile(b"testRemoveFile")
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
d.addCallback(_removeFile)
|
||||
d.addCallback(_removeFile)
|
||||
return self.assertFailure(d, filetransfer.SFTPError)
|
||||
|
||||
def test_renameFile(self):
|
||||
d = self.client.getAttrs(b"testRenameFile")
|
||||
self._emptyBuffers()
|
||||
|
||||
def _rename(attrs):
|
||||
d = self.client.renameFile(b"testRenameFile", b"testRenamedFile")
|
||||
self._emptyBuffers()
|
||||
d.addCallback(_testRenamed, attrs)
|
||||
return d
|
||||
|
||||
def _testRenamed(_, attrs):
|
||||
d = self.client.getAttrs(b"testRenamedFile")
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, attrs)
|
||||
|
||||
return d.addCallback(_rename)
|
||||
|
||||
def test_directoryBad(self):
|
||||
d = self.client.getAttrs(b"testMakeDirectory")
|
||||
self._emptyBuffers()
|
||||
return self.assertFailure(d, filetransfer.SFTPError)
|
||||
|
||||
def test_directoryCreation(self):
|
||||
d = self.client.makeDirectory(b"testMakeDirectory", {})
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getAttrs(_):
|
||||
d = self.client.getAttrs(b"testMakeDirectory")
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
# XXX not until version 4/5
|
||||
# self.assertEqual(filetransfer.FILEXFER_TYPE_DIRECTORY&attrs['type'],
|
||||
# filetransfer.FILEXFER_TYPE_DIRECTORY)
|
||||
|
||||
def _removeDirectory(_):
|
||||
d = self.client.removeDirectory(b"testMakeDirectory")
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
d.addCallback(_getAttrs)
|
||||
d.addCallback(_removeDirectory)
|
||||
d.addCallback(_getAttrs)
|
||||
return self.assertFailure(d, filetransfer.SFTPError)
|
||||
|
||||
def test_openDirectory(self):
|
||||
d = self.client.openDirectory(b"")
|
||||
self._emptyBuffers()
|
||||
files = []
|
||||
|
||||
def _getFiles(openDir):
|
||||
def append(f):
|
||||
files.append(f)
|
||||
return openDir
|
||||
|
||||
d = defer.maybeDeferred(openDir.next)
|
||||
self._emptyBuffers()
|
||||
d.addCallback(append)
|
||||
d.addCallback(_getFiles)
|
||||
d.addErrback(_close, openDir)
|
||||
return d
|
||||
|
||||
def _checkFiles(ignored):
|
||||
fs = list(list(zip(*files))[0])
|
||||
fs.sort()
|
||||
self.assertEqual(
|
||||
fs,
|
||||
[
|
||||
b".testHiddenFile",
|
||||
b"testDirectory",
|
||||
b"testRemoveFile",
|
||||
b"testRenameFile",
|
||||
b"testfile1",
|
||||
],
|
||||
)
|
||||
|
||||
def _close(_, openDir):
|
||||
d = openDir.close()
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
d.addCallback(_getFiles)
|
||||
d.addCallback(_checkFiles)
|
||||
return d
|
||||
|
||||
def test_linkDoesntExist(self):
|
||||
d = self.client.getAttrs(b"testLink")
|
||||
self._emptyBuffers()
|
||||
return self.assertFailure(d, filetransfer.SFTPError)
|
||||
|
||||
def test_linkSharesAttrs(self):
|
||||
d = self.client.makeLink(b"testLink", b"testfile1")
|
||||
self._emptyBuffers()
|
||||
|
||||
def _getFirstAttrs(_):
|
||||
d = self.client.getAttrs(b"testLink", 1)
|
||||
self._emptyBuffers()
|
||||
return d
|
||||
|
||||
def _getSecondAttrs(firstAttrs):
|
||||
d = self.client.getAttrs(b"testfile1")
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, firstAttrs)
|
||||
return d
|
||||
|
||||
d.addCallback(_getFirstAttrs)
|
||||
return d.addCallback(_getSecondAttrs)
|
||||
|
||||
def test_linkPath(self):
|
||||
d = self.client.makeLink(b"testLink", b"testfile1")
|
||||
self._emptyBuffers()
|
||||
|
||||
def _readLink(_):
|
||||
d = self.client.readLink(b"testLink")
|
||||
self._emptyBuffers()
|
||||
testFile = FilePath(os.getcwd()).preauthChild(self.testDir.path)
|
||||
testFile = testFile.child("testfile1")
|
||||
d.addCallback(self.assertEqual, testFile.path)
|
||||
return d
|
||||
|
||||
def _realPath(_):
|
||||
d = self.client.realPath(b"testLink")
|
||||
self._emptyBuffers()
|
||||
testLink = FilePath(os.getcwd()).preauthChild(self.testDir.path)
|
||||
testLink = testLink.child("testfile1")
|
||||
d.addCallback(self.assertEqual, testLink.path)
|
||||
return d
|
||||
|
||||
d.addCallback(_readLink)
|
||||
d.addCallback(_realPath)
|
||||
return d
|
||||
|
||||
def test_extendedRequest(self):
|
||||
d = self.client.extendedRequest(b"testExtendedRequest", b"foo")
|
||||
self._emptyBuffers()
|
||||
d.addCallback(self.assertEqual, b"bar")
|
||||
d.addCallback(self._cbTestExtendedRequest)
|
||||
return d
|
||||
|
||||
def _cbTestExtendedRequest(self, ignored):
|
||||
d = self.client.extendedRequest(b"testBadRequest", b"")
|
||||
self._emptyBuffers()
|
||||
return self.assertFailure(d, NotImplementedError)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_openDirectoryIteratorDeprecated(self):
|
||||
"""
|
||||
Using client.openDirectory as an iterator is deprecated.
|
||||
"""
|
||||
d = self.client.openDirectory(b"")
|
||||
self._emptyBuffers()
|
||||
openDir = yield d
|
||||
oneFile = openDir.next()
|
||||
self._emptyBuffers()
|
||||
yield oneFile
|
||||
|
||||
warnings = self.flushWarnings()
|
||||
message = (
|
||||
"Using twisted.conch.ssh.filetransfer.ClientDirectory"
|
||||
" as an iterator was deprecated in Twisted 18.9.0."
|
||||
)
|
||||
self.assertEqual(1, len(warnings))
|
||||
self.assertEqual(DeprecationWarning, warnings[0]["category"])
|
||||
self.assertEqual(message, warnings[0]["message"])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_closedConnectionCancelsRequests(self):
|
||||
"""
|
||||
If there are requests outstanding when the connection
|
||||
is closed for any reason, they should fail.
|
||||
"""
|
||||
|
||||
d = self.client.openFile(b"testfile1", filetransfer.FXF_READ, {})
|
||||
self._emptyBuffers()
|
||||
fh = yield d
|
||||
|
||||
# Intercept the handling of the read request on the server side
|
||||
gotReadRequest = []
|
||||
|
||||
def _slowRead(offset, length):
|
||||
self.assertEqual(gotReadRequest, [])
|
||||
d = defer.Deferred()
|
||||
gotReadRequest.append(offset)
|
||||
return d
|
||||
|
||||
[serverSideFh] = self.server.openFiles.values()
|
||||
serverSideFh.readChunk = _slowRead
|
||||
del serverSideFh
|
||||
|
||||
# Make a read request, dropping the connection before the reply
|
||||
# is sent
|
||||
d = fh.readChunk(100, 200)
|
||||
self._emptyBuffers()
|
||||
self.assertEqual(len(gotReadRequest), 1)
|
||||
self.assertNoResult(d)
|
||||
|
||||
# Lost connection should cause an errback
|
||||
self.serverTransport.loseConnection()
|
||||
self.serverTransport.clearBuffer()
|
||||
self.clientTransport.clearBuffer()
|
||||
self._emptyBuffers()
|
||||
|
||||
self.assertFalse(self.client.connected)
|
||||
self.failureResultOf(d, ConnectionLost)
|
||||
|
||||
# Further attempts to use the filetransfer session should fail
|
||||
# immediately
|
||||
d = fh.getAttrs()
|
||||
self.failureResultOf(d, ConnectionLost)
|
||||
|
||||
|
||||
class FakeConn:
|
||||
def sendClose(self, channel):
|
||||
pass
|
||||
|
||||
|
||||
@skipIf(not unix, "can't run on non-posix computers")
|
||||
class FileTransferCloseTests(TestCase):
|
||||
def setUp(self):
|
||||
self.avatar = TestAvatar()
|
||||
|
||||
def buildServerConnection(self):
|
||||
# make a server connection
|
||||
conn = connection.SSHConnection()
|
||||
# server connections have a 'self.transport.avatar'.
|
||||
|
||||
class DummyTransport:
|
||||
def __init__(self):
|
||||
self.transport = self
|
||||
|
||||
def sendPacket(self, kind, data):
|
||||
pass
|
||||
|
||||
def logPrefix(self):
|
||||
return "dummy transport"
|
||||
|
||||
conn.transport = DummyTransport()
|
||||
conn.transport.avatar = self.avatar
|
||||
return conn
|
||||
|
||||
def interceptConnectionLost(self, sftpServer):
|
||||
self.connectionLostFired = False
|
||||
origConnectionLost = sftpServer.connectionLost
|
||||
|
||||
def connectionLost(reason):
|
||||
self.connectionLostFired = True
|
||||
origConnectionLost(reason)
|
||||
|
||||
sftpServer.connectionLost = connectionLost
|
||||
|
||||
def assertSFTPConnectionLost(self):
|
||||
self.assertTrue(
|
||||
self.connectionLostFired, "sftpServer's connectionLost was not called"
|
||||
)
|
||||
|
||||
def test_sessionClose(self):
|
||||
"""
|
||||
Closing a session should notify an SFTP subsystem launched by that
|
||||
session.
|
||||
"""
|
||||
# make a session
|
||||
testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar)
|
||||
|
||||
# start an SFTP subsystem on the session
|
||||
testSession.request_subsystem(common.NS(b"sftp"))
|
||||
sftpServer = testSession.client.transport.proto
|
||||
|
||||
# intercept connectionLost so we can check that it's called
|
||||
self.interceptConnectionLost(sftpServer)
|
||||
|
||||
# close session
|
||||
testSession.closeReceived()
|
||||
|
||||
self.assertSFTPConnectionLost()
|
||||
|
||||
def test_clientClosesChannelOnConnnection(self):
|
||||
"""
|
||||
A client sending CHANNEL_CLOSE should trigger closeReceived on the
|
||||
associated channel instance.
|
||||
"""
|
||||
conn = self.buildServerConnection()
|
||||
|
||||
# somehow get a session
|
||||
packet = common.NS(b"session") + struct.pack(">L", 0) * 3
|
||||
conn.ssh_CHANNEL_OPEN(packet)
|
||||
sessionChannel = conn.channels[0]
|
||||
|
||||
sessionChannel.request_subsystem(common.NS(b"sftp"))
|
||||
sftpServer = sessionChannel.client.transport.proto
|
||||
self.interceptConnectionLost(sftpServer)
|
||||
|
||||
# intercept closeReceived
|
||||
self.interceptConnectionLost(sftpServer)
|
||||
|
||||
# close the connection
|
||||
conn.ssh_CHANNEL_CLOSE(struct.pack(">L", 0))
|
||||
|
||||
self.assertSFTPConnectionLost()
|
||||
|
||||
def test_stopConnectionServiceClosesChannel(self):
|
||||
"""
|
||||
Closing an SSH connection should close all sessions within it.
|
||||
"""
|
||||
conn = self.buildServerConnection()
|
||||
|
||||
# somehow get a session
|
||||
packet = common.NS(b"session") + struct.pack(">L", 0) * 3
|
||||
conn.ssh_CHANNEL_OPEN(packet)
|
||||
sessionChannel = conn.channels[0]
|
||||
|
||||
sessionChannel.request_subsystem(common.NS(b"sftp"))
|
||||
sftpServer = sessionChannel.client.transport.proto
|
||||
self.interceptConnectionLost(sftpServer)
|
||||
|
||||
# close the connection
|
||||
conn.serviceStopped()
|
||||
|
||||
self.assertSFTPConnectionLost()
|
||||
|
||||
|
||||
@skipIf(not cryptography, "Cannot run without cryptography")
|
||||
class ConstantsTests(TestCase):
|
||||
"""
|
||||
Tests for the constants used by the SFTP protocol implementation.
|
||||
|
||||
@ivar filexferSpecExcerpts: Excerpts from the
|
||||
draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP
|
||||
protocol. There are more recent drafts of the specification, but this
|
||||
one describes version 3, which is what conch (and OpenSSH) implements.
|
||||
"""
|
||||
|
||||
filexferSpecExcerpts = [
|
||||
"""
|
||||
The following values are defined for packet types.
|
||||
|
||||
#define SSH_FXP_INIT 1
|
||||
#define SSH_FXP_VERSION 2
|
||||
#define SSH_FXP_OPEN 3
|
||||
#define SSH_FXP_CLOSE 4
|
||||
#define SSH_FXP_READ 5
|
||||
#define SSH_FXP_WRITE 6
|
||||
#define SSH_FXP_LSTAT 7
|
||||
#define SSH_FXP_FSTAT 8
|
||||
#define SSH_FXP_SETSTAT 9
|
||||
#define SSH_FXP_FSETSTAT 10
|
||||
#define SSH_FXP_OPENDIR 11
|
||||
#define SSH_FXP_READDIR 12
|
||||
#define SSH_FXP_REMOVE 13
|
||||
#define SSH_FXP_MKDIR 14
|
||||
#define SSH_FXP_RMDIR 15
|
||||
#define SSH_FXP_REALPATH 16
|
||||
#define SSH_FXP_STAT 17
|
||||
#define SSH_FXP_RENAME 18
|
||||
#define SSH_FXP_READLINK 19
|
||||
#define SSH_FXP_SYMLINK 20
|
||||
#define SSH_FXP_STATUS 101
|
||||
#define SSH_FXP_HANDLE 102
|
||||
#define SSH_FXP_DATA 103
|
||||
#define SSH_FXP_NAME 104
|
||||
#define SSH_FXP_ATTRS 105
|
||||
#define SSH_FXP_EXTENDED 200
|
||||
#define SSH_FXP_EXTENDED_REPLY 201
|
||||
|
||||
Additional packet types should only be defined if the protocol
|
||||
version number (see Section ``Protocol Initialization'') is
|
||||
incremented, and their use MUST be negotiated using the version
|
||||
number. However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY
|
||||
packets can be used to implement vendor-specific extensions. See
|
||||
Section ``Vendor-Specific-Extensions'' for more details.
|
||||
""",
|
||||
"""
|
||||
The flags bits are defined to have the following values:
|
||||
|
||||
#define SSH_FILEXFER_ATTR_SIZE 0x00000001
|
||||
#define SSH_FILEXFER_ATTR_UIDGID 0x00000002
|
||||
#define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004
|
||||
#define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008
|
||||
#define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
|
||||
|
||||
""",
|
||||
"""
|
||||
The `pflags' field is a bitmask. The following bits have been
|
||||
defined.
|
||||
|
||||
#define SSH_FXF_READ 0x00000001
|
||||
#define SSH_FXF_WRITE 0x00000002
|
||||
#define SSH_FXF_APPEND 0x00000004
|
||||
#define SSH_FXF_CREAT 0x00000008
|
||||
#define SSH_FXF_TRUNC 0x00000010
|
||||
#define SSH_FXF_EXCL 0x00000020
|
||||
""",
|
||||
"""
|
||||
Currently, the following values are defined (other values may be
|
||||
defined by future versions of this protocol):
|
||||
|
||||
#define SSH_FX_OK 0
|
||||
#define SSH_FX_EOF 1
|
||||
#define SSH_FX_NO_SUCH_FILE 2
|
||||
#define SSH_FX_PERMISSION_DENIED 3
|
||||
#define SSH_FX_FAILURE 4
|
||||
#define SSH_FX_BAD_MESSAGE 5
|
||||
#define SSH_FX_NO_CONNECTION 6
|
||||
#define SSH_FX_CONNECTION_LOST 7
|
||||
#define SSH_FX_OP_UNSUPPORTED 8
|
||||
""",
|
||||
]
|
||||
|
||||
def test_constantsAgainstSpec(self):
|
||||
"""
|
||||
The constants used by the SFTP protocol implementation match those
|
||||
found by searching through the spec.
|
||||
"""
|
||||
constants = {}
|
||||
for excerpt in self.filexferSpecExcerpts:
|
||||
for line in excerpt.splitlines():
|
||||
m = re.match(r"^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$", line)
|
||||
if m:
|
||||
constants[m.group(1)] = int(m.group(2), 0)
|
||||
self.assertTrue(
|
||||
len(constants) > 0, "No constants found (the test must be buggy)."
|
||||
)
|
||||
for k, v in constants.items():
|
||||
self.assertEqual(v, getattr(filetransfer, k))
|
||||
|
||||
|
||||
# We don't run on Windows, as we don't have an SFTP file server implemented in conch.ssh for Windows.
|
||||
# As soon as there is such an implementation, we can run these tests on Windows.
|
||||
@skipIf(not unix, "can't run on non-posix computers")
|
||||
@skipIf(not cryptography, "Cannot run without cryptography")
|
||||
class RawPacketDataServerTests(TestCase):
|
||||
"""
|
||||
Tests for L{filetransfer.FileTransferServer} which explicitly craft
|
||||
certain less common situations to exercise their handling.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.fts = filetransfer.FileTransferServer(avatar=TestAvatar())
|
||||
|
||||
def test_closeInvalidHandle(self):
|
||||
"""
|
||||
A close request with an unknown handle receives an FX_NO_SUCH_FILE error
|
||||
response.
|
||||
"""
|
||||
transport = StringTransport()
|
||||
self.fts.makeConnection(transport)
|
||||
|
||||
# any four bytes
|
||||
requestId = b"1234"
|
||||
# The handle to close, arbitrary bytes.
|
||||
handle = b"invalid handle"
|
||||
|
||||
# Construct a message packet
|
||||
# https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4
|
||||
close = common.NS(
|
||||
# Packet type - SSH_FXP_CLOSE
|
||||
# https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4.3
|
||||
bytes([4])
|
||||
+ requestId
|
||||
+ common.NS(handle)
|
||||
)
|
||||
|
||||
self.fts.dataReceived(close)
|
||||
|
||||
# An SSH_FXP_STATUS message
|
||||
# https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-9.1
|
||||
expected = common.NS(
|
||||
# Packet type SSH_FXP_STATUS
|
||||
# https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4.3
|
||||
bytes([101])
|
||||
+
|
||||
# The same request id
|
||||
requestId
|
||||
+
|
||||
# A four byte status code. SSH_FX_NO_SUCH_FILE in this case.
|
||||
bytes([0, 0, 0, 2])
|
||||
+
|
||||
# Error message
|
||||
common.NS(b"No such file or directory")
|
||||
+
|
||||
# error message language tag - conch doesn't send one at all,
|
||||
# though maybe it should
|
||||
common.NS(b"")
|
||||
)
|
||||
|
||||
assert_that(
|
||||
transport.value(),
|
||||
equal_to(expected),
|
||||
)
|
||||
|
||||
|
||||
@skipIf(not cryptography, "Cannot run without cryptography")
|
||||
class RawPacketDataTests(TestCase):
|
||||
"""
|
||||
Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
|
||||
less common protocol messages to exercise their handling.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.ftc = filetransfer.FileTransferClient()
|
||||
|
||||
def test_packetSTATUS(self):
|
||||
"""
|
||||
A STATUS packet containing a result code, a message, and a language is
|
||||
parsed to produce the result of an outstanding request L{Deferred}.
|
||||
|
||||
@see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
|
||||
of the SFTP Internet-Draft.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
d.addCallback(self._cbTestPacketSTATUS)
|
||||
self.ftc.openRequests[1] = d
|
||||
data = (
|
||||
struct.pack("!LL", 1, filetransfer.FX_OK)
|
||||
+ common.NS(b"msg")
|
||||
+ common.NS(b"lang")
|
||||
)
|
||||
self.ftc.packet_STATUS(data)
|
||||
return d
|
||||
|
||||
def _cbTestPacketSTATUS(self, result):
|
||||
"""
|
||||
Assert that the result is a two-tuple containing the message and
|
||||
language from the STATUS packet.
|
||||
"""
|
||||
self.assertEqual(result[0], b"msg")
|
||||
self.assertEqual(result[1], b"lang")
|
||||
|
||||
def test_packetSTATUSShort(self):
|
||||
"""
|
||||
A STATUS packet containing only a result code can also be parsed to
|
||||
produce the result of an outstanding request L{Deferred}. Such packets
|
||||
are sent by some SFTP implementations, though not strictly legal.
|
||||
|
||||
@see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
|
||||
of the SFTP Internet-Draft.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
d.addCallback(self._cbTestPacketSTATUSShort)
|
||||
self.ftc.openRequests[1] = d
|
||||
data = struct.pack("!LL", 1, filetransfer.FX_OK)
|
||||
self.ftc.packet_STATUS(data)
|
||||
return d
|
||||
|
||||
def _cbTestPacketSTATUSShort(self, result):
|
||||
"""
|
||||
Assert that the result is a two-tuple containing empty strings, since
|
||||
the STATUS packet had neither a message nor a language.
|
||||
"""
|
||||
self.assertEqual(result[0], b"")
|
||||
self.assertEqual(result[1], b"")
|
||||
|
||||
def test_packetSTATUSWithoutLang(self):
|
||||
"""
|
||||
A STATUS packet containing a result code and a message but no language
|
||||
can also be parsed to produce the result of an outstanding request
|
||||
L{Deferred}. Such packets are sent by some SFTP implementations, though
|
||||
not strictly legal.
|
||||
|
||||
@see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
|
||||
of the SFTP Internet-Draft.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
d.addCallback(self._cbTestPacketSTATUSWithoutLang)
|
||||
self.ftc.openRequests[1] = d
|
||||
data = struct.pack("!LL", 1, filetransfer.FX_OK) + common.NS(b"msg")
|
||||
self.ftc.packet_STATUS(data)
|
||||
return d
|
||||
|
||||
def _cbTestPacketSTATUSWithoutLang(self, result):
|
||||
"""
|
||||
Assert that the result is a two-tuple containing the message from the
|
||||
STATUS packet and an empty string, since the language was missing.
|
||||
"""
|
||||
self.assertEqual(result[0], b"msg")
|
||||
self.assertEqual(result[1], b"")
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.forwarding}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import forwarding
|
||||
|
||||
from twisted.internet.address import IPv6Address
|
||||
from twisted.internet.test.test_endpoints import deterministicResolvingReactor
|
||||
from twisted.internet.testing import MemoryReactorClock, StringTransport
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class TestSSHConnectForwardingChannel(unittest.TestCase):
|
||||
"""
|
||||
Unit and integration tests for L{SSHConnectForwardingChannel}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def makeTCPConnection(self, reactor: MemoryReactorClock) -> None:
|
||||
"""
|
||||
Fake that connection was established for first connectTCP request made
|
||||
on C{reactor}.
|
||||
|
||||
@param reactor: Reactor on which to fake the connection.
|
||||
@type reactor: A reactor.
|
||||
"""
|
||||
factory = reactor.tcpClients[0][2]
|
||||
connector = reactor.connectors[0]
|
||||
protocol = factory.buildProtocol(None)
|
||||
transport = StringTransport(peerAddress=connector.getDestination())
|
||||
protocol.makeConnection(transport)
|
||||
|
||||
def test_channelOpenHostnameRequests(self) -> None:
|
||||
"""
|
||||
When a hostname is sent as part of forwarding requests, it
|
||||
is resolved using HostnameEndpoint's resolver.
|
||||
"""
|
||||
sut = forwarding.SSHConnectForwardingChannel(hostport=("fwd.example.org", 1234))
|
||||
# Patch channel and resolver to not touch the network.
|
||||
memoryReactor = MemoryReactorClock()
|
||||
sut._reactor = deterministicResolvingReactor(memoryReactor, ["::1"])
|
||||
sut.channelOpen(None)
|
||||
|
||||
self.makeTCPConnection(memoryReactor)
|
||||
self.successResultOf(sut._channelOpenDeferred)
|
||||
# Channel is connected using a forwarding client to the resolved
|
||||
# address of the requested host.
|
||||
self.assertIsInstance(sut.client, forwarding.SSHForwardingClient)
|
||||
self.assertEqual(
|
||||
IPv6Address("TCP", "::1", 1234), sut.client.transport.getPeer()
|
||||
)
|
||||
@@ -0,0 +1,619 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_helper -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Callable
|
||||
|
||||
from twisted.conch.insults import helper
|
||||
from twisted.conch.insults.insults import (
|
||||
BLINK,
|
||||
BOLD,
|
||||
G0,
|
||||
G1,
|
||||
G2,
|
||||
G3,
|
||||
NORMAL,
|
||||
REVERSE_VIDEO,
|
||||
UNDERLINE,
|
||||
modes,
|
||||
privateModes,
|
||||
)
|
||||
from twisted.python import failure
|
||||
from twisted.trial import unittest
|
||||
|
||||
WIDTH = 80
|
||||
HEIGHT = 24
|
||||
|
||||
|
||||
class BufferTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.term = helper.TerminalBuffer()
|
||||
self.term.connectionMade()
|
||||
|
||||
def testInitialState(self) -> None:
|
||||
self.assertEqual(self.term.width, WIDTH)
|
||||
self.assertEqual(self.term.height, HEIGHT)
|
||||
self.assertEqual(self.term.__bytes__(), b"\n" * (HEIGHT - 1))
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
|
||||
def test_initialPrivateModes(self) -> None:
|
||||
"""
|
||||
Verify that only DEC Auto Wrap Mode (DECAWM) and DEC Text Cursor Enable
|
||||
Mode (DECTCEM) are initially in the Set Mode (SM) state.
|
||||
"""
|
||||
self.assertEqual(
|
||||
{privateModes.AUTO_WRAP: True, privateModes.CURSOR_MODE: True},
|
||||
self.term.privateModes,
|
||||
)
|
||||
|
||||
def test_carriageReturn(self) -> None:
|
||||
"""
|
||||
C{"\r"} moves the cursor to the first column in the current row.
|
||||
"""
|
||||
self.term.cursorForward(5)
|
||||
self.term.cursorDown(3)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (5, 3))
|
||||
self.term.insertAtCursor(b"\r")
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 3))
|
||||
|
||||
def test_linefeed(self) -> None:
|
||||
"""
|
||||
C{"\n"} moves the cursor to the next row without changing the column.
|
||||
"""
|
||||
self.term.cursorForward(5)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (5, 0))
|
||||
self.term.insertAtCursor(b"\n")
|
||||
self.assertEqual(self.term.reportCursorPosition(), (5, 1))
|
||||
|
||||
def test_newline(self) -> None:
|
||||
"""
|
||||
C{write} transforms C{"\n"} into C{"\r\n"}.
|
||||
"""
|
||||
self.term.cursorForward(5)
|
||||
self.term.cursorDown(3)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (5, 3))
|
||||
self.term.write(b"\n")
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 4))
|
||||
|
||||
def test_setPrivateModes(self) -> None:
|
||||
"""
|
||||
Verify that L{helper.TerminalBuffer.setPrivateModes} changes the Set
|
||||
Mode (SM) state to "set" for the private modes it is passed.
|
||||
"""
|
||||
expected = self.term.privateModes.copy()
|
||||
self.term.setPrivateModes([privateModes.SCROLL, privateModes.SCREEN])
|
||||
expected[privateModes.SCROLL] = True
|
||||
expected[privateModes.SCREEN] = True
|
||||
self.assertEqual(expected, self.term.privateModes)
|
||||
|
||||
def test_resetPrivateModes(self) -> None:
|
||||
"""
|
||||
Verify that L{helper.TerminalBuffer.resetPrivateModes} changes the Set
|
||||
Mode (SM) state to "reset" for the private modes it is passed.
|
||||
"""
|
||||
expected = self.term.privateModes.copy()
|
||||
self.term.resetPrivateModes([privateModes.AUTO_WRAP, privateModes.CURSOR_MODE])
|
||||
del expected[privateModes.AUTO_WRAP]
|
||||
del expected[privateModes.CURSOR_MODE]
|
||||
self.assertEqual(expected, self.term.privateModes)
|
||||
|
||||
def testCursorDown(self) -> None:
|
||||
self.term.cursorDown(3)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 3))
|
||||
self.term.cursorDown()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 4))
|
||||
self.term.cursorDown(HEIGHT)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
|
||||
|
||||
def testCursorUp(self) -> None:
|
||||
self.term.cursorUp(5)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
|
||||
self.term.cursorDown(20)
|
||||
self.term.cursorUp(1)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 19))
|
||||
|
||||
self.term.cursorUp(19)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
|
||||
def testCursorForward(self) -> None:
|
||||
self.term.cursorForward(2)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (2, 0))
|
||||
self.term.cursorForward(2)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (4, 0))
|
||||
self.term.cursorForward(WIDTH)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (WIDTH, 0))
|
||||
|
||||
def testCursorBackward(self) -> None:
|
||||
self.term.cursorForward(10)
|
||||
self.term.cursorBackward(2)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (8, 0))
|
||||
self.term.cursorBackward(7)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (1, 0))
|
||||
self.term.cursorBackward(1)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
self.term.cursorBackward(1)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
|
||||
def testCursorPositioning(self) -> None:
|
||||
self.term.cursorPosition(3, 9)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (3, 9))
|
||||
|
||||
def testSimpleWriting(self) -> None:
|
||||
s = b"Hello, world."
|
||||
self.term.write(s)
|
||||
self.assertEqual(self.term.__bytes__(), s + b"\n" + b"\n" * (HEIGHT - 2))
|
||||
|
||||
def testOvertype(self) -> None:
|
||||
s = b"hello, world."
|
||||
self.term.write(s)
|
||||
self.term.cursorBackward(len(s))
|
||||
self.term.resetModes([modes.IRM])
|
||||
self.term.write(b"H")
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(), (b"H" + s[1:]) + b"\n" + b"\n" * (HEIGHT - 2)
|
||||
)
|
||||
|
||||
def testInsert(self) -> None:
|
||||
s = b"ello, world."
|
||||
self.term.write(s)
|
||||
self.term.cursorBackward(len(s))
|
||||
self.term.setModes([modes.IRM])
|
||||
self.term.write(b"H")
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(), (b"H" + s) + b"\n" + b"\n" * (HEIGHT - 2)
|
||||
)
|
||||
|
||||
def testWritingInTheMiddle(self) -> None:
|
||||
s = b"Hello, world."
|
||||
self.term.cursorDown(5)
|
||||
self.term.cursorForward(5)
|
||||
self.term.write(s)
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
b"\n" * 5 + (self.term.fill * 5) + s + b"\n" + b"\n" * (HEIGHT - 7),
|
||||
)
|
||||
|
||||
def testWritingWrappedAtEndOfLine(self) -> None:
|
||||
s = b"Hello, world."
|
||||
self.term.cursorForward(WIDTH - 5)
|
||||
self.term.write(s)
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
s[:5].rjust(WIDTH) + b"\n" + s[5:] + b"\n" + b"\n" * (HEIGHT - 3),
|
||||
)
|
||||
|
||||
def testIndex(self) -> None:
|
||||
self.term.index()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 1))
|
||||
self.term.cursorDown(HEIGHT)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
|
||||
self.term.index()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
|
||||
|
||||
def testReverseIndex(self) -> None:
|
||||
self.term.reverseIndex()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 0))
|
||||
self.term.cursorDown(2)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 2))
|
||||
self.term.reverseIndex()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 1))
|
||||
|
||||
def test_nextLine(self) -> None:
|
||||
"""
|
||||
C{nextLine} positions the cursor at the beginning of the row below the
|
||||
current row.
|
||||
"""
|
||||
self.term.nextLine()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 1))
|
||||
self.term.cursorForward(5)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (5, 1))
|
||||
self.term.nextLine()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (0, 2))
|
||||
|
||||
def testSaveCursor(self) -> None:
|
||||
self.term.cursorDown(5)
|
||||
self.term.cursorForward(7)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (7, 5))
|
||||
self.term.saveCursor()
|
||||
self.term.cursorDown(7)
|
||||
self.term.cursorBackward(3)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (4, 12))
|
||||
self.term.restoreCursor()
|
||||
self.assertEqual(self.term.reportCursorPosition(), (7, 5))
|
||||
|
||||
def testSingleShifts(self) -> None:
|
||||
self.term.singleShift2()
|
||||
self.term.write(b"Hi")
|
||||
|
||||
ch = self.term.getCharacter(0, 0)
|
||||
self.assertEqual(ch[0], b"H")
|
||||
self.assertEqual(ch[1].charset, G2)
|
||||
|
||||
ch = self.term.getCharacter(1, 0)
|
||||
self.assertEqual(ch[0], b"i")
|
||||
self.assertEqual(ch[1].charset, G0)
|
||||
|
||||
self.term.singleShift3()
|
||||
self.term.write(b"!!")
|
||||
|
||||
ch = self.term.getCharacter(2, 0)
|
||||
self.assertEqual(ch[0], b"!")
|
||||
self.assertEqual(ch[1].charset, G3)
|
||||
|
||||
ch = self.term.getCharacter(3, 0)
|
||||
self.assertEqual(ch[0], b"!")
|
||||
self.assertEqual(ch[1].charset, G0)
|
||||
|
||||
def testShifting(self) -> None:
|
||||
s1 = b"Hello"
|
||||
s2 = b"World"
|
||||
s3 = b"Bye!"
|
||||
self.term.write(b"Hello\n")
|
||||
self.term.shiftOut()
|
||||
self.term.write(b"World\n")
|
||||
self.term.shiftIn()
|
||||
self.term.write(b"Bye!\n")
|
||||
|
||||
g = G0
|
||||
h = 0
|
||||
for s in (s1, s2, s3):
|
||||
for i in range(len(s)):
|
||||
ch = self.term.getCharacter(i, h)
|
||||
self.assertEqual(ch[0], s[i : i + 1])
|
||||
self.assertEqual(ch[1].charset, g)
|
||||
g = g == G0 and G1 or G0
|
||||
h += 1
|
||||
|
||||
def testGraphicRendition(self) -> None:
|
||||
self.term.selectGraphicRendition(BOLD, UNDERLINE, BLINK, REVERSE_VIDEO)
|
||||
self.term.write(b"W")
|
||||
self.term.selectGraphicRendition(NORMAL)
|
||||
self.term.write(b"X")
|
||||
self.term.selectGraphicRendition(BLINK)
|
||||
self.term.write(b"Y")
|
||||
self.term.selectGraphicRendition(BOLD)
|
||||
self.term.write(b"Z")
|
||||
|
||||
ch = self.term.getCharacter(0, 0)
|
||||
self.assertEqual(ch[0], b"W")
|
||||
self.assertTrue(ch[1].bold)
|
||||
self.assertTrue(ch[1].underline)
|
||||
self.assertTrue(ch[1].blink)
|
||||
self.assertTrue(ch[1].reverseVideo)
|
||||
|
||||
ch = self.term.getCharacter(1, 0)
|
||||
self.assertEqual(ch[0], b"X")
|
||||
self.assertFalse(ch[1].bold)
|
||||
self.assertFalse(ch[1].underline)
|
||||
self.assertFalse(ch[1].blink)
|
||||
self.assertFalse(ch[1].reverseVideo)
|
||||
|
||||
ch = self.term.getCharacter(2, 0)
|
||||
self.assertEqual(ch[0], b"Y")
|
||||
self.assertTrue(ch[1].blink)
|
||||
self.assertFalse(ch[1].bold)
|
||||
self.assertFalse(ch[1].underline)
|
||||
self.assertFalse(ch[1].reverseVideo)
|
||||
|
||||
ch = self.term.getCharacter(3, 0)
|
||||
self.assertEqual(ch[0], b"Z")
|
||||
self.assertTrue(ch[1].blink)
|
||||
self.assertTrue(ch[1].bold)
|
||||
self.assertFalse(ch[1].underline)
|
||||
self.assertFalse(ch[1].reverseVideo)
|
||||
|
||||
def testColorAttributes(self) -> None:
|
||||
s1 = b"Merry xmas"
|
||||
s2 = b"Just kidding"
|
||||
self.term.selectGraphicRendition(
|
||||
helper.FOREGROUND + helper.RED, helper.BACKGROUND + helper.GREEN
|
||||
)
|
||||
self.term.write(s1 + b"\n")
|
||||
self.term.selectGraphicRendition(NORMAL)
|
||||
self.term.write(s2 + b"\n")
|
||||
|
||||
for i in range(len(s1)):
|
||||
ch = self.term.getCharacter(i, 0)
|
||||
self.assertEqual(ch[0], s1[i : i + 1])
|
||||
self.assertEqual(ch[1].charset, G0)
|
||||
self.assertFalse(ch[1].bold)
|
||||
self.assertFalse(ch[1].underline)
|
||||
self.assertFalse(ch[1].blink)
|
||||
self.assertFalse(ch[1].reverseVideo)
|
||||
self.assertEqual(ch[1].foreground, helper.RED)
|
||||
self.assertEqual(ch[1].background, helper.GREEN)
|
||||
|
||||
for i in range(len(s2)):
|
||||
ch = self.term.getCharacter(i, 1)
|
||||
self.assertEqual(ch[0], s2[i : i + 1])
|
||||
self.assertEqual(ch[1].charset, G0)
|
||||
self.assertFalse(ch[1].bold)
|
||||
self.assertFalse(ch[1].underline)
|
||||
self.assertFalse(ch[1].blink)
|
||||
self.assertFalse(ch[1].reverseVideo)
|
||||
self.assertEqual(ch[1].foreground, helper.WHITE)
|
||||
self.assertEqual(ch[1].background, helper.BLACK)
|
||||
|
||||
def testEraseLine(self) -> None:
|
||||
s1 = b"line 1"
|
||||
s2 = b"line 2"
|
||||
s3 = b"line 3"
|
||||
self.term.write(b"\n".join((s1, s2, s3)) + b"\n")
|
||||
self.term.cursorPosition(1, 1)
|
||||
self.term.eraseLine()
|
||||
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
s1 + b"\n" + b"\n" + s3 + b"\n" + b"\n" * (HEIGHT - 4),
|
||||
)
|
||||
|
||||
def testEraseToLineEnd(self) -> None:
|
||||
s = b"Hello, world."
|
||||
self.term.write(s)
|
||||
self.term.cursorBackward(5)
|
||||
self.term.eraseToLineEnd()
|
||||
self.assertEqual(self.term.__bytes__(), s[:-5] + b"\n" + b"\n" * (HEIGHT - 2))
|
||||
|
||||
def testEraseToLineBeginning(self) -> None:
|
||||
s = b"Hello, world."
|
||||
self.term.write(s)
|
||||
self.term.cursorBackward(5)
|
||||
self.term.eraseToLineBeginning()
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(), s[-4:].rjust(len(s)) + b"\n" + b"\n" * (HEIGHT - 2)
|
||||
)
|
||||
|
||||
def testEraseDisplay(self) -> None:
|
||||
self.term.write(b"Hello world\n")
|
||||
self.term.write(b"Goodbye world\n")
|
||||
self.term.eraseDisplay()
|
||||
|
||||
self.assertEqual(self.term.__bytes__(), b"\n" * (HEIGHT - 1))
|
||||
|
||||
def testEraseToDisplayEnd(self) -> None:
|
||||
s1 = b"Hello world"
|
||||
s2 = b"Goodbye world"
|
||||
self.term.write(b"\n".join((s1, s2, b"")))
|
||||
self.term.cursorPosition(5, 1)
|
||||
self.term.eraseToDisplayEnd()
|
||||
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(), s1 + b"\n" + s2[:5] + b"\n" + b"\n" * (HEIGHT - 3)
|
||||
)
|
||||
|
||||
def testEraseToDisplayBeginning(self) -> None:
|
||||
s1 = b"Hello world"
|
||||
s2 = b"Goodbye world"
|
||||
self.term.write(b"\n".join((s1, s2)))
|
||||
self.term.cursorPosition(5, 1)
|
||||
self.term.eraseToDisplayBeginning()
|
||||
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
b"\n" + s2[6:].rjust(len(s2)) + b"\n" + b"\n" * (HEIGHT - 3),
|
||||
)
|
||||
|
||||
def testLineInsertion(self) -> None:
|
||||
s1 = b"Hello world"
|
||||
s2 = b"Goodbye world"
|
||||
self.term.write(b"\n".join((s1, s2)))
|
||||
self.term.cursorPosition(7, 1)
|
||||
self.term.insertLine()
|
||||
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
s1 + b"\n" + b"\n" + s2 + b"\n" + b"\n" * (HEIGHT - 4),
|
||||
)
|
||||
|
||||
def testLineDeletion(self) -> None:
|
||||
s1 = b"Hello world"
|
||||
s2 = b"Middle words"
|
||||
s3 = b"Goodbye world"
|
||||
self.term.write(b"\n".join((s1, s2, s3)))
|
||||
self.term.cursorPosition(9, 1)
|
||||
self.term.deleteLine()
|
||||
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(), s1 + b"\n" + s3 + b"\n" + b"\n" * (HEIGHT - 3)
|
||||
)
|
||||
|
||||
|
||||
class FakeDelayedCall:
|
||||
called = False
|
||||
cancelled = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fs: FakeScheduler,
|
||||
timeout: float,
|
||||
f: Callable[..., None],
|
||||
a: tuple[object, ...],
|
||||
kw: dict[str, object],
|
||||
) -> None:
|
||||
self.fs = fs
|
||||
self.timeout = timeout
|
||||
self.f = f
|
||||
self.a = a
|
||||
self.kw = kw
|
||||
|
||||
def active(self) -> bool:
|
||||
return not (self.cancelled or self.called)
|
||||
|
||||
def cancel(self) -> None:
|
||||
self.cancelled = True
|
||||
|
||||
# self.fs.calls.remove(self)
|
||||
|
||||
def call(self) -> None:
|
||||
self.called = True
|
||||
self.f(*self.a, **self.kw)
|
||||
|
||||
|
||||
class FakeScheduler:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[FakeDelayedCall] = []
|
||||
|
||||
def callLater(
|
||||
self, timeout: float, f: Callable[..., None], *a: object, **kw: object
|
||||
) -> FakeDelayedCall:
|
||||
self.calls.append(FakeDelayedCall(self, timeout, f, a, kw))
|
||||
return self.calls[-1]
|
||||
|
||||
|
||||
class ExpectTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.term = helper.ExpectableBuffer()
|
||||
self.term.connectionMade()
|
||||
self.fs = FakeScheduler()
|
||||
|
||||
def testSimpleString(self) -> None:
|
||||
result: list[re.Match[bytes]] = []
|
||||
d = self.term.expect(b"hello world", timeout=1, scheduler=self.fs)
|
||||
d.addCallback(result.append)
|
||||
|
||||
self.term.write(b"greeting puny earthlings\n")
|
||||
self.assertFalse(result)
|
||||
self.term.write(b"hello world\n")
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result[0].group(), b"hello world")
|
||||
self.assertEqual(len(self.fs.calls), 1)
|
||||
self.assertFalse(self.fs.calls[0].active())
|
||||
|
||||
def testBrokenUpString(self) -> None:
|
||||
result: list[re.Match[bytes]] = []
|
||||
d = self.term.expect(b"hello world")
|
||||
d.addCallback(result.append)
|
||||
|
||||
self.assertFalse(result)
|
||||
self.term.write(b"hello ")
|
||||
self.assertFalse(result)
|
||||
self.term.write(b"worl")
|
||||
self.assertFalse(result)
|
||||
self.term.write(b"d")
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result[0].group(), b"hello world")
|
||||
|
||||
def testMultiple(self) -> None:
|
||||
result: list[re.Match[bytes]] = []
|
||||
d1 = self.term.expect(b"hello ")
|
||||
d1.addCallback(result.append)
|
||||
d2 = self.term.expect(b"world")
|
||||
d2.addCallback(result.append)
|
||||
|
||||
self.assertFalse(result)
|
||||
self.term.write(b"hello")
|
||||
self.assertFalse(result)
|
||||
self.term.write(b" ")
|
||||
self.assertEqual(len(result), 1)
|
||||
self.term.write(b"world")
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual(result[0].group(), b"hello ")
|
||||
self.assertEqual(result[1].group(), b"world")
|
||||
|
||||
def testSynchronous(self) -> None:
|
||||
self.term.write(b"hello world")
|
||||
|
||||
result: list[re.Match[bytes]] = []
|
||||
d = self.term.expect(b"hello world")
|
||||
d.addCallback(result.append)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result[0].group(), b"hello world")
|
||||
|
||||
def testMultipleSynchronous(self) -> None:
|
||||
self.term.write(b"goodbye world")
|
||||
|
||||
result: list[re.Match[bytes]] = []
|
||||
d1 = self.term.expect(b"bye")
|
||||
d1.addCallback(result.append)
|
||||
d2 = self.term.expect(b"world")
|
||||
d2.addCallback(result.append)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual(result[0].group(), b"bye")
|
||||
self.assertEqual(result[1].group(), b"world")
|
||||
|
||||
def _cbTestTimeoutFailure(self, res: failure.Failure) -> None:
|
||||
self.assertTrue(hasattr(res, "type"))
|
||||
self.assertEqual(res.type, helper.ExpectationTimeout)
|
||||
|
||||
def testTimeoutFailure(self) -> None:
|
||||
d = self.term.expect(b"hello world", timeout=1, scheduler=self.fs)
|
||||
d.addBoth(self._cbTestTimeoutFailure)
|
||||
self.fs.calls[0].call()
|
||||
|
||||
def testOverlappingTimeout(self) -> None:
|
||||
self.term.write(b"not zoomtastic")
|
||||
|
||||
result: list[re.Match[bytes]] = []
|
||||
d1 = self.term.expect(b"hello world", timeout=1, scheduler=self.fs)
|
||||
d1.addBoth(self._cbTestTimeoutFailure)
|
||||
d2 = self.term.expect(b"zoom")
|
||||
d2.addCallback(result.append)
|
||||
|
||||
self.fs.calls[0].call()
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0].group(), b"zoom")
|
||||
|
||||
|
||||
class CharacterAttributeTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.conch.insults.helper.CharacterAttribute}.
|
||||
"""
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""
|
||||
L{CharacterAttribute}s must have matching character attribute values
|
||||
(bold, blink, underline, etc) with the same values to be considered
|
||||
equal.
|
||||
"""
|
||||
self.assertEqual(helper.CharacterAttribute(), helper.CharacterAttribute())
|
||||
|
||||
self.assertEqual(
|
||||
helper.CharacterAttribute(), helper.CharacterAttribute(charset=G0)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
helper.CharacterAttribute(
|
||||
bold=True,
|
||||
underline=True,
|
||||
blink=False,
|
||||
reverseVideo=True,
|
||||
foreground=helper.BLUE,
|
||||
),
|
||||
helper.CharacterAttribute(
|
||||
bold=True,
|
||||
underline=True,
|
||||
blink=False,
|
||||
reverseVideo=True,
|
||||
foreground=helper.BLUE,
|
||||
),
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
helper.CharacterAttribute(), helper.CharacterAttribute(charset=G1)
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
helper.CharacterAttribute(bold=True), helper.CharacterAttribute(bold=False)
|
||||
)
|
||||
|
||||
def test_wantOneDeprecated(self) -> None:
|
||||
"""
|
||||
L{twisted.conch.insults.helper.CharacterAttribute.wantOne} emits
|
||||
a deprecation warning when invoked.
|
||||
"""
|
||||
# Trigger the deprecation warning.
|
||||
helper._FormattingState().wantOne(bold=True)
|
||||
|
||||
warningsShown = self.flushWarnings([self.test_wantOneDeprecated])
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
self.assertEqual(warningsShown[0]["category"], DeprecationWarning)
|
||||
deprecatedClass = "twisted.conch.insults.helper._FormattingState.wantOne"
|
||||
self.assertEqual(
|
||||
warningsShown[0]["message"],
|
||||
"%s was deprecated in Twisted 13.1.0" % (deprecatedClass),
|
||||
)
|
||||
@@ -0,0 +1,959 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_insults -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import textwrap
|
||||
from typing import Optional, Type
|
||||
|
||||
from constantly import ValueConstant, Values
|
||||
|
||||
from twisted.conch.insults.insults import (
|
||||
BLINK,
|
||||
CS_ALTERNATE,
|
||||
CS_ALTERNATE_SPECIAL,
|
||||
CS_DRAWING,
|
||||
CS_UK,
|
||||
CS_US,
|
||||
G0,
|
||||
G1,
|
||||
UNDERLINE,
|
||||
ClientProtocol,
|
||||
ServerProtocol,
|
||||
modes,
|
||||
privateModes,
|
||||
)
|
||||
from twisted.internet.protocol import Protocol
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
def _getattr(mock, name):
|
||||
return super(Mock, mock).__getattribute__(name)
|
||||
|
||||
|
||||
def occurrences(mock):
|
||||
return _getattr(mock, "occurrences")
|
||||
|
||||
|
||||
def methods(mock):
|
||||
return _getattr(mock, "methods")
|
||||
|
||||
|
||||
def _append(mock, obj):
|
||||
occurrences(mock).append(obj)
|
||||
|
||||
|
||||
default = object()
|
||||
|
||||
|
||||
def _ecmaCodeTableCoordinate(column, row):
|
||||
"""
|
||||
Return the byte in 7- or 8-bit code table identified by C{column}
|
||||
and C{row}.
|
||||
|
||||
"An 8-bit code table consists of 256 positions arranged in 16
|
||||
columns and 16 rows. The columns and rows are numbered 00 to 15."
|
||||
|
||||
"A 7-bit code table consists of 128 positions arranged in 8
|
||||
columns and 16 rows. The columns are numbered 00 to 07 and the
|
||||
rows 00 to 15 (see figure 1)."
|
||||
|
||||
p.5 of "Standard ECMA-35: Character Code Structure and Extension
|
||||
Techniques", 6th Edition (December 1994).
|
||||
"""
|
||||
# 8 and 15 both happen to take up 4 bits, so the first number
|
||||
# should be shifted by 4 for both the 7- and 8-bit tables.
|
||||
return bytes(bytearray([(column << 4) | row]))
|
||||
|
||||
|
||||
def _makeControlFunctionSymbols(name, colOffset, names, doc):
|
||||
# the value for each name is the concatenation of the bit values
|
||||
# of its x, y locations, with an offset of 4 added to its x value.
|
||||
# so CUP is (0 + 4, 8) = (4, 8) = 4||8 = 1001000 = 72 = b"H"
|
||||
# this is how it's defined in the standard!
|
||||
attrs = {
|
||||
name: ValueConstant(_ecmaCodeTableCoordinate(i + colOffset, j))
|
||||
for j, row in enumerate(names)
|
||||
for i, name in enumerate(row)
|
||||
if name
|
||||
}
|
||||
attrs["__doc__"] = doc
|
||||
return type(name, (Values,), attrs)
|
||||
|
||||
|
||||
CSFinalByte = _makeControlFunctionSymbols(
|
||||
"CSFinalByte",
|
||||
colOffset=4,
|
||||
names=[
|
||||
# 4, 5, 6
|
||||
["ICH", "DCH", "HPA"],
|
||||
["CUU", "SSE", "HPR"],
|
||||
["CUD", "CPR", "REP"],
|
||||
["CUF", "SU", "DA"],
|
||||
["CUB", "SD", "VPA"],
|
||||
["CNL", "NP", "VPR"],
|
||||
["CPL", "PP", "HVP"],
|
||||
["CHA", "CTC", "TBC"],
|
||||
["CUP", "ECH", "SM"],
|
||||
["CHT", "CVT", "MC"],
|
||||
["ED", "CBT", "HPB"],
|
||||
["EL", "SRS", "VPB"],
|
||||
["IL", "PTX", "RM"],
|
||||
["DL", "SDS", "SGR"],
|
||||
["EF", "SIMD", "DSR"],
|
||||
["EA", None, "DAQ"],
|
||||
],
|
||||
doc=textwrap.dedent(
|
||||
"""
|
||||
Symbolic constants for all control sequence final bytes
|
||||
that do not imply intermediate bytes. This happens to cover
|
||||
movement control sequences.
|
||||
|
||||
See page 11 of "Standard ECMA 48: Control Functions for Coded
|
||||
Character Sets", 5th Edition (June 1991).
|
||||
|
||||
Each L{ValueConstant} maps a control sequence name to L{bytes}
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
C1SevenBit = _makeControlFunctionSymbols(
|
||||
"C1SevenBit",
|
||||
colOffset=4,
|
||||
names=[
|
||||
[None, "DCS"],
|
||||
[None, "PU1"],
|
||||
["BPH", "PU2"],
|
||||
["NBH", "STS"],
|
||||
[None, "CCH"],
|
||||
["NEL", "MW"],
|
||||
["SSA", "SPA"],
|
||||
["ESA", "EPA"],
|
||||
["HTS", "SOS"],
|
||||
["HTJ", None],
|
||||
["VTS", "SCI"],
|
||||
["PLD", "CSI"],
|
||||
["PLU", "ST"],
|
||||
["RI", "OSC"],
|
||||
["SS2", "PM"],
|
||||
["SS3", "APC"],
|
||||
],
|
||||
doc=textwrap.dedent(
|
||||
"""
|
||||
Symbolic constants for all 7 bit versions of the C1 control functions
|
||||
|
||||
See page 9 "Standard ECMA 48: Control Functions for Coded
|
||||
Character Sets", 5th Edition (June 1991).
|
||||
|
||||
Each L{ValueConstant} maps a control sequence name to L{bytes}
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Mock:
|
||||
callReturnValue = default
|
||||
|
||||
def __init__(self, methods=None, callReturnValue=default):
|
||||
"""
|
||||
@param methods: Mapping of names to return values
|
||||
@param callReturnValue: object __call__ should return
|
||||
"""
|
||||
self.occurrences = []
|
||||
if methods is None:
|
||||
methods = {}
|
||||
self.methods = methods
|
||||
if callReturnValue is not default:
|
||||
self.callReturnValue = callReturnValue
|
||||
|
||||
def __call__(self, *a, **kw):
|
||||
returnValue = _getattr(self, "callReturnValue")
|
||||
if returnValue is default:
|
||||
returnValue = Mock()
|
||||
# _getattr(self, 'occurrences').append(('__call__', returnValue, a, kw))
|
||||
_append(self, ("__call__", returnValue, a, kw))
|
||||
return returnValue
|
||||
|
||||
def __getattribute__(self, name):
|
||||
methods = _getattr(self, "methods")
|
||||
if name in methods:
|
||||
attrValue = Mock(callReturnValue=methods[name])
|
||||
else:
|
||||
attrValue = Mock()
|
||||
# _getattr(self, 'occurrences').append((name, attrValue))
|
||||
_append(self, (name, attrValue))
|
||||
return attrValue
|
||||
|
||||
|
||||
class MockMixin:
|
||||
def assertCall(
|
||||
self, occurrence, methodName, expectedPositionalArgs=(), expectedKeywordArgs={}
|
||||
):
|
||||
attr, mock = occurrence
|
||||
self.assertEqual(attr, methodName)
|
||||
self.assertEqual(len(occurrences(mock)), 1)
|
||||
[(call, result, args, kw)] = occurrences(mock)
|
||||
self.assertEqual(call, "__call__")
|
||||
self.assertEqual(args, expectedPositionalArgs)
|
||||
self.assertEqual(kw, expectedKeywordArgs)
|
||||
return result
|
||||
|
||||
|
||||
_byteGroupingTestTemplate = """\
|
||||
def testByte%(groupName)s(self):
|
||||
transport = StringTransport()
|
||||
proto = Mock()
|
||||
parser = self.protocolFactory(lambda: proto)
|
||||
parser.factory = self
|
||||
parser.makeConnection(transport)
|
||||
|
||||
bytes = self.TEST_BYTES
|
||||
while bytes:
|
||||
chunk = bytes[:%(bytesPer)d]
|
||||
bytes = bytes[%(bytesPer)d:]
|
||||
parser.dataReceived(chunk)
|
||||
|
||||
self.verifyResults(transport, proto, parser)
|
||||
"""
|
||||
|
||||
|
||||
class ByteGroupingsMixin(MockMixin):
|
||||
protocolFactory: Optional[Type[Protocol]] = None
|
||||
|
||||
for word, n in [
|
||||
("Pairs", 2),
|
||||
("Triples", 3),
|
||||
("Quads", 4),
|
||||
("Quints", 5),
|
||||
("Sexes", 6),
|
||||
]:
|
||||
exec(_byteGroupingTestTemplate % {"groupName": word, "bytesPer": n})
|
||||
del word, n
|
||||
|
||||
def verifyResults(self, transport, proto, parser):
|
||||
result = self.assertCall(occurrences(proto).pop(0), "makeConnection", (parser,))
|
||||
self.assertEqual(occurrences(result), [])
|
||||
|
||||
|
||||
del _byteGroupingTestTemplate
|
||||
|
||||
|
||||
class ServerArrowKeysTests(ByteGroupingsMixin, unittest.TestCase):
|
||||
protocolFactory = ServerProtocol
|
||||
|
||||
# All the arrow keys once
|
||||
TEST_BYTES = b"\x1b[A\x1b[B\x1b[C\x1b[D"
|
||||
|
||||
def verifyResults(self, transport, proto, parser):
|
||||
ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
|
||||
|
||||
for arrow in (
|
||||
parser.UP_ARROW,
|
||||
parser.DOWN_ARROW,
|
||||
parser.RIGHT_ARROW,
|
||||
parser.LEFT_ARROW,
|
||||
):
|
||||
result = self.assertCall(
|
||||
occurrences(proto).pop(0), "keystrokeReceived", (arrow, None)
|
||||
)
|
||||
self.assertEqual(occurrences(result), [])
|
||||
self.assertFalse(occurrences(proto))
|
||||
|
||||
|
||||
class PrintableCharactersTests(ByteGroupingsMixin, unittest.TestCase):
|
||||
protocolFactory = ServerProtocol
|
||||
|
||||
# Some letters and digits, first on their own, then capitalized,
|
||||
# then modified with alt
|
||||
|
||||
TEST_BYTES = b"abc123ABC!@#\x1ba\x1bb\x1bc\x1b1\x1b2\x1b3"
|
||||
|
||||
def verifyResults(self, transport, proto, parser):
|
||||
ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
|
||||
|
||||
for char in iterbytes(b"abc123ABC!@#"):
|
||||
result = self.assertCall(
|
||||
occurrences(proto).pop(0), "keystrokeReceived", (char, None)
|
||||
)
|
||||
self.assertEqual(occurrences(result), [])
|
||||
|
||||
for char in iterbytes(b"abc123"):
|
||||
result = self.assertCall(
|
||||
occurrences(proto).pop(0), "keystrokeReceived", (char, parser.ALT)
|
||||
)
|
||||
self.assertEqual(occurrences(result), [])
|
||||
|
||||
occs = occurrences(proto)
|
||||
self.assertFalse(occs, f"{occs!r} should have been []")
|
||||
|
||||
|
||||
class ServerFunctionKeysTests(ByteGroupingsMixin, unittest.TestCase):
|
||||
"""Test for parsing and dispatching function keys (F1 - F12)"""
|
||||
|
||||
protocolFactory = ServerProtocol
|
||||
|
||||
byteList = []
|
||||
for byteCodes in (
|
||||
b"OP",
|
||||
b"OQ",
|
||||
b"OR",
|
||||
b"OS", # F1 - F4
|
||||
b"15~",
|
||||
b"17~",
|
||||
b"18~",
|
||||
b"19~", # F5 - F8
|
||||
b"20~",
|
||||
b"21~",
|
||||
b"23~",
|
||||
b"24~",
|
||||
): # F9 - F12
|
||||
byteList.append(b"\x1b[" + byteCodes)
|
||||
TEST_BYTES = b"".join(byteList)
|
||||
del byteList, byteCodes
|
||||
|
||||
def verifyResults(self, transport, proto, parser):
|
||||
ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
|
||||
for funcNum in range(1, 13):
|
||||
funcArg = getattr(parser, "F%d" % (funcNum,))
|
||||
result = self.assertCall(
|
||||
occurrences(proto).pop(0), "keystrokeReceived", (funcArg, None)
|
||||
)
|
||||
self.assertEqual(occurrences(result), [])
|
||||
self.assertFalse(occurrences(proto))
|
||||
|
||||
|
||||
class ClientCursorMovementTests(ByteGroupingsMixin, unittest.TestCase):
|
||||
protocolFactory = ClientProtocol
|
||||
|
||||
d2 = b"\x1b[2B"
|
||||
r4 = b"\x1b[4C"
|
||||
u1 = b"\x1b[A"
|
||||
l2 = b"\x1b[2D"
|
||||
# Move the cursor down two, right four, up one, left two, up one, left two
|
||||
TEST_BYTES = d2 + r4 + u1 + l2 + u1 + l2
|
||||
del d2, r4, u1, l2
|
||||
|
||||
def verifyResults(self, transport, proto, parser):
|
||||
ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
|
||||
|
||||
for method, count in [
|
||||
("Down", 2),
|
||||
("Forward", 4),
|
||||
("Up", 1),
|
||||
("Backward", 2),
|
||||
("Up", 1),
|
||||
("Backward", 2),
|
||||
]:
|
||||
result = self.assertCall(
|
||||
occurrences(proto).pop(0), "cursor" + method, (count,)
|
||||
)
|
||||
self.assertEqual(occurrences(result), [])
|
||||
self.assertFalse(occurrences(proto))
|
||||
|
||||
|
||||
class ClientControlSequencesTests(unittest.TestCase, MockMixin):
|
||||
def setUp(self):
|
||||
self.transport = StringTransport()
|
||||
self.proto = Mock()
|
||||
self.parser = ClientProtocol(lambda: self.proto)
|
||||
self.parser.factory = self
|
||||
self.parser.makeConnection(self.transport)
|
||||
result = self.assertCall(
|
||||
occurrences(self.proto).pop(0), "makeConnection", (self.parser,)
|
||||
)
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
def testSimpleCardinals(self):
|
||||
self.parser.dataReceived(
|
||||
b"".join(
|
||||
b"\x1b[" + n + ch
|
||||
for ch in iterbytes(b"BACD")
|
||||
for n in (b"", b"2", b"20", b"200")
|
||||
)
|
||||
)
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
for meth in ("Down", "Up", "Forward", "Backward"):
|
||||
for count in (1, 2, 20, 200):
|
||||
result = self.assertCall(occs.pop(0), "cursor" + meth, (count,))
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testScrollRegion(self):
|
||||
self.parser.dataReceived(b"\x1b[5;22r\x1b[r")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "setScrollRegion", (5, 22))
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "setScrollRegion", (None, None))
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testHeightAndWidth(self):
|
||||
self.parser.dataReceived(b"\x1b#3\x1b#4\x1b#5\x1b#6")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "doubleHeightLine", (True,))
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "doubleHeightLine", (False,))
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "singleWidthLine")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "doubleWidthLine")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testCharacterSet(self):
|
||||
self.parser.dataReceived(
|
||||
b"".join(
|
||||
[
|
||||
b"".join([b"\x1b" + g + n for n in iterbytes(b"AB012")])
|
||||
for g in iterbytes(b"()")
|
||||
]
|
||||
)
|
||||
)
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
for which in (G0, G1):
|
||||
for charset in (
|
||||
CS_UK,
|
||||
CS_US,
|
||||
CS_DRAWING,
|
||||
CS_ALTERNATE,
|
||||
CS_ALTERNATE_SPECIAL,
|
||||
):
|
||||
result = self.assertCall(
|
||||
occs.pop(0), "selectCharacterSet", (charset, which)
|
||||
)
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testShifting(self):
|
||||
self.parser.dataReceived(b"\x15\x14")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "shiftIn")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "shiftOut")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testSingleShifts(self):
|
||||
self.parser.dataReceived(b"\x1bN\x1bO")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "singleShift2")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "singleShift3")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testKeypadMode(self):
|
||||
self.parser.dataReceived(b"\x1b=\x1b>")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "applicationKeypadMode")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "numericKeypadMode")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testCursor(self):
|
||||
self.parser.dataReceived(b"\x1b7\x1b8")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "saveCursor")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "restoreCursor")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testReset(self):
|
||||
self.parser.dataReceived(b"\x1bc")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "reset")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testIndex(self):
|
||||
self.parser.dataReceived(b"\x1bD\x1bM\x1bE")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "index")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "reverseIndex")
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "nextLine")
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testModes(self):
|
||||
self.parser.dataReceived(
|
||||
b"\x1b["
|
||||
+ b";".join(b"%d" % (m,) for m in [modes.KAM, modes.IRM, modes.LNM])
|
||||
+ b"h"
|
||||
)
|
||||
self.parser.dataReceived(
|
||||
b"\x1b["
|
||||
+ b";".join(b"%d" % (m,) for m in [modes.KAM, modes.IRM, modes.LNM])
|
||||
+ b"l"
|
||||
)
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(
|
||||
occs.pop(0), "setModes", ([modes.KAM, modes.IRM, modes.LNM],)
|
||||
)
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(
|
||||
occs.pop(0), "resetModes", ([modes.KAM, modes.IRM, modes.LNM],)
|
||||
)
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testErasure(self):
|
||||
self.parser.dataReceived(b"\x1b[K\x1b[1K\x1b[2K\x1b[J\x1b[1J\x1b[2J\x1b[3P")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
for meth in (
|
||||
"eraseToLineEnd",
|
||||
"eraseToLineBeginning",
|
||||
"eraseLine",
|
||||
"eraseToDisplayEnd",
|
||||
"eraseToDisplayBeginning",
|
||||
"eraseDisplay",
|
||||
):
|
||||
result = self.assertCall(occs.pop(0), meth)
|
||||
self.assertFalse(occurrences(result))
|
||||
|
||||
result = self.assertCall(occs.pop(0), "deleteCharacter", (3,))
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testLineDeletion(self):
|
||||
self.parser.dataReceived(b"\x1b[M\x1b[3M")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
for arg in (1, 3):
|
||||
result = self.assertCall(occs.pop(0), "deleteLine", (arg,))
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testLineInsertion(self):
|
||||
self.parser.dataReceived(b"\x1b[L\x1b[3L")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
for arg in (1, 3):
|
||||
result = self.assertCall(occs.pop(0), "insertLine", (arg,))
|
||||
self.assertFalse(occurrences(result))
|
||||
self.assertFalse(occs)
|
||||
|
||||
def testCursorPosition(self):
|
||||
methods(self.proto)["reportCursorPosition"] = (6, 7)
|
||||
self.parser.dataReceived(b"\x1b[6n")
|
||||
self.assertEqual(self.transport.value(), b"\x1b[7;8R")
|
||||
occs = occurrences(self.proto)
|
||||
|
||||
result = self.assertCall(occs.pop(0), "reportCursorPosition")
|
||||
# This isn't really an interesting assert, since it only tests that
|
||||
# our mock setup is working right, but I'll include it anyway.
|
||||
self.assertEqual(result, (6, 7))
|
||||
|
||||
def test_applicationDataBytes(self):
|
||||
"""
|
||||
Contiguous non-control bytes are passed to a single call to the
|
||||
C{write} method of the terminal to which the L{ClientProtocol} is
|
||||
connected.
|
||||
"""
|
||||
occs = occurrences(self.proto)
|
||||
self.parser.dataReceived(b"a")
|
||||
self.assertCall(occs.pop(0), "write", (b"a",))
|
||||
self.parser.dataReceived(b"bc")
|
||||
self.assertCall(occs.pop(0), "write", (b"bc",))
|
||||
|
||||
def _applicationDataTest(self, data, calls):
|
||||
occs = occurrences(self.proto)
|
||||
self.parser.dataReceived(data)
|
||||
while calls:
|
||||
self.assertCall(occs.pop(0), *calls.pop(0))
|
||||
self.assertFalse(occs, f"No other calls should happen: {occs!r}")
|
||||
|
||||
def test_shiftInAfterApplicationData(self):
|
||||
"""
|
||||
Application data bytes followed by a shift-in command are passed to a
|
||||
call to C{write} before the terminal's C{shiftIn} method is called.
|
||||
"""
|
||||
self._applicationDataTest(b"ab\x15", [("write", (b"ab",)), ("shiftIn",)])
|
||||
|
||||
def test_shiftOutAfterApplicationData(self):
|
||||
"""
|
||||
Application data bytes followed by a shift-out command are passed to a
|
||||
call to C{write} before the terminal's C{shiftOut} method is called.
|
||||
"""
|
||||
self._applicationDataTest(b"ab\x14", [("write", (b"ab",)), ("shiftOut",)])
|
||||
|
||||
def test_cursorBackwardAfterApplicationData(self):
|
||||
"""
|
||||
Application data bytes followed by a cursor-backward command are passed
|
||||
to a call to C{write} before the terminal's C{cursorBackward} method is
|
||||
called.
|
||||
"""
|
||||
self._applicationDataTest(b"ab\x08", [("write", (b"ab",)), ("cursorBackward",)])
|
||||
|
||||
def test_escapeAfterApplicationData(self):
|
||||
"""
|
||||
Application data bytes followed by an escape character are passed to a
|
||||
call to C{write} before the terminal's handler method for the escape is
|
||||
called.
|
||||
"""
|
||||
# Test a short escape
|
||||
self._applicationDataTest(b"ab\x1bD", [("write", (b"ab",)), ("index",)])
|
||||
|
||||
# And a long escape
|
||||
self._applicationDataTest(
|
||||
b"ab\x1b[4h", [("write", (b"ab",)), ("setModes", ([4],))]
|
||||
)
|
||||
|
||||
# There's some other cases too, but they're all handled by the same
|
||||
# codepaths as above.
|
||||
|
||||
|
||||
class ServerProtocolOutputTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the bytes L{ServerProtocol} writes to its transport when its
|
||||
methods are called.
|
||||
"""
|
||||
|
||||
# From ECMA 48: CSI is represented by bit combinations 01/11
|
||||
# (representing ESC) and 05/11 in a 7-bit code or by bit
|
||||
# combination 09/11 in an 8-bit code
|
||||
ESC = _ecmaCodeTableCoordinate(1, 11)
|
||||
CSI = ESC + _ecmaCodeTableCoordinate(5, 11)
|
||||
|
||||
def setUp(self):
|
||||
self.protocol = ServerProtocol()
|
||||
self.transport = StringTransport()
|
||||
self.protocol.makeConnection(self.transport)
|
||||
|
||||
def test_cursorUp(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorUp} writes the control sequence
|
||||
ending with L{CSFinalByte.CUU} to its transport.
|
||||
"""
|
||||
self.protocol.cursorUp(1)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"1" + CSFinalByte.CUU.value
|
||||
)
|
||||
|
||||
def test_cursorDown(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorDown} writes the control sequence
|
||||
ending with L{CSFinalByte.CUD} to its transport.
|
||||
"""
|
||||
self.protocol.cursorDown(1)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"1" + CSFinalByte.CUD.value
|
||||
)
|
||||
|
||||
def test_cursorForward(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorForward} writes the control sequence
|
||||
ending with L{CSFinalByte.CUF} to its transport.
|
||||
"""
|
||||
self.protocol.cursorForward(1)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"1" + CSFinalByte.CUF.value
|
||||
)
|
||||
|
||||
def test_cursorBackward(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorBackward} writes the control sequence
|
||||
ending with L{CSFinalByte.CUB} to its transport.
|
||||
"""
|
||||
self.protocol.cursorBackward(1)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"1" + CSFinalByte.CUB.value
|
||||
)
|
||||
|
||||
def test_cursorPosition(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorPosition} writes a control sequence
|
||||
ending with L{CSFinalByte.CUP} and containing the expected
|
||||
coordinates to its transport.
|
||||
"""
|
||||
self.protocol.cursorPosition(0, 0)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"1;1" + CSFinalByte.CUP.value
|
||||
)
|
||||
|
||||
def test_cursorHome(self):
|
||||
"""
|
||||
L{ServerProtocol.cursorHome} writes a control sequence ending
|
||||
with L{CSFinalByte.CUP} and no parameters, so that the client
|
||||
defaults to (1, 1).
|
||||
"""
|
||||
self.protocol.cursorHome()
|
||||
self.assertEqual(self.transport.value(), self.CSI + CSFinalByte.CUP.value)
|
||||
|
||||
def test_index(self):
|
||||
"""
|
||||
L{ServerProtocol.index} writes the control sequence ending in
|
||||
the 8-bit code table coordinates 4, 4.
|
||||
|
||||
Note that ECMA48 5th Edition removes C{IND}.
|
||||
"""
|
||||
self.protocol.index()
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.ESC + _ecmaCodeTableCoordinate(4, 4)
|
||||
)
|
||||
|
||||
def test_reverseIndex(self):
|
||||
"""
|
||||
L{ServerProtocol.reverseIndex} writes the control sequence
|
||||
ending in the L{C1SevenBit.RI}.
|
||||
"""
|
||||
self.protocol.reverseIndex()
|
||||
self.assertEqual(self.transport.value(), self.ESC + C1SevenBit.RI.value)
|
||||
|
||||
def test_nextLine(self):
|
||||
"""
|
||||
L{ServerProtocol.nextLine} writes C{"\r\n"} to its transport.
|
||||
"""
|
||||
# Why doesn't it write ESC E? Because ESC E is poorly supported. For
|
||||
# example, gnome-terminal (many different versions) fails to scroll if
|
||||
# it receives ESC E and the cursor is already on the last row.
|
||||
self.protocol.nextLine()
|
||||
self.assertEqual(self.transport.value(), b"\r\n")
|
||||
|
||||
def test_setModes(self):
|
||||
"""
|
||||
L{ServerProtocol.setModes} writes a control sequence
|
||||
containing the requested modes and ending in the
|
||||
L{CSFinalByte.SM}.
|
||||
"""
|
||||
modesToSet = [modes.KAM, modes.IRM, modes.LNM]
|
||||
self.protocol.setModes(modesToSet)
|
||||
self.assertEqual(
|
||||
self.transport.value(),
|
||||
self.CSI
|
||||
+ b";".join(b"%d" % (m,) for m in modesToSet)
|
||||
+ CSFinalByte.SM.value,
|
||||
)
|
||||
|
||||
def test_setPrivateModes(self):
|
||||
"""
|
||||
L{ServerProtocol.setPrivatesModes} writes a control sequence
|
||||
containing the requested private modes and ending in the
|
||||
L{CSFinalByte.SM}.
|
||||
"""
|
||||
privateModesToSet = [
|
||||
privateModes.ERROR,
|
||||
privateModes.COLUMN,
|
||||
privateModes.ORIGIN,
|
||||
]
|
||||
self.protocol.setModes(privateModesToSet)
|
||||
self.assertEqual(
|
||||
self.transport.value(),
|
||||
self.CSI
|
||||
+ b";".join(b"%d" % (m,) for m in privateModesToSet)
|
||||
+ CSFinalByte.SM.value,
|
||||
)
|
||||
|
||||
def test_resetModes(self):
|
||||
"""
|
||||
L{ServerProtocol.resetModes} writes the control sequence
|
||||
ending in the L{CSFinalByte.RM}.
|
||||
"""
|
||||
modesToSet = [modes.KAM, modes.IRM, modes.LNM]
|
||||
self.protocol.resetModes(modesToSet)
|
||||
self.assertEqual(
|
||||
self.transport.value(),
|
||||
self.CSI
|
||||
+ b";".join(b"%d" % (m,) for m in modesToSet)
|
||||
+ CSFinalByte.RM.value,
|
||||
)
|
||||
|
||||
def test_singleShift2(self):
|
||||
"""
|
||||
L{ServerProtocol.singleShift2} writes an escape sequence
|
||||
followed by L{C1SevenBit.SS2}
|
||||
"""
|
||||
self.protocol.singleShift2()
|
||||
self.assertEqual(self.transport.value(), self.ESC + C1SevenBit.SS2.value)
|
||||
|
||||
def test_singleShift3(self):
|
||||
"""
|
||||
L{ServerProtocol.singleShift3} writes an escape sequence
|
||||
followed by L{C1SevenBit.SS3}
|
||||
"""
|
||||
self.protocol.singleShift3()
|
||||
self.assertEqual(self.transport.value(), self.ESC + C1SevenBit.SS3.value)
|
||||
|
||||
def test_selectGraphicRendition(self):
|
||||
"""
|
||||
L{ServerProtocol.selectGraphicRendition} writes a control
|
||||
sequence containing the requested attributes and ending with
|
||||
L{CSFinalByte.SGR}
|
||||
"""
|
||||
self.protocol.selectGraphicRendition(str(BLINK), str(UNDERLINE))
|
||||
self.assertEqual(
|
||||
self.transport.value(),
|
||||
self.CSI + b"%d;%d" % (BLINK, UNDERLINE) + CSFinalByte.SGR.value,
|
||||
)
|
||||
|
||||
def test_horizontalTabulationSet(self):
|
||||
"""
|
||||
L{ServerProtocol.horizontalTabulationSet} writes the escape
|
||||
sequence ending in L{C1SevenBit.HTS}
|
||||
"""
|
||||
self.protocol.horizontalTabulationSet()
|
||||
self.assertEqual(self.transport.value(), self.ESC + C1SevenBit.HTS.value)
|
||||
|
||||
def test_eraseToLineEnd(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseToLineEnd} writes the control sequence
|
||||
sequence ending in L{CSFinalByte.EL} and no parameters,
|
||||
forcing the client to default to 0 (from the active present
|
||||
position's current location to the end of the line.)
|
||||
"""
|
||||
self.protocol.eraseToLineEnd()
|
||||
self.assertEqual(self.transport.value(), self.CSI + CSFinalByte.EL.value)
|
||||
|
||||
def test_eraseToLineBeginning(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseToLineBeginning} writes the control
|
||||
sequence sequence ending in L{CSFinalByte.EL} and a parameter
|
||||
of 1 (from the beginning of the line up to and include the
|
||||
active present position's current location.)
|
||||
"""
|
||||
self.protocol.eraseToLineBeginning()
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"1" + CSFinalByte.EL.value)
|
||||
|
||||
def test_eraseLine(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseLine} writes the control
|
||||
sequence sequence ending in L{CSFinalByte.EL} and a parameter
|
||||
of 2 (the entire line.)
|
||||
"""
|
||||
self.protocol.eraseLine()
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"2" + CSFinalByte.EL.value)
|
||||
|
||||
def test_eraseToDisplayEnd(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseToDisplayEnd} writes the control
|
||||
sequence sequence ending in L{CSFinalByte.ED} and no parameters,
|
||||
forcing the client to default to 0 (from the active present
|
||||
position's current location to the end of the page.)
|
||||
"""
|
||||
self.protocol.eraseToDisplayEnd()
|
||||
self.assertEqual(self.transport.value(), self.CSI + CSFinalByte.ED.value)
|
||||
|
||||
def test_eraseToDisplayBeginning(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseToDisplayBeginning} writes the control
|
||||
sequence sequence ending in L{CSFinalByte.ED} a parameter of 1
|
||||
(from the beginning of the page up to and include the active
|
||||
present position's current location.)
|
||||
"""
|
||||
self.protocol.eraseToDisplayBeginning()
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"1" + CSFinalByte.ED.value)
|
||||
|
||||
def test_eraseToDisplay(self):
|
||||
"""
|
||||
L{ServerProtocol.eraseDisplay} writes the control sequence
|
||||
sequence ending in L{CSFinalByte.ED} a parameter of 2 (the
|
||||
entire page)
|
||||
"""
|
||||
self.protocol.eraseDisplay()
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"2" + CSFinalByte.ED.value)
|
||||
|
||||
def test_deleteCharacter(self):
|
||||
"""
|
||||
L{ServerProtocol.deleteCharacter} writes the control sequence
|
||||
containing the number of characters to delete and ending in
|
||||
L{CSFinalByte.DCH}
|
||||
"""
|
||||
self.protocol.deleteCharacter(4)
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"4" + CSFinalByte.DCH.value
|
||||
)
|
||||
|
||||
def test_insertLine(self):
|
||||
"""
|
||||
L{ServerProtocol.insertLine} writes the control sequence
|
||||
containing the number of lines to insert and ending in
|
||||
L{CSFinalByte.IL}
|
||||
"""
|
||||
self.protocol.insertLine(5)
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"5" + CSFinalByte.IL.value)
|
||||
|
||||
def test_deleteLine(self):
|
||||
"""
|
||||
L{ServerProtocol.deleteLine} writes the control sequence
|
||||
containing the number of lines to delete and ending in
|
||||
L{CSFinalByte.DL}
|
||||
"""
|
||||
self.protocol.deleteLine(6)
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"6" + CSFinalByte.DL.value)
|
||||
|
||||
def test_setScrollRegionNoArgs(self):
|
||||
"""
|
||||
With no arguments, L{ServerProtocol.setScrollRegion} writes a
|
||||
control sequence with no parameters, but a parameter
|
||||
separator, and ending in C{b'r'}.
|
||||
"""
|
||||
self.protocol.setScrollRegion()
|
||||
self.assertEqual(self.transport.value(), self.CSI + b";" + b"r")
|
||||
|
||||
def test_setScrollRegionJustFirst(self):
|
||||
"""
|
||||
With just a value for its C{first} argument,
|
||||
L{ServerProtocol.setScrollRegion} writes a control sequence with
|
||||
that parameter, a parameter separator, and finally a C{b'r'}.
|
||||
"""
|
||||
self.protocol.setScrollRegion(first=1)
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"1;" + b"r")
|
||||
|
||||
def test_setScrollRegionJustLast(self):
|
||||
"""
|
||||
With just a value for its C{last} argument,
|
||||
L{ServerProtocol.setScrollRegion} writes a control sequence with
|
||||
a parameter separator, that parameter, and finally a C{b'r'}.
|
||||
"""
|
||||
self.protocol.setScrollRegion(last=1)
|
||||
self.assertEqual(self.transport.value(), self.CSI + b";1" + b"r")
|
||||
|
||||
def test_setScrollRegionFirstAndLast(self):
|
||||
"""
|
||||
When given both C{first} and C{last}
|
||||
L{ServerProtocol.setScrollRegion} writes a control sequence with
|
||||
the first parameter, a parameter separator, the last
|
||||
parameter, and finally a C{b'r'}.
|
||||
"""
|
||||
self.protocol.setScrollRegion(first=1, last=2)
|
||||
self.assertEqual(self.transport.value(), self.CSI + b"1;2" + b"r")
|
||||
|
||||
def test_reportCursorPosition(self):
|
||||
"""
|
||||
L{ServerProtocol.reportCursorPosition} writes a control
|
||||
sequence ending in L{CSFinalByte.DSR} with a parameter of 6
|
||||
(the Device Status Report returns the current active
|
||||
position.)
|
||||
"""
|
||||
self.protocol.reportCursorPosition()
|
||||
self.assertEqual(
|
||||
self.transport.value(), self.CSI + b"6" + CSFinalByte.DSR.value
|
||||
)
|
||||
1808
backend/lib/python3.12/site-packages/twisted/conch/test/test_keys.py
Normal file
1808
backend/lib/python3.12/site-packages/twisted/conch/test/test_keys.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_manhole -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
# pylint: disable=I0011,W9401,W9402
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.manhole}.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
ssh: Optional[bool] = None
|
||||
|
||||
from twisted.conch import manhole
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.conch.test.test_recvline import (
|
||||
_SSHMixin,
|
||||
_StdioMixin,
|
||||
_TelnetMixin,
|
||||
ssh,
|
||||
stdio,
|
||||
)
|
||||
from twisted.internet import defer, error
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
def determineDefaultFunctionName():
|
||||
"""
|
||||
Return the string used by Python as the name for code objects which are
|
||||
compiled from interactive input or at the top-level of modules.
|
||||
"""
|
||||
try:
|
||||
1 // 0
|
||||
except BaseException:
|
||||
# The last frame is this function. The second to last frame is this
|
||||
# function's caller, which is module-scope, which is what we want,
|
||||
# so -2.
|
||||
return traceback.extract_stack()[-2][2]
|
||||
|
||||
|
||||
defaultFunctionName = determineDefaultFunctionName()
|
||||
|
||||
|
||||
class ManholeInterpreterTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{manhole.ManholeInterpreter}.
|
||||
"""
|
||||
|
||||
def test_resetBuffer(self):
|
||||
"""
|
||||
L{ManholeInterpreter.resetBuffer} should empty the input buffer.
|
||||
"""
|
||||
interpreter = manhole.ManholeInterpreter(None)
|
||||
interpreter.buffer.extend(["1", "2"])
|
||||
interpreter.resetBuffer()
|
||||
self.assertFalse(interpreter.buffer)
|
||||
|
||||
|
||||
class ManholeProtocolTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{manhole.Manhole}.
|
||||
"""
|
||||
|
||||
def test_interruptResetsInterpreterBuffer(self):
|
||||
"""
|
||||
L{manhole.Manhole.handle_INT} should cause the interpreter input buffer
|
||||
to be reset.
|
||||
"""
|
||||
transport = StringTransport()
|
||||
terminal = insults.ServerProtocol(manhole.Manhole)
|
||||
terminal.makeConnection(transport)
|
||||
protocol = terminal.terminalProtocol
|
||||
interpreter = protocol.interpreter
|
||||
interpreter.buffer.extend(["1", "2"])
|
||||
protocol.handle_INT()
|
||||
self.assertFalse(interpreter.buffer)
|
||||
|
||||
|
||||
class WriterTests(unittest.TestCase):
|
||||
def test_Integer(self):
|
||||
"""
|
||||
Colorize an integer.
|
||||
"""
|
||||
manhole.lastColorizedLine("1")
|
||||
|
||||
def test_DoubleQuoteString(self):
|
||||
"""
|
||||
Colorize an integer in double quotes.
|
||||
"""
|
||||
manhole.lastColorizedLine('"1"')
|
||||
|
||||
def test_SingleQuoteString(self):
|
||||
"""
|
||||
Colorize an integer in single quotes.
|
||||
"""
|
||||
manhole.lastColorizedLine("'1'")
|
||||
|
||||
def test_TripleSingleQuotedString(self):
|
||||
"""
|
||||
Colorize an integer in triple quotes.
|
||||
"""
|
||||
manhole.lastColorizedLine("'''1'''")
|
||||
|
||||
def test_TripleDoubleQuotedString(self):
|
||||
"""
|
||||
Colorize an integer in triple and double quotes.
|
||||
"""
|
||||
manhole.lastColorizedLine('"""1"""')
|
||||
|
||||
def test_FunctionDefinition(self):
|
||||
"""
|
||||
Colorize a function definition.
|
||||
"""
|
||||
manhole.lastColorizedLine("def foo():")
|
||||
|
||||
def test_ClassDefinition(self):
|
||||
"""
|
||||
Colorize a class definition.
|
||||
"""
|
||||
manhole.lastColorizedLine("class foo:")
|
||||
|
||||
def test_unicode(self):
|
||||
"""
|
||||
Colorize a Unicode string.
|
||||
"""
|
||||
res = manhole.lastColorizedLine("\u0438")
|
||||
self.assertTrue(isinstance(res, bytes))
|
||||
|
||||
def test_bytes(self):
|
||||
"""
|
||||
Colorize a UTF-8 byte string.
|
||||
"""
|
||||
res = manhole.lastColorizedLine(b"\xd0\xb8")
|
||||
self.assertTrue(isinstance(res, bytes))
|
||||
|
||||
def test_identicalOutput(self):
|
||||
"""
|
||||
The output of UTF-8 bytestrings and Unicode strings are identical.
|
||||
"""
|
||||
self.assertEqual(
|
||||
manhole.lastColorizedLine(b"\xd0\xb8"), manhole.lastColorizedLine("\u0438")
|
||||
)
|
||||
|
||||
|
||||
class ManholeLoopbackMixin:
|
||||
serverProtocol = manhole.ColoredManhole
|
||||
|
||||
def test_SimpleExpression(self):
|
||||
"""
|
||||
Evaluate simple expression.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(b"1 + 1\n" b"done")
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer([b">>> 1 + 1", b"2", b">>> done"])
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
def test_TripleQuoteLineContinuation(self):
|
||||
"""
|
||||
Evaluate line continuation in triple quotes.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(b"'''\n'''\n" b"done")
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer([b">>> '''", b"... '''", b"'\\n'", b">>> done"])
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
def test_FunctionDefinition(self):
|
||||
"""
|
||||
Evaluate function definition.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(b"def foo(bar):\n" b"\tprint(bar)\n\n" b"foo(42)\n" b"done")
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer(
|
||||
[
|
||||
b">>> def foo(bar):",
|
||||
b"... print(bar)",
|
||||
b"... ",
|
||||
b">>> foo(42)",
|
||||
b"42",
|
||||
b">>> done",
|
||||
]
|
||||
)
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
def test_ClassDefinition(self):
|
||||
"""
|
||||
Evaluate class definition.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
self._testwrite(
|
||||
b"class Foo:\n"
|
||||
b"\tdef bar(self):\n"
|
||||
b"\t\tprint('Hello, world!')\n\n"
|
||||
b"Foo().bar()\n"
|
||||
b"done"
|
||||
)
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer(
|
||||
[
|
||||
b">>> class Foo:",
|
||||
b"... def bar(self):",
|
||||
b"... print('Hello, world!')",
|
||||
b"... ",
|
||||
b">>> Foo().bar()",
|
||||
b"Hello, world!",
|
||||
b">>> done",
|
||||
]
|
||||
)
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
def test_Exception(self):
|
||||
"""
|
||||
Evaluate raising an exception.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(b"raise Exception('foo bar baz')\n" b"done")
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer(
|
||||
[
|
||||
b">>> raise Exception('foo bar baz')",
|
||||
b"Traceback (most recent call last):",
|
||||
b' File "<console>", line 1, in '
|
||||
+ defaultFunctionName.encode("utf-8"),
|
||||
b"Exception: foo bar baz",
|
||||
b">>> done",
|
||||
],
|
||||
)
|
||||
|
||||
done.addCallback(finished)
|
||||
return done
|
||||
|
||||
def test_ExceptionWithCustomExcepthook(
|
||||
self,
|
||||
):
|
||||
"""
|
||||
Raised exceptions are handled the same way even if L{sys.excepthook}
|
||||
has been modified from its original value.
|
||||
"""
|
||||
self.patch(sys, "excepthook", lambda *args: None)
|
||||
return self.test_Exception()
|
||||
|
||||
def test_ControlC(self):
|
||||
"""
|
||||
Evaluate interrupting with CTRL-C.
|
||||
"""
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(b"cancelled line" + manhole.CTRL_C + b"done")
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer(
|
||||
[b">>> cancelled line", b"KeyboardInterrupt", b">>> done"]
|
||||
)
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
def test_interruptDuringContinuation(self):
|
||||
"""
|
||||
Sending ^C to Manhole while in a state where more input is required to
|
||||
complete a statement should discard the entire ongoing statement and
|
||||
reset the input prompt to the non-continuation prompt.
|
||||
"""
|
||||
continuing = self.recvlineClient.expect(b"things")
|
||||
|
||||
self._testwrite(b"(\nthings")
|
||||
|
||||
def gotContinuation(ignored):
|
||||
self._assertBuffer([b">>> (", b"... things"])
|
||||
interrupted = self.recvlineClient.expect(b">>> ")
|
||||
self._testwrite(manhole.CTRL_C)
|
||||
return interrupted
|
||||
|
||||
continuing.addCallback(gotContinuation)
|
||||
|
||||
def gotInterruption(ignored):
|
||||
self._assertBuffer([b">>> (", b"... things", b"KeyboardInterrupt", b">>> "])
|
||||
|
||||
continuing.addCallback(gotInterruption)
|
||||
return continuing
|
||||
|
||||
def test_ControlBackslash(self):
|
||||
r"""
|
||||
Evaluate cancelling with CTRL-\.
|
||||
"""
|
||||
self._testwrite(b"cancelled line")
|
||||
partialLine = self.recvlineClient.expect(b"cancelled line")
|
||||
|
||||
def gotPartialLine(ign):
|
||||
self._assertBuffer([b">>> cancelled line"])
|
||||
self._testwrite(manhole.CTRL_BACKSLASH)
|
||||
|
||||
d = self.recvlineClient.onDisconnection
|
||||
return self.assertFailure(d, error.ConnectionDone)
|
||||
|
||||
def gotClearedLine(ign):
|
||||
self._assertBuffer([b""])
|
||||
|
||||
return partialLine.addCallback(gotPartialLine).addCallback(gotClearedLine)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_controlD(self):
|
||||
"""
|
||||
A CTRL+D in the middle of a line doesn't close a connection,
|
||||
but at the beginning of a line it does.
|
||||
"""
|
||||
self._testwrite(b"1 + 1")
|
||||
yield self.recvlineClient.expect(rb"\+ 1")
|
||||
self._assertBuffer([b">>> 1 + 1"])
|
||||
|
||||
self._testwrite(manhole.CTRL_D + b" + 1")
|
||||
yield self.recvlineClient.expect(rb"\+ 1")
|
||||
self._assertBuffer([b">>> 1 + 1 + 1"])
|
||||
|
||||
self._testwrite(b"\n")
|
||||
yield self.recvlineClient.expect(b"3\n>>> ")
|
||||
|
||||
self._testwrite(manhole.CTRL_D)
|
||||
d = self.recvlineClient.onDisconnection
|
||||
yield self.assertFailure(d, error.ConnectionDone)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_ControlL(self):
|
||||
"""
|
||||
CTRL+L is generally used as a redraw-screen command in terminal
|
||||
applications. Manhole doesn't currently respect this usage of it,
|
||||
but it should at least do something reasonable in response to this
|
||||
event (rather than, say, eating your face).
|
||||
"""
|
||||
# Start off with a newline so that when we clear the display we can
|
||||
# tell by looking for the missing first empty prompt line.
|
||||
self._testwrite(b"\n1 + 1")
|
||||
yield self.recvlineClient.expect(rb"\+ 1")
|
||||
self._assertBuffer([b">>> ", b">>> 1 + 1"])
|
||||
|
||||
self._testwrite(manhole.CTRL_L + b" + 1")
|
||||
yield self.recvlineClient.expect(rb"1 \+ 1 \+ 1")
|
||||
self._assertBuffer([b">>> 1 + 1 + 1"])
|
||||
|
||||
def test_controlA(self):
|
||||
"""
|
||||
CTRL-A can be used as HOME - returning cursor to beginning of
|
||||
current line buffer.
|
||||
"""
|
||||
self._testwrite(b'rint "hello"' + b"\x01" + b"p")
|
||||
d = self.recvlineClient.expect(b'print "hello"')
|
||||
|
||||
def cb(ignore):
|
||||
self._assertBuffer([b'>>> print "hello"'])
|
||||
|
||||
return d.addCallback(cb)
|
||||
|
||||
def test_controlE(self):
|
||||
"""
|
||||
CTRL-E can be used as END - setting cursor to end of current
|
||||
line buffer.
|
||||
"""
|
||||
self._testwrite(b'rint "hello' + b"\x01" + b"p" + b"\x05" + b'"')
|
||||
d = self.recvlineClient.expect(b'print "hello"')
|
||||
|
||||
def cb(ignore):
|
||||
self._assertBuffer([b'>>> print "hello"'])
|
||||
|
||||
return d.addCallback(cb)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_deferred(self):
|
||||
"""
|
||||
When a deferred is returned to the manhole REPL, it is displayed with
|
||||
a sequence number, and when the deferred fires, the result is printed.
|
||||
"""
|
||||
self._testwrite(
|
||||
b"from twisted.internet import defer, reactor\n"
|
||||
b"d = defer.Deferred()\n"
|
||||
b"d\n"
|
||||
)
|
||||
|
||||
yield self.recvlineClient.expect(b"<Deferred #0>")
|
||||
|
||||
self._testwrite(b"c = reactor.callLater(0.1, d.callback, 'Hi!')\n")
|
||||
yield self.recvlineClient.expect(b">>> ")
|
||||
|
||||
yield self.recvlineClient.expect(b"Deferred #0 called back: 'Hi!'\n>>> ")
|
||||
self._assertBuffer(
|
||||
[
|
||||
b">>> from twisted.internet import defer, reactor",
|
||||
b">>> d = defer.Deferred()",
|
||||
b">>> d",
|
||||
b"<Deferred #0>",
|
||||
b">>> c = reactor.callLater(0.1, d.callback, 'Hi!')",
|
||||
b"Deferred #0 called back: 'Hi!'",
|
||||
b">>> ",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ManholeLoopbackTelnetTests(_TelnetMixin, unittest.TestCase, ManholeLoopbackMixin):
|
||||
"""
|
||||
Test manhole loopback over Telnet.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ManholeLoopbackSSHTests(_SSHMixin, unittest.TestCase, ManholeLoopbackMixin):
|
||||
"""
|
||||
Test manhole loopback over SSH.
|
||||
"""
|
||||
|
||||
if ssh is None:
|
||||
skip = "cryptography requirements missing"
|
||||
|
||||
|
||||
class ManholeLoopbackStdioTests(_StdioMixin, unittest.TestCase, ManholeLoopbackMixin):
|
||||
"""
|
||||
Test manhole loopback over standard IO.
|
||||
"""
|
||||
|
||||
if stdio is None:
|
||||
skip = "Terminal requirements missing"
|
||||
else:
|
||||
serverProtocol = stdio.ConsoleManhole
|
||||
|
||||
|
||||
class ManholeMainTests(unittest.TestCase):
|
||||
"""
|
||||
Test the I{main} method from the I{manhole} module.
|
||||
"""
|
||||
|
||||
if stdio is None:
|
||||
skip = "Terminal requirements missing"
|
||||
|
||||
def test_mainClassNotFound(self):
|
||||
"""
|
||||
Will raise an exception when called with an argument which is a
|
||||
dotted patch which can not be imported..
|
||||
"""
|
||||
exception = self.assertRaises(
|
||||
ValueError,
|
||||
stdio.main,
|
||||
argv=["no-such-class"],
|
||||
)
|
||||
|
||||
self.assertEqual("Empty module name", exception.args[0])
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.manhole_tap}.
|
||||
"""
|
||||
|
||||
from twisted.application.internet import StreamServerEndpointService
|
||||
from twisted.application.service import MultiService
|
||||
from twisted.conch import telnet
|
||||
from twisted.cred import error
|
||||
from twisted.cred.credentials import UsernamePassword
|
||||
from twisted.python import usage
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch import manhole_ssh, manhole_tap
|
||||
|
||||
|
||||
class MakeServiceTests(TestCase):
|
||||
"""
|
||||
Tests for L{manhole_tap.makeService}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
usernamePassword = (b"iamuser", b"thisispassword")
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create a passwd-like file with a user.
|
||||
"""
|
||||
self.filename = self.mktemp()
|
||||
with open(self.filename, "wb") as f:
|
||||
f.write(b":".join(self.usernamePassword))
|
||||
self.options = manhole_tap.Options()
|
||||
|
||||
def test_requiresPort(self) -> None:
|
||||
"""
|
||||
L{manhole_tap.makeService} requires either 'telnetPort' or 'sshPort' to
|
||||
be given.
|
||||
"""
|
||||
with self.assertRaises(usage.UsageError) as e:
|
||||
manhole_tap.Options().parseOptions([])
|
||||
|
||||
self.assertEqual(
|
||||
e.exception.args[0],
|
||||
("At least one of --telnetPort " "and --sshPort must be specified"),
|
||||
)
|
||||
|
||||
def test_telnetPort(self) -> None:
|
||||
"""
|
||||
L{manhole_tap.makeService} will make a telnet service on the port
|
||||
defined by C{--telnetPort}. It will not make a SSH service.
|
||||
"""
|
||||
self.options.parseOptions(["--telnetPort", "tcp:222"])
|
||||
service = manhole_tap.makeService(self.options)
|
||||
self.assertIsInstance(service, MultiService)
|
||||
self.assertEqual(len(service.services), 1)
|
||||
self.assertIsInstance(service.services[0], StreamServerEndpointService)
|
||||
self.assertIsInstance(
|
||||
service.services[0].factory.protocol, manhole_tap.makeTelnetProtocol
|
||||
)
|
||||
self.assertEqual(service.services[0].endpoint._port, 222)
|
||||
|
||||
def test_sshPort(self) -> None:
|
||||
"""
|
||||
L{manhole_tap.makeService} will make a SSH service on the port
|
||||
defined by C{--sshPort}. It will not make a telnet service.
|
||||
"""
|
||||
# Why the sshKeyDir and sshKeySize params? To prevent it stomping over
|
||||
# (or using!) the user's private key, we just make a super small one
|
||||
# which will never be used in a temp directory.
|
||||
self.options.parseOptions(
|
||||
[
|
||||
"--sshKeyDir",
|
||||
self.mktemp(),
|
||||
"--sshKeySize",
|
||||
"1024",
|
||||
"--sshPort",
|
||||
"tcp:223",
|
||||
]
|
||||
)
|
||||
service = manhole_tap.makeService(self.options)
|
||||
self.assertIsInstance(service, MultiService)
|
||||
self.assertEqual(len(service.services), 1)
|
||||
self.assertIsInstance(service.services[0], StreamServerEndpointService)
|
||||
self.assertIsInstance(service.services[0].factory, manhole_ssh.ConchFactory)
|
||||
self.assertEqual(service.services[0].endpoint._port, 223)
|
||||
|
||||
def test_passwd(self) -> None:
|
||||
"""
|
||||
The C{--passwd} command-line option will load a passwd-like file.
|
||||
"""
|
||||
self.options.parseOptions(["--telnetPort", "tcp:22", "--passwd", self.filename])
|
||||
service = manhole_tap.makeService(self.options)
|
||||
portal = service.services[0].factory.protocol.portal
|
||||
|
||||
self.assertEqual(len(portal.checkers.keys()), 2)
|
||||
|
||||
# Ensure it's the passwd file we wanted by trying to authenticate
|
||||
self.assertTrue(
|
||||
self.successResultOf(
|
||||
portal.login(
|
||||
UsernamePassword(*self.usernamePassword),
|
||||
None,
|
||||
telnet.ITelnetProtocol,
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertIsInstance(
|
||||
self.failureResultOf(
|
||||
portal.login(
|
||||
UsernamePassword(b"wrong", b"user"), None, telnet.ITelnetProtocol
|
||||
)
|
||||
).value,
|
||||
error.UnauthorizedLogin,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- twisted.conch.test.test_mixin -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
from __future__ import annotations
|
||||
|
||||
from twisted.conch import mixin
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class TestBufferingProto(mixin.BufferingMixin):
|
||||
scheduled = False
|
||||
rescheduled = 0
|
||||
transport: StringTransport
|
||||
|
||||
def schedule(self) -> object:
|
||||
self.scheduled = True
|
||||
return object()
|
||||
|
||||
def reschedule(self, token: object) -> None:
|
||||
self.rescheduled += 1
|
||||
|
||||
|
||||
class BufferingTests(unittest.TestCase):
|
||||
def testBuffering(self) -> None:
|
||||
p = TestBufferingProto()
|
||||
t = p.transport = StringTransport()
|
||||
|
||||
self.assertFalse(p.scheduled)
|
||||
|
||||
L = [b"foo", b"bar", b"baz", b"quux"]
|
||||
|
||||
p.write(b"foo")
|
||||
self.assertTrue(p.scheduled)
|
||||
self.assertFalse(p.rescheduled)
|
||||
|
||||
for s in L:
|
||||
n = p.rescheduled
|
||||
p.write(s)
|
||||
self.assertEqual(p.rescheduled, n + 1)
|
||||
self.assertEqual(t.value(), b"")
|
||||
|
||||
p.flush()
|
||||
self.assertEqual(t.value(), b"foo" + b"".join(L))
|
||||
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.openssh_compat}.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.conch.ssh._kex import getDHGeneratorAndPrime
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.test.test_process import MockOS
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
doSkip = False
|
||||
skipReason = ""
|
||||
if requireModule("cryptography"):
|
||||
from twisted.conch.openssh_compat.factory import OpenSSHFactory
|
||||
else:
|
||||
doSkip = True
|
||||
skipReason = "Cannot run without cryptography"
|
||||
|
||||
if not hasattr(os, "geteuid"):
|
||||
doSkip = True
|
||||
skipReason = "geteuid/seteuid not available"
|
||||
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
class OpenSSHFactoryTests(TestCase):
|
||||
"""
|
||||
Tests for L{OpenSSHFactory}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.factory = OpenSSHFactory()
|
||||
self.keysDir = FilePath(self.mktemp())
|
||||
self.keysDir.makedirs()
|
||||
self.factory.dataRoot = self.keysDir.path
|
||||
self.moduliDir = FilePath(self.mktemp())
|
||||
self.moduliDir.makedirs()
|
||||
self.factory.moduliRoot = self.moduliDir.path
|
||||
|
||||
self.keysDir.child("ssh_host_foo").setContent(b"foo")
|
||||
self.keysDir.child("bar_key").setContent(b"foo")
|
||||
self.keysDir.child("ssh_host_one_key").setContent(keydata.privateRSA_openssh)
|
||||
self.keysDir.child("ssh_host_two_key").setContent(keydata.privateDSA_openssh)
|
||||
self.keysDir.child("ssh_host_three_key").setContent(b"not a key content")
|
||||
|
||||
self.keysDir.child("ssh_host_one_key.pub").setContent(keydata.publicRSA_openssh)
|
||||
|
||||
self.moduliDir.child("moduli").setContent(
|
||||
b"\n"
|
||||
b"# $OpenBSD: moduli,v 1.xx 2016/07/26 12:34:56 jhacker Exp $i\n"
|
||||
b"# Time Type Tests Tries Size Generator Modulus\n"
|
||||
b"20030501000000 2 6 100 2047 2 "
|
||||
b"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74"
|
||||
b"020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437"
|
||||
b"4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
|
||||
b"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05"
|
||||
b"98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB"
|
||||
b"9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
|
||||
b"E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718"
|
||||
b"3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"
|
||||
b"\n"
|
||||
)
|
||||
|
||||
self.mockos = MockOS()
|
||||
self.patch(os, "seteuid", self.mockos.seteuid)
|
||||
self.patch(os, "setegid", self.mockos.setegid)
|
||||
|
||||
def test_getPublicKeys(self) -> None:
|
||||
"""
|
||||
L{OpenSSHFactory.getPublicKeys} should return the available public keys
|
||||
in the data directory
|
||||
"""
|
||||
keys = self.factory.getPublicKeys()
|
||||
self.assertEqual(len(keys), 1)
|
||||
keyTypes = keys.keys()
|
||||
self.assertEqual(list(keyTypes), [b"ssh-rsa"])
|
||||
|
||||
def test_getPrivateKeys(self) -> None:
|
||||
"""
|
||||
Will return the available private keys in the data directory, ignoring
|
||||
key files which failed to be loaded.
|
||||
"""
|
||||
keys = self.factory.getPrivateKeys()
|
||||
self.assertEqual(len(keys), 2)
|
||||
keyTypes = keys.keys()
|
||||
self.assertEqual(set(keyTypes), {b"ssh-rsa", b"ssh-dss"})
|
||||
self.assertEqual(self.mockos.seteuidCalls, [])
|
||||
self.assertEqual(self.mockos.setegidCalls, [])
|
||||
|
||||
def test_getPrivateKeysAsRoot(self) -> None:
|
||||
"""
|
||||
L{OpenSSHFactory.getPrivateKeys} should switch to root if the keys
|
||||
aren't readable by the current user.
|
||||
"""
|
||||
keyFile = self.keysDir.child("ssh_host_two_key")
|
||||
# Fake permission error by changing the mode
|
||||
keyFile.chmod(0000)
|
||||
self.addCleanup(keyFile.chmod, 0o777)
|
||||
# And restore the right mode when seteuid is called
|
||||
savedSeteuid = os.seteuid
|
||||
|
||||
def seteuid(euid: int) -> None:
|
||||
keyFile.chmod(0o777)
|
||||
return savedSeteuid(euid)
|
||||
|
||||
self.patch(os, "seteuid", seteuid)
|
||||
keys = self.factory.getPrivateKeys()
|
||||
self.assertEqual(len(keys), 2)
|
||||
keyTypes = keys.keys()
|
||||
self.assertEqual(set(keyTypes), {b"ssh-rsa", b"ssh-dss"})
|
||||
self.assertEqual(self.mockos.seteuidCalls, [0, os.geteuid()])
|
||||
self.assertEqual(self.mockos.setegidCalls, [0, os.getegid()])
|
||||
|
||||
def test_getPrimes(self) -> None:
|
||||
"""
|
||||
L{OpenSSHFactory.getPrimes} should return the available primes
|
||||
in the moduli directory.
|
||||
"""
|
||||
primes = self.factory.getPrimes()
|
||||
self.assertEqual(
|
||||
primes,
|
||||
{
|
||||
2048: [getDHGeneratorAndPrime(b"diffie-hellman-group14-sha1")],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,801 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_recvline -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.recvline} and fixtures for testing related
|
||||
functionality.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.conch import recvline
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.cred import portal
|
||||
from twisted.internet import defer, error
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.python import components, filepath, reflect
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial.unittest import SkipTest, TestCase
|
||||
|
||||
stdio = requireModule("twisted.conch.stdio")
|
||||
properEnv = dict(os.environ)
|
||||
properEnv["PYTHONPATH"] = os.pathsep.join(sys.path)
|
||||
|
||||
|
||||
class ArrowsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.underlyingTransport = StringTransport()
|
||||
self.pt = insults.ServerProtocol()
|
||||
self.p = recvline.HistoricRecvLine()
|
||||
self.pt.protocolFactory = lambda: self.p
|
||||
self.pt.factory = self
|
||||
self.pt.makeConnection(self.underlyingTransport)
|
||||
|
||||
def test_printableCharacters(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a printable character,
|
||||
it adds it to the current line buffer.
|
||||
"""
|
||||
self.p.keystrokeReceived(b"x", None)
|
||||
self.p.keystrokeReceived(b"y", None)
|
||||
self.p.keystrokeReceived(b"z", None)
|
||||
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
def test_horizontalArrows(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a LEFT_ARROW or
|
||||
RIGHT_ARROW keystroke it moves the cursor left or right
|
||||
in the current line buffer, respectively.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
for ch in iterbytes(b"xyz"):
|
||||
kR(ch)
|
||||
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.RIGHT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xy", b"z"))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"x", b"yz"))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b"xyz"))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b"xyz"))
|
||||
|
||||
kR(self.pt.RIGHT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"x", b"yz"))
|
||||
|
||||
kR(self.pt.RIGHT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xy", b"z"))
|
||||
|
||||
kR(self.pt.RIGHT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.RIGHT_ARROW)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
def test_newline(self):
|
||||
"""
|
||||
When {HistoricRecvLine} receives a newline, it adds the current
|
||||
line buffer to the end of its history buffer.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz\nabc\n123\n"):
|
||||
kR(ch)
|
||||
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz", b"abc", b"123"), ()))
|
||||
|
||||
kR(b"c")
|
||||
kR(b"b")
|
||||
kR(b"a")
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz", b"abc", b"123"), ()))
|
||||
|
||||
kR(b"\n")
|
||||
self.assertEqual(
|
||||
self.p.currentHistoryBuffer(), ((b"xyz", b"abc", b"123", b"cba"), ())
|
||||
)
|
||||
|
||||
def test_verticalArrows(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives UP_ARROW or DOWN_ARROW
|
||||
keystrokes it move the current index in the current history
|
||||
buffer up or down, and resets the current line buffer to the
|
||||
previous or next line in history, respectively for each.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz\nabc\n123\n"):
|
||||
kR(ch)
|
||||
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz", b"abc", b"123"), ()))
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b""))
|
||||
|
||||
kR(self.pt.UP_ARROW)
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz", b"abc"), (b"123",)))
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"123", b""))
|
||||
|
||||
kR(self.pt.UP_ARROW)
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz",), (b"abc", b"123")))
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"abc", b""))
|
||||
|
||||
kR(self.pt.UP_ARROW)
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((), (b"xyz", b"abc", b"123")))
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.UP_ARROW)
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((), (b"xyz", b"abc", b"123")))
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
for i in range(4):
|
||||
kR(self.pt.DOWN_ARROW)
|
||||
self.assertEqual(self.p.currentHistoryBuffer(), ((b"xyz", b"abc", b"123"), ()))
|
||||
|
||||
def test_home(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a HOME keystroke it moves the
|
||||
cursor to the beginning of the current line buffer.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"hello, world"):
|
||||
kR(ch)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"hello, world", b""))
|
||||
|
||||
kR(self.pt.HOME)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b"hello, world"))
|
||||
|
||||
def test_end(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives an END keystroke it moves the cursor
|
||||
to the end of the current line buffer.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"hello, world"):
|
||||
kR(ch)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"hello, world", b""))
|
||||
|
||||
kR(self.pt.HOME)
|
||||
kR(self.pt.END)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"hello, world", b""))
|
||||
|
||||
def test_backspace(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a BACKSPACE keystroke it deletes
|
||||
the character immediately before the cursor.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz"):
|
||||
kR(ch)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.BACKSPACE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xy", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(self.pt.BACKSPACE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b"y"))
|
||||
|
||||
kR(self.pt.BACKSPACE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b"y"))
|
||||
|
||||
def test_delete(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a DELETE keystroke, it
|
||||
delets the character immediately after the cursor.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz"):
|
||||
kR(ch)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.DELETE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyz", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(self.pt.DELETE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xy", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(self.pt.DELETE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"x", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(self.pt.DELETE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b""))
|
||||
|
||||
kR(self.pt.DELETE)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b""))
|
||||
|
||||
def test_insert(self):
|
||||
"""
|
||||
When not in INSERT mode, L{HistoricRecvLine} inserts the typed
|
||||
character at the cursor before the next character.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz"):
|
||||
kR(ch)
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(b"A")
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyA", b"z"))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(b"B")
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyB", b"Az"))
|
||||
|
||||
def test_typeover(self):
|
||||
"""
|
||||
When in INSERT mode and upon receiving a keystroke with a printable
|
||||
character, L{HistoricRecvLine} replaces the character at
|
||||
the cursor with the typed character rather than inserting before.
|
||||
Ah, the ironies of INSERT mode.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
|
||||
for ch in iterbytes(b"xyz"):
|
||||
kR(ch)
|
||||
|
||||
kR(self.pt.INSERT)
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(b"A")
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyA", b""))
|
||||
|
||||
kR(self.pt.LEFT_ARROW)
|
||||
kR(b"B")
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"xyB", b""))
|
||||
|
||||
def test_unprintableCharacters(self):
|
||||
"""
|
||||
When L{HistoricRecvLine} receives a keystroke for an unprintable
|
||||
function key with no assigned behavior, the line buffer is unmodified.
|
||||
"""
|
||||
kR = lambda ch: self.p.keystrokeReceived(ch, None)
|
||||
pt = self.pt
|
||||
|
||||
for ch in (
|
||||
pt.F1,
|
||||
pt.F2,
|
||||
pt.F3,
|
||||
pt.F4,
|
||||
pt.F5,
|
||||
pt.F6,
|
||||
pt.F7,
|
||||
pt.F8,
|
||||
pt.F9,
|
||||
pt.F10,
|
||||
pt.F11,
|
||||
pt.F12,
|
||||
pt.PGUP,
|
||||
pt.PGDN,
|
||||
):
|
||||
kR(ch)
|
||||
self.assertEqual(self.p.currentLineBuffer(), (b"", b""))
|
||||
|
||||
|
||||
from twisted.conch import telnet
|
||||
from twisted.conch.insults import helper
|
||||
from twisted.conch.test.loopback import LoopbackRelay
|
||||
|
||||
|
||||
class EchoServer(recvline.HistoricRecvLine):
|
||||
def lineReceived(self, line):
|
||||
self.terminal.write(line + b"\n" + self.ps[self.pn])
|
||||
|
||||
|
||||
# An insults API for this would be nice.
|
||||
left = b"\x1b[D"
|
||||
right = b"\x1b[C"
|
||||
up = b"\x1b[A"
|
||||
down = b"\x1b[B"
|
||||
insert = b"\x1b[2~"
|
||||
home = b"\x1b[1~"
|
||||
delete = b"\x1b[3~"
|
||||
end = b"\x1b[4~"
|
||||
backspace = b"\x7f"
|
||||
|
||||
from twisted.cred import checkers
|
||||
|
||||
try:
|
||||
from twisted.conch.manhole_ssh import (
|
||||
ConchFactory,
|
||||
TerminalRealm,
|
||||
TerminalSession,
|
||||
TerminalSessionTransport,
|
||||
TerminalUser,
|
||||
)
|
||||
from twisted.conch.ssh import (
|
||||
channel,
|
||||
connection,
|
||||
keys,
|
||||
session,
|
||||
transport,
|
||||
userauth,
|
||||
)
|
||||
except ImportError:
|
||||
ssh = False
|
||||
else:
|
||||
ssh = True
|
||||
|
||||
class SessionChannel(channel.SSHChannel):
|
||||
name = b"session"
|
||||
|
||||
def __init__(
|
||||
self, protocolFactory, protocolArgs, protocolKwArgs, width, height, *a, **kw
|
||||
):
|
||||
channel.SSHChannel.__init__(self, *a, **kw)
|
||||
|
||||
self.protocolFactory = protocolFactory
|
||||
self.protocolArgs = protocolArgs
|
||||
self.protocolKwArgs = protocolKwArgs
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def channelOpen(self, data):
|
||||
term = session.packRequest_pty_req(
|
||||
b"vt102", (self.height, self.width, 0, 0), b""
|
||||
)
|
||||
self.conn.sendRequest(self, b"pty-req", term)
|
||||
self.conn.sendRequest(self, b"shell", b"")
|
||||
|
||||
self._protocolInstance = self.protocolFactory(
|
||||
*self.protocolArgs, **self.protocolKwArgs
|
||||
)
|
||||
self._protocolInstance.factory = self
|
||||
self._protocolInstance.makeConnection(self)
|
||||
|
||||
def closed(self):
|
||||
self._protocolInstance.connectionLost(error.ConnectionDone())
|
||||
|
||||
def dataReceived(self, data):
|
||||
self._protocolInstance.dataReceived(data)
|
||||
|
||||
class TestConnection(connection.SSHConnection):
|
||||
def __init__(
|
||||
self, protocolFactory, protocolArgs, protocolKwArgs, width, height, *a, **kw
|
||||
):
|
||||
connection.SSHConnection.__init__(self, *a, **kw)
|
||||
|
||||
self.protocolFactory = protocolFactory
|
||||
self.protocolArgs = protocolArgs
|
||||
self.protocolKwArgs = protocolKwArgs
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def serviceStarted(self):
|
||||
self.__channel = SessionChannel(
|
||||
self.protocolFactory,
|
||||
self.protocolArgs,
|
||||
self.protocolKwArgs,
|
||||
self.width,
|
||||
self.height,
|
||||
)
|
||||
self.openChannel(self.__channel)
|
||||
|
||||
def write(self, data):
|
||||
return self.__channel.write(data)
|
||||
|
||||
class TestAuth(userauth.SSHUserAuthClient):
|
||||
def __init__(self, username, password, *a, **kw):
|
||||
userauth.SSHUserAuthClient.__init__(self, username, *a, **kw)
|
||||
self.password = password
|
||||
|
||||
def getPassword(self):
|
||||
return defer.succeed(self.password)
|
||||
|
||||
class TestTransport(transport.SSHClientTransport):
|
||||
def __init__(
|
||||
self,
|
||||
protocolFactory,
|
||||
protocolArgs,
|
||||
protocolKwArgs,
|
||||
username,
|
||||
password,
|
||||
width,
|
||||
height,
|
||||
*a,
|
||||
**kw,
|
||||
):
|
||||
self.protocolFactory = protocolFactory
|
||||
self.protocolArgs = protocolArgs
|
||||
self.protocolKwArgs = protocolKwArgs
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def verifyHostKey(self, hostKey, fingerprint):
|
||||
return defer.succeed(True)
|
||||
|
||||
def connectionSecure(self):
|
||||
self.__connection = TestConnection(
|
||||
self.protocolFactory,
|
||||
self.protocolArgs,
|
||||
self.protocolKwArgs,
|
||||
self.width,
|
||||
self.height,
|
||||
)
|
||||
self.requestService(
|
||||
TestAuth(self.username, self.password, self.__connection)
|
||||
)
|
||||
|
||||
def write(self, data):
|
||||
return self.__connection.write(data)
|
||||
|
||||
class TestSessionTransport(TerminalSessionTransport):
|
||||
def protocolFactory(self):
|
||||
return self.avatar.conn.transport.factory.serverProtocol()
|
||||
|
||||
class TestSession(TerminalSession):
|
||||
transportFactory = TestSessionTransport
|
||||
|
||||
class TestUser(TerminalUser):
|
||||
pass
|
||||
|
||||
components.registerAdapter(TestSession, TestUser, session.ISession)
|
||||
|
||||
|
||||
class NotifyingExpectableBuffer(helper.ExpectableBuffer):
|
||||
def __init__(self):
|
||||
self.onConnection = defer.Deferred()
|
||||
self.onDisconnection = defer.Deferred()
|
||||
|
||||
def connectionMade(self):
|
||||
helper.ExpectableBuffer.connectionMade(self)
|
||||
self.onConnection.callback(self)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.onDisconnection.errback(reason)
|
||||
|
||||
|
||||
class _BaseMixin:
|
||||
WIDTH = 80
|
||||
HEIGHT = 24
|
||||
|
||||
def _assertBuffer(self, lines):
|
||||
receivedLines = self.recvlineClient.__bytes__().splitlines()
|
||||
expectedLines = lines + ([b""] * (self.HEIGHT - len(lines) - 1))
|
||||
self.assertEqual(receivedLines, expectedLines)
|
||||
|
||||
def _trivialTest(self, inputLine, output):
|
||||
done = self.recvlineClient.expect(b"done")
|
||||
|
||||
self._testwrite(inputLine)
|
||||
|
||||
def finished(ign):
|
||||
self._assertBuffer(output)
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
|
||||
class _SSHMixin(_BaseMixin):
|
||||
def setUp(self):
|
||||
if not ssh:
|
||||
raise SkipTest(
|
||||
"cryptography requirements missing, can't run historic "
|
||||
"recvline tests over ssh"
|
||||
)
|
||||
|
||||
u, p = b"testuser", b"testpass"
|
||||
rlm = TerminalRealm()
|
||||
rlm.userFactory = TestUser
|
||||
rlm.chainedProtocolFactory = lambda: insultsServer
|
||||
|
||||
checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
checker.addUser(u, p)
|
||||
ptl = portal.Portal(rlm)
|
||||
ptl.registerChecker(checker)
|
||||
sshFactory = ConchFactory(ptl)
|
||||
|
||||
sshKey = keys._getPersistentRSAKey(
|
||||
filepath.FilePath(self.mktemp()), keySize=1024
|
||||
)
|
||||
sshFactory.publicKeys[b"ssh-rsa"] = sshKey
|
||||
sshFactory.privateKeys[b"ssh-rsa"] = sshKey
|
||||
|
||||
sshFactory.serverProtocol = self.serverProtocol
|
||||
sshFactory.startFactory()
|
||||
|
||||
recvlineServer = self.serverProtocol()
|
||||
insultsServer = insults.ServerProtocol(lambda: recvlineServer)
|
||||
sshServer = sshFactory.buildProtocol(None)
|
||||
clientTransport = LoopbackRelay(sshServer)
|
||||
|
||||
recvlineClient = NotifyingExpectableBuffer()
|
||||
insultsClient = insults.ClientProtocol(lambda: recvlineClient)
|
||||
sshClient = TestTransport(
|
||||
lambda: insultsClient, (), {}, u, p, self.WIDTH, self.HEIGHT
|
||||
)
|
||||
serverTransport = LoopbackRelay(sshClient)
|
||||
|
||||
sshClient.makeConnection(clientTransport)
|
||||
sshServer.makeConnection(serverTransport)
|
||||
|
||||
self.recvlineClient = recvlineClient
|
||||
self.sshClient = sshClient
|
||||
self.sshServer = sshServer
|
||||
self.clientTransport = clientTransport
|
||||
self.serverTransport = serverTransport
|
||||
|
||||
return recvlineClient.onConnection
|
||||
|
||||
def _testwrite(self, data):
|
||||
self.sshClient.write(data)
|
||||
|
||||
|
||||
from twisted.conch.test import test_telnet
|
||||
|
||||
|
||||
class TestInsultsClientProtocol(insults.ClientProtocol, test_telnet.TestProtocol):
|
||||
pass
|
||||
|
||||
|
||||
class TestInsultsServerProtocol(insults.ServerProtocol, test_telnet.TestProtocol):
|
||||
pass
|
||||
|
||||
|
||||
class _TelnetMixin(_BaseMixin):
|
||||
def setUp(self):
|
||||
recvlineServer = self.serverProtocol()
|
||||
insultsServer = TestInsultsServerProtocol(lambda: recvlineServer)
|
||||
telnetServer = telnet.TelnetTransport(lambda: insultsServer)
|
||||
clientTransport = LoopbackRelay(telnetServer)
|
||||
|
||||
recvlineClient = NotifyingExpectableBuffer()
|
||||
insultsClient = TestInsultsClientProtocol(lambda: recvlineClient)
|
||||
telnetClient = telnet.TelnetTransport(lambda: insultsClient)
|
||||
serverTransport = LoopbackRelay(telnetClient)
|
||||
|
||||
telnetClient.makeConnection(clientTransport)
|
||||
telnetServer.makeConnection(serverTransport)
|
||||
|
||||
serverTransport.clearBuffer()
|
||||
clientTransport.clearBuffer()
|
||||
|
||||
self.recvlineClient = recvlineClient
|
||||
self.telnetClient = telnetClient
|
||||
self.clientTransport = clientTransport
|
||||
self.serverTransport = serverTransport
|
||||
|
||||
return recvlineClient.onConnection
|
||||
|
||||
def _testwrite(self, data):
|
||||
self.telnetClient.write(data)
|
||||
|
||||
|
||||
class _StdioMixin(_BaseMixin):
|
||||
def setUp(self):
|
||||
# A memory-only terminal emulator, into which the server will
|
||||
# write things and make other state changes. What ends up
|
||||
# here is basically what a user would have seen on their
|
||||
# screen.
|
||||
testTerminal = NotifyingExpectableBuffer()
|
||||
|
||||
# An insults client protocol which will translate bytes
|
||||
# received from the child process into keystroke commands for
|
||||
# an ITerminalProtocol.
|
||||
insultsClient = insults.ClientProtocol(lambda: testTerminal)
|
||||
|
||||
# A process protocol which will translate stdout and stderr
|
||||
# received from the child process to dataReceived calls and
|
||||
# error reporting on an insults client protocol.
|
||||
processClient = stdio.TerminalProcessProtocol(insultsClient)
|
||||
|
||||
# Run twisted/conch/stdio.py with the name of a class
|
||||
# implementing ITerminalProtocol. This class will be used to
|
||||
# handle bytes we send to the child process.
|
||||
exe = sys.executable
|
||||
module = stdio.__file__
|
||||
if module.endswith(".pyc") or module.endswith(".pyo"):
|
||||
module = module[:-1]
|
||||
args = [exe, module, reflect.qual(self.serverProtocol)]
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
clientTransport = reactor.spawnProcess(
|
||||
processClient, exe, args, env=properEnv, usePTY=True
|
||||
)
|
||||
|
||||
self.recvlineClient = self.testTerminal = testTerminal
|
||||
self.processClient = processClient
|
||||
self.clientTransport = clientTransport
|
||||
|
||||
# Wait for the process protocol and test terminal to become
|
||||
# connected before proceeding. The former should always
|
||||
# happen first, but it doesn't hurt to be safe.
|
||||
return defer.gatherResults(
|
||||
filter(None, [processClient.onConnection, testTerminal.expect(b">>> ")])
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
# Kill the child process. We're done with it.
|
||||
try:
|
||||
self.clientTransport.signalProcess("KILL")
|
||||
except (error.ProcessExitedAlready, OSError):
|
||||
pass
|
||||
|
||||
def trap(failure):
|
||||
failure.trap(error.ProcessTerminated)
|
||||
self.assertIsNone(failure.value.exitCode)
|
||||
self.assertEqual(failure.value.status, 9)
|
||||
|
||||
return self.testTerminal.onDisconnection.addErrback(trap)
|
||||
|
||||
def _testwrite(self, data):
|
||||
self.clientTransport.write(data)
|
||||
|
||||
|
||||
class RecvlineLoopbackMixin:
|
||||
serverProtocol = EchoServer
|
||||
|
||||
def testSimple(self):
|
||||
return self._trivialTest(
|
||||
b"first line\ndone", [b">>> first line", b"first line", b">>> done"]
|
||||
)
|
||||
|
||||
def testLeftArrow(self):
|
||||
return self._trivialTest(
|
||||
insert + b"first line" + left * 4 + b"xxxx\ndone",
|
||||
[b">>> first xxxx", b"first xxxx", b">>> done"],
|
||||
)
|
||||
|
||||
def testRightArrow(self):
|
||||
return self._trivialTest(
|
||||
insert + b"right line" + left * 4 + right * 2 + b"xx\ndone",
|
||||
[b">>> right lixx", b"right lixx", b">>> done"],
|
||||
)
|
||||
|
||||
def testBackspace(self):
|
||||
return self._trivialTest(
|
||||
b"second line" + backspace * 4 + b"xxxx\ndone",
|
||||
[b">>> second xxxx", b"second xxxx", b">>> done"],
|
||||
)
|
||||
|
||||
def testDelete(self):
|
||||
return self._trivialTest(
|
||||
b"delete xxxx" + left * 4 + delete * 4 + b"line\ndone",
|
||||
[b">>> delete line", b"delete line", b">>> done"],
|
||||
)
|
||||
|
||||
def testInsert(self):
|
||||
return self._trivialTest(
|
||||
b"third ine" + left * 3 + b"l\ndone",
|
||||
[b">>> third line", b"third line", b">>> done"],
|
||||
)
|
||||
|
||||
def testTypeover(self):
|
||||
return self._trivialTest(
|
||||
b"fourth xine" + left * 4 + insert + b"l\ndone",
|
||||
[b">>> fourth line", b"fourth line", b">>> done"],
|
||||
)
|
||||
|
||||
def testHome(self):
|
||||
return self._trivialTest(
|
||||
insert + b"blah line" + home + b"home\ndone",
|
||||
[b">>> home line", b"home line", b">>> done"],
|
||||
)
|
||||
|
||||
def testEnd(self):
|
||||
return self._trivialTest(
|
||||
b"end " + left * 4 + end + b"line\ndone",
|
||||
[b">>> end line", b"end line", b">>> done"],
|
||||
)
|
||||
|
||||
|
||||
class RecvlineLoopbackTelnetTests(_TelnetMixin, TestCase, RecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
class RecvlineLoopbackSSHTests(_SSHMixin, TestCase, RecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
@skipIf(not stdio, "Terminal requirements missing, can't run recvline tests over stdio")
|
||||
class RecvlineLoopbackStdioTests(_StdioMixin, TestCase, RecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
class HistoricRecvlineLoopbackMixin:
|
||||
serverProtocol = EchoServer
|
||||
|
||||
def testUpArrow(self):
|
||||
return self._trivialTest(
|
||||
b"first line\n" + up + b"\ndone",
|
||||
[
|
||||
b">>> first line",
|
||||
b"first line",
|
||||
b">>> first line",
|
||||
b"first line",
|
||||
b">>> done",
|
||||
],
|
||||
)
|
||||
|
||||
def test_DownArrowToPartialLineInHistory(self):
|
||||
"""
|
||||
Pressing down arrow to visit an entry that was added to the
|
||||
history by pressing the up arrow instead of return does not
|
||||
raise a L{TypeError}.
|
||||
|
||||
@see: U{http://twistedmatrix.com/trac/ticket/9031}
|
||||
|
||||
@return: A L{defer.Deferred} that fires when C{b"done"} is
|
||||
echoed back.
|
||||
"""
|
||||
|
||||
return self._trivialTest(
|
||||
b"first line\n" + b"partial line" + up + down + b"\ndone",
|
||||
[
|
||||
b">>> first line",
|
||||
b"first line",
|
||||
b">>> partial line",
|
||||
b"partial line",
|
||||
b">>> done",
|
||||
],
|
||||
)
|
||||
|
||||
def testDownArrow(self):
|
||||
return self._trivialTest(
|
||||
b"first line\nsecond line\n" + up * 2 + down + b"\ndone",
|
||||
[
|
||||
b">>> first line",
|
||||
b"first line",
|
||||
b">>> second line",
|
||||
b"second line",
|
||||
b">>> second line",
|
||||
b"second line",
|
||||
b">>> done",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class HistoricRecvlineLoopbackTelnetTests(
|
||||
_TelnetMixin, TestCase, HistoricRecvlineLoopbackMixin
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class HistoricRecvlineLoopbackSSHTests(
|
||||
_SSHMixin, TestCase, HistoricRecvlineLoopbackMixin
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@skipIf(
|
||||
not stdio,
|
||||
"Terminal requirements missing, " "can't run historic recvline tests over stdio",
|
||||
)
|
||||
class HistoricRecvlineLoopbackStdioTests(
|
||||
_StdioMixin, TestCase, HistoricRecvlineLoopbackMixin
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class TransportSequenceTests(TestCase):
|
||||
"""
|
||||
L{twisted.conch.recvline.TransportSequence}
|
||||
"""
|
||||
|
||||
def test_invalidSequence(self):
|
||||
"""
|
||||
Initializing a L{recvline.TransportSequence} with no args
|
||||
raises an assertion.
|
||||
"""
|
||||
self.assertRaises(AssertionError, recvline.TransportSequence)
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the command-line interfaces to conch.
|
||||
"""
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.python.test.test_shellcomp import ZshScriptTestMixin
|
||||
from twisted.scripts.test.test_scripts import ScriptTestsMixin
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
doSkip = False
|
||||
skipReason = ""
|
||||
|
||||
if not requireModule("cryptography"):
|
||||
doSkip = True
|
||||
cryptoSkip = "can't run w/o cryptography"
|
||||
|
||||
if not requireModule("tty"):
|
||||
doSkip = True
|
||||
ttySkip = "can't run w/o tty"
|
||||
|
||||
try:
|
||||
import tkinter
|
||||
except ImportError:
|
||||
doSkip = True
|
||||
skipReason = "can't run w/o tkinter"
|
||||
else:
|
||||
try:
|
||||
tkinter.Tk().destroy()
|
||||
except (tkinter.TclError, RuntimeError) as e:
|
||||
# On GitHub Action the macOS Python might not support the version of TK
|
||||
# provided by the OS and it will raise a RuntimeError
|
||||
# See: https://github.com/actions/setup-python/issues/649
|
||||
doSkip = True
|
||||
skipReason = "Can't test Tkinter: " + str(e)
|
||||
|
||||
|
||||
@skipIf(doSkip, skipReason)
|
||||
class ScriptTests(TestCase, ScriptTestsMixin):
|
||||
"""
|
||||
Tests for the Conch scripts.
|
||||
"""
|
||||
|
||||
def test_conch(self) -> None:
|
||||
self.scriptTest("conch/conch")
|
||||
|
||||
def test_cftp(self) -> None:
|
||||
self.scriptTest("conch/cftp")
|
||||
|
||||
def test_ckeygen(self) -> None:
|
||||
self.scriptTest("conch/ckeygen")
|
||||
|
||||
def test_tkconch(self) -> None:
|
||||
self.scriptTest("conch/tkconch")
|
||||
|
||||
|
||||
class ZshIntegrationTests(TestCase, ZshScriptTestMixin):
|
||||
"""
|
||||
Test that zsh completion functions are generated without error
|
||||
"""
|
||||
|
||||
generateFor = [
|
||||
("conch", "twisted.conch.scripts.conch.ClientOptions"),
|
||||
("cftp", "twisted.conch.scripts.cftp.ClientOptions"),
|
||||
("ckeygen", "twisted.conch.scripts.ckeygen.GeneralOptions"),
|
||||
("tkconch", "twisted.conch.scripts.tkconch.GeneralOptions"),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
1041
backend/lib/python3.12/site-packages/twisted/conch/test/test_ssh.py
Normal file
1041
backend/lib/python3.12/site-packages/twisted/conch/test/test_ssh.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.tap}.
|
||||
"""
|
||||
|
||||
from typing import Any, Tuple, Union
|
||||
|
||||
from twisted.application.internet import StreamServerEndpointService
|
||||
from twisted.cred import error
|
||||
from twisted.cred.checkers import FilePasswordDB, ICredentialsChecker
|
||||
from twisted.cred.credentials import ISSHPrivateKey, IUsernamePassword, UsernamePassword
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
unix = requireModule("twisted.conch.unix")
|
||||
|
||||
|
||||
if cryptography and unix:
|
||||
from twisted.conch import tap
|
||||
from twisted.conch.openssh_compat.factory import OpenSSHFactory
|
||||
|
||||
|
||||
class MakeServiceTests(TestCase):
|
||||
"""
|
||||
Tests for L{tap.makeService}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not unix:
|
||||
skip = "can't run on non-posix computers"
|
||||
|
||||
usernamePassword = (b"iamuser", b"thisispassword")
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create a file with two users.
|
||||
"""
|
||||
self.filename = self.mktemp()
|
||||
with open(self.filename, "wb+") as f:
|
||||
f.write(b":".join(self.usernamePassword))
|
||||
self.options = tap.Options()
|
||||
|
||||
def test_basic(self) -> None:
|
||||
"""
|
||||
L{tap.makeService} returns a L{StreamServerEndpointService} instance
|
||||
running on TCP port 22, and the linked protocol factory is an instance
|
||||
of L{OpenSSHFactory}.
|
||||
"""
|
||||
config = tap.Options()
|
||||
service = tap.makeService(config)
|
||||
self.assertIsInstance(service, StreamServerEndpointService)
|
||||
self.assertEqual(service.endpoint._port, 22)
|
||||
self.assertIsInstance(service.factory, OpenSSHFactory)
|
||||
|
||||
def test_defaultAuths(self) -> None:
|
||||
"""
|
||||
Make sure that if the C{--auth} command-line option is not passed,
|
||||
the default checkers are (for backwards compatibility): SSH and UNIX
|
||||
"""
|
||||
numCheckers = 2
|
||||
|
||||
self.assertIn(
|
||||
ISSHPrivateKey,
|
||||
self.options["credInterfaces"],
|
||||
"SSH should be one of the default checkers",
|
||||
)
|
||||
self.assertIn(
|
||||
IUsernamePassword,
|
||||
self.options["credInterfaces"],
|
||||
"UNIX should be one of the default checkers",
|
||||
)
|
||||
self.assertEqual(
|
||||
numCheckers,
|
||||
len(self.options["credCheckers"]),
|
||||
"There should be %d checkers by default" % (numCheckers,),
|
||||
)
|
||||
|
||||
def test_authAdded(self) -> None:
|
||||
"""
|
||||
The C{--auth} command-line option will add a checker to the list of
|
||||
checkers, and it should be the only auth checker
|
||||
"""
|
||||
self.options.parseOptions(["--auth", "file:" + self.filename])
|
||||
self.assertEqual(len(self.options["credCheckers"]), 1)
|
||||
|
||||
def test_multipleAuthAdded(self) -> None:
|
||||
"""
|
||||
Multiple C{--auth} command-line options will add all checkers specified
|
||||
to the list ofcheckers, and there should only be the specified auth
|
||||
checkers (no default checkers).
|
||||
"""
|
||||
self.options.parseOptions(
|
||||
[
|
||||
"--auth",
|
||||
"file:" + self.filename,
|
||||
"--auth",
|
||||
"memory:testuser:testpassword",
|
||||
]
|
||||
)
|
||||
self.assertEqual(len(self.options["credCheckers"]), 2)
|
||||
|
||||
def test_authFailure(self) -> Any:
|
||||
"""
|
||||
The checker created by the C{--auth} command-line option returns a
|
||||
L{Deferred} that fails with L{UnauthorizedLogin} when
|
||||
presented with credentials that are unknown to that checker.
|
||||
"""
|
||||
self.options.parseOptions(["--auth", "file:" + self.filename])
|
||||
checker: FilePasswordDB = self.options["credCheckers"][-1]
|
||||
self.assertIsInstance(checker, FilePasswordDB)
|
||||
invalid = UsernamePassword(self.usernamePassword[0], b"fake")
|
||||
# Wrong password should raise error
|
||||
return self.assertFailure(
|
||||
checker.requestAvatarId(invalid), error.UnauthorizedLogin
|
||||
)
|
||||
|
||||
def test_authSuccess(self) -> Deferred[None]:
|
||||
"""
|
||||
The checker created by the C{--auth} command-line option returns a
|
||||
L{Deferred} that returns the avatar id when presented with credentials
|
||||
that are known to that checker.
|
||||
"""
|
||||
self.options.parseOptions(["--auth", "file:" + self.filename])
|
||||
checker: ICredentialsChecker = self.options["credCheckers"][-1]
|
||||
correct = UsernamePassword(*self.usernamePassword)
|
||||
d = checker.requestAvatarId(correct)
|
||||
|
||||
def checkSuccess(username: Union[bytes, Tuple[()]]) -> None:
|
||||
self.assertEqual(username, correct.username)
|
||||
|
||||
return d.addCallback(checkSuccess)
|
||||
|
||||
def test_checkers(self) -> None:
|
||||
"""
|
||||
The L{OpenSSHFactory} built by L{tap.makeService} has a portal with
|
||||
L{ISSHPrivateKey} and L{IUsernamePassword} interfaces registered as
|
||||
checkers.
|
||||
"""
|
||||
config = tap.Options()
|
||||
service = tap.makeService(config)
|
||||
portal = service.factory.portal
|
||||
self.assertEqual(
|
||||
set(portal.checkers.keys()), {ISSHPrivateKey, IUsernamePassword}
|
||||
)
|
||||
@@ -0,0 +1,778 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_telnet -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.telnet}.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.conch import telnet
|
||||
from twisted.internet import defer
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
@implementer(telnet.ITelnetProtocol)
|
||||
class TestProtocol:
|
||||
localEnableable = ()
|
||||
remoteEnableable = ()
|
||||
|
||||
def __init__(self):
|
||||
self.data = b""
|
||||
self.subcmd = []
|
||||
self.calls = []
|
||||
|
||||
self.enabledLocal = []
|
||||
self.enabledRemote = []
|
||||
self.disabledLocal = []
|
||||
self.disabledRemote = []
|
||||
|
||||
def makeConnection(self, transport):
|
||||
d = transport.negotiationMap = {}
|
||||
d[b"\x12"] = self.neg_TEST_COMMAND
|
||||
|
||||
d = transport.commandMap = transport.commandMap.copy()
|
||||
for cmd in ("EOR", "NOP", "DM", "BRK", "IP", "AO", "AYT", "EC", "EL", "GA"):
|
||||
d[getattr(telnet, cmd)] = lambda arg, cmd=cmd: self.calls.append(cmd)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.data += data
|
||||
|
||||
def connectionLost(self, reason):
|
||||
pass
|
||||
|
||||
def neg_TEST_COMMAND(self, payload):
|
||||
self.subcmd = payload
|
||||
|
||||
def enableLocal(self, option):
|
||||
if option in self.localEnableable:
|
||||
self.enabledLocal.append(option)
|
||||
return True
|
||||
return False
|
||||
|
||||
def disableLocal(self, option):
|
||||
self.disabledLocal.append(option)
|
||||
|
||||
def enableRemote(self, option):
|
||||
if option in self.remoteEnableable:
|
||||
self.enabledRemote.append(option)
|
||||
return True
|
||||
return False
|
||||
|
||||
def disableRemote(self, option):
|
||||
self.disabledRemote.append(option)
|
||||
|
||||
def connectionMade(self):
|
||||
# IProtocol.connectionMade
|
||||
pass
|
||||
|
||||
def unhandledCommand(self, command, argument):
|
||||
# ITelnetProtocol.unhandledCommand
|
||||
pass
|
||||
|
||||
def unhandledSubnegotiation(self, command, data):
|
||||
# ITelnetProtocol.unhandledSubnegotiation
|
||||
pass
|
||||
|
||||
|
||||
class InterfacesTests(unittest.TestCase):
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{telnet.TelnetProtocol} implements L{telnet.ITelnetProtocol}
|
||||
"""
|
||||
p = telnet.TelnetProtocol()
|
||||
verifyObject(telnet.ITelnetProtocol, p)
|
||||
|
||||
|
||||
class TelnetTransportTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{telnet.TelnetTransport}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.p = telnet.TelnetTransport(TestProtocol)
|
||||
self.t = proto_helpers.StringTransport()
|
||||
self.p.makeConnection(self.t)
|
||||
|
||||
def testRegularBytes(self):
|
||||
# Just send a bunch of bytes. None of these do anything
|
||||
# with telnet. They should pass right through to the
|
||||
# application layer.
|
||||
h = self.p.protocol
|
||||
|
||||
L = [
|
||||
b"here are some bytes la la la",
|
||||
b"some more arrive here",
|
||||
b"lots of bytes to play with",
|
||||
b"la la la",
|
||||
b"ta de da",
|
||||
b"dum",
|
||||
]
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(h.data, b"".join(L))
|
||||
|
||||
def testNewlineHandling(self):
|
||||
# Send various kinds of newlines and make sure they get translated
|
||||
# into \n.
|
||||
h = self.p.protocol
|
||||
|
||||
L = [
|
||||
b"here is the first line\r\n",
|
||||
b"here is the second line\r\0",
|
||||
b"here is the third line\r\n",
|
||||
b"here is the last line\r\0",
|
||||
]
|
||||
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(
|
||||
h.data,
|
||||
L[0][:-2]
|
||||
+ b"\n"
|
||||
+ L[1][:-2]
|
||||
+ b"\r"
|
||||
+ L[2][:-2]
|
||||
+ b"\n"
|
||||
+ L[3][:-2]
|
||||
+ b"\r",
|
||||
)
|
||||
|
||||
def testIACEscape(self):
|
||||
# Send a bunch of bytes and a couple quoted \xFFs. Unquoted,
|
||||
# \xFF is a telnet command. Quoted, one of them from each pair
|
||||
# should be passed through to the application layer.
|
||||
h = self.p.protocol
|
||||
|
||||
L = [
|
||||
b"here are some bytes\xff\xff with an embedded IAC",
|
||||
b"and here is a test of a border escape\xff",
|
||||
b"\xff did you get that IAC?",
|
||||
]
|
||||
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(h.data, b"".join(L).replace(b"\xff\xff", b"\xff"))
|
||||
|
||||
def _simpleCommandTest(self, cmdName):
|
||||
# Send a single simple telnet command and make sure
|
||||
# it gets noticed and the appropriate method gets
|
||||
# called.
|
||||
h = self.p.protocol
|
||||
|
||||
cmd = telnet.IAC + getattr(telnet, cmdName)
|
||||
L = [b"Here's some bytes, tra la la", b"But ono!" + cmd + b" an interrupt"]
|
||||
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(h.calls, [cmdName])
|
||||
self.assertEqual(h.data, b"".join(L).replace(cmd, b""))
|
||||
|
||||
def testInterrupt(self):
|
||||
self._simpleCommandTest("IP")
|
||||
|
||||
def testEndOfRecord(self):
|
||||
self._simpleCommandTest("EOR")
|
||||
|
||||
def testNoOperation(self):
|
||||
self._simpleCommandTest("NOP")
|
||||
|
||||
def testDataMark(self):
|
||||
self._simpleCommandTest("DM")
|
||||
|
||||
def testBreak(self):
|
||||
self._simpleCommandTest("BRK")
|
||||
|
||||
def testAbortOutput(self):
|
||||
self._simpleCommandTest("AO")
|
||||
|
||||
def testAreYouThere(self):
|
||||
self._simpleCommandTest("AYT")
|
||||
|
||||
def testEraseCharacter(self):
|
||||
self._simpleCommandTest("EC")
|
||||
|
||||
def testEraseLine(self):
|
||||
self._simpleCommandTest("EL")
|
||||
|
||||
def testGoAhead(self):
|
||||
self._simpleCommandTest("GA")
|
||||
|
||||
def testSubnegotiation(self):
|
||||
# Send a subnegotiation command and make sure it gets
|
||||
# parsed and that the correct method is called.
|
||||
h = self.p.protocol
|
||||
|
||||
cmd = telnet.IAC + telnet.SB + b"\x12hello world" + telnet.IAC + telnet.SE
|
||||
L = [b"These are some bytes but soon" + cmd, b"there will be some more"]
|
||||
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(h.data, b"".join(L).replace(cmd, b""))
|
||||
self.assertEqual(h.subcmd, list(iterbytes(b"hello world")))
|
||||
|
||||
def testSubnegotiationWithEmbeddedSE(self):
|
||||
# Send a subnegotiation command with an embedded SE. Make sure
|
||||
# that SE gets passed to the correct method.
|
||||
h = self.p.protocol
|
||||
|
||||
cmd = telnet.IAC + telnet.SB + b"\x12" + telnet.SE + telnet.IAC + telnet.SE
|
||||
|
||||
L = [b"Some bytes are here" + cmd + b"and here", b"and here"]
|
||||
|
||||
for b in L:
|
||||
self.p.dataReceived(b)
|
||||
|
||||
self.assertEqual(h.data, b"".join(L).replace(cmd, b""))
|
||||
self.assertEqual(h.subcmd, [telnet.SE])
|
||||
|
||||
def testBoundarySubnegotiation(self):
|
||||
# Send a subnegotiation command. Split it at every possible byte boundary
|
||||
# and make sure it always gets parsed and that it is passed to the correct
|
||||
# method.
|
||||
cmd = (
|
||||
telnet.IAC
|
||||
+ telnet.SB
|
||||
+ b"\x12"
|
||||
+ telnet.SE
|
||||
+ b"hello"
|
||||
+ telnet.IAC
|
||||
+ telnet.SE
|
||||
)
|
||||
|
||||
for i in range(len(cmd)):
|
||||
h = self.p.protocol = TestProtocol()
|
||||
h.makeConnection(self.p)
|
||||
|
||||
a, b = cmd[:i], cmd[i:]
|
||||
L = [b"first part" + a, b + b"last part"]
|
||||
|
||||
for data in L:
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(h.data, b"".join(L).replace(cmd, b""))
|
||||
self.assertEqual(h.subcmd, [telnet.SE] + list(iterbytes(b"hello")))
|
||||
|
||||
def _enabledHelper(self, o, eL=[], eR=[], dL=[], dR=[]):
|
||||
self.assertEqual(o.enabledLocal, eL)
|
||||
self.assertEqual(o.enabledRemote, eR)
|
||||
self.assertEqual(o.disabledLocal, dL)
|
||||
self.assertEqual(o.disabledRemote, dR)
|
||||
|
||||
def testRefuseWill(self):
|
||||
# Try to enable an option. The server should refuse to enable it.
|
||||
cmd = telnet.IAC + telnet.WILL + b"\x12"
|
||||
|
||||
data = b"surrounding bytes" + cmd + b"to spice things up"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + b"\x12")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testRefuseDo(self):
|
||||
# Try to enable an option. The server should refuse to enable it.
|
||||
cmd = telnet.IAC + telnet.DO + b"\x12"
|
||||
|
||||
data = b"surrounding bytes" + cmd + b"to spice things up"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.WONT + b"\x12")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testAcceptDo(self):
|
||||
# Try to enable an option. The option is in our allowEnable
|
||||
# list, so we will allow it to be enabled.
|
||||
cmd = telnet.IAC + telnet.DO + b"\x19"
|
||||
data = b"padding" + cmd + b"trailer"
|
||||
|
||||
h = self.p.protocol
|
||||
h.localEnableable = (b"\x19",)
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.WILL + b"\x19")
|
||||
self._enabledHelper(h, eL=[b"\x19"])
|
||||
|
||||
def testAcceptWill(self):
|
||||
# Same as testAcceptDo, but reversed.
|
||||
cmd = telnet.IAC + telnet.WILL + b"\x91"
|
||||
data = b"header" + cmd + b"padding"
|
||||
|
||||
h = self.p.protocol
|
||||
h.remoteEnableable = (b"\x91",)
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + b"\x91")
|
||||
self._enabledHelper(h, eR=[b"\x91"])
|
||||
|
||||
def testAcceptWont(self):
|
||||
# Try to disable an option. The server must allow any option to
|
||||
# be disabled at any time. Make sure it disables it and sends
|
||||
# back an acknowledgement of this.
|
||||
cmd = telnet.IAC + telnet.WONT + b"\x29"
|
||||
|
||||
# Jimmy it - after these two lines, the server will be in a state
|
||||
# such that it believes the option to have been previously enabled
|
||||
# via normal negotiation.
|
||||
s = self.p.getOptionState(b"\x29")
|
||||
s.him.state = "yes"
|
||||
|
||||
data = b"fiddle dee" + cmd
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + b"\x29")
|
||||
self.assertEqual(s.him.state, "no")
|
||||
self._enabledHelper(self.p.protocol, dR=[b"\x29"])
|
||||
|
||||
def testAcceptDont(self):
|
||||
# Try to disable an option. The server must allow any option to
|
||||
# be disabled at any time. Make sure it disables it and sends
|
||||
# back an acknowledgement of this.
|
||||
cmd = telnet.IAC + telnet.DONT + b"\x29"
|
||||
|
||||
# Jimmy it - after these two lines, the server will be in a state
|
||||
# such that it believes the option to have beenp previously enabled
|
||||
# via normal negotiation.
|
||||
s = self.p.getOptionState(b"\x29")
|
||||
s.us.state = "yes"
|
||||
|
||||
data = b"fiddle dum " + cmd
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.WONT + b"\x29")
|
||||
self.assertEqual(s.us.state, "no")
|
||||
self._enabledHelper(self.p.protocol, dL=[b"\x29"])
|
||||
|
||||
def testIgnoreWont(self):
|
||||
# Try to disable an option. The option is already disabled. The
|
||||
# server should send nothing in response to this.
|
||||
cmd = telnet.IAC + telnet.WONT + b"\x47"
|
||||
|
||||
data = b"dum de dum" + cmd + b"tra la la"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), b"")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testIgnoreDont(self):
|
||||
# Try to disable an option. The option is already disabled. The
|
||||
# server should send nothing in response to this. Doing so could
|
||||
# lead to a negotiation loop.
|
||||
cmd = telnet.IAC + telnet.DONT + b"\x47"
|
||||
|
||||
data = b"dum de dum" + cmd + b"tra la la"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), b"")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testIgnoreWill(self):
|
||||
# Try to enable an option. The option is already enabled. The
|
||||
# server should send nothing in response to this. Doing so could
|
||||
# lead to a negotiation loop.
|
||||
cmd = telnet.IAC + telnet.WILL + b"\x56"
|
||||
|
||||
# Jimmy it - after these two lines, the server will be in a state
|
||||
# such that it believes the option to have been previously enabled
|
||||
# via normal negotiation.
|
||||
s = self.p.getOptionState(b"\x56")
|
||||
s.him.state = "yes"
|
||||
|
||||
data = b"tra la la" + cmd + b"dum de dum"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), b"")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testIgnoreDo(self):
|
||||
# Try to enable an option. The option is already enabled. The
|
||||
# server should send nothing in response to this. Doing so could
|
||||
# lead to a negotiation loop.
|
||||
cmd = telnet.IAC + telnet.DO + b"\x56"
|
||||
|
||||
# Jimmy it - after these two lines, the server will be in a state
|
||||
# such that it believes the option to have been previously enabled
|
||||
# via normal negotiation.
|
||||
s = self.p.getOptionState(b"\x56")
|
||||
s.us.state = "yes"
|
||||
|
||||
data = b"tra la la" + cmd + b"dum de dum"
|
||||
self.p.dataReceived(data)
|
||||
|
||||
self.assertEqual(self.p.protocol.data, data.replace(cmd, b""))
|
||||
self.assertEqual(self.t.value(), b"")
|
||||
self._enabledHelper(self.p.protocol)
|
||||
|
||||
def testAcceptedEnableRequest(self):
|
||||
# Try to enable an option through the user-level API. This
|
||||
# returns a Deferred that fires when negotiation about the option
|
||||
# finishes. Make sure it fires, make sure state gets updated
|
||||
# properly, make sure the result indicates the option was enabled.
|
||||
d = self.p.do(b"\x42")
|
||||
|
||||
h = self.p.protocol
|
||||
h.remoteEnableable = (b"\x42",)
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + b"\x42")
|
||||
|
||||
self.p.dataReceived(telnet.IAC + telnet.WILL + b"\x42")
|
||||
|
||||
d.addCallback(self.assertEqual, True)
|
||||
d.addCallback(lambda _: self._enabledHelper(h, eR=[b"\x42"]))
|
||||
return d
|
||||
|
||||
def test_refusedEnableRequest(self):
|
||||
"""
|
||||
If the peer refuses to enable an option we request it to enable, the
|
||||
L{Deferred} returned by L{TelnetProtocol.do} fires with an
|
||||
L{OptionRefused} L{Failure}.
|
||||
"""
|
||||
# Try to enable an option through the user-level API. This returns a
|
||||
# Deferred that fires when negotiation about the option finishes. Make
|
||||
# sure it fires, make sure state gets updated properly, make sure the
|
||||
# result indicates the option was enabled.
|
||||
self.p.protocol.remoteEnableable = (b"\x42",)
|
||||
d = self.p.do(b"\x42")
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + b"\x42")
|
||||
|
||||
s = self.p.getOptionState(b"\x42")
|
||||
self.assertEqual(s.him.state, "no")
|
||||
self.assertEqual(s.us.state, "no")
|
||||
self.assertTrue(s.him.negotiating)
|
||||
self.assertFalse(s.us.negotiating)
|
||||
|
||||
self.p.dataReceived(telnet.IAC + telnet.WONT + b"\x42")
|
||||
|
||||
d = self.assertFailure(d, telnet.OptionRefused)
|
||||
d.addCallback(lambda ignored: self._enabledHelper(self.p.protocol))
|
||||
d.addCallback(lambda ignored: self.assertFalse(s.him.negotiating))
|
||||
return d
|
||||
|
||||
def test_refusedEnableOffer(self):
|
||||
"""
|
||||
If the peer refuses to allow us to enable an option, the L{Deferred}
|
||||
returned by L{TelnetProtocol.will} fires with an L{OptionRefused}
|
||||
L{Failure}.
|
||||
"""
|
||||
# Try to offer an option through the user-level API. This returns a
|
||||
# Deferred that fires when negotiation about the option finishes. Make
|
||||
# sure it fires, make sure state gets updated properly, make sure the
|
||||
# result indicates the option was enabled.
|
||||
self.p.protocol.localEnableable = (b"\x42",)
|
||||
d = self.p.will(b"\x42")
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.WILL + b"\x42")
|
||||
|
||||
s = self.p.getOptionState(b"\x42")
|
||||
self.assertEqual(s.him.state, "no")
|
||||
self.assertEqual(s.us.state, "no")
|
||||
self.assertFalse(s.him.negotiating)
|
||||
self.assertTrue(s.us.negotiating)
|
||||
|
||||
self.p.dataReceived(telnet.IAC + telnet.DONT + b"\x42")
|
||||
|
||||
d = self.assertFailure(d, telnet.OptionRefused)
|
||||
d.addCallback(lambda ignored: self._enabledHelper(self.p.protocol))
|
||||
d.addCallback(lambda ignored: self.assertFalse(s.us.negotiating))
|
||||
return d
|
||||
|
||||
def testAcceptedDisableRequest(self):
|
||||
# Try to disable an option through the user-level API. This
|
||||
# returns a Deferred that fires when negotiation about the option
|
||||
# finishes. Make sure it fires, make sure state gets updated
|
||||
# properly, make sure the result indicates the option was enabled.
|
||||
s = self.p.getOptionState(b"\x42")
|
||||
s.him.state = "yes"
|
||||
|
||||
d = self.p.dont(b"\x42")
|
||||
|
||||
self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + b"\x42")
|
||||
|
||||
self.p.dataReceived(telnet.IAC + telnet.WONT + b"\x42")
|
||||
|
||||
d.addCallback(self.assertEqual, True)
|
||||
d.addCallback(lambda _: self._enabledHelper(self.p.protocol, dR=[b"\x42"]))
|
||||
return d
|
||||
|
||||
def testNegotiationBlocksFurtherNegotiation(self):
|
||||
# Try to disable an option, then immediately try to enable it, then
|
||||
# immediately try to disable it. Ensure that the 2nd and 3rd calls
|
||||
# fail quickly with the right exception.
|
||||
s = self.p.getOptionState(b"\x24")
|
||||
s.him.state = "yes"
|
||||
self.p.dont(b"\x24") # fires after the first line of _final
|
||||
|
||||
def _do(x):
|
||||
d = self.p.do(b"\x24")
|
||||
return self.assertFailure(d, telnet.AlreadyNegotiating)
|
||||
|
||||
def _dont(x):
|
||||
d = self.p.dont(b"\x24")
|
||||
return self.assertFailure(d, telnet.AlreadyNegotiating)
|
||||
|
||||
def _final(x):
|
||||
self.p.dataReceived(telnet.IAC + telnet.WONT + b"\x24")
|
||||
# an assertion that only passes if d2 has fired
|
||||
self._enabledHelper(self.p.protocol, dR=[b"\x24"])
|
||||
# Make sure we allow this
|
||||
self.p.protocol.remoteEnableable = (b"\x24",)
|
||||
d = self.p.do(b"\x24")
|
||||
self.p.dataReceived(telnet.IAC + telnet.WILL + b"\x24")
|
||||
d.addCallback(self.assertEqual, True)
|
||||
d.addCallback(
|
||||
lambda _: self._enabledHelper(
|
||||
self.p.protocol, eR=[b"\x24"], dR=[b"\x24"]
|
||||
)
|
||||
)
|
||||
return d
|
||||
|
||||
d = _do(None)
|
||||
d.addCallback(_dont)
|
||||
d.addCallback(_final)
|
||||
return d
|
||||
|
||||
def testSuperfluousDisableRequestRaises(self):
|
||||
# Try to disable a disabled option. Make sure it fails properly.
|
||||
d = self.p.dont(b"\xab")
|
||||
return self.assertFailure(d, telnet.AlreadyDisabled)
|
||||
|
||||
def testSuperfluousEnableRequestRaises(self):
|
||||
# Try to disable a disabled option. Make sure it fails properly.
|
||||
s = self.p.getOptionState(b"\xab")
|
||||
s.him.state = "yes"
|
||||
d = self.p.do(b"\xab")
|
||||
return self.assertFailure(d, telnet.AlreadyEnabled)
|
||||
|
||||
def testLostConnectionFailsDeferreds(self):
|
||||
d1 = self.p.do(b"\x12")
|
||||
d2 = self.p.do(b"\x23")
|
||||
d3 = self.p.do(b"\x34")
|
||||
|
||||
class TestException(Exception):
|
||||
pass
|
||||
|
||||
self.p.connectionLost(TestException("Total failure!"))
|
||||
|
||||
d1 = self.assertFailure(d1, TestException)
|
||||
d2 = self.assertFailure(d2, TestException)
|
||||
d3 = self.assertFailure(d3, TestException)
|
||||
return defer.gatherResults([d1, d2, d3])
|
||||
|
||||
|
||||
class TestTelnet(telnet.Telnet):
|
||||
"""
|
||||
A trivial extension of the telnet protocol class useful to unit tests.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
telnet.Telnet.__init__(self)
|
||||
self.events = []
|
||||
|
||||
def applicationDataReceived(self, data):
|
||||
"""
|
||||
Record the given data in C{self.events}.
|
||||
"""
|
||||
self.events.append(("bytes", data))
|
||||
|
||||
def unhandledCommand(self, command, data):
|
||||
"""
|
||||
Record the given command in C{self.events}.
|
||||
"""
|
||||
self.events.append(("command", command, data))
|
||||
|
||||
def unhandledSubnegotiation(self, command, data):
|
||||
"""
|
||||
Record the given subnegotiation command in C{self.events}.
|
||||
"""
|
||||
self.events.append(("negotiate", command, data))
|
||||
|
||||
|
||||
class TelnetTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{telnet.Telnet}.
|
||||
|
||||
L{telnet.Telnet} implements the TELNET protocol (RFC 854), including option
|
||||
and suboption negotiation, and option state tracking.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an unconnected L{telnet.Telnet} to be used by tests.
|
||||
"""
|
||||
self.protocol = TestTelnet()
|
||||
|
||||
def test_enableLocal(self):
|
||||
"""
|
||||
L{telnet.Telnet.enableLocal} should reject all options, since
|
||||
L{telnet.Telnet} does not know how to implement any options.
|
||||
"""
|
||||
self.assertFalse(self.protocol.enableLocal(b"\0"))
|
||||
|
||||
def test_enableRemote(self):
|
||||
"""
|
||||
L{telnet.Telnet.enableRemote} should reject all options, since
|
||||
L{telnet.Telnet} does not know how to implement any options.
|
||||
"""
|
||||
self.assertFalse(self.protocol.enableRemote(b"\0"))
|
||||
|
||||
def test_disableLocal(self):
|
||||
"""
|
||||
It is an error for L{telnet.Telnet.disableLocal} to be called, since
|
||||
L{telnet.Telnet.enableLocal} will never allow any options to be enabled
|
||||
locally. If a subclass overrides enableLocal, it must also override
|
||||
disableLocal.
|
||||
"""
|
||||
self.assertRaises(NotImplementedError, self.protocol.disableLocal, b"\0")
|
||||
|
||||
def test_disableRemote(self):
|
||||
"""
|
||||
It is an error for L{telnet.Telnet.disableRemote} to be called, since
|
||||
L{telnet.Telnet.enableRemote} will never allow any options to be
|
||||
enabled remotely. If a subclass overrides enableRemote, it must also
|
||||
override disableRemote.
|
||||
"""
|
||||
self.assertRaises(NotImplementedError, self.protocol.disableRemote, b"\0")
|
||||
|
||||
def test_requestNegotiation(self):
|
||||
"""
|
||||
L{telnet.Telnet.requestNegotiation} formats the feature byte and the
|
||||
payload bytes into the subnegotiation format and sends them.
|
||||
|
||||
See RFC 855.
|
||||
"""
|
||||
transport = proto_helpers.StringTransport()
|
||||
self.protocol.makeConnection(transport)
|
||||
self.protocol.requestNegotiation(b"\x01", b"\x02\x03")
|
||||
self.assertEqual(
|
||||
transport.value(),
|
||||
# IAC SB feature bytes IAC SE
|
||||
b"\xff\xfa\x01\x02\x03\xff\xf0",
|
||||
)
|
||||
|
||||
def test_requestNegotiationEscapesIAC(self):
|
||||
"""
|
||||
If the payload for a subnegotiation includes I{IAC}, it is escaped by
|
||||
L{telnet.Telnet.requestNegotiation} with another I{IAC}.
|
||||
|
||||
See RFC 855.
|
||||
"""
|
||||
transport = proto_helpers.StringTransport()
|
||||
self.protocol.makeConnection(transport)
|
||||
self.protocol.requestNegotiation(b"\x01", b"\xff")
|
||||
self.assertEqual(transport.value(), b"\xff\xfa\x01\xff\xff\xff\xf0")
|
||||
|
||||
def _deliver(self, data, *expected):
|
||||
"""
|
||||
Pass the given bytes to the protocol's C{dataReceived} method and
|
||||
assert that the given events occur.
|
||||
"""
|
||||
received = self.protocol.events = []
|
||||
self.protocol.dataReceived(data)
|
||||
self.assertEqual(received, list(expected))
|
||||
|
||||
def test_oneApplicationDataByte(self):
|
||||
"""
|
||||
One application-data byte in the default state gets delivered right
|
||||
away.
|
||||
"""
|
||||
self._deliver(b"a", ("bytes", b"a"))
|
||||
|
||||
def test_twoApplicationDataBytes(self):
|
||||
"""
|
||||
Two application-data bytes in the default state get delivered
|
||||
together.
|
||||
"""
|
||||
self._deliver(b"bc", ("bytes", b"bc"))
|
||||
|
||||
def test_threeApplicationDataBytes(self):
|
||||
"""
|
||||
Three application-data bytes followed by a control byte get
|
||||
delivered, but the control byte doesn't.
|
||||
"""
|
||||
self._deliver(b"def" + telnet.IAC, ("bytes", b"def"))
|
||||
|
||||
def test_escapedControl(self):
|
||||
"""
|
||||
IAC in the escaped state gets delivered and so does another
|
||||
application-data byte following it.
|
||||
"""
|
||||
self._deliver(telnet.IAC)
|
||||
self._deliver(telnet.IAC + b"g", ("bytes", telnet.IAC + b"g"))
|
||||
|
||||
def test_carriageReturn(self):
|
||||
"""
|
||||
A carriage return only puts the protocol into the newline state. A
|
||||
linefeed in the newline state causes just the newline to be
|
||||
delivered. A nul in the newline state causes a carriage return to
|
||||
be delivered. An IAC in the newline state causes a carriage return
|
||||
to be delivered and puts the protocol into the escaped state.
|
||||
Anything else causes a carriage return and that thing to be
|
||||
delivered.
|
||||
"""
|
||||
self._deliver(b"\r")
|
||||
self._deliver(b"\n", ("bytes", b"\n"))
|
||||
self._deliver(b"\r\n", ("bytes", b"\n"))
|
||||
|
||||
self._deliver(b"\r")
|
||||
self._deliver(b"\0", ("bytes", b"\r"))
|
||||
self._deliver(b"\r\0", ("bytes", b"\r"))
|
||||
|
||||
self._deliver(b"\r")
|
||||
self._deliver(b"a", ("bytes", b"\ra"))
|
||||
self._deliver(b"\ra", ("bytes", b"\ra"))
|
||||
|
||||
self._deliver(b"\r")
|
||||
self._deliver(
|
||||
telnet.IAC + telnet.IAC + b"x", ("bytes", b"\r" + telnet.IAC + b"x")
|
||||
)
|
||||
|
||||
def test_applicationDataBeforeSimpleCommand(self):
|
||||
"""
|
||||
Application bytes received before a command are delivered before the
|
||||
command is processed.
|
||||
"""
|
||||
self._deliver(
|
||||
b"x" + telnet.IAC + telnet.NOP,
|
||||
("bytes", b"x"),
|
||||
("command", telnet.NOP, None),
|
||||
)
|
||||
|
||||
def test_applicationDataBeforeCommand(self):
|
||||
"""
|
||||
Application bytes received before a WILL/WONT/DO/DONT are delivered
|
||||
before the command is processed.
|
||||
"""
|
||||
self.protocol.commandMap = {}
|
||||
self._deliver(
|
||||
b"y" + telnet.IAC + telnet.WILL + b"\x00",
|
||||
("bytes", b"y"),
|
||||
("command", telnet.WILL, b"\x00"),
|
||||
)
|
||||
|
||||
def test_applicationDataBeforeSubnegotiation(self):
|
||||
"""
|
||||
Application bytes received before a subnegotiation command are
|
||||
delivered before the negotiation is processed.
|
||||
"""
|
||||
self._deliver(
|
||||
b"z" + telnet.IAC + telnet.SB + b"Qx" + telnet.IAC + telnet.SE,
|
||||
("bytes", b"z"),
|
||||
("negotiate", b"Q", [b"x"]),
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_text -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.conch.insults import text
|
||||
from twisted.conch.insults.text import attributes as A
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class FormattedTextTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for assembling formatted text.
|
||||
"""
|
||||
|
||||
def test_trivial(self) -> None:
|
||||
"""
|
||||
Using no formatting attributes produces no VT102 control sequences in
|
||||
the flattened output.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(A.normal["Hello, world."]), "Hello, world."
|
||||
)
|
||||
|
||||
def test_bold(self) -> None:
|
||||
"""
|
||||
The bold formatting attribute, L{A.bold}, emits the VT102 control
|
||||
sequence to enable bold when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(A.bold["Hello, world."]), "\x1b[1mHello, world."
|
||||
)
|
||||
|
||||
def test_underline(self) -> None:
|
||||
"""
|
||||
The underline formatting attribute, L{A.underline}, emits the VT102
|
||||
control sequence to enable underlining when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(A.underline["Hello, world."]),
|
||||
"\x1b[4mHello, world.",
|
||||
)
|
||||
|
||||
def test_blink(self) -> None:
|
||||
"""
|
||||
The blink formatting attribute, L{A.blink}, emits the VT102 control
|
||||
sequence to enable blinking when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(A.blink["Hello, world."]), "\x1b[5mHello, world."
|
||||
)
|
||||
|
||||
def test_reverseVideo(self) -> None:
|
||||
"""
|
||||
The reverse-video formatting attribute, L{A.reverseVideo}, emits the
|
||||
VT102 control sequence to enable reversed video when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(A.reverseVideo["Hello, world."]),
|
||||
"\x1b[7mHello, world.",
|
||||
)
|
||||
|
||||
def test_minus(self) -> None:
|
||||
"""
|
||||
Formatting attributes prefixed with a minus (C{-}) temporarily disable
|
||||
the prefixed attribute, emitting no VT102 control sequence to enable
|
||||
it in the flattened output.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(
|
||||
A.bold[A.blink["Hello", -A.bold[" world"], "."]]
|
||||
),
|
||||
"\x1b[1;5mHello\x1b[0;5m world\x1b[1;5m.",
|
||||
)
|
||||
|
||||
def test_foreground(self) -> None:
|
||||
"""
|
||||
The foreground color formatting attribute, L{A.fg}, emits the VT102
|
||||
control sequence to set the selected foreground color when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(
|
||||
A.normal[A.fg.red["Hello, "], A.fg.green["world!"]]
|
||||
),
|
||||
"\x1b[31mHello, \x1b[32mworld!",
|
||||
)
|
||||
|
||||
def test_background(self) -> None:
|
||||
"""
|
||||
The background color formatting attribute, L{A.bg}, emits the VT102
|
||||
control sequence to set the selected background color when flattened.
|
||||
"""
|
||||
self.assertEqual(
|
||||
text.assembleFormattedText(
|
||||
A.normal[A.bg.red["Hello, "], A.bg.green["world!"]]
|
||||
),
|
||||
"\x1b[41mHello, \x1b[42mworld!",
|
||||
)
|
||||
|
||||
def test_flattenDeprecated(self) -> None:
|
||||
"""
|
||||
L{twisted.conch.insults.text.flatten} emits a deprecation warning when
|
||||
imported or accessed.
|
||||
"""
|
||||
warningsShown = self.flushWarnings([self.test_flattenDeprecated])
|
||||
self.assertEqual(len(warningsShown), 0)
|
||||
|
||||
# Trigger the deprecation warning.
|
||||
text.flatten
|
||||
|
||||
warningsShown = self.flushWarnings([self.test_flattenDeprecated])
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
self.assertEqual(warningsShown[0]["category"], DeprecationWarning)
|
||||
self.assertEqual(
|
||||
warningsShown[0]["message"],
|
||||
"twisted.conch.insults.text.flatten was deprecated in Twisted "
|
||||
"13.1.0: Use twisted.conch.insults.text.assembleFormattedText "
|
||||
"instead.",
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_unix -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.interfaces import IConchUser
|
||||
from twisted.cred.checkers import (
|
||||
AllowAnonymousAccess,
|
||||
InMemoryUsernamePasswordDatabaseDontUse,
|
||||
)
|
||||
from twisted.cred.credentials import (
|
||||
Anonymous,
|
||||
IAnonymous,
|
||||
IUsernamePassword,
|
||||
UsernamePassword,
|
||||
)
|
||||
from twisted.cred.error import LoginDenied
|
||||
from twisted.cred.portal import Portal
|
||||
from twisted.internet.interfaces import IReactorProcess
|
||||
from twisted.python.fakepwd import UserDatabase
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
from .test_session import StubClient, StubConnection
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
unix = requireModule("twisted.conch.unix")
|
||||
|
||||
if unix is not None:
|
||||
from twisted.conch.unix import UnixConchUser, UnixSSHRealm
|
||||
|
||||
|
||||
@implementer(IReactorProcess)
|
||||
class MockProcessSpawner:
|
||||
"""
|
||||
An L{IReactorProcess} that logs calls to C{spawnProcess}.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._spawnProcessCalls = []
|
||||
|
||||
def spawnProcess(
|
||||
self,
|
||||
processProtocol,
|
||||
executable,
|
||||
args=(),
|
||||
env={},
|
||||
path=None,
|
||||
uid=None,
|
||||
gid=None,
|
||||
usePTY=0,
|
||||
childFDs=None,
|
||||
):
|
||||
"""
|
||||
Log a call to C{spawnProcess}. Do not actually spawn a process.
|
||||
"""
|
||||
self._spawnProcessCalls.append(
|
||||
{
|
||||
"processProtocol": processProtocol,
|
||||
"executable": executable,
|
||||
"args": args,
|
||||
"env": env,
|
||||
"path": path,
|
||||
"uid": uid,
|
||||
"gid": gid,
|
||||
"usePTY": usePTY,
|
||||
"childFDs": childFDs,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
shouldSkip = (
|
||||
"Cannot run without cryptography"
|
||||
if cryptography is None
|
||||
else "Unix system required"
|
||||
if unix is None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
class TestSSHSessionForUnixConchUser(unittest.TestCase):
|
||||
skip = shouldSkip
|
||||
|
||||
def testExecCommandEnvironment(self) -> None:
|
||||
"""
|
||||
C{execCommand} sets the C{HOME} environment variable to the avatar's home
|
||||
directory.
|
||||
"""
|
||||
userdb = UserDatabase()
|
||||
homeDirectory = "/made/up/path/"
|
||||
userName = "user"
|
||||
userdb.addUser(userName, home=homeDirectory)
|
||||
self.patch(unix, "pwd", userdb)
|
||||
mockReactor = MockProcessSpawner()
|
||||
avatar = UnixConchUser(userName)
|
||||
avatar.conn = StubConnection(transport=StubClient())
|
||||
session = unix.SSHSessionForUnixConchUser(avatar, reactor=mockReactor)
|
||||
protocol = None
|
||||
command = ["not-actually-executed"]
|
||||
session.execCommand(protocol, command)
|
||||
[call] = mockReactor._spawnProcessCalls
|
||||
self.assertEqual(homeDirectory, call["env"]["HOME"])
|
||||
|
||||
|
||||
class TestUnixSSHRealm(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{UnixSSHRealm}.
|
||||
"""
|
||||
|
||||
skip = shouldSkip
|
||||
|
||||
def test_unixSSHRealm(self) -> None:
|
||||
"""
|
||||
L{UnixSSHRealm} is an L{IRealm} whose C{.requestAvatar} method returns
|
||||
a L{UnixConchUser}.
|
||||
"""
|
||||
userdb = UserDatabase()
|
||||
home = "/testing/home/value"
|
||||
userdb.addUser("user", home=home)
|
||||
self.patch(unix, "pwd", userdb)
|
||||
pwdb = InMemoryUsernamePasswordDatabaseDontUse(user=b"password")
|
||||
p = Portal(UnixSSHRealm(), [pwdb])
|
||||
|
||||
# there seems to be a bug in mypy-zope where sometimes things don't
|
||||
# implement their superinterfaces; 0.3.11, when we upgrade to 0.9.0
|
||||
# this type declaration will be extraneous
|
||||
creds: IUsernamePassword = UsernamePassword(b"user", b"password")
|
||||
result = p.login(creds, None, IConchUser)
|
||||
resultInterface, avatar, logout = self.successResultOf(result)
|
||||
self.assertIsInstance(avatar, UnixConchUser)
|
||||
assert isinstance(avatar, UnixConchUser) # legibility for mypy
|
||||
self.assertEqual(avatar.getHomeDir(), home)
|
||||
|
||||
def test_unixSSHRefusesAnonymousLogins(self) -> None:
|
||||
"""
|
||||
L{UnixSSHRealm} will refuse anonymous logins.
|
||||
"""
|
||||
p = Portal(UnixSSHRealm(), [AllowAnonymousAccess()])
|
||||
result = p.login(IAnonymous(Anonymous()), None, IConchUser)
|
||||
loginDenied = self.failureResultOf(result)
|
||||
self.assertIsInstance(loginDenied.value, LoginDenied)
|
||||
@@ -0,0 +1,974 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the implementation of the ssh-userauth service.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.error import ConchError, ValidPublicKey
|
||||
from twisted.cred.checkers import ICredentialsChecker
|
||||
from twisted.cred.credentials import IAnonymous, ISSHPrivateKey, IUsernamePassword
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.cred.portal import IRealm, Portal
|
||||
from twisted.internet import defer, task
|
||||
from twisted.protocols import loopback
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
|
||||
keys: Optional[ModuleType] = None
|
||||
if requireModule("cryptography"):
|
||||
from twisted.conch.checkers import SSHProtocolChecker
|
||||
from twisted.conch.ssh import keys, transport, userauth
|
||||
from twisted.conch.ssh.common import NS
|
||||
from twisted.conch.test import keydata
|
||||
else:
|
||||
|
||||
class transport: # type: ignore[no-redef]
|
||||
class SSHTransportBase:
|
||||
"""
|
||||
A stub class so that later class definitions won't die.
|
||||
"""
|
||||
|
||||
class userauth: # type: ignore[no-redef]
|
||||
class SSHUserAuthClient:
|
||||
"""
|
||||
A stub class so that later class definitions won't die.
|
||||
"""
|
||||
|
||||
|
||||
class ClientUserAuth(userauth.SSHUserAuthClient):
|
||||
"""
|
||||
A mock user auth client.
|
||||
"""
|
||||
|
||||
def getPublicKey(self):
|
||||
"""
|
||||
If this is the first time we've been called, return a blob for
|
||||
the DSA key. Otherwise, return a blob
|
||||
for the RSA key.
|
||||
"""
|
||||
if self.lastPublicKey:
|
||||
return keys.Key.fromString(keydata.publicRSA_openssh)
|
||||
else:
|
||||
return defer.succeed(keys.Key.fromString(keydata.publicDSA_openssh))
|
||||
|
||||
def getPrivateKey(self):
|
||||
"""
|
||||
Return the private key object for the RSA key.
|
||||
"""
|
||||
return defer.succeed(keys.Key.fromString(keydata.privateRSA_openssh))
|
||||
|
||||
def getPassword(self, prompt=None):
|
||||
"""
|
||||
Return 'foo' as the password.
|
||||
"""
|
||||
return defer.succeed(b"foo")
|
||||
|
||||
def getGenericAnswers(self, name, information, answers):
|
||||
"""
|
||||
Return 'foo' as the answer to two questions.
|
||||
"""
|
||||
return defer.succeed(("foo", "foo"))
|
||||
|
||||
|
||||
class OldClientAuth(userauth.SSHUserAuthClient):
|
||||
"""
|
||||
The old SSHUserAuthClient returned a cryptography key object from
|
||||
getPrivateKey() and a string from getPublicKey
|
||||
"""
|
||||
|
||||
def getPrivateKey(self):
|
||||
return defer.succeed(keys.Key.fromString(keydata.privateRSA_openssh).keyObject)
|
||||
|
||||
def getPublicKey(self):
|
||||
return keys.Key.fromString(keydata.publicRSA_openssh).blob()
|
||||
|
||||
|
||||
class ClientAuthWithoutPrivateKey(userauth.SSHUserAuthClient):
|
||||
"""
|
||||
This client doesn't have a private key, but it does have a public key.
|
||||
"""
|
||||
|
||||
def getPrivateKey(self):
|
||||
return
|
||||
|
||||
def getPublicKey(self):
|
||||
return keys.Key.fromString(keydata.publicRSA_openssh)
|
||||
|
||||
|
||||
class FakeTransport(transport.SSHTransportBase):
|
||||
"""
|
||||
L{userauth.SSHUserAuthServer} expects an SSH transport which has a factory
|
||||
attribute which has a portal attribute. Because the portal is important for
|
||||
testing authentication, we need to be able to provide an interesting portal
|
||||
object to the L{SSHUserAuthServer}.
|
||||
|
||||
In addition, we want to be able to capture any packets sent over the
|
||||
transport.
|
||||
|
||||
@ivar packets: a list of 2-tuples: (messageType, data). Each 2-tuple is
|
||||
a sent packet.
|
||||
@type packets: C{list}
|
||||
@param lostConnecion: True if loseConnection has been called on us.
|
||||
@type lostConnection: L{bool}
|
||||
"""
|
||||
|
||||
class Service:
|
||||
"""
|
||||
A mock service, representing the other service offered by the server.
|
||||
"""
|
||||
|
||||
name = b"nancy"
|
||||
|
||||
def serviceStarted(self):
|
||||
pass
|
||||
|
||||
class Factory:
|
||||
"""
|
||||
A mock factory, representing the factory that spawned this user auth
|
||||
service.
|
||||
"""
|
||||
|
||||
def getService(self, transport, service):
|
||||
"""
|
||||
Return our fake service.
|
||||
"""
|
||||
if service == b"none":
|
||||
return FakeTransport.Service
|
||||
|
||||
def __init__(self, portal):
|
||||
self.factory = self.Factory()
|
||||
self.factory.portal = portal
|
||||
self.lostConnection = False
|
||||
self.transport = self
|
||||
self.packets = []
|
||||
|
||||
def sendPacket(self, messageType, message):
|
||||
"""
|
||||
Record the packet sent by the service.
|
||||
"""
|
||||
self.packets.append((messageType, message))
|
||||
|
||||
def isEncrypted(self, direction):
|
||||
"""
|
||||
Pretend that this transport encrypts traffic in both directions. The
|
||||
SSHUserAuthServer disables password authentication if the transport
|
||||
isn't encrypted.
|
||||
"""
|
||||
return True
|
||||
|
||||
def loseConnection(self):
|
||||
self.lostConnection = True
|
||||
|
||||
|
||||
@implementer(IRealm)
|
||||
class Realm:
|
||||
"""
|
||||
A mock realm for testing L{userauth.SSHUserAuthServer}.
|
||||
|
||||
This realm is not actually used in the course of testing, so it returns the
|
||||
simplest thing that could possibly work.
|
||||
"""
|
||||
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
return defer.succeed((interfaces[0], None, lambda: None))
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class PasswordChecker:
|
||||
"""
|
||||
A very simple username/password checker which authenticates anyone whose
|
||||
password matches their username and rejects all others.
|
||||
"""
|
||||
|
||||
credentialInterfaces = (IUsernamePassword,)
|
||||
|
||||
def requestAvatarId(self, creds):
|
||||
if creds.username == creds.password:
|
||||
return defer.succeed(creds.username)
|
||||
return defer.fail(UnauthorizedLogin("Invalid username/password pair"))
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class PrivateKeyChecker:
|
||||
"""
|
||||
A very simple public key checker which authenticates anyone whose
|
||||
public/private keypair is the same keydata.public/privateRSA_openssh.
|
||||
"""
|
||||
|
||||
credentialInterfaces = (ISSHPrivateKey,)
|
||||
|
||||
def requestAvatarId(self, creds):
|
||||
if creds.blob == keys.Key.fromString(keydata.publicRSA_openssh).blob():
|
||||
if creds.signature is not None:
|
||||
obj = keys.Key.fromString(creds.blob)
|
||||
if obj.verify(creds.signature, creds.sigData):
|
||||
return creds.username
|
||||
else:
|
||||
raise ValidPublicKey()
|
||||
raise UnauthorizedLogin()
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class AnonymousChecker:
|
||||
"""
|
||||
A simple checker which isn't supported by L{SSHUserAuthServer}.
|
||||
"""
|
||||
|
||||
credentialInterfaces = (IAnonymous,)
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
# ICredentialsChecker.requestAvatarId
|
||||
pass
|
||||
|
||||
|
||||
class SSHUserAuthServerTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for SSHUserAuthServer.
|
||||
"""
|
||||
|
||||
if keys is None:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
def setUp(self):
|
||||
self.realm = Realm()
|
||||
self.portal = Portal(self.realm)
|
||||
self.portal.registerChecker(PasswordChecker())
|
||||
self.portal.registerChecker(PrivateKeyChecker())
|
||||
self.authServer = userauth.SSHUserAuthServer()
|
||||
self.authServer.transport = FakeTransport(self.portal)
|
||||
self.authServer.serviceStarted()
|
||||
self.authServer.supportedAuthentications.sort() # give a consistent
|
||||
# order
|
||||
|
||||
def tearDown(self):
|
||||
self.authServer.serviceStopped()
|
||||
self.authServer = None
|
||||
|
||||
def _checkFailed(self, ignored):
|
||||
"""
|
||||
Check that the authentication has failed.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.authServer.transport.packets[-1],
|
||||
(userauth.MSG_USERAUTH_FAILURE, NS(b"password,publickey") + b"\x00"),
|
||||
)
|
||||
|
||||
def test_noneAuthentication(self):
|
||||
"""
|
||||
A client may request a list of authentication 'method name' values
|
||||
that may continue by using the "none" authentication 'method name'.
|
||||
|
||||
See RFC 4252 Section 5.2.
|
||||
"""
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(
|
||||
NS(b"foo") + NS(b"service") + NS(b"none")
|
||||
)
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_successfulPasswordAuthentication(self):
|
||||
"""
|
||||
When provided with correct password authentication information, the
|
||||
server should respond by sending a MSG_USERAUTH_SUCCESS message with
|
||||
no other data.
|
||||
|
||||
See RFC 4252, Section 5.1.
|
||||
"""
|
||||
packet = b"".join([NS(b"foo"), NS(b"none"), NS(b"password"), b"\0", NS(b"foo")])
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
|
||||
def check(ignored):
|
||||
self.assertEqual(
|
||||
self.authServer.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_SUCCESS, b"")],
|
||||
)
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_failedPasswordAuthentication(self):
|
||||
"""
|
||||
When provided with invalid authentication details, the server should
|
||||
respond by sending a MSG_USERAUTH_FAILURE message which states whether
|
||||
the authentication was partially successful, and provides other, open
|
||||
options for authentication.
|
||||
|
||||
See RFC 4252, Section 5.1.
|
||||
"""
|
||||
# packet = username, next_service, authentication type, FALSE, password
|
||||
packet = b"".join([NS(b"foo"), NS(b"none"), NS(b"password"), b"\0", NS(b"bar")])
|
||||
self.authServer.clock = task.Clock()
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
self.assertEqual(self.authServer.transport.packets, [])
|
||||
self.authServer.clock.advance(2)
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_successfulPrivateKeyAuthentication(self):
|
||||
"""
|
||||
Test that private key authentication completes successfully,
|
||||
"""
|
||||
blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
|
||||
obj = keys.Key.fromString(keydata.privateRSA_openssh)
|
||||
packet = (
|
||||
NS(b"foo")
|
||||
+ NS(b"none")
|
||||
+ NS(b"publickey")
|
||||
+ b"\xff"
|
||||
+ NS(obj.sshType())
|
||||
+ NS(blob)
|
||||
)
|
||||
self.authServer.transport.sessionID = b"test"
|
||||
signature = obj.sign(
|
||||
NS(b"test") + bytes((userauth.MSG_USERAUTH_REQUEST,)) + packet
|
||||
)
|
||||
packet += NS(signature)
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
|
||||
def check(ignored):
|
||||
self.assertEqual(
|
||||
self.authServer.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_SUCCESS, b"")],
|
||||
)
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_requestRaisesConchError(self):
|
||||
"""
|
||||
ssh_USERAUTH_REQUEST should raise a ConchError if tryAuth returns
|
||||
None. Added to catch a bug noticed by pyflakes.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
|
||||
def mockCbFinishedAuth(self, ignored):
|
||||
self.fail("request should have raised ConochError")
|
||||
|
||||
def mockTryAuth(kind, user, data):
|
||||
return None
|
||||
|
||||
def mockEbBadAuth(reason):
|
||||
d.errback(reason.value)
|
||||
|
||||
self.patch(self.authServer, "tryAuth", mockTryAuth)
|
||||
self.patch(self.authServer, "_cbFinishedAuth", mockCbFinishedAuth)
|
||||
self.patch(self.authServer, "_ebBadAuth", mockEbBadAuth)
|
||||
|
||||
packet = NS(b"user") + NS(b"none") + NS(b"public-key") + NS(b"data")
|
||||
# If an error other than ConchError is raised, this will trigger an
|
||||
# exception.
|
||||
self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
return self.assertFailure(d, ConchError)
|
||||
|
||||
def test_verifyValidPrivateKey(self):
|
||||
"""
|
||||
Test that verifying a valid private key works.
|
||||
"""
|
||||
blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
|
||||
packet = (
|
||||
NS(b"foo")
|
||||
+ NS(b"none")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x00"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ NS(blob)
|
||||
)
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
|
||||
def check(ignored):
|
||||
self.assertEqual(
|
||||
self.authServer.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_PK_OK, NS(b"ssh-rsa") + NS(blob))],
|
||||
)
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_failedPrivateKeyAuthenticationWithoutSignature(self):
|
||||
"""
|
||||
Test that private key authentication fails when the public key
|
||||
is invalid.
|
||||
"""
|
||||
blob = keys.Key.fromString(keydata.publicDSA_openssh).blob()
|
||||
packet = (
|
||||
NS(b"foo")
|
||||
+ NS(b"none")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x00"
|
||||
+ NS(b"ssh-dsa")
|
||||
+ NS(blob)
|
||||
)
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_failedPrivateKeyAuthenticationWithSignature(self):
|
||||
"""
|
||||
Test that private key authentication fails when the public key
|
||||
is invalid.
|
||||
"""
|
||||
blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
|
||||
obj = keys.Key.fromString(keydata.privateRSA_openssh)
|
||||
packet = (
|
||||
NS(b"foo")
|
||||
+ NS(b"none")
|
||||
+ NS(b"publickey")
|
||||
+ b"\xff"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ NS(blob)
|
||||
+ NS(obj.sign(blob))
|
||||
)
|
||||
self.authServer.transport.sessionID = b"test"
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_unsupported_publickey(self):
|
||||
"""
|
||||
Private key authentication fails when the public key type is
|
||||
unsupported or the public key is corrupt.
|
||||
"""
|
||||
blob = keys.Key.fromString(keydata.publicDSA_openssh).blob()
|
||||
|
||||
# Change the blob to a bad type
|
||||
blob = NS(b"ssh-bad-type") + blob[11:]
|
||||
|
||||
packet = (
|
||||
NS(b"foo")
|
||||
+ NS(b"none")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x00"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ NS(blob)
|
||||
)
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_ignoreUnknownCredInterfaces(self):
|
||||
"""
|
||||
L{SSHUserAuthServer} sets up
|
||||
C{SSHUserAuthServer.supportedAuthentications} by checking the portal's
|
||||
credentials interfaces and mapping them to SSH authentication method
|
||||
strings. If the Portal advertises an interface that
|
||||
L{SSHUserAuthServer} can't map, it should be ignored. This is a white
|
||||
box test.
|
||||
"""
|
||||
server = userauth.SSHUserAuthServer()
|
||||
server.transport = FakeTransport(self.portal)
|
||||
self.portal.registerChecker(AnonymousChecker())
|
||||
server.serviceStarted()
|
||||
server.serviceStopped()
|
||||
server.supportedAuthentications.sort() # give a consistent order
|
||||
self.assertEqual(server.supportedAuthentications, [b"password", b"publickey"])
|
||||
|
||||
def test_removePasswordIfUnencrypted(self):
|
||||
"""
|
||||
Test that the userauth service does not advertise password
|
||||
authentication if the password would be send in cleartext.
|
||||
"""
|
||||
self.assertIn(b"password", self.authServer.supportedAuthentications)
|
||||
# no encryption
|
||||
clearAuthServer = userauth.SSHUserAuthServer()
|
||||
clearAuthServer.transport = FakeTransport(self.portal)
|
||||
clearAuthServer.transport.isEncrypted = lambda x: False
|
||||
clearAuthServer.serviceStarted()
|
||||
clearAuthServer.serviceStopped()
|
||||
self.assertNotIn(b"password", clearAuthServer.supportedAuthentications)
|
||||
# only encrypt incoming (the direction the password is sent)
|
||||
halfAuthServer = userauth.SSHUserAuthServer()
|
||||
halfAuthServer.transport = FakeTransport(self.portal)
|
||||
halfAuthServer.transport.isEncrypted = lambda x: x == "in"
|
||||
halfAuthServer.serviceStarted()
|
||||
halfAuthServer.serviceStopped()
|
||||
self.assertIn(b"password", halfAuthServer.supportedAuthentications)
|
||||
|
||||
def test_unencryptedConnectionWithoutPasswords(self):
|
||||
"""
|
||||
If the L{SSHUserAuthServer} is not advertising passwords, then an
|
||||
unencrypted connection should not cause any warnings or exceptions.
|
||||
This is a white box test.
|
||||
"""
|
||||
# create a Portal without password authentication
|
||||
portal = Portal(self.realm)
|
||||
portal.registerChecker(PrivateKeyChecker())
|
||||
|
||||
# no encryption
|
||||
clearAuthServer = userauth.SSHUserAuthServer()
|
||||
clearAuthServer.transport = FakeTransport(portal)
|
||||
clearAuthServer.transport.isEncrypted = lambda x: False
|
||||
clearAuthServer.serviceStarted()
|
||||
clearAuthServer.serviceStopped()
|
||||
self.assertEqual(clearAuthServer.supportedAuthentications, [b"publickey"])
|
||||
|
||||
# only encrypt incoming (the direction the password is sent)
|
||||
halfAuthServer = userauth.SSHUserAuthServer()
|
||||
halfAuthServer.transport = FakeTransport(portal)
|
||||
halfAuthServer.transport.isEncrypted = lambda x: x == "in"
|
||||
halfAuthServer.serviceStarted()
|
||||
halfAuthServer.serviceStopped()
|
||||
self.assertEqual(clearAuthServer.supportedAuthentications, [b"publickey"])
|
||||
|
||||
def test_loginTimeout(self):
|
||||
"""
|
||||
Test that the login times out.
|
||||
"""
|
||||
timeoutAuthServer = userauth.SSHUserAuthServer()
|
||||
timeoutAuthServer.clock = task.Clock()
|
||||
timeoutAuthServer.transport = FakeTransport(self.portal)
|
||||
timeoutAuthServer.serviceStarted()
|
||||
timeoutAuthServer.clock.advance(11 * 60 * 60)
|
||||
timeoutAuthServer.serviceStopped()
|
||||
self.assertEqual(
|
||||
timeoutAuthServer.transport.packets,
|
||||
[
|
||||
(
|
||||
transport.MSG_DISCONNECT,
|
||||
b"\x00" * 3
|
||||
+ bytes((transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,))
|
||||
+ NS(b"you took too long")
|
||||
+ NS(b""),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertTrue(timeoutAuthServer.transport.lostConnection)
|
||||
|
||||
def test_cancelLoginTimeout(self):
|
||||
"""
|
||||
Test that stopping the service also stops the login timeout.
|
||||
"""
|
||||
timeoutAuthServer = userauth.SSHUserAuthServer()
|
||||
timeoutAuthServer.clock = task.Clock()
|
||||
timeoutAuthServer.transport = FakeTransport(self.portal)
|
||||
timeoutAuthServer.serviceStarted()
|
||||
timeoutAuthServer.serviceStopped()
|
||||
timeoutAuthServer.clock.advance(11 * 60 * 60)
|
||||
self.assertEqual(timeoutAuthServer.transport.packets, [])
|
||||
self.assertFalse(timeoutAuthServer.transport.lostConnection)
|
||||
|
||||
def test_tooManyAttempts(self):
|
||||
"""
|
||||
Test that the server disconnects if the client fails authentication
|
||||
too many times.
|
||||
"""
|
||||
packet = b"".join([NS(b"foo"), NS(b"none"), NS(b"password"), b"\0", NS(b"bar")])
|
||||
self.authServer.clock = task.Clock()
|
||||
for i in range(21):
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
self.authServer.clock.advance(2)
|
||||
|
||||
def check(ignored):
|
||||
self.assertEqual(
|
||||
self.authServer.transport.packets[-1],
|
||||
(
|
||||
transport.MSG_DISCONNECT,
|
||||
b"\x00" * 3
|
||||
+ bytes((transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,))
|
||||
+ NS(b"too many bad auths")
|
||||
+ NS(b""),
|
||||
),
|
||||
)
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_failIfUnknownService(self):
|
||||
"""
|
||||
If the user requests a service that we don't support, the
|
||||
authentication should fail.
|
||||
"""
|
||||
packet = NS(b"foo") + NS(b"") + NS(b"password") + b"\0" + NS(b"foo")
|
||||
self.authServer.clock = task.Clock()
|
||||
d = self.authServer.ssh_USERAUTH_REQUEST(packet)
|
||||
return d.addCallback(self._checkFailed)
|
||||
|
||||
def test_tryAuthEdgeCases(self):
|
||||
"""
|
||||
tryAuth() has two edge cases that are difficult to reach.
|
||||
|
||||
1) an authentication method auth_* returns None instead of a Deferred.
|
||||
2) an authentication type that is defined does not have a matching
|
||||
auth_* method.
|
||||
|
||||
Both these cases should return a Deferred which fails with a
|
||||
ConchError.
|
||||
"""
|
||||
|
||||
def mockAuth(packet):
|
||||
return None
|
||||
|
||||
self.patch(self.authServer, "auth_publickey", mockAuth) # first case
|
||||
self.patch(self.authServer, "auth_password", None) # second case
|
||||
|
||||
def secondTest(ignored):
|
||||
d2 = self.authServer.tryAuth(b"password", None, None)
|
||||
return self.assertFailure(d2, ConchError)
|
||||
|
||||
d1 = self.authServer.tryAuth(b"publickey", None, None)
|
||||
return self.assertFailure(d1, ConchError).addCallback(secondTest)
|
||||
|
||||
|
||||
class SSHUserAuthClientTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for SSHUserAuthClient.
|
||||
"""
|
||||
|
||||
if keys is None:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
def setUp(self):
|
||||
self.authClient = ClientUserAuth(b"foo", FakeTransport.Service())
|
||||
self.authClient.transport = FakeTransport(None)
|
||||
self.authClient.transport.sessionID = b"test"
|
||||
self.authClient.serviceStarted()
|
||||
|
||||
def tearDown(self):
|
||||
self.authClient.serviceStopped()
|
||||
self.authClient = None
|
||||
|
||||
def test_init(self):
|
||||
"""
|
||||
Test that client is initialized properly.
|
||||
"""
|
||||
self.assertEqual(self.authClient.user, b"foo")
|
||||
self.assertEqual(self.authClient.instance.name, b"nancy")
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_REQUEST, NS(b"foo") + NS(b"nancy") + NS(b"none"))],
|
||||
)
|
||||
|
||||
def test_USERAUTH_SUCCESS(self):
|
||||
"""
|
||||
Test that the client succeeds properly.
|
||||
"""
|
||||
instance = [None]
|
||||
|
||||
def stubSetService(service):
|
||||
instance[0] = service
|
||||
|
||||
self.authClient.transport.setService = stubSetService
|
||||
self.authClient.ssh_USERAUTH_SUCCESS(b"")
|
||||
self.assertEqual(instance[0], self.authClient.instance)
|
||||
|
||||
def test_publickey(self):
|
||||
"""
|
||||
Test that the client can authenticate with a public key.
|
||||
"""
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"publickey") + b"\x00")
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
NS(b"foo")
|
||||
+ NS(b"nancy")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x00"
|
||||
+ NS(b"ssh-dss")
|
||||
+ NS(keys.Key.fromString(keydata.publicDSA_openssh).blob()),
|
||||
),
|
||||
)
|
||||
# that key isn't good
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"publickey") + b"\x00")
|
||||
blob = NS(keys.Key.fromString(keydata.publicRSA_openssh).blob())
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
(
|
||||
NS(b"foo")
|
||||
+ NS(b"nancy")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x00"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ blob
|
||||
),
|
||||
),
|
||||
)
|
||||
self.authClient.ssh_USERAUTH_PK_OK(
|
||||
NS(b"ssh-rsa") + NS(keys.Key.fromString(keydata.publicRSA_openssh).blob())
|
||||
)
|
||||
sigData = (
|
||||
NS(self.authClient.transport.sessionID)
|
||||
+ bytes((userauth.MSG_USERAUTH_REQUEST,))
|
||||
+ NS(b"foo")
|
||||
+ NS(b"nancy")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x01"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ blob
|
||||
)
|
||||
obj = keys.Key.fromString(keydata.privateRSA_openssh)
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
NS(b"foo")
|
||||
+ NS(b"nancy")
|
||||
+ NS(b"publickey")
|
||||
+ b"\x01"
|
||||
+ NS(b"ssh-rsa")
|
||||
+ blob
|
||||
+ NS(obj.sign(sigData)),
|
||||
),
|
||||
)
|
||||
|
||||
def test_publickey_without_privatekey(self):
|
||||
"""
|
||||
If the SSHUserAuthClient doesn't return anything from signData,
|
||||
the client should start the authentication over again by requesting
|
||||
'none' authentication.
|
||||
"""
|
||||
authClient = ClientAuthWithoutPrivateKey(b"foo", FakeTransport.Service())
|
||||
|
||||
authClient.transport = FakeTransport(None)
|
||||
authClient.transport.sessionID = b"test"
|
||||
authClient.serviceStarted()
|
||||
authClient.tryAuth(b"publickey")
|
||||
authClient.transport.packets = []
|
||||
self.assertIsNone(authClient.ssh_USERAUTH_PK_OK(b""))
|
||||
self.assertEqual(
|
||||
authClient.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_REQUEST, NS(b"foo") + NS(b"nancy") + NS(b"none"))],
|
||||
)
|
||||
|
||||
def test_no_publickey(self):
|
||||
"""
|
||||
If there's no public key, auth_publickey should return a Deferred
|
||||
called back with a False value.
|
||||
"""
|
||||
self.authClient.getPublicKey = lambda x: None
|
||||
d = self.authClient.tryAuth(b"publickey")
|
||||
|
||||
def check(result):
|
||||
self.assertFalse(result)
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
def test_password(self):
|
||||
"""
|
||||
Test that the client can authentication with a password. This
|
||||
includes changing the password.
|
||||
"""
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"password") + b"\x00")
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
NS(b"foo") + NS(b"nancy") + NS(b"password") + b"\x00" + NS(b"foo"),
|
||||
),
|
||||
)
|
||||
self.authClient.ssh_USERAUTH_PK_OK(NS(b"") + NS(b""))
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
NS(b"foo") + NS(b"nancy") + NS(b"password") + b"\xff" + NS(b"foo") * 2,
|
||||
),
|
||||
)
|
||||
|
||||
def test_no_password(self):
|
||||
"""
|
||||
If getPassword returns None, tryAuth should return False.
|
||||
"""
|
||||
self.authClient.getPassword = lambda: None
|
||||
self.assertFalse(self.authClient.tryAuth(b"password"))
|
||||
|
||||
def test_keyboardInteractive(self):
|
||||
"""
|
||||
Make sure that the client can authenticate with the keyboard
|
||||
interactive method.
|
||||
"""
|
||||
self.authClient.ssh_USERAUTH_PK_OK_keyboard_interactive(
|
||||
NS(b"")
|
||||
+ NS(b"")
|
||||
+ NS(b"")
|
||||
+ b"\x00\x00\x00\x01"
|
||||
+ NS(b"Password: ")
|
||||
+ b"\x00"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_INFO_RESPONSE,
|
||||
b"\x00\x00\x00\x02" + NS(b"foo") + NS(b"foo"),
|
||||
),
|
||||
)
|
||||
|
||||
def test_USERAUTH_PK_OK_unknown_method(self):
|
||||
"""
|
||||
If C{SSHUserAuthClient} gets a MSG_USERAUTH_PK_OK packet when it's not
|
||||
expecting it, it should fail the current authentication and move on to
|
||||
the next type.
|
||||
"""
|
||||
self.authClient.lastAuth = b"unknown"
|
||||
self.authClient.transport.packets = []
|
||||
self.authClient.ssh_USERAUTH_PK_OK(b"")
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_REQUEST, NS(b"foo") + NS(b"nancy") + NS(b"none"))],
|
||||
)
|
||||
|
||||
def test_USERAUTH_FAILURE_sorting(self):
|
||||
"""
|
||||
ssh_USERAUTH_FAILURE should sort the methods by their position
|
||||
in SSHUserAuthClient.preferredOrder. Methods that are not in
|
||||
preferredOrder should be sorted at the end of that list.
|
||||
"""
|
||||
|
||||
def auth_firstmethod():
|
||||
self.authClient.transport.sendPacket(255, b"here is data")
|
||||
|
||||
def auth_anothermethod():
|
||||
self.authClient.transport.sendPacket(254, b"other data")
|
||||
return True
|
||||
|
||||
self.authClient.auth_firstmethod = auth_firstmethod
|
||||
self.authClient.auth_anothermethod = auth_anothermethod
|
||||
|
||||
# although they shouldn't get called, method callbacks auth_* MUST
|
||||
# exist in order for the test to work properly.
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"anothermethod,password") + b"\x00")
|
||||
# should send password packet
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
userauth.MSG_USERAUTH_REQUEST,
|
||||
NS(b"foo") + NS(b"nancy") + NS(b"password") + b"\x00" + NS(b"foo"),
|
||||
),
|
||||
)
|
||||
self.authClient.ssh_USERAUTH_FAILURE(
|
||||
NS(b"firstmethod,anothermethod,password") + b"\xff"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-2:],
|
||||
[(255, b"here is data"), (254, b"other data")],
|
||||
)
|
||||
|
||||
def test_disconnectIfNoMoreAuthentication(self):
|
||||
"""
|
||||
If there are no more available user authentication messages,
|
||||
the SSHUserAuthClient should disconnect with code
|
||||
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE.
|
||||
"""
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"password") + b"\x00")
|
||||
self.authClient.ssh_USERAUTH_FAILURE(NS(b"password") + b"\xff")
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets[-1],
|
||||
(
|
||||
transport.MSG_DISCONNECT,
|
||||
b"\x00\x00\x00\x0e"
|
||||
+ NS(b"no more authentication methods available")
|
||||
+ b"\x00\x00\x00\x00",
|
||||
),
|
||||
)
|
||||
|
||||
def test_ebAuth(self):
|
||||
"""
|
||||
_ebAuth (the generic authentication error handler) should send
|
||||
a request for the 'none' authentication method.
|
||||
"""
|
||||
self.authClient.transport.packets = []
|
||||
self.authClient._ebAuth(None)
|
||||
self.assertEqual(
|
||||
self.authClient.transport.packets,
|
||||
[(userauth.MSG_USERAUTH_REQUEST, NS(b"foo") + NS(b"nancy") + NS(b"none"))],
|
||||
)
|
||||
|
||||
def test_defaults(self):
|
||||
"""
|
||||
getPublicKey() should return None. getPrivateKey() should return a
|
||||
failed Deferred. getPassword() should return a failed Deferred.
|
||||
getGenericAnswers() should return a failed Deferred.
|
||||
"""
|
||||
authClient = userauth.SSHUserAuthClient(b"foo", FakeTransport.Service())
|
||||
self.assertIsNone(authClient.getPublicKey())
|
||||
|
||||
def check(result):
|
||||
result.trap(NotImplementedError)
|
||||
d = authClient.getPassword()
|
||||
return d.addCallback(self.fail).addErrback(check2)
|
||||
|
||||
def check2(result):
|
||||
result.trap(NotImplementedError)
|
||||
d = authClient.getGenericAnswers(None, None, None)
|
||||
return d.addCallback(self.fail).addErrback(check3)
|
||||
|
||||
def check3(result):
|
||||
result.trap(NotImplementedError)
|
||||
|
||||
d = authClient.getPrivateKey()
|
||||
return d.addCallback(self.fail).addErrback(check)
|
||||
|
||||
|
||||
class LoopbackTests(unittest.TestCase):
|
||||
if keys is None:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
class Factory:
|
||||
class Service:
|
||||
name = b"TestService"
|
||||
|
||||
def serviceStarted(self):
|
||||
self.transport.loseConnection()
|
||||
|
||||
def serviceStopped(self):
|
||||
pass
|
||||
|
||||
def getService(self, avatar, name):
|
||||
return self.Service
|
||||
|
||||
def test_loopback(self):
|
||||
"""
|
||||
Test that the userauth server and client play nicely with each other.
|
||||
"""
|
||||
server = userauth.SSHUserAuthServer()
|
||||
client = ClientUserAuth(b"foo", self.Factory.Service())
|
||||
|
||||
# set up transports
|
||||
server.transport = transport.SSHTransportBase()
|
||||
server.transport.service = server
|
||||
server.transport.isEncrypted = lambda x: True
|
||||
client.transport = transport.SSHTransportBase()
|
||||
client.transport.service = client
|
||||
server.transport.sessionID = client.transport.sessionID = b""
|
||||
# don't send key exchange packet
|
||||
server.transport.sendKexInit = client.transport.sendKexInit = lambda: None
|
||||
|
||||
# set up server authentication
|
||||
server.transport.factory = self.Factory()
|
||||
server.passwordDelay = 0 # remove bad password delay
|
||||
realm = Realm()
|
||||
portal = Portal(realm)
|
||||
checker = SSHProtocolChecker()
|
||||
checker.registerChecker(PasswordChecker())
|
||||
checker.registerChecker(PrivateKeyChecker())
|
||||
checker.areDone = lambda aId: (len(checker.successfulCredentials[aId]) == 2)
|
||||
portal.registerChecker(checker)
|
||||
server.transport.factory.portal = portal
|
||||
|
||||
d = loopback.loopbackAsync(server.transport, client.transport)
|
||||
server.transport.transport.logPrefix = lambda: "_ServerLoopback"
|
||||
client.transport.transport.logPrefix = lambda: "_ClientLoopback"
|
||||
|
||||
server.serviceStarted()
|
||||
client.serviceStarted()
|
||||
|
||||
def check(ignored):
|
||||
self.assertEqual(server.transport.service.name, b"TestService")
|
||||
|
||||
return d.addCallback(check)
|
||||
|
||||
|
||||
class ModuleInitializationTests(unittest.TestCase):
|
||||
if keys is None:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
def test_messages(self):
|
||||
# Several message types have value 60, check that MSG_USERAUTH_PK_OK
|
||||
# is always the one which is mapped.
|
||||
self.assertEqual(
|
||||
userauth.SSHUserAuthServer.protocolMessages[60], "MSG_USERAUTH_PK_OK"
|
||||
)
|
||||
self.assertEqual(
|
||||
userauth.SSHUserAuthClient.protocolMessages[60], "MSG_USERAUTH_PK_OK"
|
||||
)
|
||||
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Tests for the insults windowing module, L{twisted.conch.insults.window}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from twisted.conch.insults.insults import ServerProtocol
|
||||
from twisted.conch.insults.window import (
|
||||
ScrolledArea,
|
||||
Selection,
|
||||
TextOutput,
|
||||
TopWindow,
|
||||
Widget,
|
||||
)
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class TopWindowTests(TestCase):
|
||||
"""
|
||||
Tests for L{TopWindow}, the root window container class.
|
||||
"""
|
||||
|
||||
def test_paintScheduling(self) -> None:
|
||||
"""
|
||||
Verify that L{TopWindow.repaint} schedules an actual paint to occur
|
||||
using the scheduling object passed to its initializer.
|
||||
"""
|
||||
paints: list[None] = []
|
||||
scheduled: list[Callable[[], object]] = []
|
||||
root = TopWindow(lambda: paints.append(None), scheduled.append)
|
||||
|
||||
# Nothing should have happened yet.
|
||||
self.assertEqual(paints, [])
|
||||
self.assertEqual(scheduled, [])
|
||||
|
||||
# Cause a paint to be scheduled.
|
||||
root.repaint()
|
||||
self.assertEqual(paints, [])
|
||||
self.assertEqual(len(scheduled), 1)
|
||||
|
||||
# Do another one to verify nothing else happens as long as the previous
|
||||
# one is still pending.
|
||||
root.repaint()
|
||||
self.assertEqual(paints, [])
|
||||
self.assertEqual(len(scheduled), 1)
|
||||
|
||||
# Run the actual paint call.
|
||||
scheduled.pop()()
|
||||
self.assertEqual(len(paints), 1)
|
||||
self.assertEqual(scheduled, [])
|
||||
|
||||
# Do one more to verify that now that the previous one is finished
|
||||
# future paints will succeed.
|
||||
root.repaint()
|
||||
self.assertEqual(len(paints), 1)
|
||||
self.assertEqual(len(scheduled), 1)
|
||||
|
||||
|
||||
class ScrolledAreaTests(TestCase):
|
||||
"""
|
||||
Tests for L{ScrolledArea}, a widget which creates a viewport containing
|
||||
another widget and can reposition that viewport using scrollbars.
|
||||
"""
|
||||
|
||||
def test_parent(self) -> None:
|
||||
"""
|
||||
The parent of the widget passed to L{ScrolledArea} is set to a new
|
||||
L{Viewport} created by the L{ScrolledArea} which itself has the
|
||||
L{ScrolledArea} instance as its parent.
|
||||
"""
|
||||
widget = TextOutput()
|
||||
scrolled = ScrolledArea(widget)
|
||||
self.assertIs(widget.parent, scrolled._viewport)
|
||||
self.assertIs(scrolled._viewport.parent, scrolled)
|
||||
|
||||
|
||||
class SelectionTests(TestCase):
|
||||
"""
|
||||
Change focused entry in L{Selection} using function keys.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create L{ScrolledArea} widget with 10 elements and position selection to 5th element.
|
||||
"""
|
||||
seq: list[bytes] = [f"{_num}".encode("ascii") for _num in range(10)]
|
||||
self.widget = Selection(seq, None)
|
||||
self.widget.height = 10
|
||||
self.widget.focusedIndex = 5
|
||||
|
||||
def test_selectionDownArrow(self) -> None:
|
||||
"""
|
||||
Send DOWN_ARROW to select element just below the current one.
|
||||
"""
|
||||
self.widget.keystrokeReceived(ServerProtocol.DOWN_ARROW, None) # type: ignore[attr-defined]
|
||||
self.assertIs(self.widget.focusedIndex, 6)
|
||||
|
||||
def test_selectionUpArrow(self) -> None:
|
||||
"""
|
||||
Send UP_ARROW to select element just above the current one.
|
||||
"""
|
||||
self.widget.keystrokeReceived(ServerProtocol.UP_ARROW, None) # type: ignore[attr-defined]
|
||||
self.assertIs(self.widget.focusedIndex, 4)
|
||||
|
||||
def test_selectionPGDN(self) -> None:
|
||||
"""
|
||||
Send PGDN to select element one page down (here: last element).
|
||||
"""
|
||||
self.widget.keystrokeReceived(ServerProtocol.PGDN, None) # type: ignore[attr-defined]
|
||||
self.assertIs(self.widget.focusedIndex, 9)
|
||||
|
||||
def test_selectionPGUP(self) -> None:
|
||||
"""
|
||||
Send PGUP to select element one page up (here: first element).
|
||||
"""
|
||||
self.widget.keystrokeReceived(ServerProtocol.PGUP, None) # type: ignore[attr-defined]
|
||||
self.assertIs(self.widget.focusedIndex, 0)
|
||||
|
||||
|
||||
class RecordingWidget(Widget):
|
||||
"""
|
||||
A dummy Widget implementation to test handling of function keys by
|
||||
recording keyReceived events.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
Widget.__init__(self)
|
||||
self.triggered: list[str] = []
|
||||
|
||||
def func_F1(self, modifier: str) -> None:
|
||||
self.triggered.append("F1")
|
||||
|
||||
def func_HOME(self, modifier: str) -> None:
|
||||
self.triggered.append("HOME")
|
||||
|
||||
def func_DOWN_ARROW(self, modifier: str) -> None:
|
||||
self.triggered.append("DOWN_ARROW")
|
||||
|
||||
def func_UP_ARROW(self, modifier: str) -> None:
|
||||
self.triggered.append("UP_ARROW")
|
||||
|
||||
def func_PGDN(self, modifier: str) -> None:
|
||||
self.triggered.append("PGDN")
|
||||
|
||||
def func_PGUP(self, modifier: str) -> None:
|
||||
self.triggered.append("PGUP")
|
||||
|
||||
|
||||
class WidgetFunctionKeyTests(TestCase):
|
||||
"""
|
||||
Call functionKeyReceived with key values from insults.ServerProtocol
|
||||
"""
|
||||
|
||||
def test_functionKeyReceivedDispatch(self) -> None:
|
||||
"""
|
||||
L{Widget.functionKeyReceived} dispatches its input, a constant on
|
||||
ServerProtocol, to a matched C{func_KEY} method.
|
||||
"""
|
||||
widget = RecordingWidget()
|
||||
|
||||
def checkOneKey(key: str) -> None:
|
||||
widget.functionKeyReceived(getattr(ServerProtocol, key), None)
|
||||
self.assertEqual([key], widget.triggered)
|
||||
widget.triggered.clear()
|
||||
|
||||
checkOneKey("F1")
|
||||
checkOneKey("HOME")
|
||||
checkOneKey("DOWN_ARROW")
|
||||
checkOneKey("UP_ARROW")
|
||||
checkOneKey("PGDN")
|
||||
checkOneKey("PGUP")
|
||||
Reference in New Issue
Block a user