You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

websspi_windows.go 19KB

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