Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
|
|
@ -0,0 +1 @@
|
|||
'conch tests'
|
||||
576
venv/lib/python3.9/site-packages/twisted/conch/test/keydata.py
Normal file
576
venv/lib/python3.9/site-packages/twisted/conch/test/keydata.py
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
# -*- 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 __future__ import absolute_import, division
|
||||
|
||||
from twisted.python.compat import long, _b64decodebytes as decodebytes
|
||||
|
||||
RSAData = {
|
||||
'n': long('269413617238113438198661010376758399219880277968382122687862697'
|
||||
'296942471209955603071120391975773283844560230371884389952067978'
|
||||
'789684135947515341209478065209455427327369102356204259106807047'
|
||||
'964139525310539133073743116175821417513079706301100600025815509'
|
||||
'786721808719302671068052414466483676821987505720384645561708425'
|
||||
'794379383191274856941628512616355437197560712892001107828247792'
|
||||
'561858327085521991407807015047750218508971611590850575870321007'
|
||||
'991909043252470730134547038841839367764074379439843108550888709'
|
||||
'430958143271417044750314742880542002948053835745429446485015316'
|
||||
'60749404403945254975473896534482849256068133525751'),
|
||||
'e': long(65537),
|
||||
'd': long('420335724286999695680502438485489819800002417295071059780489811'
|
||||
'840828351636754206234982682752076205397047218449504537476523960'
|
||||
'987613148307573487322720481066677105211155388802079519869249746'
|
||||
'774085882219244493290663802569201213676433159425782937159766786'
|
||||
'329742053214957933941260042101377175565683849732354700525628975'
|
||||
'239000548651346620826136200952740446562751690924335365940810658'
|
||||
'931238410612521441739702170503547025018016868116037053013935451'
|
||||
'477930426013703886193016416453215950072147440344656137718959053'
|
||||
'897268663969428680144841987624962928576808352739627262941675617'
|
||||
'7724661940425316604626522633351193810751757014073'),
|
||||
'p': long('152689878451107675391723141129365667732639179427453246378763774'
|
||||
'448531436802867910180261906924087589684175595016060014593521649'
|
||||
'964959248408388984465569934780790357826811592229318702991401054'
|
||||
'226302790395714901636384511513449977061729214247279176398290513'
|
||||
'085108930550446985490864812445551198848562639933888780317'),
|
||||
'q': long('176444974592327996338888725079951900172097062203378367409936859'
|
||||
'072670162290963119826394224277287608693818012745872307600855894'
|
||||
'647300295516866118620024751601329775653542084052616260193174546'
|
||||
'400544176890518564317596334518015173606460860373958663673307503'
|
||||
'231977779632583864454001476729233959405710696795574874403'),
|
||||
'u': long('936018002388095842969518498561007090965136403384715613439364803'
|
||||
'229386793506402222847415019772053080458257034241832795210460612'
|
||||
'924445085372678524176842007912276654532773301546269997020970818'
|
||||
'155956828553418266110329867222673040098885651348225673298948529'
|
||||
'93885224775891490070400861134282266967852120152546563278')
|
||||
}
|
||||
|
||||
DSAData = {
|
||||
'g': long("10253261326864117157640690761723586967382334319435778695"
|
||||
"29171533815411392477819921538350732400350395446211982054"
|
||||
"96512489289702949127531056893725702005035043292195216541"
|
||||
"11525058911428414042792836395195432445511200566318251789"
|
||||
"10575695836669396181746841141924498545494149998282951407"
|
||||
"18645344764026044855941864175"),
|
||||
'p': long("10292031726231756443208850082191198787792966516790381991"
|
||||
"77502076899763751166291092085666022362525614129374702633"
|
||||
"26262930887668422949051881895212412718444016917144560705"
|
||||
"45675251775747156453237145919794089496168502517202869160"
|
||||
"78674893099371444940800865897607102159386345313384716752"
|
||||
"18590012064772045092956919481"),
|
||||
'q': long(1393384845225358996250882900535419012502712821577),
|
||||
'x': long(1220877188542930584999385210465204342686893855021),
|
||||
'y': long("14604423062661947579790240720337570315008549983452208015"
|
||||
"39426429789435409684914513123700756086453120500041882809"
|
||||
"10283610277194188071619191739512379408443695946763554493"
|
||||
"86398594314468629823767964702559709430618263927529765769"
|
||||
"10270265745700231533660131769648708944711006508965764877"
|
||||
"684264272082256183140297951")
|
||||
}
|
||||
|
||||
ECDatanistp256 = {
|
||||
'x': long('762825130203920963171185031449647317742997734817505505433829043'
|
||||
'45687059013883'),
|
||||
'y': long('815431978646028526322656647694416475343443758943143196810611371'
|
||||
'59310646683104'),
|
||||
'privateValue': long('3463874347721034170096400845565569825355565567882605'
|
||||
'9678074967909361042656500'),
|
||||
'curve': b'ecdsa-sha2-nistp256'
|
||||
}
|
||||
|
||||
ECDatanistp384 = {
|
||||
'privateValue': long('280814107134858470598753916394807521398239633534281633982576099083'
|
||||
'35787109896602102090002196616273211495718603965098'),
|
||||
'x': long('10036914308591746758780165503819213553101287571902957054148542'
|
||||
'504671046744460374996612408381962208627004841444205030'),
|
||||
'y': long('17337335659928075994560513699823544906448896792102247714689323'
|
||||
'575406618073069185107088229463828921069465902299522926'),
|
||||
'curve': b'ecdsa-sha2-nistp384'
|
||||
}
|
||||
|
||||
ECDatanistp521 = {
|
||||
'x': long('12944742826257420846659527752683763193401384271391513286022917'
|
||||
'29910013082920512632908350502247952686156279140016049549948975'
|
||||
'670668730618745449113644014505462'),
|
||||
'y': long('10784108810271976186737587749436295782985563640368689081052886'
|
||||
'16296815984553198866894145509329328086635278430266482551941240'
|
||||
'591605833440825557820439734509311'),
|
||||
'privateValue': long('662751235215460886290293902658128847495347691199214706697089140769'
|
||||
'672273950767961331442265530524063943548846724348048614239791498442'
|
||||
'5997823106818915698960565'),
|
||||
'curve': b'ecdsa-sha2-nistp521'
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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-----
|
||||
"""
|
||||
|
||||
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-----'''
|
||||
|
||||
# Some versions of OpenSSH generate these (slightly different keys): the PKCS#1
|
||||
# structure is wrapped in an extra ASN.1 SEQUENCE and there's an empty SEQUENCE
|
||||
# following it. It is not any standard key format and was probably a bug in
|
||||
# OpenSSH at some point.
|
||||
privateRSA_openssh_alternate = b"""-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEqTCCBKMCAQACggEBANVqrHgj1tYb7CWhUMR3Y1CERQFVQhQqKuDQYO7U6aOtSvo5Bl6EVXVf
|
||||
ADa/b6oqP4MmN8FpLlv98PPSfdaYzTpAeNXKqBjAEZMkCQyBTI/3nO0TFmqkBOlJd8PkVWSzeWie
|
||||
LAjrrOgELSF3BaeO71MwDaXluz1q4gk2b/00031vRv+H2qkpJ6r/rfWF5j4auHodSrHqwFr3MN8f
|
||||
wqTk7z+RSZZA1Rl3LTfDXuydpjpEpcKkKd3Vupw9RbPGLBhk1bo936t/zUKsp/EYC6BYFWILpCpu
|
||||
Q8PkBJ81o0eORu0zpWW9vDspbgILV9906BO0NzV+g18gJmCm3K2Lxmx5mPcCAwEAAQKCAQAhTAhm
|
||||
oijVtPuOD3IbhQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z
|
||||
+LMkYIS5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln
|
||||
5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8Uq9MNY
|
||||
zVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvtklozBw/fhgj5
|
||||
kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AoGBANlwBtjivNR4kVCU1MEbiThsRmRaUaCaBz1IjwNR
|
||||
zGsSjn0asWXncXU54DIFdY0YTK+TsUmxZl94YnrRDMrmTUOznPRrfeYMmNzPIWKO1S4S3gSu1yRu
|
||||
gzGiFaJEPSKpYiYiubLtVAqdCIOnBw3/GRiO2Ksd2kicMWgRoWZt49gdAoGBAPtEF4ukNr4eNx2n
|
||||
9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/R/FqSS
|
||||
EnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8XOmR15LFmB5W
|
||||
07EjAoGAUa/pkp+UC0qEZT6UszuSELV0uIzJ78kOATL6L2gSoQMmrs9RaBRMJpsopAIzCF/hp3CY
|
||||
ATR5XlKOxM82vB9LVazrwVOEx+FhqErUov9ADYAfEqlQwCoYdZQMBpsWUKhL7EHNe+/3S8l1AmjE
|
||||
mLiGiBhaQ+cCM5ciZJODDEUqfO0CgYBpZjfGQN0hxQTzsLg+R5R8dvwt6z85PJXDQwFRxEKX8+gW
|
||||
pMbu7NRJEFA4BO47zdfQzMwyaZAHoBtan/4xzR46fnEeGZQaTk8M319S1dEXbuzXnLZVnduOIV+8
|
||||
JIi2/K+r8O+kLLDcn4awAxK4i+LdD8DuIz1KUP4vuClGWL+2JwKBgQCFSxt6mxIQN54frV7a/saW
|
||||
/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3RTFy8PdIm
|
||||
Jv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnRzrg0x4PXlUzjAA
|
||||
-----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', 'privateRSA_openssh_alternate']
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Loopback helper used in test_ssh and test_recvline
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.protocols import loopback
|
||||
class LoopbackRelay(loopback.LoopbackRelay):
|
||||
clearCall = None
|
||||
|
||||
def logPrefix(self):
|
||||
return "LoopbackRelay(%r)" % (self.target.__class__.__name__,)
|
||||
|
||||
|
||||
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,50 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{SSHTransportAddrress} in ssh/address.py
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.test.test_address import AddressTestCaseMixin
|
||||
|
||||
from twisted.conch.ssh.address import SSHTransportAddress
|
||||
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Like C{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return SSHTransportAddress(IPv4Address("TCP", "127.0.0.2", 22))
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.agent}.
|
||||
"""
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.test import iosim
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
|
||||
try:
|
||||
import pyasn1
|
||||
except ImportError:
|
||||
pyasn1 = None
|
||||
|
||||
if cryptography and pyasn1:
|
||||
from twisted.conch.ssh import keys, agent
|
||||
else:
|
||||
keys = agent = None
|
||||
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.conch.error import ConchError, MissingKeyStoreError
|
||||
|
||||
|
||||
class StubFactory(object):
|
||||
"""
|
||||
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 iosim is None:
|
||||
skip = "iosim requires SSL, but SSL is not available"
|
||||
elif agent is None or keys is None:
|
||||
skip = "Cannot run without cryptography or PyASN1"
|
||||
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
1507
venv/lib/python3.9/site-packages/twisted/conch/test/test_cftp.py
Normal file
1507
venv/lib/python3.9/site-packages/twisted/conch/test/test_cftp.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,355 @@
|
|||
# Copyright Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test ssh/channel.py.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
try:
|
||||
from twisted.conch.ssh import channel
|
||||
from twisted.conch.ssh.address import SSHTransportAddress
|
||||
from twisted.conch.ssh.transport import SSHServerTransport
|
||||
from twisted.conch.ssh.service import SSHService
|
||||
from twisted.internet import interfaces
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
skipTest = None
|
||||
except ImportError:
|
||||
skipTest = 'Conch SSH not supported.'
|
||||
SSHService = object
|
||||
from twisted.trial import unittest
|
||||
from twisted.python.compat import intToBytes
|
||||
|
||||
|
||||
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):
|
||||
self.data = {}
|
||||
self.extData = {}
|
||||
self.closes = {}
|
||||
|
||||
|
||||
def logPrefix(self):
|
||||
"""
|
||||
Return our logging prefix.
|
||||
"""
|
||||
return "MockConnection"
|
||||
|
||||
|
||||
def sendData(self, channel, data):
|
||||
"""
|
||||
Record the sent data.
|
||||
"""
|
||||
self.data.setdefault(channel, []).append(data)
|
||||
|
||||
|
||||
def sendExtendedData(self, channel, type, data):
|
||||
"""
|
||||
Record the sent extended data.
|
||||
"""
|
||||
self.extData.setdefault(channel, []).append((type, data))
|
||||
|
||||
|
||||
def sendClose(self, channel):
|
||||
"""
|
||||
Record that the channel sent a close message.
|
||||
"""
|
||||
self.closes[channel] = True
|
||||
|
||||
|
||||
|
||||
def connectSSHTransport(service, hostAddress=None, peerAddress=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)
|
||||
|
||||
|
||||
|
||||
class ChannelTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{SSHChannel}.
|
||||
"""
|
||||
|
||||
skip = skipTest
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
L{SSHChannel} instances provide L{interfaces.ITransport}.
|
||||
"""
|
||||
self.assertTrue(verifyObject(interfaces.ITransport, self.channel))
|
||||
|
||||
|
||||
def test_init(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Test that addWindowBytes adds bytes to the window and resumes writing
|
||||
if it was paused.
|
||||
"""
|
||||
cb = [False]
|
||||
def stubStartWriting():
|
||||
cb[0] = True
|
||||
self.channel.startWriting = stubStartWriting
|
||||
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):
|
||||
"""
|
||||
Test that requestReceived handles requests by dispatching them to
|
||||
request_* methods.
|
||||
"""
|
||||
self.channel.request_test_method = lambda data: data == b''
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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():
|
||||
cb[0] = True
|
||||
# no window to start with
|
||||
self.channel.stopWriting = stubStopWriting
|
||||
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):
|
||||
"""
|
||||
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():
|
||||
cb[0] = True
|
||||
# no window to start with
|
||||
self.channel.stopWriting = stubStopWriting
|
||||
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):
|
||||
"""
|
||||
Test that writeSequence is equivalent to write(''.join(sequece)).
|
||||
"""
|
||||
self.channel.addWindowBytes(20)
|
||||
self.channel.writeSequence(map(intToBytes, range(10)))
|
||||
self.assertEqual(self.conn.data[self.channel], [b'0123456789'])
|
||||
|
||||
|
||||
def test_loseConnection(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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,875 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.checkers}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
try:
|
||||
import crypt
|
||||
except ImportError:
|
||||
cryptSkip = 'cannot run without crypt module'
|
||||
else:
|
||||
cryptSkip = None
|
||||
|
||||
import os
|
||||
|
||||
from collections import namedtuple
|
||||
from io import BytesIO
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python import util
|
||||
from twisted.python.compat import _b64encodebytes
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
|
||||
from twisted.cred.credentials import UsernamePassword, IUsernamePassword, \
|
||||
SSHPrivateKey, ISSHPrivateKey
|
||||
from twisted.cred.error import UnhandledCredentials, UnauthorizedLogin
|
||||
from twisted.python.fakepwd import UserDatabase, ShadowDatabase
|
||||
from twisted.test.test_process import MockOS
|
||||
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
dependencySkip = None
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.conch import checkers
|
||||
from twisted.conch.error import NotEnoughAuthentication, ValidPublicKey
|
||||
from twisted.conch.test import keydata
|
||||
else:
|
||||
dependencySkip = "can't run without cryptography and PyASN1"
|
||||
|
||||
if getattr(os, 'geteuid', None) is None:
|
||||
euidSkip = "Cannot run without effective UIDs (questionable)"
|
||||
else:
|
||||
euidSkip = None
|
||||
|
||||
|
||||
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' % (
|
||||
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 %s' % (
|
||||
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 %s' % (
|
||||
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):
|
||||
self.checker = checkers.SSHPublicKeyDatabase()
|
||||
self.key1 = _b64encodebytes(b"foobar")
|
||||
self.key2 = _b64encodebytes(b"eggspam")
|
||||
self.content = (b"t1 " + self.key1 + b" foo\nt2 " + self.key2 +
|
||||
b" egg\n")
|
||||
|
||||
self.mockos = MockOS()
|
||||
self.mockos.path = FilePath(self.mktemp())
|
||||
self.mockos.path.makedirs()
|
||||
self.patch(util, 'os', self.mockos)
|
||||
self.sshDir = self.mockos.path.child('.ssh')
|
||||
self.sshDir.makedirs()
|
||||
|
||||
userdb = UserDatabase()
|
||||
userdb.addUser(
|
||||
b'user', b'password', 1, 2, b'first last',
|
||||
self.mockos.path.path, b'/bin/shell')
|
||||
self.checker._userdb = userdb
|
||||
|
||||
|
||||
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, username):
|
||||
"""
|
||||
Assert that the L{Deferred} passed in is called back with the value
|
||||
'username'. This represents a valid 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{Deferred}
|
||||
"""
|
||||
result = []
|
||||
d.addBoth(result.append)
|
||||
self.assertEqual(len(result), 1, "login incomplete")
|
||||
if isinstance(result[0], Failure):
|
||||
result[0].raiseException()
|
||||
self.assertEqual(result[0], 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.assertRaises(
|
||||
checkers.UnauthorizedLogin, self.assertLoggedIn, d, 'bogus value')
|
||||
|
||||
|
||||
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'username')
|
||||
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, username]
|
||||
self.patch(checkers, 'verifyCryptedPassword', verifyCryptedPassword)
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam])
|
||||
credential = UsernamePassword(b'username', b'username')
|
||||
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, username]
|
||||
self.patch(checkers, 'verifyCryptedPassword', verifyCryptedPassword)
|
||||
checker = checkers.UNIXPasswordDatabase([getpwnam1, getpwnam2])
|
||||
credential = UsernamePassword(b'username', b'username')
|
||||
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):
|
||||
mockos = MockOS()
|
||||
mockos.path = FilePath(self.mktemp())
|
||||
mockos.path.makedirs()
|
||||
|
||||
self.userdb = UserDatabase()
|
||||
self.userdb.addUser(b'alice', b'password', 1, 2, b'alice lastname',
|
||||
mockos.path.path, b'/bin/shell')
|
||||
|
||||
self.sshDir = mockos.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('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,625 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.scripts.ckeygen}.
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
from twisted.python.compat import unicode, _PY3
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.ssh.keys import (Key, BadKeyError,
|
||||
BadFingerPrintFormat, FingerprintFormats)
|
||||
from twisted.conch.scripts.ckeygen import (
|
||||
changePassPhrase, displayPublicKey, printFingerprint,
|
||||
_saveKey, enumrepresentation)
|
||||
else:
|
||||
skip = "cryptography and pyasn1 required for twisted.conch.scripts.ckeygen"
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.conch.test.keydata import (
|
||||
publicRSA_openssh, privateRSA_openssh, privateRSA_openssh_encrypted, privateECDSA_openssh)
|
||||
|
||||
|
||||
|
||||
def makeGetpass(*passphrases):
|
||||
"""
|
||||
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}.
|
||||
"""
|
||||
passphrases = iter(passphrases)
|
||||
|
||||
def fakeGetpass(_):
|
||||
return next(passphrases)
|
||||
|
||||
return fakeGetpass
|
||||
|
||||
|
||||
|
||||
class KeyGenTests(TestCase):
|
||||
"""
|
||||
Tests for various functions used to implement the I{ckeygen} script.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Patch C{sys.stdout} so tests can make assertions about what's printed.
|
||||
"""
|
||||
if _PY3:
|
||||
self.stdout = StringIO()
|
||||
else:
|
||||
self.stdout = BytesIO()
|
||||
self.patch(sys, 'stdout', self.stdout)
|
||||
|
||||
|
||||
def _testrun(self, keyType, keySize=None, privateKeySubtype=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')
|
||||
else:
|
||||
self.assertEqual(privKey.type(), keyType.upper())
|
||||
self.assertTrue(pubKey.isPublic())
|
||||
|
||||
|
||||
def test_keygeneration(self):
|
||||
self._testrun('ecdsa', '384')
|
||||
self._testrun('ecdsa', '384', privateKeySubtype='v1')
|
||||
self._testrun('ecdsa')
|
||||
self._testrun('ecdsa', privateKeySubtype='v1')
|
||||
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):
|
||||
filename = self.mktemp()
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
subprocess.check_call(['ckeygen', '-t', 'foo', '-f', filename])
|
||||
|
||||
|
||||
|
||||
def test_enumrepresentation(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Test for format L{FingerprintFormats.SHA256-BASE64}.
|
||||
"""
|
||||
options = enumrepresentation({'format': 'sha256-base64'})
|
||||
self.assertIs(options['format'],
|
||||
FingerprintFormats.SHA256_BASE64)
|
||||
|
||||
|
||||
|
||||
def test_enumrepresentationBadFormat(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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_saveKey(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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_saveKeysha256(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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_saveKeyNoFilename(self):
|
||||
"""
|
||||
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
|
||||
|
||||
import twisted.conch.scripts.ckeygen
|
||||
self.patch(twisted.conch.scripts.ckeygen, 'raw_input', lambda _: keyPath)
|
||||
key = Key.fromString(privateRSA_openssh)
|
||||
_saveKey(key, {'filename': None, 'no-passphrase': True,
|
||||
'format': 'md5-hex'})
|
||||
|
||||
persistedKeyContent = base.child('custom_key').getContent()
|
||||
persistedKey = key.fromString(persistedKeyContent, None, b'')
|
||||
self.assertEqual(key, persistedKey)
|
||||
|
||||
|
||||
def test_saveKeySubtypeV1(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyEncrypted(self):
|
||||
"""
|
||||
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')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyEncryptedPassphrasePrompt(self):
|
||||
"""
|
||||
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')
|
||||
if isinstance(displayed, unicode):
|
||||
displayed = displayed.encode("ascii")
|
||||
self.assertEqual(
|
||||
displayed,
|
||||
pubKey.toString('openssh'))
|
||||
|
||||
|
||||
def test_displayPublicKeyWrongPassphrase(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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})
|
||||
|
||||
if _PY3:
|
||||
expected = "Could not change passphrase: cannot guess the type of b'foobar'"
|
||||
else:
|
||||
expected = "Could not change passphrase: cannot guess the type of 'foobar'"
|
||||
self.assertEqual(expected, str(error))
|
||||
self.assertEqual(b'foobar', FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphraseCreateError(self):
|
||||
"""
|
||||
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, **kwargs):
|
||||
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):
|
||||
"""
|
||||
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, **kwargs):
|
||||
return ''
|
||||
|
||||
self.patch(Key, 'toString', toString)
|
||||
|
||||
error = self.assertRaises(
|
||||
SystemExit, changePassPhrase,
|
||||
{'filename': filename, 'newpass': 'newencrypt'})
|
||||
|
||||
if _PY3:
|
||||
expected = (
|
||||
"Could not change passphrase: cannot guess the type of b''")
|
||||
else:
|
||||
expected = (
|
||||
"Could not change passphrase: cannot guess the type of ''")
|
||||
self.assertEqual(expected, str(error))
|
||||
|
||||
self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
|
||||
|
||||
|
||||
def test_changePassphrasePublicKey(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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'))
|
||||
|
|
@ -0,0 +1,832 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_conch -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import os, sys, socket
|
||||
import subprocess
|
||||
from itertools import count
|
||||
|
||||
from zope.interface import implementer
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.cred import portal
|
||||
from twisted.internet import reactor, defer, protocol
|
||||
from twisted.internet.error import ProcessExitedAlready
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.utils import getProcessValue
|
||||
from twisted.python import filepath, log, runtime
|
||||
from twisted.python.compat import unicode, _PYPY
|
||||
from twisted.trial import unittest
|
||||
from twisted.conch.test.test_ssh import ConchTestRealm
|
||||
from twisted.python.procutils import which
|
||||
|
||||
from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
|
||||
from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import SkipTest
|
||||
|
||||
try:
|
||||
from twisted.conch.test.test_ssh import ConchTestServerFactory, \
|
||||
conchTestPublicKeyChecker
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import pyasn1
|
||||
except ImportError:
|
||||
pyasn1 = None
|
||||
|
||||
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:
|
||||
pass
|
||||
try:
|
||||
from twisted.conch.scripts.conch import (
|
||||
SSHSession as StdioInteractingSession
|
||||
)
|
||||
except ImportError as e:
|
||||
StdioInteractingSession = None
|
||||
_reason = str(e)
|
||||
del e
|
||||
|
||||
|
||||
|
||||
def _has_ipv6():
|
||||
""" Returns True if the system can bind an IPv6 address."""
|
||||
sock = None
|
||||
has_ipv6 = False
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET6)
|
||||
sock.bind(('::1', 0))
|
||||
has_ipv6 = True
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
if sock:
|
||||
sock.close()
|
||||
return has_ipv6
|
||||
|
||||
|
||||
HAS_IPV6 = _has_ipv6()
|
||||
|
||||
|
||||
class FakeStdio(object):
|
||||
"""
|
||||
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(unittest.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(.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, unicode):
|
||||
arg = arg.encode("utf-8")
|
||||
madeArgs.append(arg)
|
||||
return madeArgs
|
||||
|
||||
|
||||
|
||||
class ConchServerSetupMixin:
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
# FIXME: https://twistedmatrix.com/trac/ticket/8506
|
||||
|
||||
# This should be un-skipped on Travis after the ticket is fixed. For now
|
||||
# is enabled so that we can continue with fixing other stuff using Travis.
|
||||
if _PYPY:
|
||||
skip = 'PyPy known_host not working yet on Travis.'
|
||||
|
||||
realmFactory = staticmethod(lambda: ConchTestRealm(b'testuser'))
|
||||
|
||||
def _createFiles(self):
|
||||
for f in ['rsa_test','rsa_test.pub','dsa_test','dsa_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)
|
||||
with open('dsa_test.pub', 'wb') as f:
|
||||
f.write(publicDSA_openssh)
|
||||
with open('dsa_test', 'wb') as f:
|
||||
f.write(privateDSA_openssh)
|
||||
os.chmod('dsa_test', 0o600)
|
||||
os.chmod('rsa_test', 0o600)
|
||||
permissions = FilePath('dsa_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.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
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}
|
||||
"""
|
||||
# PubkeyAcceptedKeyTypes does not exist prior to OpenSSH 7.0 so we
|
||||
# first need to check if we can set it. If we can, -V will just print
|
||||
# the version without doing anything else; if we can't, we will get a
|
||||
# configuration error.
|
||||
d = getProcessValue(
|
||||
which('ssh')[0], ('-o', 'PubkeyAcceptedKeyTypes=ssh-dss', '-V'))
|
||||
def hasPAKT(status):
|
||||
if status == 0:
|
||||
opts = '-oPubkeyAcceptedKeyTypes=ssh-dss '
|
||||
else:
|
||||
opts = ''
|
||||
|
||||
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 '
|
||||
'-oUserKnownHostsFile=kh_test '
|
||||
'-oPasswordAuthentication=no '
|
||||
# Always use the RSA key, since that's the one in kh_test.
|
||||
'-oHostKeyAlgorithms=ssh-rsa '
|
||||
'-a '
|
||||
'-i dsa_test ') + opts + sshArgs + \
|
||||
' 127.0.0.1 ' + remoteCommand
|
||||
port = self.conchServer.getHost().port
|
||||
cmds = (cmdline % port).split()
|
||||
encodedCmds = []
|
||||
for cmd in cmds:
|
||||
if isinstance(cmd, unicode):
|
||||
cmd = cmd.encode("utf-8")
|
||||
encodedCmds.append(cmd)
|
||||
reactor.spawnProcess(process, which('ssh')[0], encodedCmds)
|
||||
return process.deferred
|
||||
return d.addCallback(hasPAKT)
|
||||
|
||||
|
||||
|
||||
class OpenSSHKeyExchangeTests(ConchServerSetupMixin, OpenSSHClientMixin,
|
||||
unittest.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:
|
||||
pass
|
||||
|
||||
if keyExchangeAlgo not in kexAlgorithms:
|
||||
raise unittest.SkipTest(
|
||||
"{} not supported by ssh client".format(
|
||||
keyExchangeAlgo))
|
||||
|
||||
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(unittest.SkipTest,
|
||||
self.assertExecuteWithKexAlgorithm,
|
||||
'unsupported-algorithm')
|
||||
|
||||
|
||||
|
||||
class OpenSSHClientForwardingTests(ForwardingMixin, OpenSSHClientMixin,
|
||||
unittest.TestCase):
|
||||
"""
|
||||
Connection forwarding tests run against the OpenSSL command line client.
|
||||
"""
|
||||
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
|
||||
if not HAS_IPV6:
|
||||
test_localToRemoteForwardingV6.skip = "Requires IPv6 support"
|
||||
|
||||
|
||||
|
||||
class OpenSSHClientRekeyTests(RekeyTestsMixin, OpenSSHClientMixin,
|
||||
unittest.TestCase):
|
||||
"""
|
||||
Rekeying tests run against the OpenSSL command line client.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class CmdLineClientTests(ForwardingMixin, unittest.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 dsa_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, unicode):
|
||||
cmd = cmd.encode("utf-8")
|
||||
encodedCmds.append(cmd)
|
||||
for var in env:
|
||||
val = env[var]
|
||||
if isinstance(var, unicode):
|
||||
var = var.encode("utf-8")
|
||||
if isinstance(val, unicode):
|
||||
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,761 @@
|
|||
# Copyright (c) 2007-2010 Twisted Matrix Laboratories.
|
||||
# See LICENSE for details
|
||||
|
||||
"""
|
||||
This module tests twisted.conch.ssh.connection.
|
||||
"""
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
|
||||
from twisted.conch import error
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import common, connection
|
||||
else:
|
||||
class connection:
|
||||
class SSHConnection: pass
|
||||
|
||||
from twisted.conch.ssh import channel
|
||||
from twisted.python.compat import long
|
||||
from twisted.trial import unittest
|
||||
from twisted.conch.test import test_userauth
|
||||
|
||||
|
||||
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"
|
||||
if test_userauth.transport is None:
|
||||
skip = "Cannot run without both cryptography and pyasn1"
|
||||
|
||||
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, "Expected one error, got: %r" % (errors,))
|
||||
self.assertEqual(errors[0].value.args, (long(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_lookupChannelErrorLongCode(self):
|
||||
"""
|
||||
Like L{test_lookupChannelError}, but for the case where the failure code
|
||||
is represented as a L{long} instead of a L{int}.
|
||||
"""
|
||||
self._lookupChannelErrorTest(long(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"
|
||||
|
||||
if test_userauth.transport is None:
|
||||
skip = "Cannot run without both cryptography and pyasn1"
|
||||
|
||||
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,334 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.client.default}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.client.agent import SSHAgentClient
|
||||
from twisted.conch.client.default import SSHUserAuthClient
|
||||
from twisted.conch.client.options import ConchOptions
|
||||
from twisted.conch.client import default
|
||||
from twisted.conch.ssh.keys import Key
|
||||
skip = None
|
||||
else:
|
||||
skip = "cryptography and PyASN1 required for twisted.conch.client.default."
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.runtime import platform
|
||||
|
||||
if platform.isWindows():
|
||||
windowsSkip = (
|
||||
"genericAnswers and getPassword does not work on Windows."
|
||||
" Should be fixed as part of fixing bug 6409 and 6410")
|
||||
else:
|
||||
windowsSkip = skip
|
||||
|
||||
ttySkip = None
|
||||
if not sys.stdin.isatty():
|
||||
ttySkip = "sys.stdin is not an interactive tty"
|
||||
if not sys.stdout.isatty():
|
||||
ttySkip = "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,
|
||||
"Enter passphrase for key '%s': " % (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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
test_getPassword.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
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
|
||||
|
||||
test_getPasswordPrompt.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
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)
|
||||
|
||||
test_getPasswordConchError.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
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, 'raw_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
|
||||
|
||||
test_getGenericAnswers.skip = windowsSkip or ttySkip
|
||||
|
||||
|
||||
|
||||
|
||||
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,881 @@
|
|||
# -*- 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}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
unix = requireModule("twisted.conch.unix")
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch import avatar
|
||||
from twisted.conch.ssh import common, connection, filetransfer, session
|
||||
else:
|
||||
class avatar:
|
||||
class ConchUser: pass
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.protocols import loopback
|
||||
from twisted.python import components
|
||||
from twisted.python.compat import long, _PY37PLUS
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
|
||||
class TestAvatar(avatar.ConchUser):
|
||||
def __init__(self):
|
||||
avatar.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 unix:
|
||||
if not hasattr(unix, 'SFTPServerForUnixConchUser'):
|
||||
# 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,))
|
||||
unix = None
|
||||
else:
|
||||
class FileTransferForTestAvatar(unix.SFTPServerForUnixConchUser):
|
||||
|
||||
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(unittest.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')
|
||||
|
||||
|
||||
class OurServerOurClientTests(SFTPTestBase):
|
||||
|
||||
if not unix:
|
||||
skip = "can't run on non-posix computers"
|
||||
|
||||
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),
|
||||
"ISFTPServer not provided by %r" % (self.server.client,))
|
||||
|
||||
|
||||
def test_openedFileClosedWithConnection(self):
|
||||
"""
|
||||
A file opened with C{openFile} is close 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_openDirectoryIterator(self):
|
||||
"""
|
||||
Check that the object returned by
|
||||
L{filetransfer.FileTransferClient.openDirectory} can be used
|
||||
as an iterator.
|
||||
"""
|
||||
|
||||
# This function is a little more complicated than it would be
|
||||
# normally, since we need to call _emptyBuffers() after
|
||||
# creating any SSH-related Deferreds, but before waiting on
|
||||
# them via yield.
|
||||
|
||||
d = self.client.openDirectory(b'')
|
||||
self._emptyBuffers()
|
||||
openDir = yield d
|
||||
|
||||
filenames = set()
|
||||
try:
|
||||
for f in openDir:
|
||||
self._emptyBuffers()
|
||||
(filename, _, fileattrs) = yield f
|
||||
filenames.add(filename)
|
||||
finally:
|
||||
d = openDir.close()
|
||||
self._emptyBuffers()
|
||||
yield d
|
||||
|
||||
self._emptyBuffers()
|
||||
|
||||
self.assertEqual(filenames,
|
||||
set([b'.testHiddenFile', b'testDirectory',
|
||||
b'testRemoveFile', b'testRenameFile',
|
||||
b'testfile1']))
|
||||
|
||||
|
||||
if _PY37PLUS:
|
||||
test_openDirectoryIterator.skip = (
|
||||
"Broken by PEP 479 and deprecated.")
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_openDirectoryIteratorDeprecated(self):
|
||||
"""
|
||||
Using client.openDirectory as an iterator is deprecated.
|
||||
"""
|
||||
d = self.client.openDirectory(b'')
|
||||
self._emptyBuffers()
|
||||
openDir = yield d
|
||||
openDir.next()
|
||||
|
||||
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'])
|
||||
|
||||
|
||||
|
||||
class FakeConn:
|
||||
def sendClose(self, channel):
|
||||
pass
|
||||
|
||||
|
||||
class FileTransferCloseTests(unittest.TestCase):
|
||||
|
||||
if not unix:
|
||||
skip = "can't run on non-posix computers"
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
class ConstantsTests(unittest.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.
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
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('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line)
|
||||
if m:
|
||||
constants[m.group(1)] = long(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))
|
||||
|
||||
|
||||
|
||||
class RawPacketDataTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
|
||||
less common protocol messages to exercise their handling.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
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,63 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh.forwarding}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
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.trial import unittest
|
||||
from twisted.internet.test.test_endpoints import deterministicResolvingReactor
|
||||
from twisted.test.proto_helpers import MemoryReactorClock, StringTransport
|
||||
|
||||
|
||||
class TestSSHConnectForwardingChannel(unittest.TestCase):
|
||||
"""
|
||||
Unit and integration tests for L{SSHConnectForwardingChannel}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "Cannot run without cryptography"
|
||||
|
||||
def makeTCPConnection(self, reactor):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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,658 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_helper -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.conch.insults import helper
|
||||
from twisted.conch.insults.insults import G0, G1, G2, G3
|
||||
from twisted.conch.insults.insults import modes, privateModes
|
||||
from twisted.conch.insults.insults import (
|
||||
NORMAL, BOLD, UNDERLINE, BLINK, REVERSE_VIDEO)
|
||||
|
||||
from twisted.python.compat import _PY3
|
||||
from twisted.trial import unittest
|
||||
|
||||
WIDTH = 80
|
||||
HEIGHT = 24
|
||||
|
||||
class BufferTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.term = helper.TerminalBuffer()
|
||||
self.term.connectionMade()
|
||||
|
||||
def testInitialState(self):
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
self.term.cursorPosition(3, 9)
|
||||
self.assertEqual(self.term.reportCursorPosition(), (3, 9))
|
||||
|
||||
|
||||
def testSimpleWriting(self):
|
||||
s = b"Hello, world."
|
||||
self.term.write(s)
|
||||
self.assertEqual(
|
||||
self.term.__bytes__(),
|
||||
s + b'\n' +
|
||||
b'\n' * (HEIGHT - 2))
|
||||
|
||||
|
||||
def testOvertype(self):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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, timeout, f, a, kw):
|
||||
self.fs = fs
|
||||
self.timeout = timeout
|
||||
self.f = f
|
||||
self.a = a
|
||||
self.kw = kw
|
||||
|
||||
|
||||
def active(self):
|
||||
return not (self.cancelled or self.called)
|
||||
|
||||
|
||||
def cancel(self):
|
||||
self.cancelled = True
|
||||
# self.fs.calls.remove(self)
|
||||
|
||||
|
||||
def call(self):
|
||||
self.called = True
|
||||
self.f(*self.a, **self.kw)
|
||||
|
||||
|
||||
|
||||
class FakeScheduler:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
|
||||
def callLater(self, timeout, f, *a, **kw):
|
||||
self.calls.append(FakeDelayedCall(self, timeout, f, a, kw))
|
||||
return self.calls[-1]
|
||||
|
||||
|
||||
|
||||
class ExpectTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.term = helper.ExpectableBuffer()
|
||||
self.term.connectionMade()
|
||||
self.fs = FakeScheduler()
|
||||
|
||||
|
||||
def testSimpleString(self):
|
||||
result = []
|
||||
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):
|
||||
result = []
|
||||
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):
|
||||
result = []
|
||||
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):
|
||||
self.term.write(b"hello world")
|
||||
|
||||
result = []
|
||||
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):
|
||||
self.term.write(b"goodbye world")
|
||||
|
||||
result = []
|
||||
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):
|
||||
self.assertTrue(hasattr(res, 'type'))
|
||||
self.assertEqual(res.type, helper.ExpectationTimeout)
|
||||
|
||||
|
||||
def testTimeoutFailure(self):
|
||||
d = self.term.expect(b"hello world", timeout=1, scheduler=self.fs)
|
||||
d.addBoth(self._cbTestTimeoutFailure)
|
||||
self.fs.calls[0].call()
|
||||
|
||||
|
||||
def testOverlappingTimeout(self):
|
||||
self.term.write(b"not zoomtastic")
|
||||
|
||||
result = []
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
if _PY3:
|
||||
deprecatedClass = (
|
||||
"twisted.conch.insults.helper._FormattingState.wantOne")
|
||||
else:
|
||||
deprecatedClass = "twisted.conch.insults.helper.wantOne"
|
||||
self.assertEqual(
|
||||
warningsShown[0]['message'],
|
||||
'%s was deprecated in Twisted 13.1.0' % (deprecatedClass))
|
||||
|
|
@ -0,0 +1,935 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_insults -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
|
||||
from twisted.conch.insults.insults import ServerProtocol, ClientProtocol
|
||||
from twisted.conch.insults.insults import (CS_UK, CS_US, CS_DRAWING,
|
||||
CS_ALTERNATE,
|
||||
CS_ALTERNATE_SPECIAL,
|
||||
BLINK, UNDERLINE)
|
||||
from twisted.conch.insults.insults import G0, G1
|
||||
from twisted.conch.insults.insults import modes, privateModes
|
||||
from twisted.python.compat import intToBytes, iterbytes
|
||||
from twisted.python.constants import ValueConstant, Values
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
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(object):
|
||||
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 = 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, "%r should have been []" % (occs,))
|
||||
|
||||
|
||||
|
||||
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''.join([b'\x1b[' + n + ch
|
||||
for n in (b'', intToBytes(2), intToBytes(20), intToBytes(200))]
|
||||
) for ch in iterbytes(b'BACD')
|
||||
]))
|
||||
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(map(intToBytes, [modes.KAM, modes.IRM, modes.LNM])) + b"h")
|
||||
self.parser.dataReceived(
|
||||
b"\x1b[" + b';'.join(map(intToBytes, [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, "No other calls should happen: %r" % (occs,))
|
||||
|
||||
|
||||
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(map(intToBytes, 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(map(intToBytes, 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(map(intToBytes, 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 +
|
||||
intToBytes(BLINK) + b';' + intToBytes(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)
|
||||
1385
venv/lib/python3.9/site-packages/twisted/conch/test/test_keys.py
Normal file
1385
venv/lib/python3.9/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,481 @@
|
|||
# -*- 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 traceback
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import error, defer
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
from twisted.conch.test.test_recvline import (
|
||||
_TelnetMixin, _SSHMixin, _StdioMixin, stdio, ssh)
|
||||
from twisted.conch import manhole
|
||||
from twisted.conch.insults import insults
|
||||
|
||||
|
||||
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:
|
||||
# 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(u"\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(u"\u0438"))
|
||||
|
||||
|
||||
|
||||
class ManholeLoopbackMixin:
|
||||
serverProtocol = manhole.ColoredManhole
|
||||
|
||||
|
||||
def wfd(self, d):
|
||||
return defer.waitForDeferred(d)
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
return done.addCallback(finished)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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(br"\+ 1")
|
||||
self._assertBuffer([b">>> 1 + 1"])
|
||||
|
||||
self._testwrite(manhole.CTRL_D + b" + 1")
|
||||
yield self.recvlineClient.expect(br"\+ 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(br"\+ 1")
|
||||
self._assertBuffer([b">>> ", b">>> 1 + 1"])
|
||||
|
||||
self._testwrite(manhole.CTRL_L + b" + 1")
|
||||
yield self.recvlineClient.expect(br"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,123 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.manhole_tap}.
|
||||
"""
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
|
||||
try:
|
||||
import pyasn1
|
||||
except ImportError:
|
||||
pyasn1 = None
|
||||
|
||||
if cryptography and pyasn1:
|
||||
from twisted.conch import manhole_tap, manhole_ssh
|
||||
|
||||
from twisted.application.internet import StreamServerEndpointService
|
||||
from twisted.application.service import MultiService
|
||||
|
||||
from twisted.cred import error
|
||||
from twisted.cred.credentials import UsernamePassword
|
||||
|
||||
from twisted.conch import telnet
|
||||
|
||||
from twisted.python import usage
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
|
||||
class MakeServiceTests(TestCase):
|
||||
"""
|
||||
Tests for L{manhole_tap.makeService}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
usernamePassword = (b'iamuser', b'thisispassword')
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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", "512",
|
||||
"--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):
|
||||
"""
|
||||
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,43 @@
|
|||
# -*- twisted.conch.test.test_mixin -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
|
||||
from twisted.conch import mixin
|
||||
|
||||
|
||||
class TestBufferingProto(mixin.BufferingMixin):
|
||||
scheduled = False
|
||||
rescheduled = 0
|
||||
def schedule(self):
|
||||
self.scheduled = True
|
||||
return object()
|
||||
|
||||
def reschedule(self, token):
|
||||
self.rescheduled += 1
|
||||
|
||||
|
||||
|
||||
class BufferingTests(unittest.TestCase):
|
||||
def testBuffering(self):
|
||||
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,122 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.openssh_compat}.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.openssh_compat.factory import OpenSSHFactory
|
||||
else:
|
||||
OpenSSHFactory = None
|
||||
|
||||
from twisted.conch.ssh._kex import getDHGeneratorAndPrime
|
||||
from twisted.conch.test import keydata
|
||||
from twisted.test.test_process import MockOS
|
||||
|
||||
|
||||
class OpenSSHFactoryTests(TestCase):
|
||||
"""
|
||||
Tests for L{OpenSSHFactory}.
|
||||
"""
|
||||
if getattr(os, "geteuid", None) is None:
|
||||
skip = "geteuid/seteuid not available"
|
||||
elif OpenSSHFactory is None:
|
||||
skip = "Cannot run without cryptography or PyASN1"
|
||||
|
||||
|
||||
def setUp(self):
|
||||
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"""
|
||||
# $OpenBSD: moduli,v 1.xx 2016/07/26 12:34:56 jhacker Exp $
|
||||
# Time Type Tests Tries Size Generator Modulus
|
||||
20030501000000 2 6 100 2047 2 FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF
|
||||
|
||||
""")
|
||||
|
||||
self.mockos = MockOS()
|
||||
self.patch(os, "seteuid", self.mockos.seteuid)
|
||||
self.patch(os, "setegid", self.mockos.setegid)
|
||||
|
||||
|
||||
def test_getPublicKeys(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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), set([b'ssh-rsa', b'ssh-dss']))
|
||||
self.assertEqual(self.mockos.seteuidCalls, [])
|
||||
self.assertEqual(self.mockos.setegidCalls, [])
|
||||
|
||||
|
||||
def test_getPrivateKeysAsRoot(self):
|
||||
"""
|
||||
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):
|
||||
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), set([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):
|
||||
"""
|
||||
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,810 @@
|
|||
# -*- 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 twisted.conch.insults import insults
|
||||
from twisted.conch import recvline
|
||||
|
||||
from twisted.python import reflect, components, filepath
|
||||
from twisted.python.compat import iterbytes, bytesEnviron
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.internet import defer, error
|
||||
from twisted.trial import unittest
|
||||
from twisted.cred import portal
|
||||
from twisted.test.proto_helpers import StringTransport
|
||||
|
||||
if platform.isWindows():
|
||||
properEnv = dict(os.environ)
|
||||
properEnv["PYTHONPATH"] = os.pathsep.join(sys.path)
|
||||
else:
|
||||
properEnv = bytesEnviron()
|
||||
properEnv[b"PYTHONPATH"] = os.pathsep.join(sys.path).encode(
|
||||
sys.getfilesystemencoding())
|
||||
|
||||
|
||||
|
||||
class ArrowsTests(unittest.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.ssh import (userauth, transport, channel, connection,
|
||||
session, keys)
|
||||
from twisted.conch.manhole_ssh import TerminalUser, TerminalSession, TerminalRealm, TerminalSessionTransport, ConchFactory
|
||||
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(len(receivedLines), len(expectedLines))
|
||||
for i in range(len(receivedLines)):
|
||||
self.assertEqual(
|
||||
receivedLines[i], expectedLines[i],
|
||||
b"".join(receivedLines[max(0, i-1):i+1]) +
|
||||
b" != " +
|
||||
b"".join(expectedLines[max(0, i-1):i+1]))
|
||||
|
||||
|
||||
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 unittest.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=512)
|
||||
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)
|
||||
|
||||
try:
|
||||
from twisted.conch import stdio
|
||||
except ImportError:
|
||||
stdio = None
|
||||
|
||||
|
||||
|
||||
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)]
|
||||
if not platform.isWindows():
|
||||
args = [arg.encode(sys.getfilesystemencoding()) for arg in args]
|
||||
|
||||
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, unittest.TestCase, RecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class RecvlineLoopbackSSHTests(_SSHMixin, unittest.TestCase, RecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class RecvlineLoopbackStdioTests(_StdioMixin, unittest.TestCase, RecvlineLoopbackMixin):
|
||||
if stdio is None:
|
||||
skip = "Terminal requirements missing, can't run recvline tests over stdio"
|
||||
|
||||
|
||||
|
||||
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, unittest.TestCase, HistoricRecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class HistoricRecvlineLoopbackSSHTests(_SSHMixin, unittest.TestCase, HistoricRecvlineLoopbackMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class HistoricRecvlineLoopbackStdioTests(_StdioMixin, unittest.TestCase, HistoricRecvlineLoopbackMixin):
|
||||
if stdio is None:
|
||||
skip = "Terminal requirements missing, can't run historic recvline tests over stdio"
|
||||
|
||||
|
||||
|
||||
class TransportSequenceTests(unittest.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,77 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the command-line interfaces to conch.
|
||||
"""
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
if requireModule('pyasn1'):
|
||||
pyasn1Skip = None
|
||||
else:
|
||||
pyasn1Skip = "Cannot run without PyASN1"
|
||||
|
||||
if requireModule('cryptography'):
|
||||
cryptoSkip = None
|
||||
else:
|
||||
cryptoSkip = "can't run w/o cryptography"
|
||||
|
||||
if requireModule('tty'):
|
||||
ttySkip = None
|
||||
else:
|
||||
ttySkip = "can't run w/o tty"
|
||||
|
||||
try:
|
||||
import Tkinter
|
||||
except ImportError:
|
||||
tkskip = "can't run w/o Tkinter"
|
||||
else:
|
||||
try:
|
||||
Tkinter.Tk().destroy()
|
||||
except Tkinter.TclError as e:
|
||||
tkskip = "Can't test Tkinter: " + str(e)
|
||||
else:
|
||||
tkskip = None
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.scripts.test.test_scripts import ScriptTestsMixin
|
||||
from twisted.python.test.test_shellcomp import ZshScriptTestMixin
|
||||
|
||||
|
||||
|
||||
class ScriptTests(TestCase, ScriptTestsMixin):
|
||||
"""
|
||||
Tests for the Conch scripts.
|
||||
"""
|
||||
skip = pyasn1Skip or cryptoSkip
|
||||
|
||||
|
||||
def test_conch(self):
|
||||
self.scriptTest("conch/conch")
|
||||
test_conch.skip = ttySkip or skip
|
||||
|
||||
|
||||
def test_cftp(self):
|
||||
self.scriptTest("conch/cftp")
|
||||
test_cftp.skip = ttySkip or skip
|
||||
|
||||
|
||||
def test_ckeygen(self):
|
||||
self.scriptTest("conch/ckeygen")
|
||||
|
||||
|
||||
def test_tkconch(self):
|
||||
self.scriptTest("conch/tkconch")
|
||||
test_tkconch.skip = tkskip or skip
|
||||
|
||||
|
||||
|
||||
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'),
|
||||
]
|
||||
1200
venv/lib/python3.9/site-packages/twisted/conch/test/test_session.py
Normal file
1200
venv/lib/python3.9/site-packages/twisted/conch/test/test_session.py
Normal file
File diff suppressed because it is too large
Load diff
997
venv/lib/python3.9/site-packages/twisted/conch/test/test_ssh.py
Normal file
997
venv/lib/python3.9/site-packages/twisted/conch/test/test_ssh.py
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.ssh}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
pyasn1 = requireModule("pyasn1")
|
||||
|
||||
if cryptography:
|
||||
from twisted.conch.ssh import common, forwarding, session, _kex
|
||||
from twisted.conch import avatar, error
|
||||
else:
|
||||
class avatar:
|
||||
class ConchUser: pass
|
||||
|
||||
from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
|
||||
from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
|
||||
from twisted.cred import portal
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.error import ProcessTerminated
|
||||
from twisted.python import failure, log
|
||||
from twisted.trial import unittest
|
||||
|
||||
from twisted.conch.test.loopback import LoopbackRelay
|
||||
|
||||
|
||||
|
||||
class ConchTestRealm(object):
|
||||
"""
|
||||
A realm which expects a particular avatarId to log in once and creates a
|
||||
L{ConchTestAvatar} for that request.
|
||||
|
||||
@ivar expectedAvatarID: The only avatarID that this realm will produce an
|
||||
avatar for.
|
||||
|
||||
@ivar avatar: A reference to the avatar after it is requested.
|
||||
"""
|
||||
avatar = None
|
||||
|
||||
def __init__(self, expectedAvatarID):
|
||||
self.expectedAvatarID = expectedAvatarID
|
||||
|
||||
|
||||
def requestAvatar(self, avatarID, mind, *interfaces):
|
||||
"""
|
||||
Return a new L{ConchTestAvatar} if the avatarID matches the expected one
|
||||
and this is the first avatar request.
|
||||
"""
|
||||
if avatarID == self.expectedAvatarID:
|
||||
if self.avatar is not None:
|
||||
raise UnauthorizedLogin("Only one login allowed")
|
||||
self.avatar = ConchTestAvatar()
|
||||
return interfaces[0], self.avatar, self.avatar.logout
|
||||
raise UnauthorizedLogin(
|
||||
"Only %r may log in, not %r" % (self.expectedAvatarID, avatarID))
|
||||
|
||||
|
||||
|
||||
class ConchTestAvatar(avatar.ConchUser):
|
||||
"""
|
||||
An avatar against which various SSH features can be tested.
|
||||
|
||||
@ivar loggedOut: A flag indicating whether the avatar logout method has been
|
||||
called.
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
loggedOut = False
|
||||
|
||||
def __init__(self):
|
||||
avatar.ConchUser.__init__(self)
|
||||
self.listeners = {}
|
||||
self.globalRequests = {}
|
||||
self.channelLookup.update(
|
||||
{b'session': session.SSHSession,
|
||||
b'direct-tcpip':forwarding.openConnectForwardingClient})
|
||||
self.subsystemLookup.update({b'crazy': CrazySubsystem})
|
||||
|
||||
|
||||
def global_foo(self, data):
|
||||
self.globalRequests['foo'] = data
|
||||
return 1
|
||||
|
||||
|
||||
def global_foo_2(self, data):
|
||||
self.globalRequests['foo_2'] = data
|
||||
return 1, b'data'
|
||||
|
||||
|
||||
def global_tcpip_forward(self, data):
|
||||
host, port = forwarding.unpackGlobal_tcpip_forward(data)
|
||||
try:
|
||||
listener = reactor.listenTCP(
|
||||
port, forwarding.SSHListenForwardingFactory(
|
||||
self.conn, (host, port),
|
||||
forwarding.SSHListenServerForwardingChannel),
|
||||
interface=host)
|
||||
except:
|
||||
log.err(None, "something went wrong with remote->local forwarding")
|
||||
return 0
|
||||
else:
|
||||
self.listeners[(host, port)] = listener
|
||||
return 1
|
||||
|
||||
|
||||
def global_cancel_tcpip_forward(self, data):
|
||||
host, port = forwarding.unpackGlobal_tcpip_forward(data)
|
||||
listener = self.listeners.get((host, port), None)
|
||||
if not listener:
|
||||
return 0
|
||||
del self.listeners[(host, port)]
|
||||
listener.stopListening()
|
||||
return 1
|
||||
|
||||
|
||||
def logout(self):
|
||||
self.loggedOut = True
|
||||
for listener in self.listeners.values():
|
||||
log.msg('stopListening %s' % listener)
|
||||
listener.stopListening()
|
||||
|
||||
|
||||
|
||||
class ConchSessionForTestAvatar(object):
|
||||
"""
|
||||
An ISession adapter for ConchTestAvatar.
|
||||
"""
|
||||
def __init__(self, avatar):
|
||||
"""
|
||||
Initialize the session and create a reference to it on the avatar for
|
||||
later inspection.
|
||||
"""
|
||||
self.avatar = avatar
|
||||
self.avatar._testSession = self
|
||||
self.cmd = None
|
||||
self.proto = None
|
||||
self.ptyReq = False
|
||||
self.eof = 0
|
||||
self.onClose = defer.Deferred()
|
||||
|
||||
|
||||
def getPty(self, term, windowSize, attrs):
|
||||
log.msg('pty req')
|
||||
self._terminalType = term
|
||||
self._windowSize = windowSize
|
||||
self.ptyReq = True
|
||||
|
||||
|
||||
def openShell(self, proto):
|
||||
log.msg('opening shell')
|
||||
self.proto = proto
|
||||
EchoTransport(proto)
|
||||
self.cmd = b'shell'
|
||||
|
||||
|
||||
def execCommand(self, proto, cmd):
|
||||
self.cmd = cmd
|
||||
self.proto = proto
|
||||
f = cmd.split()[0]
|
||||
if f == b'false':
|
||||
t = FalseTransport(proto)
|
||||
# Avoid disconnecting this immediately. If the channel is closed
|
||||
# before execCommand even returns the caller gets confused.
|
||||
reactor.callLater(0, t.loseConnection)
|
||||
elif f == b'echo':
|
||||
t = EchoTransport(proto)
|
||||
t.write(cmd[5:])
|
||||
t.loseConnection()
|
||||
elif f == b'secho':
|
||||
t = SuperEchoTransport(proto)
|
||||
t.write(cmd[6:])
|
||||
t.loseConnection()
|
||||
elif f == b'eecho':
|
||||
t = ErrEchoTransport(proto)
|
||||
t.write(cmd[6:])
|
||||
t.loseConnection()
|
||||
else:
|
||||
raise error.ConchError('bad exec')
|
||||
self.avatar.conn.transport.expectedLoseConnection = 1
|
||||
|
||||
|
||||
def eofReceived(self):
|
||||
self.eof = 1
|
||||
|
||||
|
||||
def closed(self):
|
||||
log.msg('closed cmd "%s"' % self.cmd)
|
||||
self.remoteWindowLeftAtClose = self.proto.session.remoteWindowLeft
|
||||
self.onClose.callback(None)
|
||||
|
||||
from twisted.python import components
|
||||
|
||||
if cryptography:
|
||||
components.registerAdapter(ConchSessionForTestAvatar, ConchTestAvatar,
|
||||
session.ISession)
|
||||
|
||||
class CrazySubsystem(protocol.Protocol):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
pass
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
good ... good
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class FalseTransport:
|
||||
"""
|
||||
False transport should act like a /bin/false execution, i.e. just exit with
|
||||
nonzero status, writing nothing to the terminal.
|
||||
|
||||
@ivar proto: The protocol associated with this transport.
|
||||
@ivar closed: A flag tracking whether C{loseConnection} has been called yet.
|
||||
"""
|
||||
|
||||
def __init__(self, p):
|
||||
"""
|
||||
@type p L{twisted.conch.ssh.session.SSHSessionProcessProtocol} instance
|
||||
"""
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Disconnect the protocol associated with this transport.
|
||||
"""
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(255, None, None)))
|
||||
|
||||
|
||||
|
||||
class EchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
log.msg(repr(data))
|
||||
self.proto.outReceived(data)
|
||||
self.proto.outReceived(b'\r\n')
|
||||
if b'\x00' in data: # mimic 'exit' for the shell test
|
||||
self.loseConnection()
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
class ErrEchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
self.proto.errReceived(data)
|
||||
self.proto.errReceived(b'\r\n')
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
class SuperEchoTransport:
|
||||
|
||||
def __init__(self, p):
|
||||
self.proto = p
|
||||
p.makeConnection(self)
|
||||
self.closed = 0
|
||||
|
||||
def write(self, data):
|
||||
self.proto.outReceived(data)
|
||||
self.proto.outReceived(b'\r\n')
|
||||
self.proto.errReceived(data)
|
||||
self.proto.errReceived(b'\r\n')
|
||||
|
||||
def loseConnection(self):
|
||||
if self.closed: return
|
||||
self.closed = 1
|
||||
self.proto.inConnectionLost()
|
||||
self.proto.outConnectionLost()
|
||||
self.proto.errConnectionLost()
|
||||
self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
|
||||
|
||||
|
||||
if cryptography is not None and pyasn1 is not None:
|
||||
from twisted.conch import checkers
|
||||
from twisted.conch.ssh import channel, connection, factory, keys
|
||||
from twisted.conch.ssh import transport, userauth
|
||||
|
||||
class ConchTestPasswordChecker:
|
||||
credentialInterfaces = checkers.IUsernamePassword,
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
if credentials.username == b'testuser' and credentials.password == b'testpass':
|
||||
return defer.succeed(credentials.username)
|
||||
return defer.fail(Exception("Bad credentials"))
|
||||
|
||||
|
||||
class ConchTestSSHChecker(checkers.SSHProtocolChecker):
|
||||
|
||||
def areDone(self, avatarId):
|
||||
if avatarId != b'testuser' or len(self.successfulCredentials[avatarId]) < 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ConchTestServerFactory(factory.SSHFactory):
|
||||
noisy = 0
|
||||
|
||||
services = {
|
||||
b'ssh-userauth':userauth.SSHUserAuthServer,
|
||||
b'ssh-connection':connection.SSHConnection
|
||||
}
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = ConchTestServer()
|
||||
proto.supportedPublicKeys = self.privateKeys.keys()
|
||||
proto.factory = self
|
||||
|
||||
if hasattr(self, 'expectedLoseConnection'):
|
||||
proto.expectedLoseConnection = self.expectedLoseConnection
|
||||
|
||||
self.proto = proto
|
||||
return proto
|
||||
|
||||
def getPublicKeys(self):
|
||||
return {
|
||||
b'ssh-rsa': keys.Key.fromString(publicRSA_openssh),
|
||||
b'ssh-dss': keys.Key.fromString(publicDSA_openssh)
|
||||
}
|
||||
|
||||
def getPrivateKeys(self):
|
||||
return {
|
||||
b'ssh-rsa': keys.Key.fromString(privateRSA_openssh),
|
||||
b'ssh-dss': keys.Key.fromString(privateDSA_openssh)
|
||||
}
|
||||
|
||||
def getPrimes(self):
|
||||
"""
|
||||
Diffie-Hellman primes that can be used for the
|
||||
diffie-hellman-group-exchange-sha1 key exchange.
|
||||
|
||||
@return: The primes and generators.
|
||||
@rtype: L{dict} mapping the key size to a C{list} of
|
||||
C{(generator, prime)} tupple.
|
||||
"""
|
||||
# In these tests, we hardwire the prime values to those defined by
|
||||
# the diffie-hellman-group14-sha1 key exchange algorithm, to avoid
|
||||
# requiring a moduli file when running tests.
|
||||
# See OpenSSHFactory.getPrimes.
|
||||
return {
|
||||
2048: [
|
||||
_kex.getDHGeneratorAndPrime(
|
||||
b'diffie-hellman-group14-sha1')]
|
||||
}
|
||||
|
||||
def getService(self, trans, name):
|
||||
return factory.SSHFactory.getService(self, trans, name)
|
||||
|
||||
class ConchTestBase:
|
||||
|
||||
done = 0
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.done:
|
||||
return
|
||||
if not hasattr(self, 'expectedLoseConnection'):
|
||||
raise unittest.FailTest(
|
||||
'unexpectedly lost connection %s\n%s' % (self, reason))
|
||||
self.done = 1
|
||||
|
||||
def receiveError(self, reasonCode, desc):
|
||||
self.expectedLoseConnection = 1
|
||||
# Some versions of OpenSSH (for example, OpenSSH_5.3p1) will
|
||||
# send a DISCONNECT_BY_APPLICATION error before closing the
|
||||
# connection. Other, older versions (for example,
|
||||
# OpenSSH_5.1p1), won't. So accept this particular error here,
|
||||
# but no others.
|
||||
if reasonCode != transport.DISCONNECT_BY_APPLICATION:
|
||||
log.err(
|
||||
Exception(
|
||||
'got disconnect for %s: reason %s, desc: %s' % (
|
||||
self, reasonCode, desc)))
|
||||
self.loseConnection()
|
||||
|
||||
def receiveUnimplemented(self, seqID):
|
||||
raise unittest.FailTest('got unimplemented: seqid %s' % (seqID,))
|
||||
self.expectedLoseConnection = 1
|
||||
self.loseConnection()
|
||||
|
||||
class ConchTestServer(ConchTestBase, transport.SSHServerTransport):
|
||||
|
||||
def connectionLost(self, reason):
|
||||
ConchTestBase.connectionLost(self, reason)
|
||||
transport.SSHServerTransport.connectionLost(self, reason)
|
||||
|
||||
|
||||
class ConchTestClient(ConchTestBase, transport.SSHClientTransport):
|
||||
"""
|
||||
@ivar _channelFactory: A callable which accepts an SSH connection and
|
||||
returns a channel which will be attached to a new channel on that
|
||||
connection.
|
||||
"""
|
||||
def __init__(self, channelFactory):
|
||||
self._channelFactory = channelFactory
|
||||
|
||||
def connectionLost(self, reason):
|
||||
ConchTestBase.connectionLost(self, reason)
|
||||
transport.SSHClientTransport.connectionLost(self, reason)
|
||||
|
||||
def verifyHostKey(self, key, fp):
|
||||
keyMatch = key == keys.Key.fromString(publicRSA_openssh).blob()
|
||||
fingerprintMatch = (
|
||||
fp == b'85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da')
|
||||
if keyMatch and fingerprintMatch:
|
||||
return defer.succeed(1)
|
||||
return defer.fail(Exception("Key or fingerprint mismatch"))
|
||||
|
||||
def connectionSecure(self):
|
||||
self.requestService(ConchTestClientAuth(b'testuser',
|
||||
ConchTestClientConnection(self._channelFactory)))
|
||||
|
||||
|
||||
class ConchTestClientAuth(userauth.SSHUserAuthClient):
|
||||
|
||||
hasTriedNone = 0 # have we tried the 'none' auth yet?
|
||||
canSucceedPublicKey = 0 # can we succeed with this yet?
|
||||
canSucceedPassword = 0
|
||||
|
||||
def ssh_USERAUTH_SUCCESS(self, packet):
|
||||
if not self.canSucceedPassword and self.canSucceedPublicKey:
|
||||
raise unittest.FailTest(
|
||||
'got USERAUTH_SUCCESS before password and publickey')
|
||||
userauth.SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
|
||||
|
||||
def getPassword(self):
|
||||
self.canSucceedPassword = 1
|
||||
return defer.succeed(b'testpass')
|
||||
|
||||
def getPrivateKey(self):
|
||||
self.canSucceedPublicKey = 1
|
||||
return defer.succeed(keys.Key.fromString(privateDSA_openssh))
|
||||
|
||||
def getPublicKey(self):
|
||||
return keys.Key.fromString(publicDSA_openssh)
|
||||
|
||||
|
||||
class ConchTestClientConnection(connection.SSHConnection):
|
||||
"""
|
||||
@ivar _completed: A L{Deferred} which will be fired when the number of
|
||||
results collected reaches C{totalResults}.
|
||||
"""
|
||||
name = b'ssh-connection'
|
||||
results = 0
|
||||
totalResults = 8
|
||||
|
||||
def __init__(self, channelFactory):
|
||||
connection.SSHConnection.__init__(self)
|
||||
self._channelFactory = channelFactory
|
||||
|
||||
def serviceStarted(self):
|
||||
self.openChannel(self._channelFactory(conn=self))
|
||||
|
||||
|
||||
class SSHTestChannel(channel.SSHChannel):
|
||||
|
||||
def __init__(self, name, opened, *args, **kwargs):
|
||||
self.name = name
|
||||
self._opened = opened
|
||||
self.received = []
|
||||
self.receivedExt = []
|
||||
self.onClose = defer.Deferred()
|
||||
channel.SSHChannel.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
def openFailed(self, reason):
|
||||
self._opened.errback(reason)
|
||||
|
||||
|
||||
def channelOpen(self, ignore):
|
||||
self._opened.callback(self)
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.received.append(data)
|
||||
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
if dataType == connection.EXTENDED_DATA_STDERR:
|
||||
self.receivedExt.append(data)
|
||||
else:
|
||||
log.msg("Unrecognized extended data: %r" % (dataType,))
|
||||
|
||||
|
||||
def request_exit_status(self, status):
|
||||
[self.status] = struct.unpack('>L', status)
|
||||
|
||||
|
||||
def eofReceived(self):
|
||||
self.eofCalled = True
|
||||
|
||||
|
||||
def closed(self):
|
||||
self.onClose.callback(None)
|
||||
|
||||
|
||||
def conchTestPublicKeyChecker():
|
||||
"""
|
||||
Produces a SSHPublicKeyChecker with an in-memory key mapping with
|
||||
a single use: 'testuser'
|
||||
|
||||
@return: L{twisted.conch.checkers.SSHPublicKeyChecker}
|
||||
"""
|
||||
conchTestPublicKeyDB = checkers.InMemorySSHKeyDB(
|
||||
{b'testuser': [keys.Key.fromString(publicDSA_openssh)]})
|
||||
return checkers.SSHPublicKeyChecker(conchTestPublicKeyDB)
|
||||
|
||||
|
||||
|
||||
class SSHProtocolTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for communication between L{SSHServerTransport} and
|
||||
L{SSHClientTransport}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
def _ourServerOurClientTest(self, name=b'session', **kwargs):
|
||||
"""
|
||||
Create a connected SSH client and server protocol pair and return a
|
||||
L{Deferred} which fires with an L{SSHTestChannel} instance connected to
|
||||
a channel on that SSH connection.
|
||||
"""
|
||||
result = defer.Deferred()
|
||||
self.realm = ConchTestRealm(b'testuser')
|
||||
p = portal.Portal(self.realm)
|
||||
sshpc = ConchTestSSHChecker()
|
||||
sshpc.registerChecker(ConchTestPasswordChecker())
|
||||
sshpc.registerChecker(conchTestPublicKeyChecker())
|
||||
p.registerChecker(sshpc)
|
||||
fac = ConchTestServerFactory()
|
||||
fac.portal = p
|
||||
fac.startFactory()
|
||||
self.server = fac.buildProtocol(None)
|
||||
self.clientTransport = LoopbackRelay(self.server)
|
||||
self.client = ConchTestClient(
|
||||
lambda conn: SSHTestChannel(name, result, conn=conn, **kwargs))
|
||||
|
||||
self.serverTransport = LoopbackRelay(self.client)
|
||||
|
||||
self.server.makeConnection(self.serverTransport)
|
||||
self.client.makeConnection(self.clientTransport)
|
||||
return result
|
||||
|
||||
|
||||
def test_subsystemsAndGlobalRequests(self):
|
||||
"""
|
||||
Run the Conch server against the Conch client. Set up several different
|
||||
channels which exercise different behaviors and wait for them to
|
||||
complete. Verify that the channels with errors log them.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbSubsystem(channel):
|
||||
self.channel = channel
|
||||
return self.assertFailure(
|
||||
channel.conn.sendRequest(
|
||||
channel, b'subsystem', common.NS(b'not-crazy'), 1),
|
||||
Exception)
|
||||
channel.addCallback(cbSubsystem)
|
||||
|
||||
def cbNotCrazyFailed(ignored):
|
||||
channel = self.channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'subsystem', common.NS(b'crazy'), 1)
|
||||
channel.addCallback(cbNotCrazyFailed)
|
||||
|
||||
def cbGlobalRequests(ignored):
|
||||
channel = self.channel
|
||||
d1 = channel.conn.sendGlobalRequest(b'foo', b'bar', 1)
|
||||
|
||||
d2 = channel.conn.sendGlobalRequest(b'foo-2', b'bar2', 1)
|
||||
d2.addCallback(self.assertEqual, b'data')
|
||||
|
||||
d3 = self.assertFailure(
|
||||
channel.conn.sendGlobalRequest(b'bar', b'foo', 1),
|
||||
Exception)
|
||||
|
||||
return defer.gatherResults([d1, d2, d3])
|
||||
channel.addCallback(cbGlobalRequests)
|
||||
|
||||
def disconnect(ignored):
|
||||
self.assertEqual(
|
||||
self.realm.avatar.globalRequests,
|
||||
{"foo": b"bar", "foo_2": b"bar2"})
|
||||
channel = self.channel
|
||||
channel.conn.transport.expectedLoseConnection = True
|
||||
channel.conn.serviceStopped()
|
||||
channel.loseConnection()
|
||||
channel.addCallback(disconnect)
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
def test_shell(self):
|
||||
"""
|
||||
L{SSHChannel.sendRequest} can open a shell with a I{pty-req} request,
|
||||
specifying a terminal type and window size.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
data = session.packRequest_pty_req(
|
||||
b'conch-test-term', (24, 80, 0, 0), b'')
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(channel, b'pty-req', data, 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbPty(ignored):
|
||||
# The server-side object corresponding to our client side channel.
|
||||
session = self.realm.avatar.conn.channels[0].session
|
||||
self.assertIs(session.avatar, self.realm.avatar)
|
||||
self.assertEqual(session._terminalType, b'conch-test-term')
|
||||
self.assertEqual(session._windowSize, (24, 80, 0, 0))
|
||||
self.assertTrue(session.ptyReq)
|
||||
channel = self.channel
|
||||
return channel.conn.sendRequest(channel, b'shell', b'', 1)
|
||||
channel.addCallback(cbPty)
|
||||
|
||||
def cbShell(ignored):
|
||||
self.channel.write(b'testing the shell!\x00')
|
||||
self.channel.conn.sendEOF(self.channel)
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbShell)
|
||||
|
||||
def cbExited(ignored):
|
||||
if self.channel.status != 0:
|
||||
log.msg(
|
||||
'shell exit status was not 0: %i' % (self.channel.status,))
|
||||
self.assertEqual(
|
||||
b"".join(self.channel.received),
|
||||
b'testing the shell!\x00\r\n')
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertTrue(
|
||||
self.realm.avatar._testSession.eof)
|
||||
channel.addCallback(cbExited)
|
||||
return channel
|
||||
|
||||
|
||||
def test_failedExec(self):
|
||||
"""
|
||||
If L{SSHChannel.sendRequest} issues an exec which the server responds to
|
||||
with an error, the L{Deferred} it returns fires its errback.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return self.assertFailure(
|
||||
channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'jumboliah'), 1),
|
||||
Exception)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbFailed(ignored):
|
||||
# The server logs this exception when it cannot perform the
|
||||
# requested exec.
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(errors[0].value.args, ('bad exec', None))
|
||||
channel.addCallback(cbFailed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_falseChannel(self):
|
||||
"""
|
||||
When the process started by a L{SSHChannel.sendRequest} exec request
|
||||
exits, the exit status is reported to the channel.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest()
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'false'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return self.channel.onClose
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
# No data is expected
|
||||
self.assertEqual(self.channel.received, [])
|
||||
self.assertNotEqual(self.channel.status, 0)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_errorChannel(self):
|
||||
"""
|
||||
Bytes sent over the extended channel for stderr data are delivered to
|
||||
the channel's C{extReceived} method.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'eecho hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.received, [])
|
||||
self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertEqual(self.channel.localWindowLeft, 4)
|
||||
self.assertEqual(
|
||||
self.channel.localWindowLeft,
|
||||
self.realm.avatar._testSession.remoteWindowLeftAtClose)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_unknownChannel(self):
|
||||
"""
|
||||
When an attempt is made to open an unknown channel type, the L{Deferred}
|
||||
returned by L{SSHChannel.sendRequest} fires its errback.
|
||||
"""
|
||||
d = self.assertFailure(
|
||||
self._ourServerOurClientTest(b'crazy-unknown-channel'), Exception)
|
||||
def cbFailed(ignored):
|
||||
errors = self.flushLoggedErrors(error.ConchError)
|
||||
self.assertEqual(errors[0].value.args, (3, 'unknown channel'))
|
||||
self.assertEqual(len(errors), 1)
|
||||
d.addCallback(cbFailed)
|
||||
return d
|
||||
|
||||
|
||||
def test_maxPacket(self):
|
||||
"""
|
||||
An L{SSHChannel} can be configured with a maximum packet size to
|
||||
receive.
|
||||
"""
|
||||
# localWindow needs to be at least 11 otherwise the assertion about it
|
||||
# in cbClosed is invalid.
|
||||
channel = self._ourServerOurClientTest(
|
||||
localWindow=11, localMaxPacket=1)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'secho hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbExec(ignored):
|
||||
return self.channel.onClose
|
||||
channel.addCallback(cbExec)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
|
||||
self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
|
||||
self.assertEqual(self.channel.localWindowLeft, 11)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
def test_echo(self):
|
||||
"""
|
||||
Normal standard out bytes are sent to the channel's C{dataReceived}
|
||||
method.
|
||||
"""
|
||||
channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
|
||||
|
||||
def cbChannel(channel):
|
||||
self.channel = channel
|
||||
return channel.conn.sendRequest(
|
||||
channel, b'exec', common.NS(b'echo hello'), 1)
|
||||
channel.addCallback(cbChannel)
|
||||
|
||||
def cbEcho(ignored):
|
||||
return defer.gatherResults([
|
||||
self.channel.onClose,
|
||||
self.realm.avatar._testSession.onClose])
|
||||
channel.addCallback(cbEcho)
|
||||
|
||||
def cbClosed(ignored):
|
||||
self.assertEqual(self.channel.status, 0)
|
||||
self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
|
||||
self.assertEqual(self.channel.localWindowLeft, 4)
|
||||
self.assertTrue(self.channel.eofCalled)
|
||||
self.assertEqual(
|
||||
self.channel.localWindowLeft,
|
||||
self.realm.avatar._testSession.remoteWindowLeftAtClose)
|
||||
channel.addCallback(cbClosed)
|
||||
return channel
|
||||
|
||||
|
||||
|
||||
class SSHFactoryTests(unittest.TestCase):
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
def makeSSHFactory(self, primes=None):
|
||||
sshFactory = factory.SSHFactory()
|
||||
gpk = lambda: {'ssh-rsa' : keys.Key(None)}
|
||||
sshFactory.getPrimes = lambda: primes
|
||||
sshFactory.getPublicKeys = sshFactory.getPrivateKeys = gpk
|
||||
sshFactory.startFactory()
|
||||
return sshFactory
|
||||
|
||||
|
||||
def test_buildProtocol(self):
|
||||
"""
|
||||
By default, buildProtocol() constructs an instance of
|
||||
SSHServerTransport.
|
||||
"""
|
||||
factory = self.makeSSHFactory()
|
||||
protocol = factory.buildProtocol(None)
|
||||
self.assertIsInstance(protocol, transport.SSHServerTransport)
|
||||
|
||||
|
||||
def test_buildProtocolRespectsProtocol(self):
|
||||
"""
|
||||
buildProtocol() calls 'self.protocol()' to construct a protocol
|
||||
instance.
|
||||
"""
|
||||
calls = []
|
||||
def makeProtocol(*args):
|
||||
calls.append(args)
|
||||
return transport.SSHServerTransport()
|
||||
factory = self.makeSSHFactory()
|
||||
factory.protocol = makeProtocol
|
||||
factory.buildProtocol(None)
|
||||
self.assertEqual([()], calls)
|
||||
|
||||
|
||||
def test_buildProtocolNoPrimes(self):
|
||||
"""
|
||||
Group key exchanges are not supported when we don't have the primes
|
||||
database.
|
||||
"""
|
||||
f1 = self.makeSSHFactory(primes=None)
|
||||
|
||||
p1 = f1.buildProtocol(None)
|
||||
|
||||
self.assertNotIn(
|
||||
b'diffie-hellman-group-exchange-sha1', p1.supportedKeyExchanges)
|
||||
self.assertNotIn(
|
||||
b'diffie-hellman-group-exchange-sha256', p1.supportedKeyExchanges)
|
||||
|
||||
|
||||
def test_buildProtocolWithPrimes(self):
|
||||
"""
|
||||
Group key exchanges are supported when we have the primes database.
|
||||
"""
|
||||
f2 = self.makeSSHFactory(primes={1:(2,3)})
|
||||
|
||||
p2 = f2.buildProtocol(None)
|
||||
|
||||
self.assertIn(
|
||||
b'diffie-hellman-group-exchange-sha1', p2.supportedKeyExchanges)
|
||||
self.assertIn(
|
||||
b'diffie-hellman-group-exchange-sha256', p2.supportedKeyExchanges)
|
||||
|
||||
|
||||
|
||||
class MPTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{common.getMP}.
|
||||
|
||||
@cvar getMP: a method providing a MP parser.
|
||||
@type getMP: C{callable}
|
||||
"""
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
if cryptography:
|
||||
getMP = staticmethod(common.getMP)
|
||||
|
||||
def test_getMP(self):
|
||||
"""
|
||||
L{common.getMP} should parse the a multiple precision integer from a
|
||||
string: a 4-byte length followed by length bytes of the integer.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'),
|
||||
(1, b''))
|
||||
|
||||
|
||||
def test_getMPBigInteger(self):
|
||||
"""
|
||||
L{common.getMP} should be able to parse a big enough integer
|
||||
(that doesn't fit on one byte).
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x01\x02\x03\x04'),
|
||||
(16909060, b''))
|
||||
|
||||
|
||||
def test_multipleGetMP(self):
|
||||
"""
|
||||
L{common.getMP} has the ability to parse multiple integer in the same
|
||||
string.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'
|
||||
b'\x00\x00\x00\x04\x00\x00\x00\x02', 2),
|
||||
(1, 2, b''))
|
||||
|
||||
|
||||
def test_getMPRemainingData(self):
|
||||
"""
|
||||
When more data than needed is sent to L{common.getMP}, it should return
|
||||
the remaining data.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01foo'),
|
||||
(1, b'foo'))
|
||||
|
||||
|
||||
def test_notEnoughData(self):
|
||||
"""
|
||||
When the string passed to L{common.getMP} doesn't even make 5 bytes,
|
||||
it should raise a L{struct.error}.
|
||||
"""
|
||||
self.assertRaises(struct.error, self.getMP, b'\x02\x00')
|
||||
|
||||
|
||||
class GMPYInstallDeprecationTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the deprecation of former GMPY accidental public API.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "cannot run without cryptography"
|
||||
|
||||
def test_deprecated(self):
|
||||
"""
|
||||
L{twisted.conch.ssh.common.install} is deprecated.
|
||||
"""
|
||||
common.install()
|
||||
warnings = self.flushWarnings([self.test_deprecated])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(
|
||||
warnings[0]["message"],
|
||||
"twisted.conch.ssh.common.install was deprecated in Twisted 16.5.0"
|
||||
)
|
||||
152
venv/lib/python3.9/site-packages/twisted/conch/test/test_tap.py
Normal file
152
venv/lib/python3.9/site-packages/twisted/conch/test/test_tap.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.tap}.
|
||||
"""
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
|
||||
try:
|
||||
import pyasn1
|
||||
except ImportError:
|
||||
pyasn1 = None
|
||||
|
||||
try:
|
||||
from twisted.conch import unix
|
||||
except ImportError:
|
||||
unix = None
|
||||
|
||||
if cryptography and pyasn1 and unix:
|
||||
from twisted.conch import tap
|
||||
from twisted.conch.openssh_compat.factory import OpenSSHFactory
|
||||
|
||||
from twisted.application.internet import StreamServerEndpointService
|
||||
from twisted.cred import error
|
||||
from twisted.cred.credentials import ISSHPrivateKey
|
||||
from twisted.cred.credentials import IUsernamePassword, UsernamePassword
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
|
||||
class MakeServiceTests(TestCase):
|
||||
"""
|
||||
Tests for L{tap.makeService}.
|
||||
"""
|
||||
|
||||
if not cryptography:
|
||||
skip = "can't run without cryptography"
|
||||
|
||||
if not pyasn1:
|
||||
skip = "Cannot run without PyASN1"
|
||||
|
||||
if not unix:
|
||||
skip = "can't run on non-posix computers"
|
||||
|
||||
usernamePassword = (b'iamuser', b'thisispassword')
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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 = self.options['credCheckers'][-1]
|
||||
invalid = UsernamePassword(self.usernamePassword[0], 'fake')
|
||||
# Wrong password should raise error
|
||||
return self.assertFailure(
|
||||
checker.requestAvatarId(invalid), error.UnauthorizedLogin)
|
||||
|
||||
|
||||
def test_authSuccess(self):
|
||||
"""
|
||||
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 = self.options['credCheckers'][-1]
|
||||
correct = UsernamePassword(*self.usernamePassword)
|
||||
d = checker.requestAvatarId(correct)
|
||||
|
||||
def checkSuccess(username):
|
||||
self.assertEqual(username, correct.username)
|
||||
|
||||
return d.addCallback(checkSuccess)
|
||||
|
||||
|
||||
def test_checkers(self):
|
||||
"""
|
||||
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()),
|
||||
set([ISSHPrivateKey, IUsernamePassword]))
|
||||
|
|
@ -0,0 +1,811 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_telnet -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.conch.telnet}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from twisted.conch import telnet
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.python.compat import iterbytes
|
||||
|
||||
|
||||
@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 ('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)
|
||||
|
||||
|
||||
|
||||
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 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']))
|
||||
121
venv/lib/python3.9/site-packages/twisted/conch/test/test_text.py
Normal file
121
venv/lib/python3.9/site-packages/twisted/conch/test/test_text.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_text -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from twisted.conch.insults import text
|
||||
from twisted.conch.insults.text import attributes as A
|
||||
|
||||
|
||||
|
||||
class FormattedTextTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for assembling formatted text.
|
||||
"""
|
||||
def test_trivial(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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,93 @@
|
|||
# -*- test-case-name: twisted.conch.test.test_unix -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet.interfaces import IReactorProcess
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
|
||||
cryptography = requireModule("cryptography")
|
||||
unix = requireModule('twisted.conch.unix')
|
||||
|
||||
|
||||
|
||||
@implementer(IReactorProcess)
|
||||
class MockProcessSpawner(object):
|
||||
"""
|
||||
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})
|
||||
|
||||
|
||||
|
||||
class StubUnixConchUser(object):
|
||||
"""
|
||||
Enough of UnixConchUser to exercise SSHSessionForUnixConchUser in the
|
||||
tests below.
|
||||
"""
|
||||
|
||||
def __init__(self, homeDirectory):
|
||||
from .test_session import StubConnection, StubClient
|
||||
|
||||
self._homeDirectory = homeDirectory
|
||||
self.conn = StubConnection(transport=StubClient())
|
||||
|
||||
|
||||
def getUserGroupId(self):
|
||||
return (None, None)
|
||||
|
||||
|
||||
def getHomeDir(self):
|
||||
return self._homeDirectory
|
||||
|
||||
|
||||
def getShell(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TestSSHSessionForUnixConchUser(unittest.TestCase):
|
||||
|
||||
if cryptography is None:
|
||||
skip = "Cannot run without cryptography"
|
||||
elif unix is None:
|
||||
skip = "Unix system required"
|
||||
|
||||
|
||||
def testExecCommandEnvironment(self):
|
||||
"""
|
||||
C{execCommand} sets the C{HOME} environment variable to the avatar's home
|
||||
directory.
|
||||
"""
|
||||
mockReactor = MockProcessSpawner()
|
||||
homeDirectory = "/made/up/path/"
|
||||
avatar = StubUnixConchUser(homeDirectory)
|
||||
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'])
|
||||
|
|
@ -0,0 +1,906 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the implementation of the ssh-userauth service.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.cred.checkers import ICredentialsChecker
|
||||
from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
|
||||
from twisted.cred.credentials import IAnonymous
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.cred.portal import IRealm, Portal
|
||||
from twisted.conch.error import ConchError, ValidPublicKey
|
||||
from twisted.internet import defer, task
|
||||
from twisted.protocols import loopback
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
from twisted.python.compat import _bytesChr as chr
|
||||
|
||||
if requireModule('cryptography') and requireModule('pyasn1'):
|
||||
from twisted.conch.ssh.common import NS
|
||||
from twisted.conch.checkers import SSHProtocolChecker
|
||||
from twisted.conch.ssh import keys, userauth, transport
|
||||
from twisted.conch.test import keydata
|
||||
else:
|
||||
keys = None
|
||||
|
||||
|
||||
class transport:
|
||||
class SSHTransportBase:
|
||||
"""
|
||||
A stub class so that later class definitions won't die.
|
||||
"""
|
||||
|
||||
class userauth:
|
||||
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(object):
|
||||
"""
|
||||
A mock service, representing the other service offered by the server.
|
||||
"""
|
||||
name = b'nancy'
|
||||
|
||||
|
||||
def serviceStarted(self):
|
||||
pass
|
||||
|
||||
|
||||
class Factory(object):
|
||||
"""
|
||||
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(object):
|
||||
"""
|
||||
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(object):
|
||||
"""
|
||||
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(object):
|
||||
"""
|
||||
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(object):
|
||||
"""
|
||||
A simple checker which isn't supported by L{SSHUserAuthServer}.
|
||||
"""
|
||||
credentialInterfaces = (IAnonymous,)
|
||||
|
||||
|
||||
|
||||
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'), chr(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'), chr(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') + chr(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 +
|
||||
chr(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'), chr(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 +
|
||||
chr(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') + chr(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)
|
||||
+ chr(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 or PyASN1"
|
||||
|
||||
|
||||
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 or PyASN1"
|
||||
|
||||
|
||||
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,67 @@
|
|||
|
||||
"""
|
||||
Tests for the insults windowing module, L{twisted.conch.insults.window}.
|
||||
"""
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from twisted.conch.insults.window import TopWindow, ScrolledArea, TextOutput
|
||||
|
||||
|
||||
class TopWindowTests(TestCase):
|
||||
"""
|
||||
Tests for L{TopWindow}, the root window container class.
|
||||
"""
|
||||
|
||||
def test_paintScheduling(self):
|
||||
"""
|
||||
Verify that L{TopWindow.repaint} schedules an actual paint to occur
|
||||
using the scheduling object passed to its initializer.
|
||||
"""
|
||||
paints = []
|
||||
scheduled = []
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue