1package websspi 2 3import ( 4 "context" 5 "encoding/base64" 6 "encoding/gob" 7 "errors" 8 "fmt" 9 "log" 10 "net/http" 11 "os/user" 12 "reflect" 13 "strings" 14 "sync" 15 "syscall" 16 "time" 17 "unsafe" 18 19 "github.com/quasoft/websspi/secctx" 20) 21 22// The Config object determines the behaviour of the Authenticator. 23// 24// To resolve group membership of authenticated principals, set EnumerateGroups to true. 25// Currently there are two options to resolve group membership - both return different results. 26// To resolve the "static" local or AD group membership, additionally set "ServerName" to a Windows server or Active Directory. 27// 28type Config struct { 29 contextStore secctx.Store 30 authAPI API 31 KrbPrincipal string // Name of Kerberos principle used by the service (optional). 32 AuthUserKey string // Key of header to fill with authenticated username, eg. "X-Authenticated-User" or "REMOTE_USER" (optional). 33 EnumerateGroups bool // If true, groups the user is a member of are enumerated and stored in request context (default false) 34 ServerName string // Specifies the DNS or NetBIOS name of the remote server which to query about user groups. Use an empty value to query the groups granted on a real login. Ignored if EnumerateGroups is false. 35 ResolveLinked bool // Resolve a linked token. 36} 37 38// NewConfig creates a configuration object with default values. 39func NewConfig() *Config { 40 return &Config{ 41 contextStore: secctx.NewCookieStore(), 42 authAPI: &Win32{}, 43 } 44} 45 46// Validate makes basic validation of configuration to make sure that important and required fields 47// have been set with values in expected format. 48func (c *Config) Validate() error { 49 if c.contextStore == nil { 50 return errors.New("Store for context handles not specified in Config") 51 } 52 if c.authAPI == nil { 53 return errors.New("Authentication API not specified in Config") 54 } 55 return nil 56} 57 58// contextKey represents a custom key for values stored in context.Context 59type contextKey string 60 61func (c contextKey) String() string { 62 return "websspi-key-" + string(c) 63} 64 65var ( 66 UserInfoKey = contextKey("UserInfo") 67) 68 69// The Authenticator type provides middleware methods for authentication of http requests. 70// A single authenticator object can be shared by concurrent goroutines. 71type Authenticator struct { 72 Config Config 73 serverCred *CredHandle 74 credExpiry *time.Time 75 ctxList []CtxtHandle 76 ctxListMux *sync.Mutex 77} 78 79// New creates a new Authenticator object with the given configuration options. 80func New(config *Config) (*Authenticator, error) { 81 err := config.Validate() 82 if err != nil { 83 return nil, fmt.Errorf("invalid config: %v", err) 84 } 85 86 var auth = &Authenticator{ 87 Config: *config, 88 ctxListMux: &sync.Mutex{}, 89 } 90 91 err = auth.PrepareCredentials(config.KrbPrincipal) 92 if err != nil { 93 return nil, fmt.Errorf("could not acquire credentials handle for the service: %v", err) 94 } 95 log.Printf("Credential handle expiry: %v\n", *auth.credExpiry) 96 97 return auth, nil 98} 99 100// PrepareCredentials method acquires a credentials handle for the specified principal 101// for use during the live of the application. 102// On success stores the handle in the serverCred field and its expiry time in the 103// credExpiry field. 104// This method must be called once - when the application is starting or when the first 105// request from a client is received. 106func (a *Authenticator) PrepareCredentials(principal string) error { 107 var principalPtr *uint16 108 if principal != "" { 109 var err error 110 principalPtr, err = syscall.UTF16PtrFromString(principal) 111 if err != nil { 112 return err 113 } 114 } 115 credentialUsePtr, err := syscall.UTF16PtrFromString(NEGOSSP_NAME) 116 if err != nil { 117 return err 118 } 119 var handle CredHandle 120 var expiry syscall.Filetime 121 status := a.Config.authAPI.AcquireCredentialsHandle( 122 principalPtr, 123 credentialUsePtr, 124 SECPKG_CRED_INBOUND, 125 nil, // logonId 126 nil, // authData 127 0, // getKeyFn 128 0, // getKeyArgument 129 &handle, 130 &expiry, 131 ) 132 if status != SEC_E_OK { 133 return fmt.Errorf("call to AcquireCredentialsHandle failed with code 0x%x", status) 134 } 135 expiryTime := time.Unix(0, expiry.Nanoseconds()) 136 a.credExpiry = &expiryTime 137 a.serverCred = &handle 138 return nil 139} 140 141// Free method should be called before shutting down the server to let 142// it release allocated Win32 resources 143func (a *Authenticator) Free() error { 144 var status SECURITY_STATUS 145 a.ctxListMux.Lock() 146 for _, ctx := range a.ctxList { 147 // TODO: Also check for stale security contexts and delete them periodically 148 status = a.Config.authAPI.DeleteSecurityContext(&ctx) 149 if status != SEC_E_OK { 150 return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) 151 } 152 } 153 a.ctxList = nil 154 a.ctxListMux.Unlock() 155 if a.serverCred != nil { 156 status = a.Config.authAPI.FreeCredentialsHandle(a.serverCred) 157 if status != SEC_E_OK { 158 return fmt.Errorf("call to FreeCredentialsHandle failed with code 0x%x", status) 159 } 160 a.serverCred = nil 161 } 162 return nil 163} 164 165// StoreCtxHandle stores the specified context to the internal list (ctxList) 166func (a *Authenticator) StoreCtxHandle(handle *CtxtHandle) { 167 if handle == nil || *handle == (CtxtHandle{}) { 168 // Should not add nil or empty handle 169 return 170 } 171 a.ctxListMux.Lock() 172 defer a.ctxListMux.Unlock() 173 a.ctxList = append(a.ctxList, *handle) 174} 175 176// ReleaseCtxHandle deletes a context handle and removes it from the internal list (ctxList) 177func (a *Authenticator) ReleaseCtxHandle(handle *CtxtHandle) error { 178 if handle == nil || *handle == (CtxtHandle{}) { 179 // Removing a nil or empty handle is not an error condition 180 return nil 181 } 182 a.ctxListMux.Lock() 183 defer a.ctxListMux.Unlock() 184 185 // First, try to delete the handle 186 status := a.Config.authAPI.DeleteSecurityContext(handle) 187 if status != SEC_E_OK { 188 return fmt.Errorf("call to DeleteSecurityContext failed with code 0x%x", status) 189 } 190 191 // Then remove it from the internal list 192 foundAt := -1 193 for i, ctx := range a.ctxList { 194 if ctx == *handle { 195 foundAt = i 196 break 197 } 198 } 199 if foundAt > -1 { 200 a.ctxList[foundAt] = a.ctxList[len(a.ctxList)-1] 201 a.ctxList = a.ctxList[:len(a.ctxList)-1] 202 } 203 return nil 204} 205 206// AcceptOrContinue tries to validate the auth-data token by calling the AcceptSecurityContext 207// function and returns and error if validation failed or continuation of the negotiation is needed. 208// No error is returned if the token was validated (user was authenticated). 209func (a *Authenticator) AcceptOrContinue(context *CtxtHandle, authData []byte) (newCtx *CtxtHandle, out []byte, exp *time.Time, err error) { 210 if authData == nil { 211 err = errors.New("input token cannot be nil") 212 return 213 } 214 215 var inputDesc SecBufferDesc 216 var inputBuf SecBuffer 217 inputDesc.BuffersCount = 1 218 inputDesc.Version = SECBUFFER_VERSION 219 inputDesc.Buffers = &inputBuf 220 inputBuf.BufferSize = uint32(len(authData)) 221 inputBuf.BufferType = SECBUFFER_TOKEN 222 inputBuf.Buffer = &authData[0] 223 224 var outputDesc SecBufferDesc 225 var outputBuf SecBuffer 226 outputDesc.BuffersCount = 1 227 outputDesc.Version = SECBUFFER_VERSION 228 outputDesc.Buffers = &outputBuf 229 outputBuf.BufferSize = 0 230 outputBuf.BufferType = SECBUFFER_TOKEN 231 outputBuf.Buffer = nil 232 233 var expiry syscall.Filetime 234 var contextAttr uint32 235 var newContextHandle CtxtHandle 236 237 var status = a.Config.authAPI.AcceptSecurityContext( 238 a.serverCred, 239 context, 240 &inputDesc, 241 ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_MUTUAL_AUTH|ASC_REQ_CONFIDENTIALITY| 242 ASC_REQ_INTEGRITY|ASC_REQ_REPLAY_DETECT|ASC_REQ_SEQUENCE_DETECT, // contextReq uint32, 243 SECURITY_NATIVE_DREP, // targDataRep uint32, 244 &newContextHandle, 245 &outputDesc, // *SecBufferDesc 246 &contextAttr, // contextAttr *uint32, 247 &expiry, // *syscall.Filetime 248 ) 249 if newContextHandle.Lower != 0 || newContextHandle.Upper != 0 { 250 newCtx = &newContextHandle 251 } 252 tm := time.Unix(0, expiry.Nanoseconds()) 253 exp = &tm 254 if status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED { 255 // Copy outputBuf.Buffer to out and free the outputBuf.Buffer 256 out = make([]byte, outputBuf.BufferSize) 257 var bufPtr = unsafe.Pointer(outputBuf.Buffer) 258 for i := 0; i < len(out); i++ { 259 out[i] = *(*byte)(bufPtr) 260 bufPtr = unsafe.Pointer(uintptr(bufPtr) + 1) 261 } 262 } 263 if outputBuf.Buffer != nil { 264 freeStatus := a.Config.authAPI.FreeContextBuffer(outputBuf.Buffer) 265 if freeStatus != SEC_E_OK { 266 status = freeStatus 267 err = fmt.Errorf("could not free output buffer; FreeContextBuffer() failed with code: 0x%x", freeStatus) 268 return 269 } 270 } 271 if status == SEC_I_CONTINUE_NEEDED { 272 err = errors.New("Negotiation should continue") 273 return 274 } else if status != SEC_E_OK { 275 err = fmt.Errorf("call to AcceptSecurityContext failed with code 0x%x", status) 276 return 277 } 278 // TODO: Check contextAttr? 279 return 280} 281 282// GetCtxHandle retrieves the context handle for this client from request's cookies 283func (a *Authenticator) GetCtxHandle(r *http.Request) (*CtxtHandle, error) { 284 sessionHandle, err := a.Config.contextStore.GetHandle(r) 285 if err != nil { 286 return nil, fmt.Errorf("could not get context handle from session: %s", err) 287 } 288 if contextHandle, ok := sessionHandle.(*CtxtHandle); ok { 289 log.Printf("CtxHandle: 0x%x\n", *contextHandle) 290 if contextHandle.Lower == 0 && contextHandle.Upper == 0 { 291 return nil, nil 292 } 293 return contextHandle, nil 294 } 295 log.Printf("CtxHandle: nil\n") 296 return nil, nil 297} 298 299// SetCtxHandle stores the context handle for this client to cookie of response 300func (a *Authenticator) SetCtxHandle(r *http.Request, w http.ResponseWriter, newContext *CtxtHandle) error { 301 // Store can't store nil value, so if newContext is nil, store an empty CtxHandle 302 ctx := &CtxtHandle{} 303 if newContext != nil { 304 ctx = newContext 305 } 306 err := a.Config.contextStore.SetHandle(r, w, ctx) 307 if err != nil { 308 return fmt.Errorf("could not save context to cookie: %s", err) 309 } 310 log.Printf("New context: 0x%x\n", *ctx) 311 return nil 312} 313 314// GetFlags returns the negotiated context flags 315func (a *Authenticator) GetFlags(context *CtxtHandle) (uint32, error) { 316 var flags SecPkgContext_Flags 317 status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_FLAGS, (*byte)(unsafe.Pointer(&flags))) 318 if status != SEC_E_OK { 319 return 0, fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 320 } 321 return flags.Flags, nil 322} 323 324// GetUsername returns the name of the user associated with the specified security context 325func (a *Authenticator) GetUsername(context *CtxtHandle) (username string, err error) { 326 var names SecPkgContext_Names 327 status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_NAMES, (*byte)(unsafe.Pointer(&names))) 328 if status != SEC_E_OK { 329 err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 330 return 331 } 332 if names.UserName != nil { 333 username = UTF16PtrToString(names.UserName, 2048) 334 status = a.Config.authAPI.FreeContextBuffer((*byte)(unsafe.Pointer(names.UserName))) 335 if status != SEC_E_OK { 336 err = fmt.Errorf("FreeContextBuffer failed with status 0x%x", status) 337 } 338 return 339 } 340 err = errors.New("QueryContextAttributes returned empty name") 341 return 342} 343 344// GetGroups returns the groups assosiated with the specified security context 345func (a *Authenticator) GetGroups(context *CtxtHandle) (groups []string, err error) { 346 var token SecPkgContext_AccessToken 347 status := a.Config.authAPI.QueryContextAttributes(context, SECPKG_ATTR_ACCESS_TOKEN, (*byte)(unsafe.Pointer(&token))) 348 if status != SEC_E_OK { 349 err = fmt.Errorf("QueryContextAttributes failed with status 0x%x", status) 350 return 351 } 352 var requiredMemory uint32 353 354 // 1. Get buffer size 355 ec := a.Config.authAPI.GetTokenInformation( 356 syscall.Token(token.AccessToken), 357 syscall.TokenGroups, 358 nil, 0, &requiredMemory, 359 ) 360 361 if ec != syscall.ERROR_INSUFFICIENT_BUFFER { 362 err = fmt.Errorf("GetTokenInformation failed with %+v (while getting required memory)", ec) 363 return 364 } 365 366 tokenInformation := make([]byte, requiredMemory) 367 // 2. Get data 368 ec = a.Config.authAPI.GetTokenInformation( 369 syscall.Token(token.AccessToken), 370 syscall.TokenGroups, 371 &tokenInformation[0], uint32(len(tokenInformation)), &requiredMemory, 372 ) 373 374 if ec != nil { 375 err = fmt.Errorf("GetTokenInformation failed with %+v (when looking up group membership)", ec) 376 return 377 } 378 379 // The struct ends with a variable amount of SIDAndAttributes structs. 380 var tokens *TokenGroups = (*TokenGroups)(unsafe.Pointer(&tokenInformation[0])) 381 var allSidAndAttributes []syscall.SIDAndAttributes 382 hdr := (*reflect.SliceHeader)(unsafe.Pointer(&allSidAndAttributes)) 383 hdr.Data = uintptr(unsafe.Pointer(&tokens.Groups)) 384 hdr.Len = int(tokens.GroupCount) 385 hdr.Cap = int(tokens.GroupCount) 386 387 for _, sidAndAttributes := range allSidAndAttributes { 388 // SE_GROUP_ENABLED 389 if sidAndAttributes.Attributes&4 == 4 { 390 str, _ := sidAndAttributes.Sid.String() 391 group, err := user.LookupGroupId(str) 392 if err != nil { // Non-group SIDs - can happen sometimes. 393 continue 394 } 395 groups = append(groups, group.Name) 396 } 397 } 398 399 return 400} 401 402// GetUserGroups returns the groups the user is a member of 403func (a *Authenticator) GetUserGroups(userName string) (groups []string, err error) { 404 var serverNamePtr *uint16 405 if a.Config.ServerName != "" { 406 serverNamePtr, err = syscall.UTF16PtrFromString(a.Config.ServerName) 407 if err != nil { 408 return 409 } 410 } 411 412 userNamePtr, err := syscall.UTF16PtrFromString(userName) 413 if err != nil { 414 return 415 } 416 var buf *byte 417 var entriesRead uint32 418 var totalEntries uint32 419 err = a.Config.authAPI.NetUserGetGroups( 420 serverNamePtr, 421 userNamePtr, 422 0, 423 &buf, 424 MAX_PREFERRED_LENGTH, 425 &entriesRead, 426 &totalEntries, 427 ) 428 if buf == nil { 429 err = fmt.Errorf("NetUserGetGroups(): returned nil buffer, error: %s", err) 430 return 431 } 432 defer func() { 433 freeErr := a.Config.authAPI.NetApiBufferFree(buf) 434 if freeErr != nil { 435 err = freeErr 436 } 437 }() 438 if err != nil { 439 return 440 } 441 if entriesRead < totalEntries { 442 err = fmt.Errorf("NetUserGetGroups(): could not read all entries, read only %d entries of %d", entriesRead, totalEntries) 443 return 444 } 445 446 ptr := unsafe.Pointer(buf) 447 for i := uint32(0); i < entriesRead; i++ { 448 groupInfo := (*GroupUsersInfo0)(ptr) 449 groupName := UTF16PtrToString(groupInfo.Grui0_name, MAX_GROUP_NAME_LENGTH) 450 if groupName != "" { 451 groups = append(groups, groupName) 452 } 453 ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(GroupUsersInfo0{})) 454 } 455 return 456} 457 458// GetUserInfo returns a structure containing the name of the user associated with the 459// specified security context and the groups to which they are a member of (if Config.EnumerateGroups) 460// is enabled 461func (a *Authenticator) GetUserInfo(context *CtxtHandle) (*UserInfo, error) { 462 // Get username 463 username, err := a.GetUsername(context) 464 if err != nil { 465 return nil, err 466 } 467 info := UserInfo{ 468 Username: username, 469 } 470 471 // Get groups 472 if a.Config.EnumerateGroups { 473 if a.Config.ServerName != "" { 474 info.Groups, err = a.GetUserGroups(username) 475 } else { 476 info.Groups, err = a.GetGroups(context) 477 } 478 if err != nil { 479 return nil, err 480 } 481 } 482 483 return &info, nil 484} 485 486// GetAuthData parses the "Authorization" header received from the client, 487// extracts the auth-data token (input token) and decodes it to []byte 488func (a *Authenticator) GetAuthData(r *http.Request, w http.ResponseWriter) (authData []byte, err error) { 489 // 1. Check if Authorization header is present 490 headers := r.Header["Authorization"] 491 if len(headers) == 0 { 492 err = errors.New("the Authorization header is not provided") 493 return 494 } 495 if len(headers) > 1 { 496 err = errors.New("received multiple Authorization headers, but expected only one") 497 return 498 } 499 500 authzHeader := strings.TrimSpace(headers[0]) 501 if authzHeader == "" { 502 err = errors.New("the Authorization header is empty") 503 return 504 } 505 // 1.1. Make sure header starts with "Negotiate" 506 if !strings.HasPrefix(strings.ToLower(authzHeader), "negotiate") { 507 err = errors.New("the Authorization header does not start with 'Negotiate'") 508 return 509 } 510 511 // 2. Extract token from Authorization header 512 authzParts := strings.Split(authzHeader, " ") 513 if len(authzParts) < 2 { 514 err = errors.New("the Authorization header does not contain token (gssapi-data)") 515 return 516 } 517 token := authzParts[len(authzParts)-1] 518 if token == "" { 519 err = errors.New("the token (gssapi-data) in the Authorization header is empty") 520 return 521 } 522 523 // 3. Decode token 524 authData, err = base64.StdEncoding.DecodeString(token) 525 if err != nil { 526 err = errors.New("could not decode token as base64 string") 527 return 528 } 529 530 return 531} 532 533// Authenticate tries to authenticate the HTTP request and returns nil 534// if authentication was successful. 535// Returns error and data for continuation if authentication was not successful. 536func (a *Authenticator) Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *UserInfo, outToken string, err error) { 537 // 1. Extract auth-data from Authorization header 538 authData, err := a.GetAuthData(r, w) 539 if err != nil { 540 err = fmt.Errorf("could not get auth data: %s", err) 541 return 542 } 543 544 // 2. Authenticate user with provided token 545 contextHandle, err := a.GetCtxHandle(r) 546 if err != nil { 547 return 548 } 549 newCtx, output, _, err := a.AcceptOrContinue(contextHandle, authData) 550 551 // If a new context was created, make sure to delete it or store it 552 // both in internal list and response Cookie 553 defer func() { 554 // Negotiation is ending if we don't expect further responses from the client 555 // (authentication was successful or no output token is going to be sent back), 556 // clear client cookie 557 endOfNegotiation := err == nil || len(output) == 0 558 559 // Current context (contextHandle) is not needed anymore and should be deleted if: 560 // - we don't expect further responses from the client 561 // - a new context has been returned by AcceptSecurityContext 562 currCtxNotNeeded := endOfNegotiation || newCtx != nil 563 if !currCtxNotNeeded { 564 // Release current context only if its different than the new context 565 if contextHandle != nil && *contextHandle != *newCtx { 566 remErr := a.ReleaseCtxHandle(contextHandle) 567 if remErr != nil { 568 err = remErr 569 return 570 } 571 } 572 } 573 574 if endOfNegotiation { 575 // Clear client cookie 576 setErr := a.SetCtxHandle(r, w, nil) 577 if setErr != nil { 578 err = fmt.Errorf("could not clear context, error: %s", setErr) 579 return 580 } 581 582 // Delete any new context handle 583 remErr := a.ReleaseCtxHandle(newCtx) 584 if remErr != nil { 585 err = remErr 586 return 587 } 588 589 // Exit defer func 590 return 591 } 592 593 if newCtx != nil { 594 // Store new context handle to internal list and response Cookie 595 a.StoreCtxHandle(newCtx) 596 setErr := a.SetCtxHandle(r, w, newCtx) 597 if setErr != nil { 598 err = setErr 599 return 600 } 601 } 602 }() 603 604 outToken = base64.StdEncoding.EncodeToString(output) 605 if err != nil { 606 err = fmt.Errorf("AcceptOrContinue failed: %s", err) 607 return 608 } 609 610 // 3. Get username and user groups 611 currentCtx := newCtx 612 if currentCtx == nil { 613 currentCtx = contextHandle 614 } 615 userInfo, err = a.GetUserInfo(currentCtx) 616 if err != nil { 617 err = fmt.Errorf("could not get username, error: %s", err) 618 return 619 } 620 621 return 622} 623 624// AppendAuthenticateHeader populates WWW-Authenticate header, 625// indicating to client that authentication is required and returns a 401 (Unauthorized) 626// response code. 627// The data parameter can be empty for the first 401 response from the server. 628// For subsequent 401 responses the data parameter should contain the gssapi-data, 629// which is required for continuation of the negotiation. 630func (a *Authenticator) AppendAuthenticateHeader(w http.ResponseWriter, data string) { 631 value := "Negotiate" 632 if data != "" { 633 value += " " + data 634 } 635 w.Header().Set("WWW-Authenticate", value) 636} 637 638// Return401 populates WWW-Authenticate header, indicating to client that authentication 639// is required and returns a 401 (Unauthorized) response code. 640// The data parameter can be empty for the first 401 response from the server. 641// For subsequent 401 responses the data parameter should contain the gssapi-data, 642// which is required for continuation of the negotiation. 643func (a *Authenticator) Return401(w http.ResponseWriter, data string) { 644 a.AppendAuthenticateHeader(w, data) 645 http.Error(w, "Error!", http.StatusUnauthorized) 646} 647 648// WithAuth authenticates the request. On successful authentication the request 649// is passed down to the next http handler. The next handler can access information 650// about the authenticated user via the GetUserName method. 651// If authentication was not successful, the server returns 401 response code with 652// a WWW-Authenticate, indicating that authentication is required. 653func (a *Authenticator) WithAuth(next http.Handler) http.Handler { 654 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 655 log.Printf("Authenticating request to %s\n", r.RequestURI) 656 657 user, data, err := a.Authenticate(r, w) 658 if err != nil { 659 log.Printf("Authentication failed with error: %v\n", err) 660 a.Return401(w, data) 661 return 662 } 663 664 log.Print("Authenticated\n") 665 // Add the UserInfo value to the reqest's context 666 r = r.WithContext(context.WithValue(r.Context(), UserInfoKey, user)) 667 // and to the request header with key Config.AuthUserKey 668 if a.Config.AuthUserKey != "" { 669 r.Header.Set(a.Config.AuthUserKey, user.Username) 670 } 671 672 // The WWW-Authenticate header might need to be sent back even 673 // on successful authentication (eg. in order to let the client complete 674 // mutual authentication). 675 if data != "" { 676 a.AppendAuthenticateHeader(w, data) 677 } 678 next.ServeHTTP(w, r) 679 }) 680} 681 682func init() { 683 gob.Register(&CtxtHandle{}) 684 gob.Register(&UserInfo{}) 685} 686