Rev 846 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Pure-PHP ssh-agent client. |
||
5 | * |
||
6 | * {@internal See http://api.libssh.org/rfc/PROTOCOL.agent} |
||
7 | * |
||
8 | * PHP version 5 |
||
9 | * |
||
10 | * Here are some examples of how to use this library: |
||
11 | * <code> |
||
12 | * <?php |
||
13 | * include 'vendor/autoload.php'; |
||
14 | * |
||
15 | * $agent = new \phpseclib3\System\SSH\Agent(); |
||
16 | * |
||
17 | * $ssh = new \phpseclib3\Net\SSH2('www.domain.tld'); |
||
18 | * if (!$ssh->login('username', $agent)) { |
||
19 | * exit('Login Failed'); |
||
20 | * } |
||
21 | * |
||
22 | * echo $ssh->exec('pwd'); |
||
23 | * echo $ssh->exec('ls -la'); |
||
24 | * ?> |
||
25 | * </code> |
||
26 | * |
||
874 | daniel-mar | 27 | * @category System |
28 | * @package SSH\Agent |
||
827 | daniel-mar | 29 | * @author Jim Wigginton <terrafrost@php.net> |
30 | * @copyright 2014 Jim Wigginton |
||
31 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
32 | * @link http://phpseclib.sourceforge.net |
||
33 | */ |
||
34 | |||
35 | namespace phpseclib3\System\SSH; |
||
36 | |||
37 | use phpseclib3\Common\Functions\Strings; |
||
38 | use phpseclib3\Crypt\PublicKeyLoader; |
||
39 | use phpseclib3\Crypt\RSA; |
||
40 | use phpseclib3\Exception\BadConfigurationException; |
||
41 | use phpseclib3\System\SSH\Agent\Identity; |
||
42 | |||
43 | /** |
||
44 | * Pure-PHP ssh-agent client identity factory |
||
45 | * |
||
46 | * requestIdentities() method pumps out \phpseclib3\System\SSH\Agent\Identity objects |
||
47 | * |
||
874 | daniel-mar | 48 | * @package SSH\Agent |
827 | daniel-mar | 49 | * @author Jim Wigginton <terrafrost@php.net> |
874 | daniel-mar | 50 | * @access public |
827 | daniel-mar | 51 | */ |
52 | class Agent |
||
53 | { |
||
54 | use Common\Traits\ReadBytes; |
||
55 | |||
56 | // Message numbers |
||
57 | |||
58 | // to request SSH1 keys you have to use SSH_AGENTC_REQUEST_RSA_IDENTITIES (1) |
||
59 | const SSH_AGENTC_REQUEST_IDENTITIES = 11; |
||
60 | // this is the SSH2 response; the SSH1 response is SSH_AGENT_RSA_IDENTITIES_ANSWER (2). |
||
61 | const SSH_AGENT_IDENTITIES_ANSWER = 12; |
||
62 | // the SSH1 request is SSH_AGENTC_RSA_CHALLENGE (3) |
||
63 | const SSH_AGENTC_SIGN_REQUEST = 13; |
||
64 | // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4) |
||
65 | const SSH_AGENT_SIGN_RESPONSE = 14; |
||
66 | |||
67 | // Agent forwarding status |
||
68 | |||
69 | // no forwarding requested and not active |
||
70 | const FORWARD_NONE = 0; |
||
71 | // request agent forwarding when opportune |
||
72 | const FORWARD_REQUEST = 1; |
||
73 | // forwarding has been request and is active |
||
74 | const FORWARD_ACTIVE = 2; |
||
75 | |||
76 | /** |
||
77 | * Unused |
||
78 | */ |
||
79 | const SSH_AGENT_FAILURE = 5; |
||
80 | |||
81 | /** |
||
82 | * Socket Resource |
||
83 | * |
||
84 | * @var resource |
||
874 | daniel-mar | 85 | * @access private |
827 | daniel-mar | 86 | */ |
87 | private $fsock; |
||
88 | |||
89 | /** |
||
90 | * Agent forwarding status |
||
91 | * |
||
92 | * @var int |
||
874 | daniel-mar | 93 | * @access private |
827 | daniel-mar | 94 | */ |
95 | private $forward_status = self::FORWARD_NONE; |
||
96 | |||
97 | /** |
||
98 | * Buffer for accumulating forwarded authentication |
||
99 | * agent data arriving on SSH data channel destined |
||
100 | * for agent unix socket |
||
101 | * |
||
102 | * @var string |
||
874 | daniel-mar | 103 | * @access private |
827 | daniel-mar | 104 | */ |
105 | private $socket_buffer = ''; |
||
106 | |||
107 | /** |
||
108 | * Tracking the number of bytes we are expecting |
||
109 | * to arrive for the agent socket on the SSH data |
||
110 | * channel |
||
111 | * |
||
112 | * @var int |
||
874 | daniel-mar | 113 | * @access private |
827 | daniel-mar | 114 | */ |
115 | private $expected_bytes = 0; |
||
116 | |||
117 | /** |
||
118 | * The current request channel |
||
119 | * |
||
120 | * @var int |
||
874 | daniel-mar | 121 | * @access private |
827 | daniel-mar | 122 | */ |
123 | private $request_channel; |
||
124 | |||
125 | /** |
||
126 | * Default Constructor |
||
127 | * |
||
128 | * @return \phpseclib3\System\SSH\Agent |
||
129 | * @throws \phpseclib3\Exception\BadConfigurationException if SSH_AUTH_SOCK cannot be found |
||
130 | * @throws \RuntimeException on connection errors |
||
874 | daniel-mar | 131 | * @access public |
827 | daniel-mar | 132 | */ |
133 | public function __construct($address = null) |
||
134 | { |
||
135 | if (!$address) { |
||
136 | switch (true) { |
||
137 | case isset($_SERVER['SSH_AUTH_SOCK']): |
||
138 | $address = $_SERVER['SSH_AUTH_SOCK']; |
||
139 | break; |
||
140 | case isset($_ENV['SSH_AUTH_SOCK']): |
||
141 | $address = $_ENV['SSH_AUTH_SOCK']; |
||
142 | break; |
||
143 | default: |
||
144 | throw new BadConfigurationException('SSH_AUTH_SOCK not found'); |
||
145 | } |
||
146 | } |
||
147 | |||
148 | $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr); |
||
149 | if (!$this->fsock) { |
||
150 | throw new \RuntimeException("Unable to connect to ssh-agent (Error $errno: $errstr)"); |
||
151 | } |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * Request Identities |
||
156 | * |
||
157 | * See "2.5.2 Requesting a list of protocol 2 keys" |
||
158 | * Returns an array containing zero or more \phpseclib3\System\SSH\Agent\Identity objects |
||
159 | * |
||
160 | * @return array |
||
161 | * @throws \RuntimeException on receipt of unexpected packets |
||
874 | daniel-mar | 162 | * @access public |
827 | daniel-mar | 163 | */ |
164 | public function requestIdentities() |
||
165 | { |
||
166 | if (!$this->fsock) { |
||
167 | return []; |
||
168 | } |
||
169 | |||
170 | $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES); |
||
171 | if (strlen($packet) != fputs($this->fsock, $packet)) { |
||
172 | throw new \RuntimeException('Connection closed while requesting identities'); |
||
173 | } |
||
174 | |||
175 | $length = current(unpack('N', $this->readBytes(4))); |
||
176 | $packet = $this->readBytes($length); |
||
177 | |||
178 | list($type, $keyCount) = Strings::unpackSSH2('CN', $packet); |
||
179 | if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) { |
||
180 | throw new \RuntimeException('Unable to request identities'); |
||
181 | } |
||
182 | |||
183 | $identities = []; |
||
184 | for ($i = 0; $i < $keyCount; $i++) { |
||
185 | list($key_blob, $comment) = Strings::unpackSSH2('ss', $packet); |
||
186 | $temp = $key_blob; |
||
187 | list($key_type) = Strings::unpackSSH2('s', $temp); |
||
188 | switch ($key_type) { |
||
189 | case 'ssh-rsa': |
||
190 | case 'ssh-dss': |
||
191 | case 'ssh-ed25519': |
||
192 | case 'ecdsa-sha2-nistp256': |
||
193 | case 'ecdsa-sha2-nistp384': |
||
194 | case 'ecdsa-sha2-nistp521': |
||
195 | $key = PublicKeyLoader::load($key_type . ' ' . base64_encode($key_blob)); |
||
196 | } |
||
197 | // resources are passed by reference by default |
||
198 | if (isset($key)) { |
||
199 | $identity = (new Identity($this->fsock)) |
||
200 | ->withPublicKey($key) |
||
201 | ->withPublicKeyBlob($key_blob); |
||
202 | $identities[] = $identity; |
||
203 | unset($key); |
||
204 | } |
||
205 | } |
||
206 | |||
207 | return $identities; |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Signal that agent forwarding should |
||
212 | * be requested when a channel is opened |
||
213 | * |
||
214 | * @return void |
||
874 | daniel-mar | 215 | * @access public |
827 | daniel-mar | 216 | */ |
217 | public function startSSHForwarding() |
||
218 | { |
||
219 | if ($this->forward_status == self::FORWARD_NONE) { |
||
220 | $this->forward_status = self::FORWARD_REQUEST; |
||
221 | } |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * Request agent forwarding of remote server |
||
226 | * |
||
227 | * @param \phpseclib3\Net\SSH2 $ssh |
||
228 | * @return bool |
||
874 | daniel-mar | 229 | * @access private |
827 | daniel-mar | 230 | */ |
231 | private function request_forwarding($ssh) |
||
232 | { |
||
233 | if (!$ssh->requestAgentForwarding()) { |
||
234 | return false; |
||
235 | } |
||
236 | |||
237 | $this->forward_status = self::FORWARD_ACTIVE; |
||
238 | |||
239 | return true; |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * On successful channel open |
||
244 | * |
||
245 | * This method is called upon successful channel |
||
246 | * open to give the SSH Agent an opportunity |
||
247 | * to take further action. i.e. request agent forwarding |
||
248 | * |
||
249 | * @param \phpseclib3\Net\SSH2 $ssh |
||
874 | daniel-mar | 250 | * @access private |
827 | daniel-mar | 251 | */ |
252 | public function registerChannelOpen($ssh) |
||
253 | { |
||
254 | if ($this->forward_status == self::FORWARD_REQUEST) { |
||
255 | $this->request_forwarding($ssh); |
||
256 | } |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * Forward data to SSH Agent and return data reply |
||
261 | * |
||
262 | * @param string $data |
||
263 | * @return string Data from SSH Agent |
||
264 | * @throws \RuntimeException on connection errors |
||
874 | daniel-mar | 265 | * @access public |
827 | daniel-mar | 266 | */ |
267 | public function forwardData($data) |
||
268 | { |
||
269 | if ($this->expected_bytes > 0) { |
||
270 | $this->socket_buffer .= $data; |
||
271 | $this->expected_bytes -= strlen($data); |
||
272 | } else { |
||
273 | $agent_data_bytes = current(unpack('N', $data)); |
||
274 | $current_data_bytes = strlen($data); |
||
275 | $this->socket_buffer = $data; |
||
276 | if ($current_data_bytes != $agent_data_bytes + 4) { |
||
277 | $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes; |
||
278 | return false; |
||
279 | } |
||
280 | } |
||
281 | |||
282 | if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) { |
||
283 | throw new \RuntimeException('Connection closed attempting to forward data to SSH agent'); |
||
284 | } |
||
285 | |||
286 | $this->socket_buffer = ''; |
||
287 | $this->expected_bytes = 0; |
||
288 | |||
289 | $agent_reply_bytes = current(unpack('N', $this->readBytes(4))); |
||
290 | |||
291 | $agent_reply_data = $this->readBytes($agent_reply_bytes); |
||
292 | $agent_reply_data = current(unpack('a*', $agent_reply_data)); |
||
293 | |||
294 | return pack('Na*', $agent_reply_bytes, $agent_reply_data); |
||
295 | } |
||
296 | } |