Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -0,0 +1 @@
'conch tests'

View 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']

View file

@ -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)

View file

@ -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))

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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())

View file

@ -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))

View file

@ -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'))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'')

View file

@ -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())

View file

@ -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))

View file

@ -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)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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])

View file

@ -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)

View file

@ -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))

View file

@ -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")],
})

View file

@ -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)

View file

@ -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'),
]

File diff suppressed because it is too large Load diff

View 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"
)

View 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]))

View file

@ -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']))

View 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

View file

@ -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'])

View file

@ -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')

View file

@ -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)