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