]> andersk Git - gssapi-openssh.git/blob - openssh/gss-serv.c
merging OPENSSH_GSSAPI_Protocol1-branch to trunk from tag
[gssapi-openssh.git] / openssh / gss-serv.c
1 /*
2  * Copyright (c) 2001 Simon Wilkinson. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS OR
14  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23  */
24
25 #include "includes.h"
26
27 #ifdef GSSAPI
28
29 #include "ssh.h"
30 #include "ssh1.h"
31 #include "ssh2.h"
32 #include "xmalloc.h"
33 #include "buffer.h"
34 #include "bufaux.h"
35 #include "packet.h"
36 #include "compat.h"
37 #include <openssl/evp.h>
38 #include "cipher.h"
39 #include "kex.h"
40 #include "auth.h"
41 #include "log.h"
42 #include "channels.h"
43 #include "session.h"
44 #include "dispatch.h"
45 #include "servconf.h"
46 #include "compat.h"
47 #include "misc.h"
48
49 #include "ssh-gss.h"
50
51 extern ServerOptions options;
52 extern u_char *session_id2;
53 extern int session_id2_len;
54
55 void    userauth_reply(Authctxt *authctxt, int authenticated);
56 static void gssapi_unsetenv(const char *var);
57
58 typedef struct ssh_gssapi_cred_cache {
59         char *filename;
60         char *envvar;
61         char *envval;
62         void *data;
63 } ssh_gssapi_cred_cache;
64
65 static struct ssh_gssapi_cred_cache gssapi_cred_store = {NULL,NULL,NULL};
66 unsigned char ssh1_key_digest[16]; /* used for ssh1 gssapi */
67
68 /*
69  * Environment variables pointing to delegated credentials
70  */
71 static char *delegation_env[] = {
72   "X509_USER_PROXY",            /* GSSAPI/SSLeay */
73   "KRB5CCNAME",                 /* Krb5 and possibly SSLeay */
74   NULL
75 };
76
77 #ifdef KRB5
78
79 #ifdef HEIMDAL
80 #include <krb5.h>
81 #else
82 #include <gssapi_krb5.h>
83 #define krb5_get_err_text(context,code) error_message(code)
84 #endif
85
86 static krb5_context krb_context = NULL;
87
88 /* Initialise the krb5 library, so we can use it for those bits that
89  * GSSAPI won't do */
90
91 int ssh_gssapi_krb5_init() {
92         krb5_error_code problem;
93         
94         if (krb_context !=NULL)
95                 return 1;
96                 
97         problem = krb5_init_context(&krb_context);
98         if (problem) {
99                 log("Cannot initialize krb5 context");
100                 return 0;
101         }
102         krb5_init_ets(krb_context);
103
104         return 1;       
105 }                       
106
107 /* Check if this user is OK to login. This only works with krb5 - other 
108  * GSSAPI mechanisms will need their own.
109  * Returns true if the user is OK to log in, otherwise returns 0
110  */
111
112 int
113 ssh_gssapi_krb5_userok(char *name) {
114         krb5_principal princ;
115         int retval;
116
117         if (ssh_gssapi_krb5_init() == 0)
118                 return 0;
119                 
120         if ((retval=krb5_parse_name(krb_context, gssapi_client_name.value, 
121                                     &princ))) {
122                 log("krb5_parse_name(): %.100s", 
123                         krb5_get_err_text(krb_context,retval));
124                 return 0;
125         }
126         if (krb5_kuserok(krb_context, princ, name)) {
127                 retval = 1;
128                 log("Authorized to %s, krb5 principal %s (krb5_kuserok)",name,
129                     (char *)gssapi_client_name.value);
130         }
131         else
132                 retval = 0;
133         
134         krb5_free_principal(krb_context, princ);
135         return retval;
136 }
137         
138 /* Make sure that this is called _after_ we've setuid to the user */
139
140 /* This writes out any forwarded credentials. Its specific to the Kerberos
141  * GSSAPI mechanism
142  *
143  * We assume that our caller has made sure that the user has selected
144  * delegated credentials, and that the client_creds structure is correctly
145  * populated.
146  */
147
148 void
149 ssh_gssapi_krb5_storecreds() {
150         krb5_ccache ccache;
151         krb5_error_code problem;
152         krb5_principal princ;
153         char ccname[35];
154         static char name[40];
155         int tmpfd;
156         OM_uint32 maj_status,min_status;
157
158
159         if (gssapi_client_creds==NULL) {
160                 debug("No credentials stored"); 
161                 return;
162         }
163                 
164         if (ssh_gssapi_krb5_init() == 0)
165                 return;
166
167         if (options.gss_use_session_ccache) {
168                 snprintf(ccname,sizeof(ccname),"/tmp/krb5cc_%d_XXXXXX",geteuid());
169        
170                 if ((tmpfd = mkstemp(ccname))==-1) {
171                         log("mkstemp(): %.100s", strerror(errno));
172                         return;
173                 }
174                 if (fchmod(tmpfd, S_IRUSR | S_IWUSR) == -1) {
175                         log("fchmod(): %.100s", strerror(errno));
176                         close(tmpfd);
177                         return;
178                 }
179         } else {
180                 snprintf(ccname,sizeof(ccname),"/tmp/krb5cc_%d",geteuid());
181                 tmpfd = open(ccname, O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
182                 if (tmpfd == -1) {
183                         log("open(): %.100s", strerror(errno));
184                         return;
185                 }
186         }
187
188         close(tmpfd);
189         snprintf(name, sizeof(name), "FILE:%s",ccname);
190  
191         if ((problem = krb5_cc_resolve(krb_context, name, &ccache))) {
192                 log("krb5_cc_default(): %.100s", 
193                         krb5_get_err_text(krb_context,problem));
194                 return;
195         }
196
197         if ((problem = krb5_parse_name(krb_context, gssapi_client_name.value, 
198                                        &princ))) {
199                 log("krb5_parse_name(): %.100s", 
200                         krb5_get_err_text(krb_context,problem));
201                 krb5_cc_destroy(krb_context,ccache);
202                 return;
203         }
204         
205         if ((problem = krb5_cc_initialize(krb_context, ccache, princ))) {
206                 log("krb5_cc_initialize(): %.100s", 
207                         krb5_get_err_text(krb_context,problem));
208                 krb5_free_principal(krb_context,princ);
209                 krb5_cc_destroy(krb_context,ccache);
210                 return;
211         }
212         
213         krb5_free_principal(krb_context,princ);
214
215         #ifdef HEIMDAL
216         if ((problem = krb5_cc_copy_cache(krb_context, 
217                                            gssapi_client_creds->ccache,
218                                            ccache))) {
219                 log("krb5_cc_copy_cache(): %.100s", 
220                         krb5_get_err_text(krb_context,problem));
221                 krb5_cc_destroy(krb_context,ccache);
222                 return;
223         }
224         #else
225         if ((maj_status = gss_krb5_copy_ccache(&min_status, 
226                                                gssapi_client_creds, 
227                                                ccache))) {
228                 log("gss_krb5_copy_ccache() failed");
229                 ssh_gssapi_error(maj_status,min_status);
230                 krb5_cc_destroy(krb_context,ccache);
231                 return;
232         }
233         #endif
234         
235         krb5_cc_close(krb_context,ccache);
236
237
238 #ifdef USE_PAM
239         do_pam_putenv("KRB5CCNAME",name);
240 #endif
241
242         gssapi_cred_store.filename=strdup(ccname);
243         gssapi_cred_store.envvar="KRB5CCNAME";
244         gssapi_cred_store.envval=strdup(name);
245
246         return;
247 }
248
249 #endif /* KRB5 */
250
251 #ifdef GSI
252 #include <globus_gss_assist.h>
253
254 /*
255  * Check if this user is OK to login under GSI. User has been authenticated
256  * as identity in global 'client_name.value' and is trying to log in as passed
257  * username in 'name'.
258  *
259  * Returns non-zero if user is authorized, 0 otherwise.
260  */
261 int
262 ssh_gssapi_gsi_userok(char *name)
263 {
264     int authorized = 0;
265     
266     /* This returns 0 on success */
267     authorized = (globus_gss_assist_userok(gssapi_client_name.value,
268                                            name) == 0);
269     
270     log("GSI user %s is%s authorized as target user %s",
271         (char *) gssapi_client_name.value, (authorized ? "" : " not"), name);
272     
273     return authorized;
274 }
275
276 /*
277  * Handle setting up child environment for GSI.
278  *
279  * Make sure that this is called _after_ we've setuid to the user.
280  */
281 void
282 ssh_gssapi_gsi_storecreds()
283 {
284         OM_uint32       major_status;
285         OM_uint32       minor_status;
286         
287         
288         if (gssapi_client_creds != NULL)
289         {
290                 char *creds_env = NULL;
291
292                 /*
293                  * This is the current hack with the GSI gssapi library to
294                  * export credentials to disk.
295                  */
296
297                 debug("Exporting delegated credentials");
298                 
299                 minor_status = 0xdee0;  /* Magic value */
300                 major_status =
301                         gss_inquire_cred(&minor_status,
302                                          gssapi_client_creds,
303                                          (gss_name_t *) &creds_env,
304                                          NULL,
305                                          NULL,
306                                          NULL);
307
308                 if ((major_status == GSS_S_COMPLETE) &&
309                     (minor_status == 0xdee1) &&
310                     (creds_env != NULL))
311                 {
312                         char            *value;
313                                 
314                         /*
315                          * String is of the form:
316                          * X509_USER_DELEG_PROXY=filename
317                          * so we parse out the filename
318                          * and then set X509_USER_PROXY
319                          * to point at it.
320                          */
321                         value = strchr(creds_env, '=');
322                         
323                         if (value != NULL)
324                         {
325                                 *value = '\0';
326                                 value++;
327 #ifdef USE_PAM
328                                 do_pam_putenv("X509_USER_PROXY",value);
329 #endif
330                                 gssapi_cred_store.filename=NULL;
331                                 gssapi_cred_store.envvar="X509_USER_PROXY";
332                                 gssapi_cred_store.envval=strdup(value);
333
334                                 return;
335                         }
336                         else
337                         {
338                                 log("Failed to parse delegated credentials string '%s'",
339                                     creds_env);
340                         }
341                 }
342                 else
343                 {
344                         log("Failed to export delegated credentials (error %ld)",
345                             major_status);
346                 }
347         }       
348 }
349
350 #endif /* GSI */
351
352 void
353 ssh_gssapi_cleanup_creds(void *ignored)
354 {
355         if (gssapi_cred_store.filename!=NULL) {
356                 /* Unlink probably isn't sufficient */
357                 debug("removing gssapi cred file\"%s\"",gssapi_cred_store.filename);
358                 unlink(gssapi_cred_store.filename);
359         }
360 }
361
362 void 
363 ssh_gssapi_storecreds()
364 {
365         switch (gssapi_client_type) {
366 #ifdef KRB5
367         case GSS_KERBEROS:
368                 ssh_gssapi_krb5_storecreds();
369                 break;
370 #endif
371 #ifdef GSI
372         case GSS_GSI:
373                 ssh_gssapi_gsi_storecreds();
374                 break;
375 #endif /* GSI */
376         case GSS_LAST_ENTRY:
377                 /* GSSAPI not used in this authentication */
378                 debug("No GSSAPI credentials stored");
379                 break;
380         default:
381                 log("ssh_gssapi_do_child: Unknown mechanism");
382         
383         }
384         
385         if (options.gss_cleanup_creds) {
386                 fatal_add_cleanup(ssh_gssapi_cleanup_creds, NULL);
387         }
388
389 }
390
391 /* This allows GSSAPI methods to do things to the childs environment based
392  * on the passed authentication process and credentials.
393  *
394  * Question: If we didn't use userauth_external for some reason, should we
395  * still delegate credentials?
396  */
397 void 
398 ssh_gssapi_do_child(char ***envp, u_int *envsizep) 
399 {
400
401         if (gssapi_cred_store.envvar!=NULL && 
402             gssapi_cred_store.envval!=NULL) {
403             
404                 debug("Setting %s to %s", gssapi_cred_store.envvar,
405                                           gssapi_cred_store.envval);                              
406                 child_set_env(envp, envsizep, gssapi_cred_store.envvar, 
407                                               gssapi_cred_store.envval);
408         }
409
410         switch(gssapi_client_type) {
411 #ifdef KRB5
412         case GSS_KERBEROS: break;
413 #endif
414 #ifdef GSI
415         case GSS_GSI: break;
416 #endif
417         case GSS_LAST_ENTRY:
418                 debug("No GSSAPI credentials stored");
419                 break;
420         default:
421                 log("ssh_gssapi_do_child: Unknown mechanism");
422         }
423 }
424
425 int
426 ssh_gssapi_userok(char *user)
427 {
428         if (gssapi_client_name.length==0 || 
429             gssapi_client_name.value==NULL) {
430                 debug("No suitable client data");
431                 return 0;
432         }
433         switch (gssapi_client_type) {
434 #ifdef KRB5
435         case GSS_KERBEROS:
436                 return(ssh_gssapi_krb5_userok(user));
437                 break; /* Not reached */
438 #endif
439 #ifdef GSI
440         case GSS_GSI:
441                 return(ssh_gssapi_gsi_userok(user));
442                 break; /* Not reached */
443 #endif /* GSI */
444         case GSS_LAST_ENTRY:
445                 debug("Client not GSSAPI");
446                 break;
447         default:
448                 debug("Unknown client authentication type");
449         }
450         return(0);
451 }
452
453 int
454 userauth_external(Authctxt *authctxt)
455 {
456         packet_check_eom();
457
458         return(ssh_gssapi_userok(authctxt->user));
459 }
460
461 void input_gssapi_token(int type, u_int32_t plen, void *ctxt);
462 void input_gssapi_exchange_complete(int type, u_int32_t plen, void *ctxt);
463
464 /* We only support those mechanisms that we know about (ie ones that we know
465  * how to check local user kuserok and the like
466  */
467 int
468 userauth_gssapi(Authctxt *authctxt)
469 {
470         gss_OID_desc    oid= {0,NULL};
471         Gssctxt         *ctxt;
472         int             mechs;
473         gss_OID_set     supported;
474         int             present;
475         OM_uint32       ms;
476         u_int           len;
477         
478         if (!authctxt->valid || authctxt->user == NULL)
479                 return 0;
480                 
481         if (datafellows & SSH_OLD_GSSAPI) {
482                 debug("Early drafts of GSSAPI userauth not supported");
483                 return 0;
484         }
485         
486         mechs=packet_get_int();
487         if (mechs==0) {
488                 debug("Mechanism negotiation is not supported");
489                 return 0;
490         }
491
492         ssh_gssapi_supported_oids(&supported);
493         do {
494                 if (oid.elements)
495                         xfree(oid.elements);
496                 oid.elements = packet_get_string(&len);
497                 oid.length = len;
498                 gss_test_oid_set_member(&ms, &oid, supported, &present);
499                 mechs--;
500         } while (mechs>0 && !present);
501         
502         if (!present) {
503                 xfree(oid.elements);
504                 return(0);
505         }
506         
507         ctxt=xmalloc(sizeof(Gssctxt));
508         authctxt->methoddata=(void *)ctxt;
509         
510         ssh_gssapi_build_ctx(ctxt);
511         ssh_gssapi_set_oid(ctxt,&oid);
512
513         if (ssh_gssapi_acquire_cred(ctxt))
514                 return 0;
515
516         /* Send SSH_MSG_USERAUTH_GSSAPI_RESPONSE */
517
518         if (!compat20)
519         packet_start(SSH_SMSG_AUTH_GSSAPI_RESPONSE);
520         else
521         packet_start(SSH2_MSG_USERAUTH_GSSAPI_RESPONSE);
522         packet_put_string(oid.elements,oid.length);
523         packet_send();
524         packet_write_wait();
525         xfree(oid.elements);
526                 
527         if (!compat20)
528         dispatch_set(SSH_MSG_AUTH_GSSAPI_TOKEN,
529                                 &input_gssapi_token);
530         else
531         dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_TOKEN, 
532                      &input_gssapi_token);
533         authctxt->postponed = 1;
534         
535         return 0;
536 }
537
538 void
539 input_gssapi_token(int type, u_int32_t plen, void *ctxt)
540 {
541         Authctxt *authctxt = ctxt;
542         Gssctxt *gssctxt;
543         gss_buffer_desc send_tok,recv_tok;
544         OM_uint32 maj_status, min_status;
545         
546         if (authctxt == NULL || authctxt->methoddata == NULL)
547                 fatal("No authentication or GSSAPI context");
548                 
549         gssctxt=authctxt->methoddata;
550
551         recv_tok.value=packet_get_string(&recv_tok.length);
552         
553         maj_status=ssh_gssapi_accept_ctx(gssctxt, &recv_tok, &send_tok, NULL);
554         packet_check_eom();
555         
556         if (GSS_ERROR(maj_status)) {
557                 /* Failure <sniff> */
558                 ssh_gssapi_send_error(maj_status,min_status);
559                 authctxt->postponed = 0;
560                 dispatch_set(SSH_MSG_AUTH_GSSAPI_TOKEN, NULL);
561                 dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_TOKEN, NULL);
562                 userauth_finish(authctxt, 0, "gssapi");
563         }
564                         
565         if (send_tok.length != 0) {
566                 /* Send a packet back to the client */
567                 if (!compat20)
568                 packet_start(SSH_MSG_AUTH_GSSAPI_TOKEN);
569                 else
570                 packet_start(SSH2_MSG_USERAUTH_GSSAPI_TOKEN);
571                 packet_put_string(send_tok.value,send_tok.length);
572                 packet_send();
573                 packet_write_wait();
574                 gss_release_buffer(&min_status, &send_tok);
575         }
576         
577         if (maj_status == GSS_S_COMPLETE) {
578                 dispatch_set(SSH_MSG_AUTH_GSSAPI_TOKEN, NULL);
579                 dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_TOKEN,NULL);
580                 /* ssh1 does not have an extra message here */
581                 if (!compat20)
582                 input_gssapi_exchange_complete(0, 0, ctxt);
583                 else
584                 dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE,
585                              &input_gssapi_exchange_complete);
586         }
587 }
588
589 /* This is called when the client thinks we've completed authentication.
590  * It should only be enabled in the dispatch handler by the function above,
591  * which only enables it once the GSSAPI exchange is complete.
592  */
593  
594 void
595 input_gssapi_exchange_complete(int type, u_int32_t plen, void *ctxt)
596 {
597         Authctxt *authctxt = ctxt;
598         Gssctxt *gssctxt;
599         int authenticated;
600
601         if(strcmp(authctxt->user,"") == 0) {
602                 char *user;
603                 char *gridmapped_name = NULL;
604                 struct passwd *pw = NULL;
605                 if(globus_gss_assist_gridmap(gssapi_client_name.value,
606                            &gridmapped_name) == 0) {
607                         user = gridmapped_name;
608                         debug("I gridmapped and got %s", user);
609                         pw = getpwnam(user);
610                         if (pw && allowed_user(pw)) {
611                                 authctxt->user = user;
612                                 authctxt->pw = pwcopy(pw);
613                                 authctxt->valid = 1;
614                         }
615                 }
616         }
617
618         
619         if (authctxt == NULL || authctxt->methoddata == NULL)
620                 fatal("No authentication or GSSAPI context");
621                 
622         gssctxt=authctxt->methoddata;
623
624         /* This should never happen, but better safe than sorry. */
625         if (gssctxt->status != GSS_S_COMPLETE) {
626                 packet_disconnect("Context negotiation is not complete");
627         }
628
629         if (ssh_gssapi_getclient(gssctxt,&gssapi_client_type,
630                                  &gssapi_client_name,
631                                  &gssapi_client_creds)) {
632                 fatal("Couldn't convert client name");
633         }
634                                                 
635         authenticated = ssh_gssapi_userok(authctxt->user);
636
637         /* ssh1 needs to exchange the hash of the keys */
638         if (!compat20) {
639                 if (authenticated) {
640
641                         OM_uint32 maj_status, min_status;
642                         gss_buffer_desc gssbuf,msg_tok;
643
644                         /* ssh1 uses wrap */
645                         gssbuf.value=ssh1_key_digest;
646                         gssbuf.length=sizeof(ssh1_key_digest);
647                         if ((maj_status=gss_wrap(&min_status,
648                                         gssctxt->context,
649                                         0,
650                                         GSS_C_QOP_DEFAULT,
651                                         &gssbuf,
652                                         NULL,
653                                         &msg_tok))) {
654                                 ssh_gssapi_error(maj_status,min_status);
655                                 fatal("Couldn't wrap keys");
656                         }
657                         packet_start(SSH_SMSG_AUTH_GSSAPI_HASH);
658                         packet_put_string((char *)msg_tok.value,msg_tok.length);
659                         packet_send();
660                         packet_write_wait();
661                         gss_release_buffer(&min_status,&msg_tok);
662                 } else {
663                     packet_start(SSH_MSG_AUTH_GSSAPI_ABORT);
664                     packet_send();
665                     packet_write_wait();
666                 }
667         }
668
669         authctxt->postponed = 0;
670         dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_TOKEN, NULL);
671         dispatch_set(SSH2_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, NULL);
672         userauth_finish(authctxt, authenticated, "gssapi");
673 }
674
675 /*
676  * Clean our environment on startup. This means removing any environment
677  * strings that might inadvertantly been in root's environment and 
678  * could cause serious security problems if we think we set them.
679  */
680 void
681 ssh_gssapi_clean_env(void)
682 {
683   char *envstr;
684   int envstr_index;
685
686   
687    for (envstr_index = 0;
688        (envstr = delegation_env[envstr_index]) != NULL;
689        envstr_index++) {
690
691      if (getenv(envstr)) {
692        debug("Clearing environment variable %s", envstr);
693        gssapi_unsetenv(envstr);
694      }
695    }
696 }
697
698 /*
699  * Wrapper around unsetenv.
700  */
701 static void
702 gssapi_unsetenv(const char *var)
703 {
704 #ifdef HAVE_UNSETENV
705     unsetenv(var);
706
707 #else /* !HAVE_UNSETENV */
708     extern char **environ;
709     char **p1 = environ;        /* New array list */
710     char **p2 = environ;        /* Current array list */
711     int len = strlen(var);
712
713     /*
714      * Walk through current environ array (p2) copying each pointer
715      * to new environ array (p1) unless the pointer is to the item
716      * we want to delete. Copy happens in place.
717      */
718     while (*p2) {
719         if ((strncmp(*p2, var, len) == 0) &&
720             ((*p2)[len] == '=')) {
721             /*
722              * *p2 points at item to be deleted, just skip over it
723              */
724             p2++;
725         } else {
726             /*
727              * *p2 points at item we want to save, so copy it
728              */
729             *p1 = *p2;
730             p1++;
731             p2++;
732         }
733     }
734
735     /* And make sure new array is NULL terminated */
736     *p1 = NULL;
737 #endif /* HAVE_UNSETENV */
738 }
739
740 #endif /* GSSAPI */
This page took 0.805297 seconds and 5 git commands to generate.