Show More
@@ -0,0 +1,108 b'' | |||
|
1 | # sshprotoext.py - Extension to test behavior of SSH protocol | |
|
2 | # | |
|
3 | # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com> | |
|
4 | # | |
|
5 | # This software may be used and distributed according to the terms of the | |
|
6 | # GNU General Public License version 2 or any later version. | |
|
7 | ||
|
8 | # This extension replaces the SSH server started via `hg serve --stdio`. | |
|
9 | # The server behaves differently depending on environment variables. | |
|
10 | ||
|
11 | from __future__ import absolute_import | |
|
12 | ||
|
13 | from mercurial import ( | |
|
14 | error, | |
|
15 | registrar, | |
|
16 | sshpeer, | |
|
17 | wireproto, | |
|
18 | wireprotoserver, | |
|
19 | ) | |
|
20 | ||
|
21 | configtable = {} | |
|
22 | configitem = registrar.configitem(configtable) | |
|
23 | ||
|
24 | configitem('sshpeer', 'mode', default=None) | |
|
25 | configitem('sshpeer', 'handshake-mode', default=None) | |
|
26 | ||
|
27 | class bannerserver(wireprotoserver.sshserver): | |
|
28 | """Server that sends a banner to stdout.""" | |
|
29 | def serve_forever(self): | |
|
30 | for i in range(10): | |
|
31 | self._fout.write(b'banner: line %d\n' % i) | |
|
32 | ||
|
33 | super(bannerserver, self).serve_forever() | |
|
34 | ||
|
35 | class prehelloserver(wireprotoserver.sshserver): | |
|
36 | """Tests behavior when connecting to <0.9.1 servers. | |
|
37 | ||
|
38 | The ``hello`` wire protocol command was introduced in Mercurial | |
|
39 | 0.9.1. Modern clients send the ``hello`` command when connecting | |
|
40 | to SSH servers. This mock server tests behavior of the handshake | |
|
41 | when ``hello`` is not supported. | |
|
42 | """ | |
|
43 | def serve_forever(self): | |
|
44 | l = self._fin.readline() | |
|
45 | assert l == b'hello\n' | |
|
46 | # Respond to unknown commands with an empty reply. | |
|
47 | self._sendresponse(b'') | |
|
48 | l = self._fin.readline() | |
|
49 | assert l == b'between\n' | |
|
50 | rsp = wireproto.dispatch(self._repo, self, b'between') | |
|
51 | self._handlers[rsp.__class__](self, rsp) | |
|
52 | ||
|
53 | super(prehelloserver, self).serve_forever() | |
|
54 | ||
|
55 | class extrahandshakecommandspeer(sshpeer.sshpeer): | |
|
56 | """An ssh peer that sends extra commands as part of initial handshake.""" | |
|
57 | # There isn't a good hook point. So we wrap _callstream() and inject | |
|
58 | # logic when the peer says "hello". | |
|
59 | def _callstream(self, cmd, **args): | |
|
60 | if cmd != b'hello': | |
|
61 | return super(extrahandshakecommandspeer, self)._callstream(cmd, | |
|
62 | **args) | |
|
63 | ||
|
64 | mode = self._ui.config(b'sshpeer', b'handshake-mode') | |
|
65 | if mode == b'pre-no-args': | |
|
66 | self._callstream(b'no-args') | |
|
67 | return super(extrahandshakecommandspeer, self)._callstream( | |
|
68 | cmd, **args) | |
|
69 | elif mode == b'pre-multiple-no-args': | |
|
70 | self._callstream(b'unknown1') | |
|
71 | self._callstream(b'unknown2') | |
|
72 | self._callstream(b'unknown3') | |
|
73 | return super(extrahandshakecommandspeer, self)._callstream( | |
|
74 | cmd, **args) | |
|
75 | else: | |
|
76 | raise error.ProgrammingError(b'unknown HANDSHAKECOMMANDMODE: %s' % | |
|
77 | mode) | |
|
78 | ||
|
79 | def registercommands(): | |
|
80 | def dummycommand(repo, proto): | |
|
81 | raise error.ProgrammingError('this should never be called') | |
|
82 | ||
|
83 | wireproto.wireprotocommand(b'no-args', b'')(dummycommand) | |
|
84 | wireproto.wireprotocommand(b'unknown1', b'')(dummycommand) | |
|
85 | wireproto.wireprotocommand(b'unknown2', b'')(dummycommand) | |
|
86 | wireproto.wireprotocommand(b'unknown3', b'')(dummycommand) | |
|
87 | ||
|
88 | def extsetup(ui): | |
|
89 | # It's easier for tests to define the server behavior via environment | |
|
90 | # variables than config options. This is because `hg serve --stdio` | |
|
91 | # has to be invoked with a certain form for security reasons and | |
|
92 | # `dummyssh` can't just add `--config` flags to the command line. | |
|
93 | servermode = ui.environ.get(b'SSHSERVERMODE') | |
|
94 | ||
|
95 | if servermode == b'banner': | |
|
96 | wireprotoserver.sshserver = bannerserver | |
|
97 | elif servermode == b'no-hello': | |
|
98 | wireprotoserver.sshserver = prehelloserver | |
|
99 | elif servermode: | |
|
100 | raise error.ProgrammingError(b'unknown server mode: %s' % servermode) | |
|
101 | ||
|
102 | peermode = ui.config(b'sshpeer', b'mode') | |
|
103 | ||
|
104 | if peermode == b'extra-handshake-commands': | |
|
105 | sshpeer.sshpeer = extrahandshakecommandspeer | |
|
106 | registercommands() | |
|
107 | elif peermode: | |
|
108 | raise error.ProgrammingError(b'unknown peer mode: %s' % peermode) |
@@ -0,0 +1,398 b'' | |||
|
1 | $ cat >> $HGRCPATH << EOF | |
|
2 | > [ui] | |
|
3 | > ssh = $PYTHON "$TESTDIR/dummyssh" | |
|
4 | > [devel] | |
|
5 | > debug.peer-request = true | |
|
6 | > [extensions] | |
|
7 | > sshprotoext = $TESTDIR/sshprotoext.py | |
|
8 | > EOF | |
|
9 | ||
|
10 | $ hg init server | |
|
11 | $ cd server | |
|
12 | $ echo 0 > foo | |
|
13 | $ hg -q add foo | |
|
14 | $ hg commit -m initial | |
|
15 | $ cd .. | |
|
16 | ||
|
17 | Test a normal behaving server, for sanity | |
|
18 | ||
|
19 | $ hg --debug debugpeer ssh://user@dummy/server | |
|
20 | running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) | |
|
21 | devel-peer-request: hello | |
|
22 | sending hello command | |
|
23 | devel-peer-request: between | |
|
24 | devel-peer-request: pairs: 81 bytes | |
|
25 | sending between command | |
|
26 | remote: 384 | |
|
27 | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
28 | remote: 1 | |
|
29 | url: ssh://user@dummy/server | |
|
30 | local: no | |
|
31 | pushable: yes | |
|
32 | ||
|
33 | Server should answer the "hello" command in isolation | |
|
34 | ||
|
35 | $ hg -R server serve --stdio << EOF | |
|
36 | > hello | |
|
37 | > EOF | |
|
38 | 384 | |
|
39 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
40 | ||
|
41 | >=0.9.1 clients send a "hello" + "between" for the null range as part of handshake. | |
|
42 | Server should reply with capabilities and should send "1\n\n" as a successful | |
|
43 | reply with empty response to the "between". | |
|
44 | ||
|
45 | $ hg -R server serve --stdio << EOF | |
|
46 | > hello | |
|
47 | > between | |
|
48 | > pairs 81 | |
|
49 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
50 | > EOF | |
|
51 | 384 | |
|
52 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
53 | 1 | |
|
54 | ||
|
55 | ||
|
56 | SSH banner is not printed by default, ignored by clients | |
|
57 | ||
|
58 | $ SSHSERVERMODE=banner hg debugpeer ssh://user@dummy/server | |
|
59 | url: ssh://user@dummy/server | |
|
60 | local: no | |
|
61 | pushable: yes | |
|
62 | ||
|
63 | --debug will print the banner | |
|
64 | ||
|
65 | $ SSHSERVERMODE=banner hg --debug debugpeer ssh://user@dummy/server | |
|
66 | running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) | |
|
67 | devel-peer-request: hello | |
|
68 | sending hello command | |
|
69 | devel-peer-request: between | |
|
70 | devel-peer-request: pairs: 81 bytes | |
|
71 | sending between command | |
|
72 | remote: banner: line 0 | |
|
73 | remote: banner: line 1 | |
|
74 | remote: banner: line 2 | |
|
75 | remote: banner: line 3 | |
|
76 | remote: banner: line 4 | |
|
77 | remote: banner: line 5 | |
|
78 | remote: banner: line 6 | |
|
79 | remote: banner: line 7 | |
|
80 | remote: banner: line 8 | |
|
81 | remote: banner: line 9 | |
|
82 | remote: 384 | |
|
83 | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
84 | remote: 1 | |
|
85 | url: ssh://user@dummy/server | |
|
86 | local: no | |
|
87 | pushable: yes | |
|
88 | ||
|
89 | And test the banner with the raw protocol | |
|
90 | ||
|
91 | $ SSHSERVERMODE=banner hg -R server serve --stdio << EOF | |
|
92 | > hello | |
|
93 | > between | |
|
94 | > pairs 81 | |
|
95 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
96 | > EOF | |
|
97 | banner: line 0 | |
|
98 | banner: line 1 | |
|
99 | banner: line 2 | |
|
100 | banner: line 3 | |
|
101 | banner: line 4 | |
|
102 | banner: line 5 | |
|
103 | banner: line 6 | |
|
104 | banner: line 7 | |
|
105 | banner: line 8 | |
|
106 | banner: line 9 | |
|
107 | 384 | |
|
108 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
109 | 1 | |
|
110 | ||
|
111 | ||
|
112 | Connecting to a <0.9.1 server that doesn't support the hello command | |
|
113 | ||
|
114 | $ SSHSERVERMODE=no-hello hg --debug debugpeer ssh://user@dummy/server | |
|
115 | running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) | |
|
116 | devel-peer-request: hello | |
|
117 | sending hello command | |
|
118 | devel-peer-request: between | |
|
119 | devel-peer-request: pairs: 81 bytes | |
|
120 | sending between command | |
|
121 | remote: 0 | |
|
122 | remote: 1 | |
|
123 | url: ssh://user@dummy/server | |
|
124 | local: no | |
|
125 | pushable: yes | |
|
126 | ||
|
127 | The client should interpret this as no capabilities | |
|
128 | ||
|
129 | $ SSHSERVERMODE=no-hello hg debugcapabilities ssh://user@dummy/server | |
|
130 | Main capabilities: | |
|
131 | ||
|
132 | Sending an unknown command to the server results in an empty response to that command | |
|
133 | ||
|
134 | $ hg -R server serve --stdio << EOF | |
|
135 | > pre-hello | |
|
136 | > hello | |
|
137 | > between | |
|
138 | > pairs 81 | |
|
139 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
140 | > EOF | |
|
141 | 0 | |
|
142 | 384 | |
|
143 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
144 | 1 | |
|
145 | ||
|
146 | ||
|
147 | $ hg --config sshpeer.mode=extra-handshake-commands --config sshpeer.handshake-mode=pre-no-args --debug debugpeer ssh://user@dummy/server | |
|
148 | running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) | |
|
149 | devel-peer-request: no-args | |
|
150 | sending no-args command | |
|
151 | devel-peer-request: hello | |
|
152 | sending hello command | |
|
153 | devel-peer-request: between | |
|
154 | devel-peer-request: pairs: 81 bytes | |
|
155 | sending between command | |
|
156 | remote: 0 | |
|
157 | remote: 384 | |
|
158 | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
159 | remote: 1 | |
|
160 | url: ssh://user@dummy/server | |
|
161 | local: no | |
|
162 | pushable: yes | |
|
163 | ||
|
164 | Send multiple unknown commands before hello | |
|
165 | ||
|
166 | $ hg -R server serve --stdio << EOF | |
|
167 | > unknown1 | |
|
168 | > unknown2 | |
|
169 | > unknown3 | |
|
170 | > hello | |
|
171 | > between | |
|
172 | > pairs 81 | |
|
173 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
174 | > EOF | |
|
175 | 0 | |
|
176 | 0 | |
|
177 | 0 | |
|
178 | 384 | |
|
179 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
180 | 1 | |
|
181 | ||
|
182 | ||
|
183 | $ hg --config sshpeer.mode=extra-handshake-commands --config sshpeer.handshake-mode=pre-multiple-no-args --debug debugpeer ssh://user@dummy/server | |
|
184 | running * "*/tests/dummyssh" 'user@dummy' 'hg -R server serve --stdio' (glob) | |
|
185 | devel-peer-request: unknown1 | |
|
186 | sending unknown1 command | |
|
187 | devel-peer-request: unknown2 | |
|
188 | sending unknown2 command | |
|
189 | devel-peer-request: unknown3 | |
|
190 | sending unknown3 command | |
|
191 | devel-peer-request: hello | |
|
192 | sending hello command | |
|
193 | devel-peer-request: between | |
|
194 | devel-peer-request: pairs: 81 bytes | |
|
195 | sending between command | |
|
196 | remote: 0 | |
|
197 | remote: 0 | |
|
198 | remote: 0 | |
|
199 | remote: 384 | |
|
200 | remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
201 | remote: 1 | |
|
202 | url: ssh://user@dummy/server | |
|
203 | local: no | |
|
204 | pushable: yes | |
|
205 | ||
|
206 | Send an unknown command before hello that has arguments | |
|
207 | ||
|
208 | $ hg -R server serve --stdio << EOF | |
|
209 | > with-args | |
|
210 | > foo 13 | |
|
211 | > value for foo | |
|
212 | > bar 13 | |
|
213 | > value for bar | |
|
214 | > hello | |
|
215 | > between | |
|
216 | > pairs 81 | |
|
217 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
218 | > EOF | |
|
219 | 0 | |
|
220 | 0 | |
|
221 | 0 | |
|
222 | 0 | |
|
223 | 0 | |
|
224 | 384 | |
|
225 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
226 | 1 | |
|
227 | ||
|
228 | ||
|
229 | Send an unknown command having an argument that looks numeric | |
|
230 | ||
|
231 | $ hg -R server serve --stdio << EOF | |
|
232 | > unknown | |
|
233 | > foo 1 | |
|
234 | > 0 | |
|
235 | > hello | |
|
236 | > between | |
|
237 | > pairs 81 | |
|
238 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
239 | > EOF | |
|
240 | 0 | |
|
241 | 0 | |
|
242 | 0 | |
|
243 | 384 | |
|
244 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
245 | 1 | |
|
246 | ||
|
247 | ||
|
248 | $ hg -R server serve --stdio << EOF | |
|
249 | > unknown | |
|
250 | > foo 1 | |
|
251 | > 1 | |
|
252 | > hello | |
|
253 | > between | |
|
254 | > pairs 81 | |
|
255 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
256 | > EOF | |
|
257 | 0 | |
|
258 | 0 | |
|
259 | 0 | |
|
260 | 384 | |
|
261 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
262 | 1 | |
|
263 | ||
|
264 | ||
|
265 | When sending a dict argument value, it is serialized to | |
|
266 | "<arg> <item count>" followed by "<key> <len>\n<value>" for each item | |
|
267 | in the dict. | |
|
268 | ||
|
269 | Dictionary value for unknown command | |
|
270 | ||
|
271 | $ hg -R server serve --stdio << EOF | |
|
272 | > unknown | |
|
273 | > dict 3 | |
|
274 | > key1 3 | |
|
275 | > foo | |
|
276 | > key2 3 | |
|
277 | > bar | |
|
278 | > key3 3 | |
|
279 | > baz | |
|
280 | > hello | |
|
281 | > EOF | |
|
282 | 0 | |
|
283 | 0 | |
|
284 | 0 | |
|
285 | 0 | |
|
286 | 0 | |
|
287 | 0 | |
|
288 | 0 | |
|
289 | 0 | |
|
290 | 384 | |
|
291 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
292 | ||
|
293 | Incomplete dictionary send | |
|
294 | ||
|
295 | $ hg -R server serve --stdio << EOF | |
|
296 | > unknown | |
|
297 | > dict 3 | |
|
298 | > key1 3 | |
|
299 | > foo | |
|
300 | > EOF | |
|
301 | 0 | |
|
302 | 0 | |
|
303 | 0 | |
|
304 | 0 | |
|
305 | ||
|
306 | Incomplete value send | |
|
307 | ||
|
308 | $ hg -R server serve --stdio << EOF | |
|
309 | > unknown | |
|
310 | > dict 3 | |
|
311 | > key1 3 | |
|
312 | > fo | |
|
313 | > EOF | |
|
314 | 0 | |
|
315 | 0 | |
|
316 | 0 | |
|
317 | 0 | |
|
318 | ||
|
319 | Send a command line with spaces | |
|
320 | ||
|
321 | $ hg -R server serve --stdio << EOF | |
|
322 | > unknown withspace | |
|
323 | > hello | |
|
324 | > between | |
|
325 | > pairs 81 | |
|
326 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
327 | > EOF | |
|
328 | 0 | |
|
329 | 384 | |
|
330 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
331 | 1 | |
|
332 | ||
|
333 | ||
|
334 | $ hg -R server serve --stdio << EOF | |
|
335 | > unknown with multiple spaces | |
|
336 | > hello | |
|
337 | > between | |
|
338 | > pairs 81 | |
|
339 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
340 | > EOF | |
|
341 | 0 | |
|
342 | 384 | |
|
343 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
344 | 1 | |
|
345 | ||
|
346 | ||
|
347 | $ hg -R server serve --stdio << EOF | |
|
348 | > unknown with spaces | |
|
349 | > key 10 | |
|
350 | > some value | |
|
351 | > hello | |
|
352 | > between | |
|
353 | > pairs 81 | |
|
354 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000 | |
|
355 | > EOF | |
|
356 | 0 | |
|
357 | 0 | |
|
358 | 0 | |
|
359 | 384 | |
|
360 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
361 | 1 | |
|
362 | ||
|
363 | ||
|
364 | Send an unknown command after the "between" | |
|
365 | ||
|
366 | $ hg -R server serve --stdio << EOF | |
|
367 | > hello | |
|
368 | > between | |
|
369 | > pairs 81 | |
|
370 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000unknown | |
|
371 | > EOF | |
|
372 | 384 | |
|
373 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
374 | 1 | |
|
375 | ||
|
376 | 0 | |
|
377 | ||
|
378 | And one with arguments | |
|
379 | ||
|
380 | $ hg -R server serve --stdio << EOF | |
|
381 | > hello | |
|
382 | > between | |
|
383 | > pairs 81 | |
|
384 | > 0000000000000000000000000000000000000000-0000000000000000000000000000000000000000unknown | |
|
385 | > foo 5 | |
|
386 | > value | |
|
387 | > bar 3 | |
|
388 | > baz | |
|
389 | > EOF | |
|
390 | 384 | |
|
391 | capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch streamreqs=generaldelta,revlogv1 $USUAL_BUNDLE2_CAPS_SERVER$ unbundle=HG10GZ,HG10BZ,HG10UN | |
|
392 | 1 | |
|
393 | ||
|
394 | 0 | |
|
395 | 0 | |
|
396 | 0 | |
|
397 | 0 | |
|
398 | 0 |
General Comments 0
You need to be logged in to leave comments.
Login now