1 |
/* |
---|
2 |
* Copyright (c) 2007, OmniTI Computer Consulting, Inc. |
---|
3 |
* All rights reserved. |
---|
4 |
* Copyright (c) 2015, Circonus, Inc. All rights reserved. |
---|
5 |
* |
---|
6 |
* Redistribution and use in source and binary forms, with or without |
---|
7 |
* modification, are permitted provided that the following conditions are |
---|
8 |
* met: |
---|
9 |
* |
---|
10 |
* * Redistributions of source code must retain the above copyright |
---|
11 |
* notice, this list of conditions and the following disclaimer. |
---|
12 |
* * Redistributions in binary form must reproduce the above |
---|
13 |
* copyright notice, this list of conditions and the following |
---|
14 |
* disclaimer in the documentation and/or other materials provided |
---|
15 |
* with the distribution. |
---|
16 |
* * Neither the name OmniTI Computer Consulting, Inc. nor the names |
---|
17 |
* of its contributors may be used to endorse or promote products |
---|
18 |
* derived from this software without specific prior written |
---|
19 |
* permission. |
---|
20 |
* |
---|
21 |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
---|
22 |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
---|
23 |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
---|
24 |
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
---|
25 |
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
---|
26 |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
---|
27 |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
---|
28 |
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
---|
29 |
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
---|
30 |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
---|
31 |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
32 |
*/ |
---|
33 |
|
---|
34 |
#include <mtev_defines.h> |
---|
35 |
|
---|
36 |
#include <stdio.h> |
---|
37 |
#include <unistd.h> |
---|
38 |
#include <errno.h> |
---|
39 |
#include <math.h> |
---|
40 |
#ifdef HAVE_SYS_FILIO_H |
---|
41 |
#include <sys/filio.h> |
---|
42 |
#endif |
---|
43 |
#include <dlfcn.h> |
---|
44 |
|
---|
45 |
#include <mtev_hash.h> |
---|
46 |
|
---|
47 |
#include "noit_module.h" |
---|
48 |
#include "noit_check.h" |
---|
49 |
#include "noit_check_tools.h" |
---|
50 |
#include "noit_mtev_bridge.h" |
---|
51 |
|
---|
52 |
#include <libssh2.h> |
---|
53 |
#ifdef HAVE_GCRYPT_H |
---|
54 |
#include <gcrypt.h> |
---|
55 |
#endif |
---|
56 |
|
---|
57 |
#define DEFAULT_SSH_PORT 22 |
---|
58 |
|
---|
59 |
typedef struct { |
---|
60 |
noit_module_t *self; |
---|
61 |
noit_check_t *check; |
---|
62 |
struct { |
---|
63 |
char *kex; |
---|
64 |
char *hostkey; |
---|
65 |
char *crypt_cs; |
---|
66 |
char *crypt_sc; |
---|
67 |
char *mac_cs; |
---|
68 |
char *mac_sc; |
---|
69 |
char *comp_cs; |
---|
70 |
char *comp_sc; |
---|
71 |
} methods; |
---|
72 |
enum { |
---|
73 |
WANT_CONNECT = 0, |
---|
74 |
WANT_CLOSE = 1 |
---|
75 |
} state; |
---|
76 |
LIBSSH2_SESSION *session; |
---|
77 |
int available; |
---|
78 |
int timed_out; |
---|
79 |
char *error; |
---|
80 |
char fingerprint[33]; /* 32 hex characters */ |
---|
81 |
eventer_t synch_fd_event; |
---|
82 |
eventer_t timeout_event; /* Only used for connect() */ |
---|
83 |
} ssh2_check_info_t; |
---|
84 |
|
---|
85 |
static mtev_log_stream_t nlerr = NULL; |
---|
86 |
static mtev_log_stream_t nldeb = NULL; |
---|
87 |
|
---|
88 |
static void ssh2_cleanup(noit_module_t *self, noit_check_t *check) { |
---|
89 |
ssh2_check_info_t *ci = check->closure; |
---|
90 |
if(ci) { |
---|
91 |
if(ci->timeout_event) { |
---|
92 |
eventer_remove(ci->timeout_event); |
---|
93 |
eventer_free(ci->timeout_event); |
---|
94 |
} |
---|
95 |
if(ci->session) { |
---|
96 |
libssh2_session_disconnect(ci->session, "Bye!"); |
---|
97 |
libssh2_session_free(ci->session); |
---|
98 |
} |
---|
99 |
if(ci->methods.kex) free(ci->methods.kex); |
---|
100 |
if(ci->methods.hostkey) free(ci->methods.hostkey); |
---|
101 |
if(ci->methods.crypt_cs) free(ci->methods.crypt_cs); |
---|
102 |
if(ci->methods.crypt_sc) free(ci->methods.crypt_sc); |
---|
103 |
if(ci->methods.mac_cs) free(ci->methods.mac_cs); |
---|
104 |
if(ci->methods.mac_sc) free(ci->methods.mac_sc); |
---|
105 |
if(ci->methods.comp_cs) free(ci->methods.comp_cs); |
---|
106 |
if(ci->methods.comp_sc) free(ci->methods.comp_sc); |
---|
107 |
if(ci->error) free(ci->error); |
---|
108 |
memset(ci, 0, sizeof(*ci)); |
---|
109 |
} |
---|
110 |
} |
---|
111 |
|
---|
112 |
#ifdef HAVE_GCRYPT_H |
---|
113 |
GCRY_THREAD_OPTION_PTHREAD_IMPL; |
---|
114 |
#endif |
---|
115 |
|
---|
116 |
static int ssh2_init(noit_module_t *self) { |
---|
117 |
#ifdef HAVE_GCRYPT_H |
---|
118 |
gcry_error_t (*control)(enum gcry_ctl_cmds CMD, ...); |
---|
119 |
#ifndef RTLD_DEFAULT |
---|
120 |
#define RTLD_DEFAULT ((void *)0) |
---|
121 |
#endif |
---|
122 |
control = dlsym(RTLD_DEFAULT, "gcry_control"); |
---|
123 |
if(control) control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread); |
---|
124 |
#endif |
---|
125 |
return 0; |
---|
126 |
} |
---|
127 |
static int ssh2_config(noit_module_t *self, mtev_hash_table *options) { |
---|
128 |
return 0; |
---|
129 |
} |
---|
130 |
static void ssh2_log_results(noit_module_t *self, noit_check_t *check) { |
---|
131 |
struct timeval duration, now; |
---|
132 |
u_int32_t mduration; |
---|
133 |
ssh2_check_info_t *ci = check->closure; |
---|
134 |
|
---|
135 |
gettimeofday(&now, NULL); |
---|
136 |
sub_timeval(now, check->last_fire_time, &duration); |
---|
137 |
mduration = duration.tv_sec * 1000 + duration.tv_usec / 1000; |
---|
138 |
noit_stats_set_whence(check, &now); |
---|
139 |
noit_stats_set_duration(check, mduration); |
---|
140 |
noit_stats_set_available(check, ci->available ? NP_AVAILABLE : NP_UNAVAILABLE); |
---|
141 |
noit_stats_set_state(check, ci->fingerprint[0] ? NP_GOOD : NP_BAD); |
---|
142 |
|
---|
143 |
if(ci->error) noit_stats_set_status(check, ci->error); |
---|
144 |
else if(ci->timed_out) noit_stats_set_status(check, "timeout"); |
---|
145 |
else if(ci->fingerprint[0]) noit_stats_set_status(check, ci->fingerprint); |
---|
146 |
else noit_stats_set_status(check, "internal error"); |
---|
147 |
|
---|
148 |
if(ci->fingerprint[0]) { |
---|
149 |
noit_stats_set_metric(check, "duration", METRIC_UINT32, &mduration); |
---|
150 |
noit_stats_set_metric(check, "fingerprint", METRIC_STRING, |
---|
151 |
ci->fingerprint); |
---|
152 |
} |
---|
153 |
noit_check_set_stats(check); |
---|
154 |
} |
---|
155 |
static int ssh2_drive_session(eventer_t e, int mask, void *closure, |
---|
156 |
struct timeval *now) { |
---|
157 |
int i; |
---|
158 |
const char *fingerprint; |
---|
159 |
ssh2_check_info_t *ci = closure; |
---|
160 |
struct timeval diff; |
---|
161 |
int timeout_ms = 10; /* 10ms, gets set below */ |
---|
162 |
if(ci->state == WANT_CLOSE) { |
---|
163 |
noit_check_t *check = ci->check; |
---|
164 |
ssh2_log_results(ci->self, ci->check); |
---|
165 |
ssh2_cleanup(ci->self, ci->check); |
---|
166 |
eventer_remove_fd(e->fd); |
---|
167 |
e->opset->close(e->fd, &mask, e); |
---|
168 |
check->flags &= ~NP_RUNNING; |
---|
169 |
return 0; |
---|
170 |
} |
---|
171 |
switch(mask) { |
---|
172 |
case EVENTER_ASYNCH_WORK: |
---|
173 |
if(eventer_set_fd_blocking(e->fd)) { |
---|
174 |
ci->timed_out = 0; |
---|
175 |
ci->error = strdup("socket error"); |
---|
176 |
return 0; |
---|
177 |
} |
---|
178 |
ci->session = libssh2_session_init(); |
---|
179 |
#define set_method(a,b) do { \ |
---|
180 |
int rv; \ |
---|
181 |
if(ci->methods.a && \ |
---|
182 |
(rv = libssh2_session_method_pref(ci->session, b, ci->methods.a)) != 0) { \ |
---|
183 |
ci->timed_out = 0; \ |
---|
184 |
ci->error = strdup((rv == LIBSSH2_ERROR_METHOD_NOT_SUPPORTED) ? \ |
---|
185 |
#a " method not supported" : "error setting " #a); \ |
---|
186 |
return 0; \ |
---|
187 |
} \ |
---|
188 |
} while(0) |
---|
189 |
set_method(kex, LIBSSH2_METHOD_KEX); |
---|
190 |
set_method(hostkey, LIBSSH2_METHOD_HOSTKEY); |
---|
191 |
set_method(crypt_cs, LIBSSH2_METHOD_CRYPT_CS); |
---|
192 |
set_method(crypt_sc, LIBSSH2_METHOD_CRYPT_SC); |
---|
193 |
set_method(mac_cs, LIBSSH2_METHOD_MAC_CS); |
---|
194 |
set_method(mac_sc, LIBSSH2_METHOD_MAC_SC); |
---|
195 |
set_method(comp_cs, LIBSSH2_METHOD_COMP_CS); |
---|
196 |
set_method(comp_sc, LIBSSH2_METHOD_COMP_SC); |
---|
197 |
if(compare_timeval(*now, e->whence) < 0) { |
---|
198 |
sub_timeval(e->whence, *now, &diff); |
---|
199 |
timeout_ms = diff.tv_sec * 1000 + diff.tv_usec / 1000; |
---|
200 |
} |
---|
201 |
#if LIBSSH2_VERSION_NUM >= 0x010209 |
---|
202 |
libssh2_session_set_timeout(ci->session, timeout_ms); |
---|
203 |
#endif |
---|
204 |
if (libssh2_session_startup(ci->session, e->fd)) { |
---|
205 |
ci->timed_out = 0; |
---|
206 |
ci->error = strdup("ssh session startup failed"); |
---|
207 |
return 0; |
---|
208 |
} |
---|
209 |
fingerprint = libssh2_hostkey_hash(ci->session, LIBSSH2_HOSTKEY_HASH_MD5); |
---|
210 |
for(i=0;i<16;i++) { |
---|
211 |
snprintf(ci->fingerprint + (i*2), 3, "%02x", |
---|
212 |
(unsigned char)fingerprint[i]); |
---|
213 |
} |
---|
214 |
ci->fingerprint[32] = '\0'; |
---|
215 |
ci->timed_out = 0; |
---|
216 |
return 0; |
---|
217 |
break; |
---|
218 |
case EVENTER_ASYNCH_CLEANUP: |
---|
219 |
if(ci->session) { |
---|
220 |
libssh2_session_disconnect(ci->session, "Bye!"); |
---|
221 |
libssh2_session_free(ci->session); |
---|
222 |
ci->session = NULL; |
---|
223 |
} |
---|
224 |
ci->state = WANT_CLOSE; |
---|
225 |
break; |
---|
226 |
default: |
---|
227 |
mtevFatal(mtev_error, "Unknown mask: 0x%04x\n", mask); |
---|
228 |
} |
---|
229 |
return 0; |
---|
230 |
} |
---|
231 |
static int ssh2_needs_bytes_as_libssh2_is_impatient(eventer_t e, int mask, void *closure, |
---|
232 |
struct timeval *now) { |
---|
233 |
ssh2_check_info_t *ci = closure; |
---|
234 |
eventer_t asynch_e; |
---|
235 |
|
---|
236 |
if(mask & EVENTER_EXCEPTION) { |
---|
237 |
noit_check_t *check = ci->check; |
---|
238 |
ci->timed_out = 0; |
---|
239 |
ci->error = strdup("ssh connection failed"); |
---|
240 |
ssh2_log_results(ci->self, ci->check); |
---|
241 |
ssh2_cleanup(ci->self, ci->check); |
---|
242 |
eventer_remove_fd(e->fd); |
---|
243 |
e->opset->close(e->fd, &mask, e); |
---|
244 |
check->flags &= ~NP_RUNNING; |
---|
245 |
return 0; |
---|
246 |
} |
---|
247 |
|
---|
248 |
/* We steal the timeout_event as it has the exact timeout we want. */ |
---|
249 |
mtevAssert(ci->timeout_event); |
---|
250 |
asynch_e = eventer_remove(ci->timeout_event); |
---|
251 |
mtevAssert(asynch_e); |
---|
252 |
ci->timeout_event = NULL; |
---|
253 |
|
---|
254 |
ci->synch_fd_event = NULL; |
---|
255 |
asynch_e->fd = e->fd; |
---|
256 |
asynch_e->callback = ssh2_drive_session; |
---|
257 |
asynch_e->closure = closure; |
---|
258 |
asynch_e->mask = EVENTER_ASYNCH; |
---|
259 |
eventer_add(asynch_e); |
---|
260 |
|
---|
261 |
eventer_remove_fd(e->fd); |
---|
262 |
return 0; |
---|
263 |
} |
---|
264 |
static int ssh2_connect_complete(eventer_t e, int mask, void *closure, |
---|
265 |
struct timeval *now) { |
---|
266 |
ssh2_check_info_t *ci = closure; |
---|
267 |
|
---|
268 |
if(mask & EVENTER_EXCEPTION) { |
---|
269 |
noit_check_t *check = ci->check; |
---|
270 |
ci->timed_out = 0; |
---|
271 |
ci->error = strdup("ssh connection failed"); |
---|
272 |
ssh2_log_results(ci->self, ci->check); |
---|
273 |
ssh2_cleanup(ci->self, ci->check); |
---|
274 |
eventer_remove_fd(e->fd); |
---|
275 |
e->opset->close(e->fd, &mask, e); |
---|
276 |
check->flags &= ~NP_RUNNING; |
---|
277 |
return 0; |
---|
278 |
} |
---|
279 |
|
---|
280 |
ci->available = 1; |
---|
281 |
e->callback = ssh2_needs_bytes_as_libssh2_is_impatient; |
---|
282 |
e->mask = EVENTER_READ | EVENTER_EXCEPTION; |
---|
283 |
return e->mask; |
---|
284 |
} |
---|
285 |
static int ssh2_connect_timeout(eventer_t e, int mask, void *closure, |
---|
286 |
struct timeval *now) { |
---|
287 |
eventer_t fde; |
---|
288 |
ssh2_check_info_t *ci = closure; |
---|
289 |
noit_check_t *check = ci->check; |
---|
290 |
|
---|
291 |
ci->timeout_event = NULL; /* This is us, return 0 will free this */ |
---|
292 |
ci->error = strdup("ssh connect timeout"); |
---|
293 |
if(ci->synch_fd_event) { |
---|
294 |
fde = ci->synch_fd_event; |
---|
295 |
eventer_remove_fd(fde->fd); |
---|
296 |
fde->opset->close(fde->fd, &mask, fde); |
---|
297 |
eventer_free(fde); |
---|
298 |
ci->synch_fd_event = NULL; |
---|
299 |
} |
---|
300 |
ssh2_log_results(ci->self, ci->check); |
---|
301 |
ssh2_cleanup(ci->self, ci->check); |
---|
302 |
check->flags &= ~NP_RUNNING; |
---|
303 |
return 0; |
---|
304 |
} |
---|
305 |
static int ssh2_initiate(noit_module_t *self, noit_check_t *check, |
---|
306 |
noit_check_t *cause) { |
---|
307 |
ssh2_check_info_t *ci = check->closure; |
---|
308 |
struct timeval p_int, __now; |
---|
309 |
int fd = -1, rv = -1; |
---|
310 |
eventer_t e; |
---|
311 |
union { |
---|
312 |
struct sockaddr_in sin; |
---|
313 |
struct sockaddr_in6 sin6; |
---|
314 |
} sockaddr; |
---|
315 |
socklen_t sockaddr_len; |
---|
316 |
unsigned short ssh_port = DEFAULT_SSH_PORT; |
---|
317 |
const char *port_str = NULL; |
---|
318 |
|
---|
319 |
/* We cannot be running */ |
---|
320 |
BAIL_ON_RUNNING_CHECK(check); |
---|
321 |
check->flags |= NP_RUNNING; |
---|
322 |
|
---|
323 |
ci->self = self; |
---|
324 |
ci->check = check; |
---|
325 |
|
---|
326 |
ci->timed_out = 1; |
---|
327 |
if(ci->timeout_event) { |
---|
328 |
eventer_remove(ci->timeout_event); |
---|
329 |
free(ci->timeout_event->closure); |
---|
330 |
eventer_free(ci->timeout_event); |
---|
331 |
ci->timeout_event = NULL; |
---|
332 |
} |
---|
333 |
gettimeofday(&__now, NULL); |
---|
334 |
memcpy(&check->last_fire_time, &__now, sizeof(__now)); |
---|
335 |
|
---|
336 |
if(check->target_ip[0] == '\0') { |
---|
337 |
ci->error = strdup("name resolution failure"); |
---|
338 |
goto fail; |
---|
339 |
} |
---|
340 |
/* Open a socket */ |
---|
341 |
fd = socket(check->target_family, NE_SOCK_CLOEXEC|SOCK_STREAM, 0); |
---|
342 |
if(fd < 0) goto fail; |
---|
343 |
|
---|
344 |
/* Make it non-blocking */ |
---|
345 |
if(eventer_set_fd_nonblocking(fd)) goto fail; |
---|
346 |
|
---|
347 |
if(mtev_hash_retr_str(check->config, "port", strlen("port"), |
---|
348 |
&port_str)) { |
---|
349 |
ssh_port = (unsigned short)atoi(port_str); |
---|
350 |
} |
---|
351 |
#define config_method(a) do { \ |
---|
352 |
const char *v; \ |
---|
353 |
if(mtev_hash_retr_str(check->config, "method_" #a, strlen("method_" #a), \ |
---|
354 |
&v)) \ |
---|
355 |
ci->methods.a = strdup(v); \ |
---|
356 |
} while(0) |
---|
357 |
config_method(kex); |
---|
358 |
config_method(hostkey); |
---|
359 |
config_method(crypt_cs); |
---|
360 |
config_method(crypt_sc); |
---|
361 |
config_method(mac_cs); |
---|
362 |
config_method(mac_sc); |
---|
363 |
config_method(comp_cs); |
---|
364 |
config_method(comp_sc); |
---|
365 |
memset(&sockaddr, 0, sizeof(sockaddr)); |
---|
366 |
sockaddr.sin6.sin6_family = check->target_family; |
---|
367 |
if(check->target_family == AF_INET) { |
---|
368 |
memcpy(&sockaddr.sin.sin_addr, |
---|
369 |
&check->target_addr.addr, sizeof(sockaddr.sin.sin_addr)); |
---|
370 |
sockaddr.sin.sin_port = htons(ssh_port); |
---|
371 |
sockaddr_len = sizeof(sockaddr.sin); |
---|
372 |
} |
---|
373 |
else { |
---|
374 |
memcpy(&sockaddr.sin6.sin6_addr, |
---|
375 |
&check->target_addr.addr6, sizeof(sockaddr.sin6.sin6_addr)); |
---|
376 |
sockaddr.sin6.sin6_port = htons(ssh_port); |
---|
377 |
sockaddr_len = sizeof(sockaddr.sin6); |
---|
378 |
} |
---|
379 |
|
---|
380 |
/* Initiate a connection */ |
---|
381 |
rv = connect(fd, (struct sockaddr *)&sockaddr, sockaddr_len); |
---|
382 |
if(rv == -1 && errno != EINPROGRESS) goto fail; |
---|
383 |
|
---|
384 |
/* Register a handler for connection completion */ |
---|
385 |
e = eventer_alloc(); |
---|
386 |
e->fd = fd; |
---|
387 |
e->mask = EVENTER_READ | EVENTER_WRITE | EVENTER_EXCEPTION; |
---|
388 |
e->callback = ssh2_connect_complete; |
---|
389 |
e->closure = ci; |
---|
390 |
ci->synch_fd_event = e; |
---|
391 |
eventer_add(e); |
---|
392 |
|
---|
393 |
e = eventer_alloc(); |
---|
394 |
e->mask = EVENTER_TIMER; |
---|
395 |
e->callback = ssh2_connect_timeout; |
---|
396 |
e->closure = ci; |
---|
397 |
memcpy(&e->whence, &__now, sizeof(__now)); |
---|
398 |
p_int.tv_sec = check->timeout / 1000; |
---|
399 |
p_int.tv_usec = (check->timeout % 1000) * 1000; |
---|
400 |
add_timeval(e->whence, p_int, &e->whence); |
---|
401 |
ci->timeout_event = e; |
---|
402 |
eventer_add(e); |
---|
403 |
return 0; |
---|
404 |
|
---|
405 |
fail: |
---|
406 |
if(fd >= 0) close(fd); |
---|
407 |
ssh2_log_results(ci->self, ci->check); |
---|
408 |
ssh2_cleanup(ci->self, ci->check); |
---|
409 |
check->flags &= ~NP_RUNNING; |
---|
410 |
return -1; |
---|
411 |
} |
---|
412 |
|
---|
413 |
static int ssh2_initiate_check(noit_module_t *self, noit_check_t *check, |
---|
414 |
int once, noit_check_t *cause) { |
---|
415 |
if(!check->closure) check->closure = calloc(1, sizeof(ssh2_check_info_t)); |
---|
416 |
INITIATE_CHECK(ssh2_initiate, self, check, cause); |
---|
417 |
return 0; |
---|
418 |
} |
---|
419 |
|
---|
420 |
static int ssh2_onload(mtev_image_t *self) { |
---|
421 |
nlerr = mtev_log_stream_find("error/ssh2"); |
---|
422 |
nldeb = mtev_log_stream_find("debug/ssh2"); |
---|
423 |
if(!nlerr) nlerr = noit_stderr; |
---|
424 |
if(!nldeb) nldeb = noit_debug; |
---|
425 |
|
---|
426 |
eventer_name_callback("http/ssh2_connect_complete", ssh2_connect_complete); |
---|
427 |
eventer_name_callback("http/ssh2_drive_session", ssh2_drive_session); |
---|
428 |
return 0; |
---|
429 |
} |
---|
430 |
|
---|
431 |
#include "ssh2.xmlh" |
---|
432 |
noit_module_t ssh2 = { |
---|
433 |
{ |
---|
434 |
.magic = NOIT_MODULE_MAGIC, |
---|
435 |
.version = NOIT_MODULE_ABI_VERSION, |
---|
436 |
.name = "ssh2", |
---|
437 |
.description = "Secure Shell version 2 checker", |
---|
438 |
.xml_description = ssh2_xml_description, |
---|
439 |
.onload = ssh2_onload |
---|
440 |
}, |
---|
441 |
ssh2_config, |
---|
442 |
ssh2_init, |
---|
443 |
ssh2_initiate_check, |
---|
444 |
ssh2_cleanup |
---|
445 |
}; |
---|
446 |
|
---|