blob: 89b52ad63c361ad5dd37ff65572b85f0062f6152 [file] [log] [blame]
Michal Vasko086311b2016-01-08 09:53:11 +01001/**
2 * \file session_server.c
3 * \author Michal Vasko <mvasko@cesnet.cz>
4 * \brief libnetconf2 server session manipulation functions
5 *
6 * Copyright (c) 2015 CESNET, z.s.p.o.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in
15 * the documentation and/or other materials provided with the
16 * distribution.
17 * 3. Neither the name of the Company nor the names of its contributors
18 * may be used to endorse or promote products derived from this
19 * software without specific prior written permission.
20 *
21 */
22
23#include <stdint.h>
24#include <stdlib.h>
25#include <errno.h>
26#include <string.h>
27#include <poll.h>
28#include <sys/types.h>
29#include <sys/socket.h>
30#include <netinet/in.h>
31#include <arpa/inet.h>
32#include <unistd.h>
Michal Vaskob48aa812016-01-18 14:13:09 +010033#include <pthread.h>
Michal Vasko11d142a2016-01-19 15:58:24 +010034#include <time.h>
Michal Vasko086311b2016-01-08 09:53:11 +010035
Michal Vasko1a38c862016-01-15 15:50:07 +010036#include "libnetconf.h"
Michal Vasko086311b2016-01-08 09:53:11 +010037#include "session_server.h"
38
Michal Vaskob48aa812016-01-18 14:13:09 +010039struct nc_server_opts server_opts = {
Michal Vasko11d142a2016-01-19 15:58:24 +010040 .ctx_lock = PTHREAD_MUTEX_INITIALIZER,
Michal Vaskob48aa812016-01-18 14:13:09 +010041 .bind_lock = PTHREAD_MUTEX_INITIALIZER
42};
Michal Vasko086311b2016-01-08 09:53:11 +010043
Michal Vasko1a38c862016-01-15 15:50:07 +010044API void
45nc_session_set_term_reason(struct nc_session *session, NC_SESSION_TERM_REASON reason)
46{
47 if (!session || !reason) {
48 ERRARG;
49 return;
50 }
51
52 session->term_reason = reason;
53}
54
Michal Vasko086311b2016-01-08 09:53:11 +010055int
Michal Vaskof05562c2016-01-20 12:06:43 +010056nc_sock_listen(const char *address, uint16_t port)
Michal Vasko086311b2016-01-08 09:53:11 +010057{
58 const int optVal = 1;
59 const socklen_t optLen = sizeof(optVal);
60 int is_ipv4, sock;
61 struct sockaddr_storage saddr;
62
63 struct sockaddr_in *saddr4;
64 struct sockaddr_in6 *saddr6;
65
66
67 if (!strchr(address, ':')) {
68 is_ipv4 = 1;
69 } else {
70 is_ipv4 = 0;
71 }
72
73 sock = socket((is_ipv4 ? AF_INET : AF_INET6), SOCK_STREAM, 0);
74 if (sock == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +010075 ERR("Failed to create socket (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +010076 goto fail;
77 }
78
79 if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&optVal, optLen)) {
Michal Vaskod083db62016-01-19 10:31:29 +010080 ERR("Could not set socket SO_REUSEADDR socket option (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +010081 goto fail;
82 }
83
84 bzero(&saddr, sizeof(struct sockaddr_storage));
85 if (is_ipv4) {
86 saddr4 = (struct sockaddr_in *)&saddr;
87
88 saddr4->sin_family = AF_INET;
89 saddr4->sin_port = htons(port);
90
91 if (inet_pton(AF_INET, address, &saddr4->sin_addr) != 1) {
Michal Vaskod083db62016-01-19 10:31:29 +010092 ERR("Failed to convert IPv4 address \"%s\".", address);
Michal Vasko086311b2016-01-08 09:53:11 +010093 goto fail;
94 }
95
96 if (bind(sock, (struct sockaddr *)saddr4, sizeof(struct sockaddr_in)) == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +010097 ERR("Could not bind \"%s\" port %d (%s).", address, port, strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +010098 goto fail;
99 }
100
101 } else {
102 saddr6 = (struct sockaddr_in6 *)&saddr;
103
104 saddr6->sin6_family = AF_INET6;
105 saddr6->sin6_port = htons(port);
106
107 if (inet_pton(AF_INET6, address, &saddr6->sin6_addr) != 1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100108 ERR("Failed to convert IPv6 address \"%s\".", address);
Michal Vasko086311b2016-01-08 09:53:11 +0100109 goto fail;
110 }
111
112 if (bind(sock, (struct sockaddr *)saddr6, sizeof(struct sockaddr_in6)) == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100113 ERR("Could not bind \"%s\" port %d (%s).", address, port, strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100114 goto fail;
115 }
116 }
117
Michal Vaskofb89d772016-01-08 12:25:35 +0100118 if (listen(sock, NC_REVERSE_QUEUE) == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100119 ERR("Unable to start listening on \"%s\" port %d (%s).", address, port, strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100120 goto fail;
121 }
122
123 return sock;
124
125fail:
126 if (sock > -1) {
127 close(sock);
128 }
129
130 return -1;
131}
132
133int
Michal Vaskof05562c2016-01-20 12:06:43 +0100134nc_sock_accept_binds(struct nc_bind *binds, uint16_t bind_count, int timeout, NC_TRANSPORT_IMPL *ti, char **host, uint16_t *port)
Michal Vasko086311b2016-01-08 09:53:11 +0100135{
136 uint16_t i;
137 struct pollfd *pfd;
138 struct sockaddr_storage saddr;
139 socklen_t saddr_len = sizeof(saddr);
140 int ret, sock = -1;
141
142 pfd = malloc(bind_count * sizeof *pfd);
143 for (i = 0; i < bind_count; ++i) {
144 pfd[i].fd = binds[i].sock;
145 pfd[i].events = POLLIN;
146 pfd[i].revents = 0;
147 }
148
149 /* poll for a new connection */
150 errno = 0;
151 ret = poll(pfd, bind_count, timeout);
152 if (!ret) {
153 /* we timeouted */
154 free(pfd);
155 return 0;
156 } else if (ret == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100157 ERR("Poll failed (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100158 free(pfd);
159 return -1;
160 }
161
162 for (i = 0; i < bind_count; ++i) {
163 if (pfd[i].revents & POLLIN) {
164 sock = pfd[i].fd;
165 break;
166 }
167 }
168 free(pfd);
169
170 if (sock == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100171 ERRINT;
Michal Vasko086311b2016-01-08 09:53:11 +0100172 return -1;
173 }
174
175 ret = accept(sock, (struct sockaddr *)&saddr, &saddr_len);
176 if (ret == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100177 ERR("Accept failed (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100178 return -1;
179 }
180
Michal Vasko9e036d52016-01-08 10:49:26 +0100181 if (ti) {
182 *ti = binds[i].ti;
183 }
184
Michal Vasko086311b2016-01-08 09:53:11 +0100185 /* host was requested */
186 if (host) {
187 if (saddr.ss_family == AF_INET) {
188 *host = malloc(15);
189 if (!inet_ntop(AF_INET, &((struct sockaddr_in *)&saddr)->sin_addr.s_addr, *host, 15)) {
Michal Vaskod083db62016-01-19 10:31:29 +0100190 ERR("inet_ntop failed (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100191 free(*host);
192 *host = NULL;
193 }
194
195 if (port) {
196 *port = ntohs(((struct sockaddr_in *)&saddr)->sin_port);
197 }
198 } else if (saddr.ss_family == AF_INET6) {
199 *host = malloc(40);
200 if (!inet_ntop(AF_INET6, ((struct sockaddr_in6 *)&saddr)->sin6_addr.s6_addr, *host, 40)) {
Michal Vaskod083db62016-01-19 10:31:29 +0100201 ERR("inet_ntop failed (%s).", strerror(errno));
Michal Vasko086311b2016-01-08 09:53:11 +0100202 free(*host);
203 *host = NULL;
204 }
205
206 if (port) {
207 *port = ntohs(((struct sockaddr_in6 *)&saddr)->sin6_port);
208 }
209 } else {
Michal Vaskod083db62016-01-19 10:31:29 +0100210 ERR("Source host of an unknown protocol family.");
Michal Vasko086311b2016-01-08 09:53:11 +0100211 }
212 }
213
214 return ret;
215}
216
Michal Vasko05ba9df2016-01-13 14:40:27 +0100217static struct nc_server_reply *
Michal Vasko428087d2016-01-14 16:04:28 +0100218nc_clb_default_get_schema(struct lyd_node *rpc, struct nc_session *UNUSED(session))
Michal Vasko05ba9df2016-01-13 14:40:27 +0100219{
220 const char *identifier = NULL, *version = NULL, *format = NULL;
221 char *model_data = NULL;
222 const struct lys_module *module;
223 struct nc_server_error *err;
224 struct lyd_node *child, *data = NULL;
Michal Vasko11d142a2016-01-19 15:58:24 +0100225 const struct lys_node *sdata = NULL;
Michal Vasko05ba9df2016-01-13 14:40:27 +0100226
227 LY_TREE_FOR(rpc->child, child) {
228 if (!strcmp(child->schema->name, "identifier")) {
229 identifier = ((struct lyd_node_leaf_list *)child)->value_str;
230 } else if (!strcmp(child->schema->name, "version")) {
231 version = ((struct lyd_node_leaf_list *)child)->value_str;
232 } else if (!strcmp(child->schema->name, "format")) {
233 format = ((struct lyd_node_leaf_list *)child)->value_str;
234 }
235 }
236
237 /* check version */
238 if (version && (strlen(version) != 10) && strcmp(version, "1.0")) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100239 err = nc_err(NC_ERR_INVALID_VALUE, NC_ERR_TYPE_APP);
240 nc_err_set_msg(err, "The requested version is not supported.", "en");
241 return nc_server_reply_err(err);
Michal Vasko05ba9df2016-01-13 14:40:27 +0100242 }
243
244 /* check and get module with the name identifier */
245 module = ly_ctx_get_module(server_opts.ctx, identifier, version);
246 if (!module) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100247 err = nc_err(NC_ERR_INVALID_VALUE, NC_ERR_TYPE_APP);
248 nc_err_set_msg(err, "The requested schema was not found.", "en");
249 return nc_server_reply_err(err);
Michal Vasko05ba9df2016-01-13 14:40:27 +0100250 }
251
252 /* check format */
253 if (!format || !strcmp(format, "yang")) {
254 lys_print_mem(&model_data, module, LYS_OUT_YANG, NULL);
255 } else if (!strcmp(format, "yin")) {
256 lys_print_mem(&model_data, module, LYS_OUT_YIN, NULL);
257 } else {
Michal Vasko1a38c862016-01-15 15:50:07 +0100258 err = nc_err(NC_ERR_INVALID_VALUE, NC_ERR_TYPE_APP);
259 nc_err_set_msg(err, "The requested format is not supported.", "en");
260 return nc_server_reply_err(err);
Michal Vasko05ba9df2016-01-13 14:40:27 +0100261 }
262
Michal Vasko0473c4c2016-01-19 10:40:06 +0100263 module = ly_ctx_get_module(server_opts.ctx, "ietf-netconf-monitoring", NULL);
264 if (module) {
265 sdata = lys_get_node(module, "/get-schema/output/data");
266 }
267 if (model_data && sdata) {
Michal Vasko11d142a2016-01-19 15:58:24 +0100268 nc_ctx_lock(-1, NULL);
Michal Vasko05ba9df2016-01-13 14:40:27 +0100269 data = lyd_output_new_anyxml(sdata, model_data);
Michal Vasko11d142a2016-01-19 15:58:24 +0100270 nc_ctx_unlock();
Michal Vasko05ba9df2016-01-13 14:40:27 +0100271 }
272 free(model_data);
273 if (!data) {
274 ERRINT;
275 return NULL;
276 }
277
278 return nc_server_reply_data(data, NC_PARAMTYPE_FREE);
279}
280
281static struct nc_server_reply *
Michal Vasko428087d2016-01-14 16:04:28 +0100282nc_clb_default_close_session(struct lyd_node *UNUSED(rpc), struct nc_session *session)
Michal Vasko05ba9df2016-01-13 14:40:27 +0100283{
Michal Vasko428087d2016-01-14 16:04:28 +0100284 session->term_reason = NC_SESSION_TERM_CLOSED;
285 return nc_server_reply_ok();
Michal Vasko05ba9df2016-01-13 14:40:27 +0100286}
287
Michal Vasko086311b2016-01-08 09:53:11 +0100288API int
289nc_server_init(struct ly_ctx *ctx)
290{
Michal Vasko05ba9df2016-01-13 14:40:27 +0100291 const struct lys_node *rpc;
Michal Vasko0473c4c2016-01-19 10:40:06 +0100292 const struct lys_module *mod;
Michal Vasko05ba9df2016-01-13 14:40:27 +0100293
Michal Vasko086311b2016-01-08 09:53:11 +0100294 if (!ctx) {
295 ERRARG;
296 return -1;
297 }
298
Michal Vasko05ba9df2016-01-13 14:40:27 +0100299 /* set default <get-schema> callback if not specified */
Michal Vasko0473c4c2016-01-19 10:40:06 +0100300 rpc = NULL;
301 mod = ly_ctx_get_module(ctx, "ietf-netconf-monitoring", NULL);
302 if (mod) {
303 rpc = lys_get_node(mod, "/get-schema");
304 }
Michal Vasko05ba9df2016-01-13 14:40:27 +0100305 if (rpc && !rpc->private) {
306 lys_set_private(rpc, nc_clb_default_get_schema);
307 }
308
309 /* set default <close-session> callback if not specififed */
Michal Vasko0473c4c2016-01-19 10:40:06 +0100310 rpc = NULL;
311 mod = ly_ctx_get_module(ctx, "ietf-netconf", NULL);
312 if (mod) {
313 rpc = lys_get_node(mod, "/close-session");
314 }
Michal Vasko05ba9df2016-01-13 14:40:27 +0100315 if (rpc && !rpc->private) {
316 lys_set_private(rpc, nc_clb_default_close_session);
317 }
318
Michal Vasko086311b2016-01-08 09:53:11 +0100319 server_opts.ctx = ctx;
Michal Vaskob48aa812016-01-18 14:13:09 +0100320
321 server_opts.new_session_id = 1;
322 pthread_spin_init(&server_opts.sid_lock, PTHREAD_PROCESS_PRIVATE);
323
Michal Vasko086311b2016-01-08 09:53:11 +0100324 return 0;
325}
326
Michal Vaskob48aa812016-01-18 14:13:09 +0100327API void
328nc_server_destroy(void)
329{
330 pthread_spin_destroy(&server_opts.sid_lock);
331
332#if defined(ENABLE_SSH) || defined(ENABLE_TLS)
333 nc_server_del_bind(NULL, 0, 0);
334#endif
335}
336
Michal Vasko086311b2016-01-08 09:53:11 +0100337API int
338nc_server_set_capab_withdefaults(NC_WD_MODE basic_mode, int also_supported)
339{
340 if (!basic_mode || (basic_mode == NC_WD_ALL_TAG)
341 || (also_supported && !(also_supported & (NC_WD_ALL | NC_WD_ALL_TAG | NC_WD_TRIM | NC_WD_EXPLICIT)))) {
342 ERRARG;
343 return -1;
344 }
345
346 server_opts.wd_basic_mode = basic_mode;
347 server_opts.wd_also_supported = also_supported;
348 return 0;
349}
350
Michal Vasko1a38c862016-01-15 15:50:07 +0100351API void
Michal Vasko086311b2016-01-08 09:53:11 +0100352nc_server_set_capab_interleave(int interleave_support)
353{
354 if (interleave_support) {
355 server_opts.interleave_capab = 1;
356 } else {
357 server_opts.interleave_capab = 0;
358 }
Michal Vasko086311b2016-01-08 09:53:11 +0100359}
360
Michal Vasko1a38c862016-01-15 15:50:07 +0100361API void
Michal Vasko086311b2016-01-08 09:53:11 +0100362nc_server_set_hello_timeout(uint16_t hello_timeout)
363{
Michal Vasko086311b2016-01-08 09:53:11 +0100364 server_opts.hello_timeout = hello_timeout;
Michal Vasko086311b2016-01-08 09:53:11 +0100365}
366
Michal Vasko1a38c862016-01-15 15:50:07 +0100367API void
Michal Vasko086311b2016-01-08 09:53:11 +0100368nc_server_set_idle_timeout(uint16_t idle_timeout)
369{
Michal Vasko086311b2016-01-08 09:53:11 +0100370 server_opts.idle_timeout = idle_timeout;
Michal Vasko086311b2016-01-08 09:53:11 +0100371}
372
373API int
Michal Vasko1a38c862016-01-15 15:50:07 +0100374nc_accept_inout(int fdin, int fdout, const char *username, struct nc_session **session)
Michal Vasko086311b2016-01-08 09:53:11 +0100375{
Michal Vasko7f1c78b2016-01-19 09:52:14 +0100376 if (!server_opts.ctx || (fdin < 0) || (fdout < 0) || !username || !session) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100377 ERRARG;
378 return -1;
Michal Vasko086311b2016-01-08 09:53:11 +0100379 }
380
381 /* prepare session structure */
Michal Vasko1a38c862016-01-15 15:50:07 +0100382 *session = calloc(1, sizeof **session);
383 if (!(*session)) {
Michal Vasko086311b2016-01-08 09:53:11 +0100384 ERRMEM;
Michal Vasko1a38c862016-01-15 15:50:07 +0100385 return -1;
Michal Vasko086311b2016-01-08 09:53:11 +0100386 }
Michal Vasko1a38c862016-01-15 15:50:07 +0100387 (*session)->status = NC_STATUS_STARTING;
388 (*session)->side = NC_SERVER;
Michal Vasko086311b2016-01-08 09:53:11 +0100389
390 /* transport specific data */
Michal Vasko1a38c862016-01-15 15:50:07 +0100391 (*session)->ti_type = NC_TI_FD;
392 (*session)->ti.fd.in = fdin;
393 (*session)->ti.fd.out = fdout;
Michal Vasko086311b2016-01-08 09:53:11 +0100394
395 /* assign context (dicionary needed for handshake) */
Michal Vasko1a38c862016-01-15 15:50:07 +0100396 (*session)->flags = NC_SESSION_SHAREDCTX;
397 (*session)->ctx = server_opts.ctx;
Michal Vasko086311b2016-01-08 09:53:11 +0100398
Michal Vaskob48aa812016-01-18 14:13:09 +0100399 /* assign new SID atomically */
400 pthread_spin_lock(&server_opts.sid_lock);
401 (*session)->id = server_opts.new_session_id++;
402 pthread_spin_unlock(&server_opts.sid_lock);
403
Michal Vasko086311b2016-01-08 09:53:11 +0100404 /* NETCONF handshake */
Michal Vasko1a38c862016-01-15 15:50:07 +0100405 if (nc_handshake(*session)) {
Michal Vasko086311b2016-01-08 09:53:11 +0100406 goto fail;
407 }
Michal Vasko1a38c862016-01-15 15:50:07 +0100408 (*session)->status = NC_STATUS_RUNNING;
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100409 (*session)->last_rpc = time(NULL);
Michal Vasko086311b2016-01-08 09:53:11 +0100410
Michal Vasko1a38c862016-01-15 15:50:07 +0100411 return 0;
Michal Vasko086311b2016-01-08 09:53:11 +0100412
413fail:
Michal Vasko1a38c862016-01-15 15:50:07 +0100414 nc_session_free(*session);
415 *session = NULL;
416 return -1;
Michal Vasko086311b2016-01-08 09:53:11 +0100417}
Michal Vasko9e036d52016-01-08 10:49:26 +0100418
Michal Vasko428087d2016-01-14 16:04:28 +0100419API struct nc_pollsession *
420nc_ps_new(void)
421{
422 return calloc(1, sizeof(struct nc_pollsession));
423}
424
425API void
426nc_ps_free(struct nc_pollsession *ps)
427{
Michal Vasko7f1c78b2016-01-19 09:52:14 +0100428 if (!ps) {
429 return;
430 }
431
Michal Vasko3a715132016-01-21 15:40:31 +0100432 free(ps->pfds);
Michal Vasko428087d2016-01-14 16:04:28 +0100433 free(ps->sessions);
434 free(ps);
435}
436
437API int
438nc_ps_add_session(struct nc_pollsession *ps, struct nc_session *session)
439{
440 if (!ps || !session) {
441 ERRARG;
442 return -1;
443 }
444
445 ++ps->session_count;
Michal Vasko3a715132016-01-21 15:40:31 +0100446 ps->pfds = realloc(ps->pfds, ps->session_count * sizeof *ps->pfds);
Michal Vasko428087d2016-01-14 16:04:28 +0100447 ps->sessions = realloc(ps->sessions, ps->session_count * sizeof *ps->sessions);
448
449 switch (session->ti_type) {
450 case NC_TI_FD:
Michal Vasko3a715132016-01-21 15:40:31 +0100451 ps->pfds[ps->session_count - 1].fd = session->ti.fd.in;
Michal Vasko428087d2016-01-14 16:04:28 +0100452 break;
453
454#ifdef ENABLE_SSH
455 case NC_TI_LIBSSH:
Michal Vasko3a715132016-01-21 15:40:31 +0100456 ps->pfds[ps->session_count - 1].fd = ssh_get_fd(session->ti.libssh.session);
Michal Vasko428087d2016-01-14 16:04:28 +0100457 break;
458#endif
459
460#ifdef ENABLE_TLS
461 case NC_TI_OPENSSL:
Michal Vasko3a715132016-01-21 15:40:31 +0100462 ps->pfds[ps->session_count - 1].fd = SSL_get_rfd(session->ti.tls);
Michal Vasko428087d2016-01-14 16:04:28 +0100463 break;
464#endif
465
466 default:
467 ERRINT;
468 return -1;
469 }
Michal Vasko3a715132016-01-21 15:40:31 +0100470 ps->pfds[ps->session_count - 1].events = POLLIN;
471 ps->pfds[ps->session_count - 1].revents = 0;
472 ps->sessions[ps->session_count - 1] = session;
Michal Vasko428087d2016-01-14 16:04:28 +0100473
474 return 0;
475}
476
477API int
478nc_ps_del_session(struct nc_pollsession *ps, struct nc_session *session)
479{
480 uint16_t i;
481
482 if (!ps || !session) {
483 ERRARG;
484 return -1;
485 }
486
487 for (i = 0; i < ps->session_count; ++i) {
Michal Vasko3a715132016-01-21 15:40:31 +0100488 if (ps->sessions[i] == session) {
Michal Vasko428087d2016-01-14 16:04:28 +0100489 --ps->session_count;
Michal Vasko3a715132016-01-21 15:40:31 +0100490 ps->sessions[i] = ps->sessions[ps->session_count];
491 memcpy(&ps->pfds[i], &ps->pfds[ps->session_count], sizeof *ps->pfds);
Michal Vasko428087d2016-01-14 16:04:28 +0100492 return 0;
493 }
494 }
495
496 return 1;
497}
498
499/* must be called holding the session lock! */
500static NC_MSG_TYPE
501nc_recv_rpc(struct nc_session *session, struct nc_server_rpc **rpc)
502{
503 struct lyxml_elem *xml = NULL;
504 NC_MSG_TYPE msgtype;
505
506 if (!session || !rpc) {
507 ERRARG;
508 return NC_MSG_ERROR;
509 } else if ((session->status != NC_STATUS_RUNNING) || (session->side != NC_SERVER)) {
Michal Vaskod083db62016-01-19 10:31:29 +0100510 ERR("Session %u: invalid session to receive RPCs.", session->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100511 return NC_MSG_ERROR;
512 }
513
514 msgtype = nc_read_msg(session, &xml);
515
516 switch (msgtype) {
517 case NC_MSG_RPC:
518 *rpc = malloc(sizeof **rpc);
Michal Vasko11d142a2016-01-19 15:58:24 +0100519 nc_ctx_lock(-1, NULL);
Michal Vasko428087d2016-01-14 16:04:28 +0100520 (*rpc)->tree = lyd_parse_xml(server_opts.ctx, &xml->child, LYD_OPT_DESTRUCT | LYD_OPT_RPC);
Michal Vasko11d142a2016-01-19 15:58:24 +0100521 nc_ctx_unlock();
Michal Vasko428087d2016-01-14 16:04:28 +0100522 (*rpc)->root = xml;
523 break;
524 case NC_MSG_HELLO:
Michal Vaskod083db62016-01-19 10:31:29 +0100525 ERR("Session %u: received another <hello> message.", session->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100526 goto error;
527 case NC_MSG_REPLY:
Michal Vaskod083db62016-01-19 10:31:29 +0100528 ERR("Session %u: received <rpc-reply> from NETCONF client.", session->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100529 goto error;
530 case NC_MSG_NOTIF:
Michal Vaskod083db62016-01-19 10:31:29 +0100531 ERR("Session %u: received <notification> from NETCONF client.", session->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100532 goto error;
533 default:
534 /* NC_MSG_ERROR - pass it out;
535 * NC_MSG_WOULDBLOCK and NC_MSG_NONE is not returned by nc_read_msg()
536 */
537 break;
538 }
539
540 return msgtype;
541
542error:
543 /* cleanup */
544 lyxml_free(server_opts.ctx, xml);
545
546 return NC_MSG_ERROR;
547}
548
549/* must be called holding the session lock! */
550static NC_MSG_TYPE
551nc_send_reply(struct nc_session *session, struct nc_server_rpc *rpc)
552{
553 nc_rpc_clb clb;
554 struct nc_server_reply *reply;
555 int ret;
556
557 /* no callback, reply with a not-implemented error */
558 if (!rpc->tree->schema->private) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100559 reply = nc_server_reply_err(nc_err(NC_ERR_OP_NOT_SUPPORTED, NC_ERR_TYPE_PROT));
Michal Vasko428087d2016-01-14 16:04:28 +0100560 } else {
561 clb = (nc_rpc_clb)rpc->tree->schema->private;
562 reply = clb(rpc->tree, session);
563 }
564
565 if (!reply) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100566 reply = nc_server_reply_err(nc_err(NC_ERR_OP_FAILED, NC_ERR_TYPE_APP));
Michal Vasko428087d2016-01-14 16:04:28 +0100567 }
568
569 ret = nc_write_msg(session, NC_MSG_REPLY, rpc->root, reply);
570
571 /* special case if term_reason was set in callback, last reply was sent (needed for <close-session> if nothing else) */
572 if ((session->status == NC_STATUS_RUNNING) && (session->term_reason != NC_SESSION_TERM_NONE)) {
573 session->status = NC_STATUS_INVALID;
574 }
575
576 if (ret == -1) {
Michal Vaskod083db62016-01-19 10:31:29 +0100577 ERR("Session %u: failed to write reply.", session->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100578 nc_server_reply_free(reply);
579 return NC_MSG_ERROR;
580 }
581 nc_server_reply_free(reply);
582
583 return NC_MSG_REPLY;
584}
585
586API int
587nc_ps_poll(struct nc_pollsession *ps, int timeout)
588{
589 int ret;
Michal Vasko96164bf2016-01-21 15:41:58 +0100590 uint16_t i, j;
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100591 time_t cur_time;
Michal Vasko428087d2016-01-14 16:04:28 +0100592 NC_MSG_TYPE msgtype;
593 struct nc_session *session;
594 struct nc_server_rpc *rpc;
Michal Vasko96164bf2016-01-21 15:41:58 +0100595 struct timespec old_ts;
Michal Vasko428087d2016-01-14 16:04:28 +0100596
597 if (!ps || !ps->session_count) {
598 ERRARG;
599 return -1;
600 }
601
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100602 cur_time = time(NULL);
603
Michal Vasko428087d2016-01-14 16:04:28 +0100604 for (i = 0; i < ps->session_count; ++i) {
Michal Vasko3a715132016-01-21 15:40:31 +0100605 if (ps->sessions[i]->status != NC_STATUS_RUNNING) {
606 ERR("Session %u: session not running.", ps->sessions[i]->id);
Michal Vasko428087d2016-01-14 16:04:28 +0100607 return -1;
608 }
Michal Vaskobd8ef262016-01-20 11:09:27 +0100609
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100610 /* TODO invalidate only sessions without subscription */
Michal Vasko3a715132016-01-21 15:40:31 +0100611 if (server_opts.idle_timeout && (ps->sessions[i]->last_rpc + server_opts.idle_timeout >= cur_time)) {
612 ERR("Session %u: session idle timeout elapsed.", ps->sessions[i]->id);
613 ps->sessions[i]->status = NC_STATUS_INVALID;
614 ps->sessions[i]->term_reason = NC_SESSION_TERM_TIMEOUT;
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100615 return 3;
616 }
617
Michal Vasko3a715132016-01-21 15:40:31 +0100618 if (ps->pfds[i].revents) {
Michal Vaskobd8ef262016-01-20 11:09:27 +0100619 break;
620 }
Michal Vasko428087d2016-01-14 16:04:28 +0100621 }
622
623 if (timeout > 0) {
624 clock_gettime(CLOCK_MONOTONIC_RAW, &old_ts);
625 }
626
Michal Vaskobd8ef262016-01-20 11:09:27 +0100627 if (i == ps->session_count) {
Michal Vasko3a715132016-01-21 15:40:31 +0100628retry_poll:
Michal Vaskobd8ef262016-01-20 11:09:27 +0100629 /* no leftover event */
630 i = 0;
Michal Vasko3a715132016-01-21 15:40:31 +0100631 ret = poll(ps->pfds, ps->session_count, timeout);
Michal Vaskobd8ef262016-01-20 11:09:27 +0100632 if (ret < 1) {
633 return ret;
634 }
Michal Vasko428087d2016-01-14 16:04:28 +0100635 }
636
Michal Vaskobd8ef262016-01-20 11:09:27 +0100637 /* find the first fd with POLLIN, we don't care if there are more now */
638 for (; i < ps->session_count; ++i) {
Michal Vasko3a715132016-01-21 15:40:31 +0100639 if (ps->pfds[i].revents & POLLHUP) {
640 ERR("Session %u: communication socket unexpectedly closed.", ps->sessions[i]->id);
641 ps->sessions[i]->status = NC_STATUS_INVALID;
642 ps->sessions[i]->term_reason = NC_SESSION_TERM_DROPPED;
Michal Vaskobd8ef262016-01-20 11:09:27 +0100643 return 3;
Michal Vasko3a715132016-01-21 15:40:31 +0100644 } else if (ps->pfds[i].revents & POLLERR) {
645 ERR("Session %u: communication socket error.", ps->sessions[i]->id);
646 ps->sessions[i]->status = NC_STATUS_INVALID;
647 ps->sessions[i]->term_reason = NC_SESSION_TERM_OTHER;
Michal Vaskobd8ef262016-01-20 11:09:27 +0100648 return 3;
Michal Vasko3a715132016-01-21 15:40:31 +0100649 } else if (ps->pfds[i].revents & POLLIN) {
Michal Vasko428087d2016-01-14 16:04:28 +0100650#ifdef ENABLE_SSH
Michal Vasko96164bf2016-01-21 15:41:58 +0100651 if (ps->sessions[i]->ti_type == NC_TI_LIBSSH) {
652 /* things are not that simple with SSH... */
653 ret = nc_ssh_pollin(ps->sessions[i], &timeout);
654
655 /* clear POLLIN on sessions sharing this session's SSH session */
656 if ((ret == 1) || (ret >= 4)) {
657 for (j = i + 1; j < ps->session_count; ++j) {
658 if (ps->pfds[j].fd == ps->pfds[i].fd) {
659 ps->pfds[j].revents = 0;
660 }
661 }
662 }
663
664 /* actual event happened */
665 if ((ret <= 0) || (ret >= 3)) {
666 ps->pfds[i].revents = 0;
667 return ret;
668
669 /* event occurred on some other channel */
670 } else if (ret == 2) {
671 ps->pfds[i].revents = 0;
Michal Vasko428087d2016-01-14 16:04:28 +0100672 if (i == ps->session_count - 1) {
673 /* last session and it is not the right channel, ... */
674 if (timeout > 0) {
675 /* ... decrease timeout, wait it all out and try again, last time */
Michal Vasko96164bf2016-01-21 15:41:58 +0100676 nc_subtract_elapsed(&timeout, &old_ts);
677 usleep(timeout * 1000);
678 timeout = 0;
679 goto retry_poll;
Michal Vasko428087d2016-01-14 16:04:28 +0100680 } else if (!timeout) {
681 /* ... timeout is 0, so that is it */
682 return 0;
683 } else {
684 /* ... retry polling reasonable time apart */
685 usleep(NC_TIMEOUT_STEP);
686 goto retry_poll;
687 }
688 }
689 /* check other sessions */
690 continue;
Michal Vasko428087d2016-01-14 16:04:28 +0100691 }
692 }
693#endif /* ENABLE_SSH */
694
Michal Vaskobd8ef262016-01-20 11:09:27 +0100695 /* we are going to process it now */
Michal Vasko3a715132016-01-21 15:40:31 +0100696 ps->pfds[i].revents = 0;
Michal Vasko428087d2016-01-14 16:04:28 +0100697 break;
698 }
699 }
700
701 if (i == ps->session_count) {
702 ERRINT;
703 return -1;
704 }
705
706 /* this is the session with some data available for reading */
Michal Vasko3a715132016-01-21 15:40:31 +0100707 session = ps->sessions[i];
Michal Vasko428087d2016-01-14 16:04:28 +0100708
709 if (timeout > 0) {
Michal Vasko96164bf2016-01-21 15:41:58 +0100710 nc_subtract_elapsed(&timeout, &old_ts);
Michal Vasko428087d2016-01-14 16:04:28 +0100711 }
712
Michal Vaskobd8ef262016-01-20 11:09:27 +0100713 /* reading an RPC and sending a reply must be atomic (no other RPC should be read) */
Michal Vasko7f1c78b2016-01-19 09:52:14 +0100714 ret = nc_timedlock(session->ti_lock, timeout, NULL);
715 if (ret != 1) {
716 /* error or timeout */
717 return ret;
Michal Vasko428087d2016-01-14 16:04:28 +0100718 }
719
720 msgtype = nc_recv_rpc(session, &rpc);
721 if (msgtype == NC_MSG_ERROR) {
Michal Vasko7f1c78b2016-01-19 09:52:14 +0100722 pthread_mutex_unlock(session->ti_lock);
Michal Vasko428087d2016-01-14 16:04:28 +0100723 if (session->status != NC_STATUS_RUNNING) {
Michal Vaskobd8ef262016-01-20 11:09:27 +0100724 return 3;
Michal Vasko428087d2016-01-14 16:04:28 +0100725 }
726 return -1;
727 }
728
729 /* process RPC */
Michal Vasko5e6f4cc2016-01-20 13:27:44 +0100730 session->last_rpc = time(NULL);
Michal Vasko428087d2016-01-14 16:04:28 +0100731 msgtype = nc_send_reply(session, rpc);
732
Michal Vasko7f1c78b2016-01-19 09:52:14 +0100733 pthread_mutex_unlock(session->ti_lock);
Michal Vasko428087d2016-01-14 16:04:28 +0100734
735 if (msgtype == NC_MSG_ERROR) {
736 nc_server_rpc_free(rpc);
737 if (session->status != NC_STATUS_RUNNING) {
Michal Vaskobd8ef262016-01-20 11:09:27 +0100738 return 3;
Michal Vasko428087d2016-01-14 16:04:28 +0100739 }
740 return -1;
741 }
742
743 nc_server_rpc_free(rpc);
Michal Vaskobd8ef262016-01-20 11:09:27 +0100744
745 /* is there some other socket waiting? */
746 for (++i; i < ps->session_count; ++i) {
Michal Vasko3a715132016-01-21 15:40:31 +0100747 if (ps->pfds[i].revents) {
Michal Vaskobd8ef262016-01-20 11:09:27 +0100748 return 2;
749 }
750 }
751
Michal Vasko428087d2016-01-14 16:04:28 +0100752 return 1;
753}
754
Michal Vasko11d142a2016-01-19 15:58:24 +0100755API int
756nc_ctx_lock(int timeout, int *elapsed)
757{
758 return nc_timedlock(&server_opts.ctx_lock, timeout, elapsed);
759}
760
761API int
762nc_ctx_unlock(void)
763{
764 int ret;
765
766 ret = pthread_mutex_unlock(&server_opts.ctx_lock);
767
768 if (ret) {
769 ERR("Mutex unlock failed (%s).", strerror(ret));
770 return -1;
771 }
772
773 return 0;
774}
775
Michal Vasko9e036d52016-01-08 10:49:26 +0100776#if defined(ENABLE_SSH) || defined(ENABLE_TLS)
777
778API int
779nc_server_add_bind_listen(const char *address, uint16_t port, NC_TRANSPORT_IMPL ti)
780{
781 int sock;
782
783 if (!address || !port || ((ti != NC_TI_LIBSSH) && (ti != NC_TI_OPENSSL))) {
784 ERRARG;
785 return -1;
786 }
787
788 sock = nc_sock_listen(address, port);
789 if (sock == -1) {
790 return -1;
791 }
792
Michal Vaskob48aa812016-01-18 14:13:09 +0100793 /* LOCK */
794 pthread_mutex_lock(&server_opts.bind_lock);
795
Michal Vasko9e036d52016-01-08 10:49:26 +0100796 ++server_opts.bind_count;
797 server_opts.binds = realloc(server_opts.binds, server_opts.bind_count * sizeof *server_opts.binds);
798
Michal Vasko11d142a2016-01-19 15:58:24 +0100799 nc_ctx_lock(-1, NULL);
800 server_opts.binds[server_opts.bind_count - 1].address = lydict_insert(server_opts.ctx, address, 0);
801 nc_ctx_unlock();
Michal Vasko9e036d52016-01-08 10:49:26 +0100802 server_opts.binds[server_opts.bind_count - 1].port = port;
803 server_opts.binds[server_opts.bind_count - 1].sock = sock;
804 server_opts.binds[server_opts.bind_count - 1].ti = ti;
805
Michal Vaskob48aa812016-01-18 14:13:09 +0100806 /* UNLOCK */
807 pthread_mutex_unlock(&server_opts.bind_lock);
808
Michal Vasko9e036d52016-01-08 10:49:26 +0100809 return 0;
810}
811
812API int
813nc_server_del_bind(const char *address, uint16_t port, NC_TRANSPORT_IMPL ti)
814{
815 uint32_t i;
816 int ret = -1;
817
Michal Vaskob48aa812016-01-18 14:13:09 +0100818 /* LOCK */
819 pthread_mutex_lock(&server_opts.bind_lock);
820
Michal Vasko1a38c862016-01-15 15:50:07 +0100821 if (!address && !port && !ti) {
Michal Vasko11d142a2016-01-19 15:58:24 +0100822 nc_ctx_lock(-1, NULL);
Michal Vasko1a38c862016-01-15 15:50:07 +0100823 for (i = 0; i < server_opts.bind_count; ++i) {
Michal Vasko9e036d52016-01-08 10:49:26 +0100824 close(server_opts.binds[i].sock);
Michal Vasko11d142a2016-01-19 15:58:24 +0100825 lydict_remove(server_opts.ctx, server_opts.binds[i].address);
Michal Vasko9e036d52016-01-08 10:49:26 +0100826
Michal Vasko9e036d52016-01-08 10:49:26 +0100827 ret = 0;
828 }
Michal Vasko11d142a2016-01-19 15:58:24 +0100829 nc_ctx_unlock();
Michal Vasko1a38c862016-01-15 15:50:07 +0100830 free(server_opts.binds);
831 server_opts.binds = NULL;
832 server_opts.bind_count = 0;
833 } else {
834 for (i = 0; i < server_opts.bind_count; ++i) {
835 if ((!address || !strcmp(server_opts.binds[i].address, address))
836 && (!port || (server_opts.binds[i].port == port))
837 && (!ti || (server_opts.binds[i].ti == ti))) {
838 close(server_opts.binds[i].sock);
Michal Vasko11d142a2016-01-19 15:58:24 +0100839 nc_ctx_lock(-1, NULL);
840 lydict_remove(server_opts.ctx, server_opts.binds[i].address);
841 nc_ctx_unlock();
Michal Vasko1a38c862016-01-15 15:50:07 +0100842
843 --server_opts.bind_count;
Michal Vasko5b003bf2016-01-19 10:56:19 +0100844 memcpy(&server_opts.binds[i], &server_opts.binds[server_opts.bind_count], sizeof *server_opts.binds);
Michal Vasko1a38c862016-01-15 15:50:07 +0100845
846 ret = 0;
847 }
848 }
Michal Vasko9e036d52016-01-08 10:49:26 +0100849 }
850
Michal Vaskob48aa812016-01-18 14:13:09 +0100851 /* UNLOCK */
852 pthread_mutex_unlock(&server_opts.bind_lock);
853
Michal Vasko9e036d52016-01-08 10:49:26 +0100854 return ret;
855}
856
Michal Vasko1a38c862016-01-15 15:50:07 +0100857API int
858nc_accept(int timeout, struct nc_session **session)
Michal Vasko9e036d52016-01-08 10:49:26 +0100859{
860 NC_TRANSPORT_IMPL ti;
Michal Vasko1a38c862016-01-15 15:50:07 +0100861 int sock, ret;
Michal Vasko9e036d52016-01-08 10:49:26 +0100862 char *host;
863 uint16_t port;
Michal Vasko9e036d52016-01-08 10:49:26 +0100864
Michal Vasko1a38c862016-01-15 15:50:07 +0100865 if (!server_opts.ctx || !server_opts.binds || !session) {
Michal Vasko9e036d52016-01-08 10:49:26 +0100866 ERRARG;
Michal Vasko1a38c862016-01-15 15:50:07 +0100867 return -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100868 }
869
Michal Vaskob48aa812016-01-18 14:13:09 +0100870 /* LOCK */
871 pthread_mutex_lock(&server_opts.bind_lock);
872
Michal Vaskof05562c2016-01-20 12:06:43 +0100873 ret = nc_sock_accept_binds(server_opts.binds, server_opts.bind_count, timeout, &ti, &host, &port);
Michal Vaskob48aa812016-01-18 14:13:09 +0100874
875 /* UNLOCK */
876 pthread_mutex_unlock(&server_opts.bind_lock);
877
Michal Vasko11d142a2016-01-19 15:58:24 +0100878 if (ret < 0) {
Michal Vaskob48aa812016-01-18 14:13:09 +0100879 return ret;
Michal Vasko9e036d52016-01-08 10:49:26 +0100880 }
Michal Vaskob48aa812016-01-18 14:13:09 +0100881 sock = ret;
Michal Vasko9e036d52016-01-08 10:49:26 +0100882
Michal Vasko1a38c862016-01-15 15:50:07 +0100883 *session = calloc(1, sizeof **session);
Michal Vasko9e036d52016-01-08 10:49:26 +0100884 if (!session) {
885 ERRMEM;
Michal Vaskoc14e3c82016-01-11 16:14:30 +0100886 close(sock);
Michal Vasko1a38c862016-01-15 15:50:07 +0100887 return -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100888 }
Michal Vasko1a38c862016-01-15 15:50:07 +0100889 (*session)->status = NC_STATUS_STARTING;
890 (*session)->side = NC_SERVER;
891 (*session)->ctx = server_opts.ctx;
892 (*session)->flags = NC_SESSION_SHAREDCTX;
Michal Vasko11d142a2016-01-19 15:58:24 +0100893 nc_ctx_lock(-1, NULL);
Michal Vasko1a38c862016-01-15 15:50:07 +0100894 (*session)->host = lydict_insert_zc(server_opts.ctx, host);
Michal Vasko11d142a2016-01-19 15:58:24 +0100895 nc_ctx_unlock();
Michal Vasko1a38c862016-01-15 15:50:07 +0100896 (*session)->port = port;
Michal Vasko9e036d52016-01-08 10:49:26 +0100897
898 /* transport lock */
Michal Vasko1a38c862016-01-15 15:50:07 +0100899 (*session)->ti_lock = malloc(sizeof *(*session)->ti_lock);
900 if (!(*session)->ti_lock) {
Michal Vasko9e036d52016-01-08 10:49:26 +0100901 ERRMEM;
Michal Vaskoc14e3c82016-01-11 16:14:30 +0100902 close(sock);
Michal Vasko1a38c862016-01-15 15:50:07 +0100903 ret = -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100904 goto fail;
905 }
Michal Vasko1a38c862016-01-15 15:50:07 +0100906 pthread_mutex_init((*session)->ti_lock, NULL);
Michal Vasko9e036d52016-01-08 10:49:26 +0100907
Michal Vaskoc14e3c82016-01-11 16:14:30 +0100908 /* sock gets assigned to session or closed */
Michal Vasko9e036d52016-01-08 10:49:26 +0100909 if (ti == NC_TI_LIBSSH) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100910 ret = nc_accept_ssh_session(*session, sock, timeout);
911 if (ret < 1) {
Michal Vasko9e036d52016-01-08 10:49:26 +0100912 goto fail;
913 }
914 } else if (ti == NC_TI_OPENSSL) {
Michal Vasko1a38c862016-01-15 15:50:07 +0100915 ret = nc_accept_tls_session(*session, sock, timeout);
916 if (ret < 1) {
Michal Vasko9e036d52016-01-08 10:49:26 +0100917 goto fail;
918 }
919 } else {
920 ERRINT;
Michal Vaskoc14e3c82016-01-11 16:14:30 +0100921 close(sock);
Michal Vasko1a38c862016-01-15 15:50:07 +0100922 ret = -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100923 goto fail;
924 }
925
Michal Vaskob48aa812016-01-18 14:13:09 +0100926 /* assign new SID atomically */
927 /* LOCK */
928 pthread_spin_lock(&server_opts.sid_lock);
929 (*session)->id = server_opts.new_session_id++;
930 /* UNLOCK */
931 pthread_spin_unlock(&server_opts.sid_lock);
932
Michal Vasko9e036d52016-01-08 10:49:26 +0100933 /* NETCONF handshake */
Michal Vasko1a38c862016-01-15 15:50:07 +0100934 if (nc_handshake(*session)) {
935 ret = -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100936 goto fail;
937 }
Michal Vasko1a38c862016-01-15 15:50:07 +0100938 (*session)->status = NC_STATUS_RUNNING;
Michal Vasko9e036d52016-01-08 10:49:26 +0100939
Michal Vasko1a38c862016-01-15 15:50:07 +0100940 return 1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100941
942fail:
Michal Vasko1a38c862016-01-15 15:50:07 +0100943 nc_session_free(*session);
944 *session = NULL;
945 return -1;
Michal Vasko9e036d52016-01-08 10:49:26 +0100946}
947
948#endif /* ENABLE_SSH || ENABLE_TLS */