2 * mod_shib.cpp -- Shibboleth module for Apache-2.0
4 * Created by: Derek Atkins <derek@ihtfp.com>
10 #include <saml/saml.h>
11 #include <shib/shib.h>
12 #include <shib/shib-threads.h>
13 #include <shib-target/shib-target.h>
15 // Apache specific header files
17 #include <http_config.h>
18 #include <http_protocol.h>
19 #include <http_main.h>
20 #include <util_script.h>
21 #include <apr_strings.h>
23 #include <http_core.h>
30 // For POST processing from Apache
31 //-- do we still need this? #undef _XOPEN_SOURCE // bombs on solaris
32 #include <apreq_params.h>
34 #include <unistd.h> // for getpid()
38 using namespace shibboleth;
39 using namespace shibtarget;
41 extern "C" AP_MODULE_DECLARE_DATA module mod_shib;
44 char* g_szSHIREURL = NULL;
45 char* g_szSHIBConfig = NULL;
46 ThreadKey* rpc_handle_key = NULL;
47 ShibTargetConfig* g_Config = NULL;
50 // per-dir module configuration structure
51 struct shib_dir_config
53 int bBasicHijack; // activate for AuthType Basic?
54 int bSSLOnly; // only over SSL?
55 SHIREConfig config; // SHIB Configuration
56 RMConfig rm_config; // RM Configuration
59 // creates per-directory config structure
60 extern "C" void* create_shib_dir_config (apr_pool_t* p, char* d)
62 shib_dir_config* dc=(shib_dir_config*)apr_pcalloc(p,sizeof(shib_dir_config));
63 dc->bBasicHijack = -1;
65 dc->config.lifetime = -1;
66 dc->config.timeout = -1;
70 // overrides server configuration in directories
71 extern "C" void* merge_shib_dir_config (apr_pool_t* p, void* base, void* sub)
73 shib_dir_config* dc=(shib_dir_config*)apr_pcalloc(p,sizeof(shib_dir_config));
74 shib_dir_config* parent=(shib_dir_config*)base;
75 shib_dir_config* child=(shib_dir_config*)sub;
77 dc->bBasicHijack=((child->bBasicHijack==-1) ? parent->bBasicHijack : child->bBasicHijack);
78 dc->bSSLOnly=((child->bSSLOnly==-1) ? parent->bSSLOnly : child->bSSLOnly);
79 dc->config.lifetime=((child->config.lifetime==-1) ? parent->config.lifetime : child->config.lifetime);
80 dc->config.timeout=((child->config.timeout==-1) ? parent->config.timeout : child->config.timeout);
84 // generic global slot handlers
85 extern "C" const char* ap_set_global_string_slot(cmd_parms* parms, void*, const char* arg)
87 *((char**)(parms->info))=apr_pstrdup(parms->pool,arg);
91 // some shortcuts for directory config slots
92 extern "C" const char* set_lifetime(cmd_parms* parms, shib_dir_config* dc, const char* arg)
94 dc->config.lifetime=atoi(arg);
98 extern "C" const char* set_timeout(cmd_parms* parms, shib_dir_config* dc, const char* arg)
100 dc->config.timeout=atoi(arg);
104 typedef const char* (*config_fn_t)(void);
106 // SHIB Module commands
108 static command_rec shib_cmds[] = {
109 {"SHIBConfig", (config_fn_t)ap_set_global_string_slot, &g_szSHIBConfig,
110 RSRC_CONF, TAKE1, "Path to SHIB ini file."},
111 {"SHIREURL", (config_fn_t)ap_set_global_string_slot, &g_szSHIREURL,
112 RSRC_CONF, TAKE1, "SHIRE POST processor URL."},
114 {"ShibBasicHijack", (config_fn_t)ap_set_flag_slot,
115 (void *) offsetof (shib_dir_config, bBasicHijack),
116 OR_AUTHCFG, FLAG, "Respond to AuthType Basic and convert to shib?"},
117 {"ShibSSLOnly", (config_fn_t)ap_set_flag_slot,
118 (void *) offsetof (shib_dir_config, bSSLOnly),
119 OR_AUTHCFG, FLAG, "Require SSL when accessing a secured directory?"},
120 {"ShibAuthLifetime", (config_fn_t)set_lifetime, NULL,
121 OR_AUTHCFG, TAKE1, "Lifetime of session in seconds."},
122 {"ShibAuthTimeout", (config_fn_t)set_timeout, NULL,
123 OR_AUTHCFG, TAKE1, "Timeout for session in seconds."},
129 void destroy_handle(void* data)
131 delete (RPCHandle*)data;
137 * Things to do when the child process is initialized.
139 extern "C" void shib_child_init(server_rec* s, apr_pool_t* p)
141 // Initialize runtime components.
143 ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,s,
144 "shib_child_init() starting");
147 ap_log_error(APLOG_MARK,APLOG_ERR|APLOG_NOERRNO,0,s,
148 "shib_child_init(): already initialized!");
153 g_Config = &(ShibTargetConfig::init(SHIBTARGET_SHIRE, g_szSHIBConfig));
155 ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,s,
156 "shib_child_init() failed to initialize SHIB Target");
160 // Create the RPC Handle TLS key.
161 rpc_handle_key=ThreadKey::create(destroy_handle);
163 ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,s,"shib_child_init() done");
171 extern "C" void shib_child_exit(server_rec* s, apr_pool_t* p)
173 delete rpc_handle_key;
174 g_Config->shutdown();
176 ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,s,"shib_child_exit() done");
179 inline char hexchar(unsigned short s)
181 return (s<=9) ? ('0' + s) : ('A' + s - 10);
184 static char* url_encode(request_rec* r, const char* s)
186 static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
187 char* ret=(char*)apr_palloc(r->pool,sizeof(char)*3*strlen(s)+1);
189 unsigned long count=0;
192 if (strchr(badchars,*s)!=NULL || *s<=0x1F || *s>=0x7F)
195 ret[count++]=hexchar(*s >> 4);
196 ret[count++]=hexchar(*s & 0x0F);
205 static const char* get_shire_location(request_rec* r, const char* target, bool encode)
207 ShibINI& ini = g_Config->getINI();
208 string shire_location;
211 shire_location = g_szSHIREURL;
212 else if (! ini.get_tag (ap_get_server_name(r), "shireURL", true, &shire_location)) {
213 ap_log_rerror(APLOG_MARK,APLOG_ERR,0,r,
214 "shire_get_location() no shireURL configuration for %s",
215 ap_get_server_name(r));
219 const char* shire = shire_location.c_str();
223 return url_encode(r,shire);
225 return apr_pstrdup(r->pool,shire);
227 const char* colon=strchr(target,':');
228 const char* slash=strchr(colon+3,'/');
230 return url_encode(r,apr_pstrcat(r->pool,
231 apr_pstrndup(r->pool,target,slash-target),
234 return apr_pstrcat(r->pool, apr_pstrndup(r->pool,target,slash-target),
238 static bool is_shire_location(request_rec* r, const char* target)
240 const char* shire = get_shire_location(r, target, false);
242 if (!shire) return false;
244 if (!strstr(target, shire))
247 return (!strcmp(target,shire));
250 static int shib_error_page(request_rec* r, const char* filename, ShibMLP& mlp)
252 ifstream infile (filename);
254 ap_log_rerror(APLOG_MARK,APLOG_ERR,0,r,
255 "shib_error_page() cannot open %s", filename);
256 return HTTP_INTERNAL_SERVER_ERROR;
259 string res = mlp.run(infile);
260 r->content_type = apr_psprintf(r->pool, "text/html");
261 ap_rprintf(r, res.c_str());
265 extern "C" int shib_check_user(request_rec* r)
267 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,"shib_check_user: ENTER");
268 shib_dir_config* dc=(shib_dir_config*)ap_get_module_config(r->per_dir_config,&mod_shib);
270 // This will always be normalized, because Apache uses ap_get_server_name in this API call.
271 char* targeturl=ap_construct_url(r->pool,r->unparsed_uri,r);
273 if (is_shire_location (r, targeturl)) {
274 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
275 "shib_check_user: REQUEST FOR SHIRE! Maybe you did not configure the SHIRE Handler?");
276 return HTTP_INTERNAL_SERVER_ERROR;
279 // Regular access to arbitrary resource...check AuthType
280 const char *auth_type=ap_auth_type (r);
284 if (strcasecmp(auth_type,"shibboleth"))
286 if (!strcasecmp(auth_type,"basic") && dc->bBasicHijack==1)
288 core_dir_config* conf=
289 (core_dir_config*)ap_get_module_config(r->per_dir_config,
290 ap_find_linked_module("http_core.c"));
291 conf->ap_auth_type="shibboleth";
297 // set the connection authtype
298 r->ap_auth_type = "shibboleth";
301 if (dc->bSSLOnly==1 && strcmp(ap_http_method(r),"https"))
303 ap_log_rerror(APLOG_MARK,APLOG_ERR|APLOG_NOERRNO,0,r,
304 "shib_check_user() blocked non-SSL access");
305 return HTTP_INTERNAL_SERVER_ERROR;
309 ostringstream threadid;
310 threadid << "[" << getpid() << "] shib" << '\0';
311 saml::NDC ndc(threadid.str().c_str());
313 ShibINI& ini = g_Config->getINI();
315 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
316 "shib_check_user() Shib check for %s", targeturl);
319 const char * shire_location = get_shire_location(r,targeturl,true);
321 return HTTP_INTERNAL_SERVER_ERROR;
322 string shire_url = get_shire_location(r,targeturl,false);
324 const char* serverName = ap_get_server_name(r);
326 bool has_tag = ini.get_tag (serverName, "checkIPAddress", true, &tag);
327 dc->config.checkIPAddress = (has_tag ? ShibINI::boolean (tag) : false);
330 if (! ini.get_tag(serverName, "cookieName", true, &shib_cookie)) {
331 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
332 "shib_check_user: no cookieName configuration for %s",
334 return HTTP_INTERNAL_SERVER_ERROR;
338 if (! ini.get_tag(serverName, "wayfURL", true, &wayfLocation)) {
339 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
340 "shib_check_user: no wayfURL configuration for %s",
342 return HTTP_INTERNAL_SERVER_ERROR;
346 if (! ini.get_tag(serverName, "shireError", true, &shireError)) {
347 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
348 "shib_check_user: no shireError configuration for %s",
350 return HTTP_INTERNAL_SERVER_ERROR;
353 // Get an RPC handle and build the SHIRE object.
354 RPCHandle* rpc_handle = (RPCHandle*)rpc_handle_key->getData();
357 rpc_handle = new RPCHandle(shib_target_sockname(), SHIBRPC_PROG, SHIBRPC_VERS_1);
358 rpc_handle_key->setData(rpc_handle);
360 SHIRE shire(rpc_handle, dc->config, shire_url);
362 // We're in charge, so check for cookie.
363 const char* session_id=NULL;
364 const char* cookies=apr_table_get(r->headers_in,"Cookie");
367 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
368 "shib_check_user() cookies found: %s",cookies);
370 if (!cookies || !(session_id=strstr(cookies,shib_cookie.c_str())))
372 // No cookie. Redirect to WAYF.
373 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
374 "shib_check_user() no cookie found -- redirecting to WAYF");
375 char* wayf=apr_pstrcat(r->pool,wayfLocation.c_str(),
376 "?shire=",shire_location,
377 "&target=",url_encode(r,targeturl),NULL);
378 apr_table_setn(r->headers_out,"Location",wayf);
379 return HTTP_MOVED_TEMPORARILY;
382 // Yep, we found a cookie -- pull it out (our session_id)
383 session_id+=strlen(shib_cookie.c_str()) + 1; /* Skip over the '=' */
384 char* cookiebuf = apr_pstrdup(r->pool,session_id);
385 char* cookieend = strchr(cookiebuf,';');
387 *cookieend = '\0'; /* Ignore anyting after a ; */
388 session_id=cookiebuf;
390 // Make sure this session is still valid
391 RPCError* status = NULL;
392 ShibMLP markupProcessor;
393 has_tag = ini.get_tag(serverName, "supportContact", true, &tag);
394 markupProcessor.insert("supportContact", has_tag ? tag : "");
395 has_tag = ini.get_tag(serverName, "logoLocation", true, &tag);
396 markupProcessor.insert("logoLocation", has_tag ? tag : "");
397 markupProcessor.insert("requestURL", targeturl);
400 status = shire.sessionIsValid(session_id, r->connection->remote_ip,targeturl);
402 catch (ShibTargetException &e) {
403 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,"shib_check_user(): %s", e.what());
404 markupProcessor.insert ("errorType", "SHIRE Processing Error");
405 markupProcessor.insert ("errorText", e.what());
406 markupProcessor.insert ("errorDesc", "An error occurred while processing your request.");
407 return shib_error_page (r, shireError.c_str(), markupProcessor);
410 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,"shib_check_user(): caught unexpected error");
411 markupProcessor.insert ("errorType", "SHIRE Processing Error");
412 markupProcessor.insert ("errorText", "Unexpected Exception");
413 markupProcessor.insert ("errorDesc", "An error occurred while processing your request.");
414 return shib_error_page (r, shireError.c_str(), markupProcessor);
418 if (status->isError()) {
419 ap_log_rerror(APLOG_MARK,APLOG_INFO|APLOG_NOERRNO,0,r,
420 "shib_check_user() session invalid: %s",
423 if (status->isRetryable()) {
424 // Oops, session is invalid. Redirect to WAYF.
425 char* wayf=apr_pstrcat(r->pool,wayfLocation.c_str(),
426 "?shire=",shire_location,
427 "&target=",url_encode(r,targeturl),NULL);
428 apr_table_setn(r->headers_out,"Location",wayf);
431 return HTTP_MOVED_TEMPORARILY;
434 // return the error page to the user
435 markupProcessor.insert (*status);
437 return shib_error_page (r, shireError.c_str(), markupProcessor);
442 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
443 "shib_check_user() success");
447 ap_log_rerror(APLOG_MARK,APLOG_ERR|APLOG_NOERRNO,0,r,"shib_check_user() server error");
448 return HTTP_INTERNAL_SERVER_ERROR;
451 extern "C" int shire_post_handler (request_rec* r)
453 ostringstream threadid;
454 threadid << "[" << getpid() << "] shire" << '\0';
455 saml::NDC ndc(threadid.str().c_str());
457 ShibINI& ini = g_Config->getINI();
458 ShibMLP markupProcessor;
460 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,"shire_post_handler() ENTER");
462 const char* targeturl=ap_construct_url(r->pool,r->unparsed_uri,r);
464 const char * shire_location = get_shire_location(r,targeturl,true);
466 return HTTP_INTERNAL_SERVER_ERROR;
467 string shire_url = get_shire_location(r,targeturl,false);
469 const char* serverName = ap_get_server_name(r);
471 bool has_tag = ini.get_tag(serverName, "checkIPAddress", true, &tag);
473 config.checkIPAddress = (has_tag ? ShibINI::boolean(tag) : false);
476 if (! ini.get_tag(serverName, "cookieName", true, &shib_cookie)) {
477 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
478 "shire_post_handler: no cookieName configuration for %s",
480 return HTTP_INTERNAL_SERVER_ERROR;
484 if (! ini.get_tag(serverName, "wayfURL", true, &wayfLocation)) {
485 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
486 "shire_post_handler: no wayfURL configuration for %s",
488 return HTTP_INTERNAL_SERVER_ERROR;
492 if (! ini.get_tag(serverName, "shireError", true, &shireError)) {
493 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
494 "shire_post_handler: no shireError configuration for %s",
496 return HTTP_INTERNAL_SERVER_ERROR;
499 has_tag = ini.get_tag(serverName, "supportContact", true, &tag);
500 markupProcessor.insert("supportContact", has_tag ? tag : "");
501 has_tag = ini.get_tag(serverName, "logoLocation", true, &tag);
502 markupProcessor.insert("logoLocation", has_tag ? tag : "");
503 markupProcessor.insert("requestURL", targeturl);
505 // Get an RPC handle and build the SHIRE object.
506 RPCHandle* rpc_handle = (RPCHandle*)rpc_handle_key->getData();
509 rpc_handle = new RPCHandle(shib_target_sockname(), SHIBRPC_PROG, SHIBRPC_VERS_1);
510 rpc_handle_key->setData(rpc_handle);
512 SHIRE shire(rpc_handle, config, shire_url);
514 // Process SHIRE POST
516 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
517 "shire_post_handler() Beginning SHIRE POST processing");
521 if (!ini.get_tag(serverName, "shireSSLOnly", true, &sslonly))
522 ap_log_rerror(APLOG_MARK,APLOG_CRIT|APLOG_NOERRNO,0,r,
523 "shire_post_handler: no shireSSLOnly configuration");
525 // Make sure this is SSL, if it should be
526 if (ShibINI::boolean(sslonly) && strcmp(ap_http_method(r),"https"))
527 throw ShibTargetException (SHIBRPC_OK,
528 "blocked non-SSL access to SHIRE POST processor");
530 // Make sure this is a POST
531 if (strcasecmp (r->method, "POST"))
532 throw ShibTargetException (SHIBRPC_OK,
533 "blocked non-POST to SHIRE POST processor");
535 // Sure sure this POST is an appropriate content type
536 const char *ct = apr_table_get (r->headers_in, "Content-type");
537 if (!ct || strcasecmp (ct, "application/x-www-form-urlencoded"))
538 throw ShibTargetException (SHIBRPC_OK,
539 apr_psprintf(r->pool,
540 "blocked bad content-type to SHIRE POST processor: %s",
543 // Make sure the "bytes sent" is a reasonable number
544 if (r->bytes_sent > 1024*1024) // 1MB?
545 throw ShibTargetException (SHIBRPC_OK,
546 "blocked too-large a post to SHIRE POST processor");
548 // Read the posted data
549 apreq_request_t *ap_req = apreq_request(r, NULL);
551 throw ShibTargetException (SHIBRPC_OK,
552 apr_psprintf(r->pool, "apreq_request() failed"));
554 // Make sure the target parameter exists
555 apreq_param_t *param = apreq_param(ap_req, "TARGET");
556 const char *target = param ? apreq_param_value(param) : NULL;
557 if (!target || *target == '\0')
559 throw ShibTargetException (SHIBRPC_OK,
560 "SHIRE POST failed to find TARGET");
562 // Make sure the SAML Response parameter exists
563 param = apreq_param(ap_req, "SAMLResponse");
564 const char *post = param ? apreq_param_value(param) : NULL;
565 if (!post || *post == '\0')
567 throw ShibTargetException (SHIBRPC_OK,
568 "SHIRE POST failed to find SAMLResponse");
570 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
571 "shire_post_handler() Processing POST for target: %s", target);
575 RPCError* status = shire.sessionCreate(post, r->connection->remote_ip, cookie);
577 if (status->isError()) {
578 ap_log_rerror(APLOG_MARK,APLOG_ERR|APLOG_NOERRNO,0,r,
579 "shire_post_handler() POST process failed (%d): %s",
580 status->getCode(), status->getText());
582 if (status->isRetryable()) {
583 ap_log_rerror(APLOG_MARK,APLOG_INFO|APLOG_NOERRNO,0,r,
584 "shire_post_handler() Retrying POST by redirecting to WAYF");
586 char* wayf=apr_pstrcat(r->pool,wayfLocation.c_str(),
587 "?shire=",shire_location,
588 "&target=",url_encode(r,target),NULL);
589 apr_table_setn(r->headers_out,"Location",wayf);
591 return HTTP_MOVED_TEMPORARILY;
594 // return this error to the user.
595 markupProcessor.insert (*status);
597 return shib_error_page (r, shireError.c_str(), markupProcessor);
601 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
602 "shire_post_handler() POST process succeeded. New cookie: %s",
605 // We've got a good session, set the cookie...
606 char * domain = NULL;
607 char * new_cookie = apr_psprintf(r->pool, "%s=%s; path=/%s%s",
610 (domain ? "; domain=" : ""),
611 (domain ? domain : ""));
613 apr_table_setn(r->err_headers_out, "Set-Cookie", new_cookie);
614 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
615 "shire_post_handler() Set cookie: %s", new_cookie);
617 // ... and redirect to the target
618 char* redir=apr_pstrcat(r->pool,url_encode(r,target),NULL);
619 apr_table_setn(r->headers_out, "Location", target);
620 return HTTP_MOVED_TEMPORARILY;
622 } catch (ShibTargetException &e) {
623 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,
624 "shire_post_handler(): %s", e.what());
626 markupProcessor.insert ("errorType", "SHIRE Processing Error");
627 markupProcessor.insert ("errorText", e.what());
628 markupProcessor.insert ("errorDesc", "An error occurred while processing your request.");
629 return shib_error_page (r, shireError.c_str(), markupProcessor);
632 ap_log_rerror(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO,0,r,"shire_post_handler(): unexpected exception");
634 markupProcessor.insert ("errorType", "SHIRE Processing Error");
635 markupProcessor.insert ("errorText", "Unexpected Exception");
636 markupProcessor.insert ("errorDesc", "An error occurred while processing your request.");
637 return shib_error_page (r, shireError.c_str(), markupProcessor);
640 ap_log_rerror(APLOG_MARK,APLOG_ERR|APLOG_NOERRNO,0,r,"shire_post_handler() server error");
641 return HTTP_INTERNAL_SERVER_ERROR;
646 handler_rec shib_handlers[] = {
647 { "shib-shire-post", shire_post_handler },
652 extern "C" void mod_shib_init (server_rec*r, apr_pool_t* p)
654 ShibTargetConfig::preinit();
658 command_rec shib_commands[] = {
662 void shib_register_hooks (apr_pool_t *p)
666 module AP_MODULE_DECLARE_DATA mod_shib = {
667 STANDARD20_MODULE_STUFF,
668 create_shib_dir_config, /* create dir config */
669 merge_shib_dir_config, /* merge dir config --- default is to override */
670 NULL, /* create server config */
671 NULL, /* merge server config */
672 shib_commands, /* command table */
673 shib_register_hooks /* register hooks */
679 mod_shib_init, /* initializer */
680 NULL, /* filename translation */
681 shib_check_user, /* check_user_id */
682 NULL, /* check auth */
683 NULL, /* check access */
684 NULL, /* type_checker */
687 NULL, /* header parser */
688 shib_child_init, /* child_init */
689 shib_child_exit, /* child_exit */
690 NULL /* post read-request */