1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Dazzle\SSH\Driver; |
4
|
|
|
|
5
|
|
|
use Dazzle\Event\BaseEventEmitterTrait; |
6
|
|
|
use Dazzle\Loop\Timer\TimerInterface; |
7
|
|
|
use Dazzle\Loop\LoopAwareTrait; |
8
|
|
|
use Dazzle\SSH\Driver\Sftp\SftpResource; |
9
|
|
|
use Dazzle\SSH\SSH2DriverInterface; |
10
|
|
|
use Dazzle\SSH\SSH2Interface; |
11
|
|
|
use Dazzle\SSH\SSH2ResourceInterface; |
12
|
|
|
use Dazzle\Throwable\Exception\Logic\ResourceUndefinedException; |
13
|
|
|
use Dazzle\Throwable\Exception\Runtime\ExecutionException; |
14
|
|
|
use Error; |
15
|
|
|
use Exception; |
16
|
|
|
|
17
|
|
|
|
18
|
|
|
class Sftp implements SSH2DriverInterface |
19
|
|
|
{ |
20
|
|
|
use BaseEventEmitterTrait; |
21
|
|
|
use LoopAwareTrait; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var int |
25
|
|
|
*/ |
26
|
|
|
const BUFFER_SIZE = 4096; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var SSH2Interface |
30
|
|
|
*/ |
31
|
|
|
protected $ssh2; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var resource |
35
|
|
|
*/ |
36
|
|
|
protected $conn; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var float |
40
|
|
|
*/ |
41
|
|
|
protected $interval; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var resource |
45
|
|
|
*/ |
46
|
|
|
protected $resource; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var SftpResource[]|SSH2ResourceInterface[] |
50
|
|
|
*/ |
51
|
|
|
protected $resources; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var bool |
55
|
|
|
*/ |
56
|
|
|
protected $paused; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var TimerInterface|null |
60
|
|
|
*/ |
61
|
|
|
private $timer; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var int |
65
|
|
|
*/ |
66
|
|
|
private $resourcesCounter; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @var string |
70
|
|
|
*/ |
71
|
|
|
private $buffer; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var string |
75
|
|
|
*/ |
76
|
|
|
private $prefix; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @param SSH2Interface $ssh2 |
80
|
|
|
* @param resource $conn |
81
|
|
|
* @param float $interval |
82
|
|
|
*/ |
83
|
21 |
View Code Duplication |
public function __construct(SSH2Interface $ssh2, $conn, $interval = 1e-1) |
|
|
|
|
84
|
|
|
{ |
85
|
21 |
|
$this->ssh2 = $ssh2; |
86
|
21 |
|
$this->conn = $conn; |
87
|
21 |
|
$this->interval = $interval; |
88
|
|
|
|
89
|
21 |
|
$this->loop = $ssh2->getLoop(); |
90
|
|
|
|
91
|
21 |
|
$this->resource = null; |
92
|
21 |
|
$this->resources = []; |
93
|
21 |
|
$this->paused = true; |
94
|
|
|
|
95
|
21 |
|
$this->timer = null; |
96
|
21 |
|
$this->resourcesCounter = 0; |
97
|
21 |
|
$this->buffer = ''; |
98
|
21 |
|
$this->prefix = ''; |
99
|
|
|
|
100
|
21 |
|
$this->resume(); |
101
|
21 |
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* |
105
|
|
|
*/ |
106
|
13 |
|
public function __destruct() |
107
|
|
|
{ |
108
|
13 |
|
$this->disconnect(); |
109
|
|
|
|
110
|
|
|
|
111
|
13 |
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @override |
115
|
|
|
* @inheritDoc |
116
|
|
|
*/ |
117
|
1 |
|
public function getName() |
118
|
|
|
{ |
119
|
1 |
|
return 'sftp'; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* @override |
124
|
|
|
* @inheritDoc |
125
|
|
|
*/ |
126
|
4 |
View Code Duplication |
public function connect() |
|
|
|
|
127
|
|
|
{ |
128
|
4 |
|
if ($this->resource !== null) |
129
|
|
|
{ |
130
|
1 |
|
return; |
131
|
|
|
} |
132
|
|
|
|
133
|
3 |
|
$resource = $this->createConnection($this->conn); |
134
|
|
|
|
135
|
3 |
|
if (!$resource || !is_resource($resource)) |
136
|
|
|
{ |
137
|
2 |
|
$this->emit('error', [ $this, new ExecutionException('SSH2:Sftp could not be connected.') ]); |
138
|
2 |
|
return; |
139
|
|
|
} |
140
|
|
|
|
141
|
1 |
|
$this->resource = $resource; |
142
|
|
|
|
143
|
1 |
|
$this->emit('connect', [ $this ]); |
144
|
1 |
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @override |
148
|
|
|
* @inheritDoc |
149
|
|
|
*/ |
150
|
16 |
View Code Duplication |
public function disconnect() |
|
|
|
|
151
|
|
|
{ |
152
|
16 |
|
if ($this->resource === null || !is_resource($this->resource)) |
153
|
|
|
{ |
154
|
13 |
|
return; |
155
|
|
|
} |
156
|
|
|
|
157
|
3 |
|
$this->pause(); |
158
|
|
|
|
159
|
3 |
|
foreach ($this->resources as $resource) |
160
|
|
|
{ |
161
|
1 |
|
$resource->close(); |
162
|
|
|
} |
163
|
|
|
|
164
|
3 |
|
$this->handleDisconnect(); |
165
|
3 |
|
$this->emit('disconnect', [ $this ]); |
166
|
3 |
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* @override |
170
|
|
|
* @inheritDoc |
171
|
|
|
*/ |
172
|
3 |
|
public function isConnected() |
173
|
|
|
{ |
174
|
3 |
|
return $this->resource !== null && is_resource($this->resource); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* @override |
179
|
|
|
* @inheritDoc |
180
|
|
|
*/ |
181
|
3 |
View Code Duplication |
public function pause() |
|
|
|
|
182
|
|
|
{ |
183
|
3 |
|
if (!$this->paused) |
184
|
|
|
{ |
185
|
2 |
|
$this->paused = true; |
186
|
|
|
|
187
|
2 |
|
if ($this->timer !== null) |
188
|
|
|
{ |
189
|
2 |
|
$this->timer->cancel(); |
190
|
2 |
|
$this->timer = null; |
191
|
|
|
} |
192
|
|
|
} |
193
|
3 |
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @override |
197
|
|
|
* @inheritDoc |
198
|
|
|
*/ |
199
|
21 |
View Code Duplication |
public function resume() |
|
|
|
|
200
|
|
|
{ |
201
|
21 |
|
if ($this->paused) |
202
|
|
|
{ |
203
|
21 |
|
$this->paused = false; |
204
|
|
|
|
205
|
21 |
|
if ($this->timer === null) |
206
|
|
|
{ |
207
|
21 |
|
$this->timer = $this->loop->addPeriodicTimer($this->interval, [ $this, 'handleHeartbeat' ]); |
208
|
|
|
} |
209
|
|
|
} |
210
|
21 |
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @override |
214
|
|
|
* @inheritDoc |
215
|
|
|
*/ |
216
|
6 |
|
public function isPaused() |
217
|
|
|
{ |
218
|
6 |
|
return $this->paused; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* @override |
223
|
|
|
* @inheritDoc |
224
|
|
|
*/ |
225
|
|
|
public function open($resource = null, $flags = 'r') |
226
|
|
|
{ |
227
|
|
|
if (!$this->isConnected()) |
228
|
|
|
{ |
229
|
|
|
throw new ResourceUndefinedException('Tried to open resource before establishing SSH2 connection!'); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$stream = @fopen("ssh2.sftp://" . $this->resource . $resource, $flags); |
233
|
|
|
|
234
|
|
|
if (!$stream) |
235
|
|
|
{ |
236
|
|
|
throw new ResourceUndefinedException("Access to SFTP resource [$resource] denied!"); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
$resource = new SftpResource($this, $stream); |
240
|
|
|
$resource->on('open', function(SSH2ResourceInterface $resource) { |
241
|
|
|
$this->emit('resource:open', [ $this, $resource ]); |
242
|
|
|
}); |
243
|
|
|
$resource->on('close', function(SSH2ResourceInterface $resource) { |
244
|
|
|
$this->removeResource($resource->getId()); |
245
|
|
|
$this->emit('resource:close', [ $this, $resource ]); |
246
|
|
|
}); |
247
|
|
|
|
248
|
|
|
$this->resources[$resource->getId()] = $resource; |
249
|
|
|
$this->resourcesCounter++; |
250
|
|
|
$this->resume(); |
251
|
|
|
|
252
|
|
|
return $resource; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Determine whether connection is still open. |
257
|
|
|
* |
258
|
|
|
* @internal |
259
|
|
|
*/ |
260
|
|
|
public function handleHeartbeat() |
261
|
|
|
{ |
262
|
|
|
$fp = @fopen("ssh2.sftp://" . $this->resource . "/.", "r"); |
263
|
|
|
|
264
|
|
|
if (!$fp || !is_resource($fp)) |
265
|
|
|
{ |
266
|
|
|
return $this->ssh2->disconnect(); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
fclose($fp); |
270
|
|
|
|
271
|
|
|
$this->handleData(); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Handle data. |
276
|
|
|
* |
277
|
|
|
* @internal |
278
|
|
|
*/ |
279
|
|
|
public function handleData() |
280
|
|
|
{ |
281
|
|
|
if ($this->paused || $this->resourcesCounter === 0) |
282
|
|
|
{ |
283
|
|
|
return; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
// handle all reading |
287
|
|
|
foreach ($this->resources as $resource) |
288
|
|
|
{ |
289
|
|
|
if (!$resource->isPaused() && $resource->isReadable()) |
|
|
|
|
290
|
|
|
{ |
291
|
|
|
$resource->handleRead(); |
|
|
|
|
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
// handle all writing |
296
|
|
|
foreach ($this->resources as $resource) |
297
|
|
|
{ |
298
|
|
|
if (!$resource->isPaused() && $resource->isWritable()) |
299
|
|
|
{ |
300
|
|
|
$resource->handleWrite(); |
|
|
|
|
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* |
307
|
|
|
*/ |
308
|
1 |
|
protected function handleDisconnect() |
309
|
|
|
{ |
310
|
1 |
|
@fclose($this->resource); |
|
|
|
|
311
|
1 |
|
$this->resource = null; |
312
|
1 |
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* @param resource $conn |
316
|
|
|
* @return resource |
317
|
|
|
*/ |
318
|
|
|
protected function createConnection($conn) |
319
|
|
|
{ |
320
|
|
|
return @ssh2_shell($conn); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Remove resource from known collection. |
325
|
|
|
* |
326
|
|
|
* @param string $prefix |
327
|
|
|
*/ |
328
|
|
View Code Duplication |
private function removeResource($prefix) |
|
|
|
|
329
|
|
|
{ |
330
|
|
|
if (!isset($this->resources[$prefix])) |
331
|
|
|
{ |
332
|
|
|
return; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
unset($this->resources[$prefix]); |
336
|
|
|
$this->resourcesCounter--; |
337
|
|
|
|
338
|
|
|
if ($this->resourcesCounter === 0) |
339
|
|
|
{ |
340
|
|
|
$this->pause(); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.