dotcloud悄无声息的远去了。我收到让转移应用的邮件才知道公司已经破产。
就在几天前,往dotcloud上部署微信机器人时还饶有兴趣瞎折腾了一下。当时发在cnodejs上
昨天,看到dotcloud提供一个secure shell ,忽然脑洞大开,我觉得又可以花式Tunnel了。
1 2 3 4 5 6 ┌─[reverland@reverland-R478-R429] - [~] - [2016 -01 -23 11 :55 :29 ] └─[0 ] <> export PATH=$PATH :$HOME /.local/bin ┌─[reverland@reverland-R478-R429] - [~] - [2016 -01 -23 11 :55 :34 ] └─[0 ] <> dcapp wechat/default run bash Connecting... [wechat/default]:~$
好奇,我能不用dcapp直接连接吗?什么原理?
于是翻了翻下载到dotcloudng的源码,开源软件就是好啊就是好。看到里头有个ssh命令
cmd = ssh_cmd (host_name, 'delete-cache' , deployment_name)
于是打印了一下,发现就是普通的ssh连接。于是抱着试试看的心理连了一次,还真可以。。
ssh -t -p 2222 -- wechat-default@sshforwarder.dotcloudapp .com TOKEN=t9Nd9ECasAgSD9UsYfcFwgysAF4bCL bash
翻翻源码没什么问题。但是,--
是啥?,token又是啥?
--
很快查到,为了防止bash解析后面的内容。但token呢?
env = 'TOKEN ={token }'.format (token =self.api.get_token()['token '])
搜索了下没找到,cctrl引用了cclib,看到get_token
1 def get_token(self):
"""
We use get_token to get the token.
"""
return self._token
那又是哪里设置了token呢?一眼看到上面的set\_token
。。。
1 2 3 4 5 def set_token (self, token) : """ We use set_token to set the token. """ self._token = token
检查set_token是在api init的时候
看看Api类,就抱着试试看的心理用试验了下。。。然而401?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var getSshToken = new Promise ((resolve, reject) => { var req = https.request({ method: 'POST' , path: '/token/' , hostname: 'api.dotcloudapp.com' , headers: { 'User-Agent' : 'pycclib/1.6.2' , 'Content-Type' : 'application/x-www-form-urlencoded' , 'Content-length' : 0 , } }, (res)=> { resolve(res); } }); req.end(); })
我在想要不要把mitmproxy打开看看呢。忽然在文件中赫然看到个DEBUG标志,于是打开,清晰看到几次请求。发现第一次请求是不带任何参数的,就是401,在header中返回了一个sshtoken。紧接着第二次请求。这次http header中Authorization中多了一些东西:
ccssh signature=rqsolg/L43mTokqnwVCgfGpCxxxxxxxxxxxxxvsdv6HxXiyXkmEAg6kKvOHSjFhCprq2AuDQbU2Z7DHUcryu9bVRmBQvNOd2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/ 92 uN4C6aUkXqCmlp16G0VC2qqE/QrEuvO72OXeMC8tL4RrU3Qn7tRzablDo2sNaCXkXMcjMtqM+DpuzqbOHZnn7lEwynbCPOtRGaGYnVRQtxxxxxxxxxxxufi6oxomKGk/ 6 ch8C7yjEE9hfbbqFcXBZQw==,fingerprint=c6: xx: 92 :8 a: 86 :xx: 6 b: af: fe: xx: 19 :62 :1 b: xx: 2 b: f0,sshtoken=unBVe7F36pCfVhtZEmPCaT,email=xxx@linuxer .me
虽然公钥和数据签名也没什么影响,还是打上码= =
sshtoken是第一步在response header中www-authenticate中给的,其他的呢。
email很显然。。fingerprint,我自己的太熟悉了。。signature是啥?
看了下源码,发现是这个函数生成的signature
1 signature = sign_token(key_path, fingerprint, sshtoken)
数据签名函数简化如下,把错误处理去掉了,还懒得管缩进。
1 def sign_token(key_path, fingerprint, data):
# from agent
pkey = get_key_from_agent(fingerprint)
# paramiko is inconsistent here in that the agent's key
# returns Message objects for 'sign_ssh_data' whereas RSAKey
# objects returns byte strings.
# Workaround: cast both return values to string and build a
# new Message object
s = str(pkey.sign_ssh_data(data))
m = Message(s)
m.rewind()
m.get_string() # == 'ssh-rsa':
return base64.b64encode(m.get_string())
于是自己查看文档试验了一下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 ┌─[reverland@reverland-R478-R429] - [~/tmp/dcwall] - [2016 -01 -23 01 :05 :51 ] └─[0 ] <> ssh -v -t -p 2222 -- wechat-default@sshforwarder.dotcloudapp.com TOKEN=tv8NczygRPK6cgp78azgXyKKrX9KPN bash OpenSSH_6.6.1 , OpenSSL 1.0 .1 f 6 Jan 2014 debug1: Reading configuration data /home/reverland/.ssh/config debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 19 : Applying options for * debug1: Connecting to sshforwarder.dotcloudapp.com [130.211 .165.15 ] port 2222 . debug1: fd 3 clearing O_NONBLOCK debug1: Connection established. debug1: identity file /home/reverland/.ssh/id_rsa type 1 debug1: identity file /home/reverland/.ssh/id_rsa-cert type -1 debug1: identity file /home/reverland/.ssh/id_dsa type 2 debug1: identity file /home/reverland/.ssh/id_dsa-cert type -1 debug1: identity file /home/reverland/.ssh/id_ecdsa type -1 debug1: identity file /home/reverland/.ssh/id_ecdsa-cert type -1 debug1: identity file /home/reverland/.ssh/id_ed25519 type -1 debug1: identity file /home/reverland/.ssh/id_ed25519-cert type -1 debug1: Enabling compatibility mode for protocol 2.0 debug1: Local version string SSH-2.0 -OpenSSH_6.6.1 p1 Ubuntu-2 ubuntu2.4 debug1: Remote protocol version 2.0 , remote software version Twisted debug1: no match: Twisted debug1: SSH2_MSG_KEXINIT sent debug1: SSH2_MSG_KEXINIT received debug1: kex: server->client aes128-ctr hmac-md5 none debug1: kex: client->server aes128-ctr hmac-md5 none debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(1024 <3072 <8192 ) sent debug1: expecting SSH2_MSG_KEX_DH_GEX_GROUP debug1: SSH2_MSG_KEX_DH_GEX_INIT sent debug1: expecting SSH2_MSG_KEX_DH_GEX_REPLY debug1: Server host key: RSA 5 a:83 :13 :7 c:d7:a1:cb:7 c:ec:29 :99 :91 :e4:bc:9 d:01 debug1: Host '[sshforwarder.dotcloudapp.com]:2222' is known and matches the RSA host key. debug1: Found key in /home/reverland/.ssh/known_hosts:3689 debug1: ssh_rsa_verify: signature correct debug1: SSH2_MSG_NEWKEYS sent debug1: expecting SSH2_MSG_NEWKEYS debug1: SSH2_MSG_NEWKEYS received debug1: SSH2_MSG_SERVICE_REQUEST sent debug1: SSH2_MSG_SERVICE_ACCEPT received debug1: Authentications that can continue : publickey debug1: Next authentication method: publickey debug1: Offering DSA public key: /home/reverland/.ssh/id_dsa debug1: Authentications that can continue : publickey debug1: Offering RSA public key: /home/reverland/.ssh/id_rsa debug1: Server accepts key: pkalg ssh-rsa blen 279 debug1: Authentication succeeded (publickey). Authenticated to sshforwarder.dotcloudapp.com ([130.211 .165.15 ]:2222 ). debug1: channel 0 : new [client-session] debug1: Entering interactive session. debug1: Sending environment. debug1: Sending env LC_IDENTIFICATION = zh_CN.UTF-8 debug1: Sending env LC_TIME = zh_CN.UTF-8 debug1: Sending env LC_NUMERIC = zh_CN.UTF-8 debug1: Sending env LC_PAPER = zh_CN.UTF-8 debug1: Sending env LC_MEASUREMENT = zh_CN.UTF-8 debug1: Sending env LC_ADDRESS = zh_CN.UTF-8 debug1: Sending env LC_MONETARY = zh_CN.UTF-8 debug1: Sending env LANG = en_US.UTF-8 debug1: Sending env LC_NAME = zh_CN.UTF-8 debug1: Sending env LC_TELEPHONE = zh_CN.UTF-8 debug1: Sending env LC_CTYPE = en_US.UTF-8 debug1: Sending command : TOKEN=tv8NczygRPK6cgp78azgXyKKrX9KPN bash Connecting... [wechat/default]:~$
想了想这个认证过程。
https请求服务器,得到sshtoken
用私钥给sshtoken的sha1哈希签名,连同公钥fingerprint,email,sshtoken一并发送给服务器
(这一步是我猜的)服务器验证fingerprint身份(之前dcuser时应该已经密码认证传过公钥,待验证),服务器使用客户公钥解密签名,将解密得到的哈希和sshtoken的sha1哈希进行对比,实现身份验证和sshtoken 验证。返回再下一步ssh连接时要传递的token
客户端ssh连接forward.dotcloudapp.com,认证通过后,服务器端需要检查Token的值来启动程序实例。为什么要检查呢?我猜,因为dotcloud免费用户控制只能运行一个Worker实例。。。
于是,自己实现了下这个过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 var exec = require ('child_process' ).exec;var https = require ('https' );var EMAIL = 'sa@linuxer.me' ;var getSshToken = new Promise ((resolve, reject) => { var req = https.request({ method: 'POST' , path: '/token/' , hostname: 'api.dotcloudapp.com' , headers: { 'User-Agent' : 'pycclib/1.6.2' , 'Content-Type' : 'application/x-www-form-urlencoded' , 'Content-length' : 0 , } }, (res)=> { if ('www-authenticate' in res.headers) { var result = /sshtoken=(.+)$/mg .exec(res.headers['www-authenticate' ]) if (!result) { reject("fail to get ssh token" ); } var sshtoken = result[1 ]; resolve(sshtoken); } }); req.end(); }) function getAuth (sshtoken ) { var p1 = getSignature(sshtoken); var p2 = getFingerPrint(); var p3 = Promise .all([p1, p2]).then((k)=>{ return new Promise ((resolve, reject)=>{ var authorization = 'ccssh ' ; authorization += ('signature=' + k[0 ] + ',' ); authorization += ('fingerprint=' + k[1 ] + ',' ); authorization += ('sshtoken=' + sshtoken + ',' ); authorization += ('email=' + EMAIL); console .log(authorization); resolve(authorization); }); }); return p3; } function getSignature (sshtoken ) { return new Promise ((resolve, reject)=>{ var cmd = 'echo -ne "' + sshtoken + '" | openssl sha1 -binary | openssl pkeyutl -sign -inkey ~/.ssh/id_rsa -pkeyopt digest:sha1' ; exec(cmd, { encoding: 'binary' , shell: '/bin/bash' , }, (error, stdout, stderr) => { if (error) { reject(error); } resolve(new Buffer(stdout, 'binary' ).toString('base64' )); }); }); } function getFingerPrint ( ) { return new Promise ((resolve, reject)=>{ exec('ssh-keygen -lf ~/.ssh/id_rsa.pub' , (error, stdout, stderr) => { if (error) { reject(error); } resolve(stdout.toString().split(' ' )[1 ]); }); }); } function getToken (authorization ) { var p = new Promise ((resolve, reject)=>{ var req = https.request({ method: 'POST' , path: '/token/' , hostname: 'api.dotcloudapp.com' , headers: { 'User-Agent' : 'pycclib/1.6.2' , 'Content-Type' : 'application/x-www-form-urlencoded' , 'Content-length' : 0 , 'Authorization' : authorization, } }, (res)=> { if (res.statusCode != 200 ) { reject("fail to get token" ); } var data = '' ; res.on('data' , (chunk)=>{ data += chunk; }); res.on('end' , ()=>{ resolve(data); }) }); req.end(); }); return p; } getSshToken.then(getAuth).then(getToken).then(console .log).catch(console .error);
一点也不顺利:
我用的v5.0.0,看了看文档里赫然写着stderror是Buffer好么
1 2 3 4 5 6 callback Function called with the output when process terminates error Error stdout Buffer stderr Buffer `
然而实际上怎么是String…..是我理解不对么?
其次,echo在/bin/sh和/bin/bash中不是一回事,一个是内置命令,一个是单独程序。。。
再次,深刻体会到该binary的时候一定得binary,字符串在我这里只能utf-8。。再Buffer后完全不是之前的binary数据。被坑得半死不活。
最后,Cheers
1 2 3 4 5 [wechat/default]:~$ curl https://twitter.com | md5sum % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 70740 100 70740 0 0 219 k 0 --:--:-- --:--:-- --:--:-- 262 k9 f9f288dbfb5fbf379244ed9a75f7ebf -
不过tunnel还是没成功,不过也不是为了tunnel不是。
总结 Just for fun!
学习下dotcloud的cli认证原理
不知道heroku是不是类似的认证方式
我要好好研究下ssh forward原理。