Line data Source code
1 : /*
2 : SSSD
3 :
4 : Pam Proxy Child
5 :
6 : Authors:
7 :
8 : Sumit Bose <sbose@redhat.com>
9 :
10 : Copyright (C) 2010 Red Hat
11 :
12 : This program is free software; you can redistribute it and/or modify
13 : it under the terms of the GNU General Public License as published by
14 : the Free Software Foundation; either version 3 of the License, or
15 : (at your option) any later version.
16 :
17 : This program is distributed in the hope that it will be useful,
18 : but WITHOUT ANY WARRANTY; without even the implied warranty of
19 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 : GNU General Public License for more details.
21 :
22 : You should have received a copy of the GNU General Public License
23 : along with this program. If not, see <http://www.gnu.org/licenses/>.
24 : */
25 :
26 : #include <stdio.h>
27 : #include <unistd.h>
28 : #include <fcntl.h>
29 : #include <sys/types.h>
30 : #include <sys/stat.h>
31 : #include <sys/socket.h>
32 : #include <sys/un.h>
33 : #include <string.h>
34 : #include <sys/time.h>
35 : #include <errno.h>
36 : #include <dlfcn.h>
37 : #include <popt.h>
38 : #include <dbus/dbus.h>
39 :
40 : #include <security/pam_appl.h>
41 : #include <security/pam_modules.h>
42 :
43 : #include "util/util.h"
44 : #include "confdb/confdb.h"
45 : #include "sbus/sssd_dbus.h"
46 : #include "providers/proxy/proxy.h"
47 :
48 : #include "providers/dp_backend.h"
49 :
50 : static int pc_pam_handler(struct sbus_request *dbus_req, void *user_data);
51 :
52 : struct data_provider_iface pc_methods = {
53 : { &data_provider_iface_meta, 0 },
54 : .RegisterService = NULL,
55 : .pamHandler = pc_pam_handler,
56 : .sudoHandler = NULL,
57 : .autofsHandler = NULL,
58 : .hostHandler = NULL,
59 : .getDomains = NULL,
60 : .getAccountInfo = NULL,
61 : };
62 :
63 : struct pc_ctx {
64 : struct tevent_context *ev;
65 : struct confdb_ctx *cdb;
66 : struct sss_domain_info *domain;
67 : const char *identity;
68 : const char *conf_path;
69 : struct sbus_connection *mon_conn;
70 : struct sbus_connection *conn;
71 : const char *pam_target;
72 : uint32_t id;
73 : };
74 :
75 0 : static int proxy_internal_conv(int num_msg, const struct pam_message **msgm,
76 : struct pam_response **response,
77 : void *appdata_ptr) {
78 : int i;
79 : struct pam_response *reply;
80 : struct authtok_conv *auth_data;
81 : const char *password;
82 : size_t pwlen;
83 : errno_t ret;
84 :
85 0 : auth_data = talloc_get_type(appdata_ptr, struct authtok_conv);
86 :
87 0 : if (num_msg <= 0) return PAM_CONV_ERR;
88 :
89 0 : reply = (struct pam_response *) calloc(num_msg,
90 : sizeof(struct pam_response));
91 0 : if (reply == NULL) return PAM_CONV_ERR;
92 :
93 0 : for (i=0; i < num_msg; i++) {
94 0 : switch( msgm[i]->msg_style ) {
95 : case PAM_PROMPT_ECHO_OFF:
96 0 : DEBUG(SSSDBG_CONF_SETTINGS,
97 : "Conversation message: [%s]\n", msgm[i]->msg);
98 0 : reply[i].resp_retcode = 0;
99 :
100 0 : ret = sss_authtok_get_password(auth_data->authtok,
101 : &password, &pwlen);
102 0 : if (ret) goto failed;
103 0 : reply[i].resp = calloc(pwlen + 1, sizeof(char));
104 0 : if (reply[i].resp == NULL) goto failed;
105 0 : memcpy(reply[i].resp, password, pwlen + 1);
106 :
107 0 : break;
108 : default:
109 0 : DEBUG(SSSDBG_CRIT_FAILURE,
110 : "Conversation style %d not supported.\n",
111 : msgm[i]->msg_style);
112 0 : goto failed;
113 : }
114 : }
115 :
116 0 : *response = reply;
117 0 : reply = NULL;
118 :
119 0 : return PAM_SUCCESS;
120 :
121 : failed:
122 0 : free(reply);
123 0 : return PAM_CONV_ERR;
124 : }
125 :
126 0 : static int proxy_chauthtok_conv(int num_msg, const struct pam_message **msgm,
127 : struct pam_response **response,
128 : void *appdata_ptr) {
129 : int i;
130 : struct pam_response *reply;
131 : struct authtok_conv *auth_data;
132 : const char *password;
133 : size_t pwlen;
134 : errno_t ret;
135 :
136 0 : auth_data = talloc_get_type(appdata_ptr, struct authtok_conv);
137 :
138 0 : if (num_msg <= 0) return PAM_CONV_ERR;
139 :
140 0 : reply = (struct pam_response *) calloc(num_msg,
141 : sizeof(struct pam_response));
142 0 : if (reply == NULL) return PAM_CONV_ERR;
143 :
144 0 : for (i=0; i < num_msg; i++) {
145 0 : switch( msgm[i]->msg_style ) {
146 : case PAM_PROMPT_ECHO_OFF:
147 0 : DEBUG(SSSDBG_CONF_SETTINGS,
148 : "Conversation message: [%s]\n", msgm[i]->msg);
149 :
150 0 : reply[i].resp_retcode = 0;
151 0 : if (!auth_data->sent_old) {
152 : /* The first prompt will be asking for the old authtok */
153 0 : ret = sss_authtok_get_password(auth_data->authtok,
154 : &password, &pwlen);
155 0 : if (ret) goto failed;
156 0 : reply[i].resp = calloc(pwlen + 1, sizeof(char));
157 0 : if (reply[i].resp == NULL) goto failed;
158 0 : memcpy(reply[i].resp, password, pwlen + 1);
159 0 : auth_data->sent_old = true;
160 : }
161 : else {
162 : /* Subsequent prompts are looking for the new authtok */
163 0 : ret = sss_authtok_get_password(auth_data->newauthtok,
164 : &password, &pwlen);
165 0 : if (ret) goto failed;
166 0 : reply[i].resp = calloc(pwlen + 1, sizeof(char));
167 0 : if (reply[i].resp == NULL) goto failed;
168 0 : memcpy(reply[i].resp, password, pwlen + 1);
169 0 : auth_data->sent_old = true;
170 : }
171 :
172 0 : break;
173 : default:
174 0 : DEBUG(SSSDBG_CRIT_FAILURE,
175 : "Conversation style %d not supported.\n",
176 : msgm[i]->msg_style);
177 0 : goto failed;
178 : }
179 : }
180 :
181 0 : *response = reply;
182 0 : reply = NULL;
183 :
184 0 : return PAM_SUCCESS;
185 :
186 : failed:
187 0 : free(reply);
188 0 : return PAM_CONV_ERR;
189 : }
190 :
191 0 : static errno_t call_pam_stack(const char *pam_target, struct pam_data *pd)
192 : {
193 : int ret;
194 : int pam_status;
195 0 : pam_handle_t *pamh=NULL;
196 : struct authtok_conv *auth_data;
197 : struct pam_conv conv;
198 :
199 0 : if (pd->cmd == SSS_PAM_CHAUTHTOK) {
200 0 : conv.conv=proxy_chauthtok_conv;
201 : }
202 : else {
203 0 : conv.conv=proxy_internal_conv;
204 : }
205 0 : auth_data = talloc_zero(pd, struct authtok_conv);
206 0 : if (auth_data == NULL) {
207 0 : DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero failed.\n");
208 0 : return ENOMEM;
209 : }
210 0 : auth_data->authtok = sss_authtok_new(auth_data);
211 0 : if (auth_data->authtok == NULL) {
212 0 : DEBUG(SSSDBG_CRIT_FAILURE, "sss_authtok_new failed.\n");
213 0 : ret = ENOMEM;
214 0 : goto fail;
215 : }
216 0 : auth_data->newauthtok = sss_authtok_new(auth_data);
217 0 : if (auth_data->newauthtok == NULL) {
218 0 : DEBUG(SSSDBG_CRIT_FAILURE, "sss_authtok_new failed.\n");
219 0 : ret = ENOMEM;
220 0 : goto fail;
221 : }
222 :
223 0 : conv.appdata_ptr=auth_data;
224 :
225 0 : ret = pam_start(pam_target, pd->user, &conv, &pamh);
226 0 : if (ret == PAM_SUCCESS) {
227 0 : DEBUG(SSSDBG_TRACE_LIBS,
228 : "Pam transaction started with service name [%s].\n",
229 : pam_target);
230 0 : ret = pam_set_item(pamh, PAM_TTY, pd->tty);
231 0 : if (ret != PAM_SUCCESS) {
232 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Setting PAM_TTY failed: %s.\n",
233 : pam_strerror(pamh, ret));
234 : }
235 0 : ret = pam_set_item(pamh, PAM_RUSER, pd->ruser);
236 0 : if (ret != PAM_SUCCESS) {
237 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Setting PAM_RUSER failed: %s.\n",
238 : pam_strerror(pamh, ret));
239 : }
240 0 : ret = pam_set_item(pamh, PAM_RHOST, pd->rhost);
241 0 : if (ret != PAM_SUCCESS) {
242 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Setting PAM_RHOST failed: %s.\n",
243 : pam_strerror(pamh, ret));
244 : }
245 0 : switch (pd->cmd) {
246 : case SSS_PAM_AUTHENTICATE:
247 0 : sss_authtok_copy(pd->authtok, auth_data->authtok);
248 0 : pam_status = pam_authenticate(pamh, 0);
249 0 : break;
250 : case SSS_PAM_SETCRED:
251 0 : pam_status=pam_setcred(pamh, 0);
252 0 : break;
253 : case SSS_PAM_ACCT_MGMT:
254 0 : pam_status=pam_acct_mgmt(pamh, 0);
255 0 : break;
256 : case SSS_PAM_OPEN_SESSION:
257 0 : pam_status=pam_open_session(pamh, 0);
258 0 : break;
259 : case SSS_PAM_CLOSE_SESSION:
260 0 : pam_status=pam_close_session(pamh, 0);
261 0 : break;
262 : case SSS_PAM_CHAUTHTOK:
263 0 : sss_authtok_copy(pd->authtok, auth_data->authtok);
264 0 : if (pd->priv != 1) {
265 0 : pam_status = pam_authenticate(pamh, 0);
266 0 : auth_data->sent_old = false;
267 0 : if (pam_status != PAM_SUCCESS) break;
268 : }
269 0 : sss_authtok_copy(pd->newauthtok, auth_data->newauthtok);
270 0 : pam_status = pam_chauthtok(pamh, 0);
271 0 : break;
272 : case SSS_PAM_CHAUTHTOK_PRELIM:
273 0 : if (pd->priv != 1) {
274 0 : sss_authtok_copy(pd->authtok, auth_data->authtok);
275 0 : pam_status = pam_authenticate(pamh, 0);
276 : } else {
277 0 : pam_status = PAM_SUCCESS;
278 : }
279 0 : break;
280 : default:
281 0 : DEBUG(SSSDBG_CRIT_FAILURE, "unknown PAM call\n");
282 0 : pam_status=PAM_ABORT;
283 : }
284 :
285 0 : DEBUG(SSSDBG_CONF_SETTINGS, "Pam result: [%d][%s]\n", pam_status,
286 : pam_strerror(pamh, pam_status));
287 :
288 0 : ret = pam_end(pamh, pam_status);
289 0 : if (ret != PAM_SUCCESS) {
290 0 : pamh=NULL;
291 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Cannot terminate pam transaction.\n");
292 : }
293 :
294 : } else {
295 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Failed to initialize pam transaction.\n");
296 0 : pam_status = PAM_SYSTEM_ERR;
297 : }
298 :
299 0 : pd->pam_status = pam_status;
300 :
301 0 : return EOK;
302 : fail:
303 0 : talloc_free(auth_data);
304 0 : return ret;
305 : }
306 :
307 0 : static int pc_pam_handler(struct sbus_request *dbus_req, void *user_data)
308 : {
309 : DBusError dbus_error;
310 : DBusMessage *reply;
311 : struct pc_ctx *pc_ctx;
312 : errno_t ret;
313 0 : struct pam_data *pd = NULL;
314 :
315 0 : pc_ctx = talloc_get_type(user_data, struct pc_ctx);
316 0 : if (!pc_ctx) {
317 0 : ret = EINVAL;
318 0 : goto done;
319 : }
320 :
321 0 : reply = dbus_message_new_method_return(dbus_req->message);
322 0 : if (!reply) {
323 0 : DEBUG(SSSDBG_CRIT_FAILURE, "dbus_message_new_method_return failed, "
324 : "cannot send reply.\n");
325 0 : ret = ENOMEM;
326 0 : goto done;
327 : }
328 :
329 0 : dbus_error_init(&dbus_error);
330 :
331 0 : ret = dp_unpack_pam_request(dbus_req->message, pc_ctx, &pd, &dbus_error);
332 0 : if (!ret) {
333 0 : DEBUG(SSSDBG_CRIT_FAILURE,"Failed, to parse message!\n");
334 0 : ret = EIO;
335 0 : goto done;
336 : }
337 :
338 0 : pd->pam_status = PAM_SYSTEM_ERR;
339 0 : pd->domain = talloc_strdup(pd, pc_ctx->domain->name);
340 0 : if (pd->domain == NULL) {
341 0 : talloc_free(pd);
342 0 : ret = ENOMEM;
343 0 : goto done;
344 : }
345 :
346 0 : DEBUG(SSSDBG_CONF_SETTINGS, "Got request with the following data\n");
347 0 : DEBUG_PAM_DATA(SSSDBG_CONF_SETTINGS, pd);
348 :
349 0 : ret = call_pam_stack(pc_ctx->pam_target, pd);
350 0 : if (ret != EOK) {
351 0 : DEBUG(SSSDBG_CRIT_FAILURE, "call_pam_stack failed.\n");
352 : }
353 :
354 0 : DEBUG(SSSDBG_CONF_SETTINGS, "Sending result [%d][%s]\n",
355 : pd->pam_status, pd->domain);
356 :
357 0 : ret = dp_pack_pam_response(reply, pd);
358 0 : if (!ret) {
359 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Failed to generate dbus reply\n");
360 0 : talloc_free(pd);
361 0 : dbus_message_unref(reply);
362 0 : ret = EIO;
363 0 : goto done;
364 : }
365 :
366 0 : ret = sbus_request_finish(dbus_req, reply);
367 0 : dbus_message_unref(reply);
368 0 : talloc_free(pd);
369 :
370 : /* We'll return the message and let the
371 : * parent process kill us.
372 : */
373 0 : return ret;
374 :
375 : done:
376 0 : exit(ret);
377 : }
378 :
379 : int proxy_child_send_id(struct sbus_connection *conn,
380 : uint16_t version,
381 : uint32_t id);
382 0 : static int proxy_cli_init(struct pc_ctx *ctx)
383 : {
384 : char *sbus_address;
385 : int ret;
386 :
387 0 : sbus_address = talloc_asprintf(ctx, "unix:path=%s/%s_%s",
388 : PIPE_PATH, PROXY_CHILD_PIPE,
389 0 : ctx->domain->name);
390 0 : if (sbus_address == NULL) {
391 0 : DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n");
392 0 : return ENOMEM;
393 : }
394 :
395 0 : ret = sbus_client_init(ctx, ctx->ev, sbus_address, &ctx->conn);
396 0 : if (ret != EOK) {
397 0 : DEBUG(SSSDBG_CRIT_FAILURE, "sbus_client_init failed.\n");
398 0 : return ret;
399 : }
400 :
401 0 : ret = sbus_conn_register_iface(ctx->conn, &pc_methods.vtable, DP_PATH, ctx);
402 0 : if (ret != EOK) {
403 0 : DEBUG(SSSDBG_FATAL_FAILURE, "Failed to export proxy.\n");
404 0 : return ret;
405 : }
406 :
407 0 : ret = proxy_child_send_id(ctx->conn, DATA_PROVIDER_VERSION, ctx->id);
408 0 : if (ret != EOK) {
409 0 : DEBUG(SSSDBG_FATAL_FAILURE, "dp_common_send_id failed.\n");
410 0 : return ret;
411 : }
412 :
413 0 : return EOK;
414 : }
415 :
416 0 : int proxy_child_send_id(struct sbus_connection *conn,
417 : uint16_t version,
418 : uint32_t id)
419 : {
420 : DBusMessage *msg;
421 : dbus_bool_t ret;
422 : int retval;
423 :
424 : /* create the message */
425 0 : msg = dbus_message_new_method_call(NULL,
426 : DP_PATH,
427 : DATA_PROVIDER_IFACE,
428 : DATA_PROVIDER_IFACE_REGISTERSERVICE);
429 0 : if (msg == NULL) {
430 0 : DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory?!\n");
431 0 : return ENOMEM;
432 : }
433 :
434 0 : DEBUG(SSSDBG_FUNC_DATA, "Sending ID to Proxy Backend: (%d,%"PRIu32")\n",
435 : version, id);
436 :
437 0 : ret = dbus_message_append_args(msg,
438 : DBUS_TYPE_UINT16, &version,
439 : DBUS_TYPE_UINT32, &id,
440 : DBUS_TYPE_INVALID);
441 0 : if (!ret) {
442 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Failed to build message\n");
443 0 : return EIO;
444 : }
445 :
446 0 : retval = sbus_conn_send(conn, msg, 30000, dp_id_callback, NULL, NULL);
447 :
448 0 : dbus_message_unref(msg);
449 0 : return retval;
450 : }
451 :
452 0 : int proxy_child_process_init(TALLOC_CTX *mem_ctx, const char *domain,
453 : struct tevent_context *ev, struct confdb_ctx *cdb,
454 : const char *pam_target, uint32_t id)
455 : {
456 : struct pc_ctx *ctx;
457 : int ret;
458 :
459 0 : ctx = talloc_zero(mem_ctx, struct pc_ctx);
460 0 : if (!ctx) {
461 0 : DEBUG(SSSDBG_FATAL_FAILURE, "fatal error initializing pc_ctx\n");
462 0 : return ENOMEM;
463 : }
464 0 : ctx->ev = ev;
465 0 : ctx->cdb = cdb;
466 0 : ctx->pam_target = talloc_steal(ctx, pam_target);
467 0 : ctx->id = id;
468 0 : ctx->conf_path = talloc_asprintf(ctx, CONFDB_DOMAIN_PATH_TMPL, domain);
469 0 : if (!ctx->conf_path) {
470 0 : DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory!?\n");
471 0 : return ENOMEM;
472 : }
473 :
474 0 : ret = confdb_get_domain(cdb, domain, &ctx->domain);
475 0 : if (ret != EOK) {
476 0 : DEBUG(SSSDBG_FATAL_FAILURE,
477 : "fatal error retrieving domain configuration\n");
478 0 : return ret;
479 : }
480 :
481 0 : ret = proxy_cli_init(ctx);
482 0 : if (ret != EOK) {
483 0 : DEBUG(SSSDBG_FATAL_FAILURE, "fatal error setting up server bus\n");
484 0 : return ret;
485 : }
486 :
487 0 : return EOK;
488 : }
489 :
490 0 : int main(int argc, const char *argv[])
491 : {
492 : int opt;
493 : poptContext pc;
494 0 : char *domain = NULL;
495 0 : char *srv_name = NULL;
496 0 : char *conf_entry = NULL;
497 : struct main_context *main_ctx;
498 : int ret;
499 : long id;
500 0 : char *pam_target = NULL;
501 : uid_t uid;
502 : gid_t gid;
503 :
504 0 : struct poptOption long_options[] = {
505 : POPT_AUTOHELP
506 0 : SSSD_MAIN_OPTS
507 0 : SSSD_SERVER_OPTS(uid, gid)
508 : {"domain", 0, POPT_ARG_STRING, &domain, 0,
509 0 : _("Domain of the information provider (mandatory)"), NULL },
510 : {"id", 0, POPT_ARG_LONG, &id, 0,
511 0 : _("Child identifier (mandatory)"), NULL },
512 : POPT_TABLEEND
513 : };
514 :
515 : /* Set debug level to invalid value so we can deside if -d 0 was used. */
516 0 : debug_level = SSSDBG_INVALID;
517 :
518 0 : pc = poptGetContext(argv[0], argc, argv, long_options, 0);
519 0 : while((opt = poptGetNextOpt(pc)) != -1) {
520 : switch(opt) {
521 : default:
522 0 : fprintf(stderr, "\nInvalid option %s: %s\n\n",
523 : poptBadOption(pc, 0), poptStrerror(opt));
524 0 : poptPrintUsage(pc, stderr, 0);
525 0 : return 1;
526 : }
527 : }
528 :
529 0 : if (domain == NULL) {
530 0 : fprintf(stderr, "\nMissing option, "
531 : "--domain is a mandatory option.\n\n");
532 0 : poptPrintUsage(pc, stderr, 0);
533 0 : return 1;
534 : }
535 :
536 0 : if (id == 0) {
537 0 : fprintf(stderr, "\nMissing option, "
538 : "--id is a mandatory option.\n\n");
539 0 : poptPrintUsage(pc, stderr, 0);
540 0 : return 1;
541 : }
542 :
543 0 : poptFreeContext(pc);
544 :
545 0 : DEBUG_INIT(debug_level);
546 :
547 : /* set up things like debug , signals, daemonization, etc... */
548 0 : debug_log_file = talloc_asprintf(NULL, "proxy_child_%s", domain);
549 0 : if (!debug_log_file) return 2;
550 :
551 0 : srv_name = talloc_asprintf(NULL, "sssd[proxy_child[%s]]", domain);
552 0 : if (!srv_name) return 2;
553 :
554 0 : conf_entry = talloc_asprintf(NULL, CONFDB_DOMAIN_PATH_TMPL, domain);
555 0 : if (!conf_entry) return 2;
556 :
557 0 : ret = server_setup(srv_name, 0, 0, 0, conf_entry, &main_ctx);
558 0 : if (ret != EOK) {
559 0 : DEBUG(SSSDBG_FATAL_FAILURE, "Could not set up mainloop [%d]\n", ret);
560 0 : return 2;
561 : }
562 :
563 0 : ret = unsetenv("_SSS_LOOPS");
564 0 : if (ret != EOK) {
565 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unset _SSS_LOOPS, "
566 : "pam modules might not work as expected.\n");
567 : }
568 :
569 0 : ret = confdb_get_string(main_ctx->confdb_ctx, main_ctx, conf_entry,
570 : CONFDB_PROXY_PAM_TARGET, NULL, &pam_target);
571 0 : if (ret != EOK) {
572 0 : DEBUG(SSSDBG_FATAL_FAILURE, "Error reading from confdb (%d) [%s]\n",
573 : ret, strerror(ret));
574 0 : return 4;
575 : }
576 0 : if (pam_target == NULL) {
577 0 : DEBUG(SSSDBG_CRIT_FAILURE, "Missing option proxy_pam_target.\n");
578 0 : return 4;
579 : }
580 :
581 0 : ret = die_if_parent_died();
582 0 : if (ret != EOK) {
583 : /* This is not fatal, don't return */
584 0 : DEBUG(SSSDBG_OP_FAILURE,
585 : "Could not set up to exit when parent process does\n");
586 : }
587 :
588 0 : ret = proxy_child_process_init(main_ctx, domain, main_ctx->event_ctx,
589 0 : main_ctx->confdb_ctx, pam_target,
590 : (uint32_t)id);
591 0 : if (ret != EOK) {
592 0 : DEBUG(SSSDBG_FATAL_FAILURE,
593 : "Could not initialize proxy child [%d].\n", ret);
594 0 : return 3;
595 : }
596 :
597 0 : DEBUG(SSSDBG_CRIT_FAILURE,
598 : "Proxy child for domain [%s] started!\n", domain);
599 :
600 : /* loop on main */
601 0 : server_loop(main_ctx);
602 :
603 0 : return 0;
604 : }
|