github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/routes_common.go (about)

     1  package common
     2  
     3  import (
     4  	"crypto/subtle"
     5  	"html"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/Azareal/Gosora/common/phrases"
    16  	"github.com/Azareal/Gosora/uutils"
    17  )
    18  
    19  // nolint
    20  var PreRoute func(http.ResponseWriter, *http.Request) (User, bool) = preRoute
    21  
    22  // TODO: Come up with a better middleware solution
    23  // nolint We need these types so people can tell what they are without scrolling to the bottom of the file
    24  var PanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*Header, PanelStats, RouteError) = panelUserCheck
    25  var SimplePanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderLite, RouteError) = simplePanelUserCheck
    26  var SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, u *User, fid int) (headerLite *HeaderLite, err RouteError) = simpleForumUserCheck
    27  var ForumUserCheck func(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (err RouteError) = forumUserCheck
    28  var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, u *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck
    29  var UserCheck func(w http.ResponseWriter, r *http.Request, u *User) (h *Header, err RouteError) = userCheck
    30  var UserCheckNano func(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, err RouteError) = userCheck2
    31  
    32  func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, u *User, fid int) (h *HeaderLite, rerr RouteError) {
    33  	h, rerr = SimpleUserCheck(w, r, u)
    34  	if rerr != nil {
    35  		return h, rerr
    36  	}
    37  	if !Forums.Exists(fid) {
    38  		return nil, PreError("The target forum doesn't exist.", w, r)
    39  	}
    40  
    41  	// Is there a better way of doing the skip AND the success flag on this hook like multiple returns?
    42  	/*skip, rerr := h.Hooks.VhookSkippable("simple_forum_check_pre_perms", w, r, u, &fid, h)
    43  	if skip || rerr != nil {
    44  		return h, rerr
    45  	}*/
    46  	skip, rerr := H_simple_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h)
    47  	if skip || rerr != nil {
    48  		return h, rerr
    49  	}
    50  
    51  	fp, err := FPStore.Get(fid, u.Group)
    52  	if err == ErrNoRows {
    53  		fp = BlankForumPerms()
    54  	} else if err != nil {
    55  		return h, InternalError(err, w, r)
    56  	}
    57  	cascadeForumPerms(fp, u)
    58  	return h, nil
    59  }
    60  
    61  func forumUserCheck(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (rerr RouteError) {
    62  	if !Forums.Exists(fid) {
    63  		return NotFound(w, r, h)
    64  	}
    65  
    66  	/*skip, rerr := h.Hooks.VhookSkippable("forum_check_pre_perms", w, r, u, &fid, h)
    67  	if skip || rerr != nil {
    68  		return rerr
    69  	}*/
    70  	/*skip, rerr := VhookSkippableTest(h.Hooks, "forum_check_pre_perms", w, r, u, &fid, h)
    71  	if skip || rerr != nil {
    72  		return rerr
    73  	}*/
    74  	skip, rerr := H_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h)
    75  	if skip || rerr != nil {
    76  		return rerr
    77  	}
    78  
    79  	fp, err := FPStore.Get(fid, u.Group)
    80  	if err == ErrNoRows {
    81  		fp = BlankForumPerms()
    82  	} else if err != nil {
    83  		return InternalError(err, w, r)
    84  	}
    85  	cascadeForumPerms(fp, u)
    86  	h.CurrentUser = u // TODO: Use a pointer instead for CurrentUser, so we don't have to do this
    87  	return rerr
    88  }
    89  
    90  // TODO: Put this on the user instance? Do we really want forum specific logic in there? Maybe, a method which spits a new pointer with the same contents as user?
    91  func cascadeForumPerms(fp *ForumPerms, u *User) {
    92  	if fp.Overrides && !u.IsSuperAdmin {
    93  		u.Perms.ViewTopic = fp.ViewTopic
    94  		u.Perms.LikeItem = fp.LikeItem
    95  		u.Perms.CreateTopic = fp.CreateTopic
    96  		u.Perms.EditTopic = fp.EditTopic
    97  		u.Perms.DeleteTopic = fp.DeleteTopic
    98  		u.Perms.CreateReply = fp.CreateReply
    99  		u.Perms.EditReply = fp.EditReply
   100  		u.Perms.DeleteReply = fp.DeleteReply
   101  		u.Perms.PinTopic = fp.PinTopic
   102  		u.Perms.CloseTopic = fp.CloseTopic
   103  		u.Perms.MoveTopic = fp.MoveTopic
   104  
   105  		if len(fp.ExtData) != 0 {
   106  			for name, perm := range fp.ExtData {
   107  				u.PluginPerms[name] = perm
   108  			}
   109  		}
   110  	}
   111  }
   112  
   113  // Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
   114  // TODO: Do a panel specific theme?
   115  func panelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, stats PanelStats, rerr RouteError) {
   116  	theme := GetThemeByReq(r)
   117  	h = &Header{
   118  		Site:     Site,
   119  		Settings: SettingBox.Load().(SettingMap),
   120  		//Themes:      Themes,
   121  		ThemesSlice: ThemesSlice,
   122  		Theme:       theme,
   123  		CurrentUser: u,
   124  		Hooks:       GetHookTable(),
   125  		Zone:        "panel",
   126  		Writer:      w,
   127  		IsoCode:     phrases.GetLangPack().IsoCode,
   128  		//StartedAt:   time.Now(),
   129  		StartedAt: uutils.Nanotime(),
   130  	}
   131  	// TODO: We should probably initialise header.ExtData
   132  	// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
   133  	//if user.IsAdmin {
   134  	//h.StartedAt = time.Now()
   135  	//}
   136  
   137  	h.AddSheet(theme.Name + "/main.css")
   138  	h.AddSheet(theme.Name + "/panel.css")
   139  	if len(theme.Resources) > 0 {
   140  		rlist := theme.Resources
   141  		for _, res := range rlist {
   142  			if res.LocID == LocGlobal || res.LocID == LocPanel {
   143  				if res.Type == ResTypeSheet {
   144  					h.AddSheet(res.Name)
   145  				} else if res.Type == ResTypeScript {
   146  					if res.Async {
   147  						h.AddScriptAsync(res.Name)
   148  					} else {
   149  						h.AddScript(res.Name)
   150  					}
   151  				}
   152  			}
   153  		}
   154  	}
   155  
   156  	//h := w.Header()
   157  	//h.Set("Content-Security-Policy", "default-src 'self'")
   158  
   159  	// TODO: GDPR. Add a global control panel notice warning the admins of staff members who don't have 2FA enabled
   160  	stats.Users = Users.Count()
   161  	stats.Groups = Groups.Count()
   162  	stats.Forums = Forums.Count()
   163  	stats.Pages = Pages.Count()
   164  	stats.Settings = len(h.Settings)
   165  	stats.WordFilters = WordFilters.EstCount()
   166  	stats.Themes = len(Themes)
   167  	stats.Reports = 0 // TODO: Do the report count. Only show open threads?
   168  
   169  	addPreScript := func(name string, i int) {
   170  		// TODO: Optimise this by removing a superfluous string alloc
   171  		if theme.OverridenMap != nil {
   172  			//fmt.Printf("name %+v\n", name)
   173  			//fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap)
   174  			if _, ok := theme.OverridenMap[name]; ok {
   175  				tname := "_" + theme.Name
   176  				//fmt.Printf("tname %+v\n", tname)
   177  				h.AddPreScriptAsync("tmpl_" + name + tname + ".js")
   178  				return
   179  			}
   180  		}
   181  		//fmt.Printf("tname %+v\n", tname)
   182  		h.AddPreScriptAsync(ucstrs[i])
   183  	}
   184  	addPreScript("alert", 3)
   185  	addPreScript("notice", 4)
   186  
   187  	return h, stats, nil
   188  }
   189  
   190  func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) {
   191  	return SimpleUserCheck(w, r, u)
   192  }
   193  
   194  // SimpleUserCheck is back from the grave, yay :D
   195  func simpleUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) {
   196  	return &HeaderLite{
   197  		Site:     Site,
   198  		Settings: SettingBox.Load().(SettingMap),
   199  		Hooks:    GetHookTable(),
   200  	}, nil
   201  }
   202  
   203  func GetThemeByReq(r *http.Request) *Theme {
   204  	theme := &Theme{Name: ""}
   205  	cookie, e := r.Cookie("current_theme")
   206  	if e == nil {
   207  		inTheme, ok := Themes[html.EscapeString(cookie.Value)]
   208  		if ok && !theme.HideFromThemes {
   209  			theme = inTheme
   210  		}
   211  	}
   212  	if theme.Name == "" {
   213  		theme = Themes[DefaultThemeBox.Load().(string)]
   214  	}
   215  	return theme
   216  }
   217  
   218  func userCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, rerr RouteError) {
   219  	return userCheck2(w, r, u, uutils.Nanotime())
   220  }
   221  
   222  // TODO: Add the ability for admins to restrict certain themes to certain groups?
   223  // ! Be careful about firing errors off here as CustomError uses this
   224  func userCheck2(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, rerr RouteError) {
   225  	theme := GetThemeByReq(r)
   226  	h = &Header{
   227  		Site:     Site,
   228  		Settings: SettingBox.Load().(SettingMap),
   229  		//Themes:      Themes,
   230  		ThemesSlice: ThemesSlice,
   231  		Theme:       theme,
   232  		CurrentUser: u, // ! Some things rely on this being a pointer downstream from this function
   233  		Hooks:       GetHookTable(),
   234  		Zone:        ucstrs[0],
   235  		Writer:      w,
   236  		IsoCode:     phrases.GetLangPack().IsoCode,
   237  		StartedAt:   nano,
   238  	}
   239  	// TODO: Optimise this by avoiding accessing a map string index
   240  	if !u.Loggedin {
   241  		h.GoogSiteVerify = h.Settings["google_site_verify"].(string)
   242  	}
   243  
   244  	if u.IsBanned {
   245  		h.AddNotice("account_banned")
   246  	}
   247  	if u.Loggedin && !u.Active {
   248  		h.AddNotice("account_inactive")
   249  	}
   250  	/*h.Scripts, _ = StrSlicePool.Get().([]string)
   251  	if h.Scripts != nil {
   252  		h.Scripts = h.Scripts[:0]
   253  	}
   254  	h.PreScriptsAsync, _ = StrSlicePool.Get().([]string)
   255  	if h.PreScriptsAsync != nil {
   256  		h.PreScriptsAsync = h.PreScriptsAsync[:0]
   257  	}*/
   258  
   259  	// An optimisation so we don't populate StartedAt for users who shouldn't see the stat anyway
   260  	// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
   261  	//if u.IsAdmin {
   262  	//h.StartedAt = time.Now()
   263  	//}
   264  
   265  	//PrepResources(u,h,theme)
   266  	return h, nil
   267  }
   268  
   269  func PrepResources(u *User, h *Header, theme *Theme) {
   270  	h.AddSheet(theme.Name + "/main.css")
   271  
   272  	if len(theme.Resources) > 0 {
   273  		rlist := theme.Resources
   274  		for _, res := range rlist {
   275  			if res.Loggedin && !u.Loggedin {
   276  				continue
   277  			}
   278  			if res.LocID == LocGlobal || res.LocID == LocFront {
   279  				if res.Type == ResTypeSheet {
   280  					h.AddSheet(res.Name)
   281  				} else if res.Type == ResTypeScript {
   282  					if res.Async {
   283  						h.AddScriptAsync(res.Name)
   284  					} else {
   285  						h.AddScript(res.Name)
   286  					}
   287  				}
   288  			}
   289  		}
   290  	}
   291  
   292  	addPreScript := func(name string, i int) {
   293  		// TODO: Optimise this by removing a superfluous string alloc
   294  		if theme.OverridenMap != nil {
   295  			//fmt.Printf("name %+v\n", name)
   296  			//fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap)
   297  			if _, ok := theme.OverridenMap[name]; ok {
   298  				tname := "_" + theme.Name
   299  				//fmt.Printf("tname %+v\n", tname)
   300  				h.AddPreScriptAsync("tmpl_" + name + tname + ".js")
   301  				return
   302  			}
   303  		}
   304  		//fmt.Printf("tname %+v\n", tname)
   305  		h.AddPreScriptAsync(ucstrs[i])
   306  	}
   307  	addPreScript("topics_topic", 1)
   308  	addPreScript("paginator", 2)
   309  	addPreScript("alert", 3)
   310  	addPreScript("notice", 4)
   311  	if u.Loggedin {
   312  		addPreScript("topic_c_edit_post", 5)
   313  		addPreScript("topic_c_attach_item", 6)
   314  		addPreScript("topic_c_poll_input", 7)
   315  	}
   316  }
   317  
   318  func pstr(name string) string {
   319  	return "tmpl_" + name + ".js"
   320  }
   321  
   322  var ucstrs = [...]string{
   323  	"frontend",
   324  
   325  	pstr("topics_topic"),
   326  	pstr("paginator"),
   327  	pstr("alert"),
   328  	pstr("notice"),
   329  
   330  	pstr("topic_c_edit_post"),
   331  	pstr("topic_c_attach_item"),
   332  	pstr("topic_c_poll_input"),
   333  }
   334  
   335  func preRoute(w http.ResponseWriter, r *http.Request) (User, bool) {
   336  	userptr, halt := Auth.SessionCheck(w, r)
   337  	if halt {
   338  		return *userptr, false
   339  	}
   340  	var usercpy *User = BlankUser()
   341  	*usercpy = *userptr
   342  	usercpy.Init() // TODO: Can we reduce the amount of work we do here?
   343  
   344  	// TODO: Add a config setting to disable this header
   345  	// TODO: Have this header cover more things
   346  	if Config.SslSchema {
   347  		w.Header().Set("Content-Security-Policy", "upgrade-insecure-requests")
   348  	}
   349  
   350  	// TODO: WIP. Refactor this to eliminate the unnecessary query
   351  	// TODO: Better take proxies into consideration
   352  	if !Config.DisableIP {
   353  		var host string
   354  		// TODO: Prefer Cf-Connecting-Ip header, fewer shenanigans
   355  		if Site.HasProxy {
   356  			// TODO: Check the right-most IP, might get tricky with multiple proxies, maybe have a setting for the number of hops we jump through
   357  			xForwardedFor := r.Header.Get("X-Forwarded-For")
   358  			if xForwardedFor != "" {
   359  				forwardedFor := strings.Split(xForwardedFor, ",")
   360  				// TODO: Check if this is a valid IP Address, reject if not
   361  				host = forwardedFor[len(forwardedFor)-1]
   362  			}
   363  		}
   364  
   365  		if host == "" {
   366  			var e error
   367  			host, _, e = net.SplitHostPort(r.RemoteAddr)
   368  			if e != nil {
   369  				_ = PreError("Bad IP", w, r)
   370  				return *usercpy, false
   371  			}
   372  		}
   373  
   374  		if !Config.DisableLastIP && usercpy.Loggedin && host != usercpy.GetIP() {
   375  			mon := time.Now().Month()
   376  			e := usercpy.UpdateIP(strconv.Itoa(int(mon)) + "-" + host)
   377  			if e != nil {
   378  				_ = InternalError(e, w, r)
   379  				return *usercpy, false
   380  			}
   381  		}
   382  		usercpy.LastIP = host
   383  	}
   384  
   385  	return *usercpy, true
   386  }
   387  
   388  func UploadAvatar(w http.ResponseWriter, r *http.Request, u *User, tuid int) (ext string, ferr RouteError) {
   389  	// We don't want multiple files
   390  	// TODO: Are we doing this correctly?
   391  	filenameMap := make(map[string]bool)
   392  	for _, fheaders := range r.MultipartForm.File {
   393  		for _, hdr := range fheaders {
   394  			if hdr.Filename == "" {
   395  				continue
   396  			}
   397  			filenameMap[hdr.Filename] = true
   398  		}
   399  	}
   400  	if len(filenameMap) > 1 {
   401  		return "", LocalError("You may only upload one avatar", w, r, u)
   402  	}
   403  
   404  	for _, fheaders := range r.MultipartForm.File {
   405  		for _, hdr := range fheaders {
   406  			if hdr.Filename == "" {
   407  				continue
   408  			}
   409  			inFile, err := hdr.Open()
   410  			if err != nil {
   411  				return "", LocalError("Upload failed", w, r, u)
   412  			}
   413  			defer inFile.Close()
   414  
   415  			if ext == "" {
   416  				extarr := strings.Split(hdr.Filename, ".")
   417  				if len(extarr) < 2 {
   418  					return "", LocalError("Bad file", w, r, u)
   419  				}
   420  				ext = extarr[len(extarr)-1]
   421  
   422  				// TODO: Can we do this without a regex?
   423  				reg, err := regexp.Compile("[^A-Za-z0-9]+")
   424  				if err != nil {
   425  					return "", LocalError("Bad file extension", w, r, u)
   426  				}
   427  				ext = reg.ReplaceAllString(ext, "")
   428  				ext = strings.ToLower(ext)
   429  
   430  				if !ImageFileExts.Contains(ext) {
   431  					return "", LocalError("You can only use an image for your avatar", w, r, u)
   432  				}
   433  			}
   434  
   435  			// TODO: Centralise this string, so we don't have to change it in two different places when it changes
   436  			outFile, err := os.Create("./uploads/avatar_" + strconv.Itoa(tuid) + "." + ext)
   437  			if err != nil {
   438  				return "", LocalError("Upload failed [File Creation Failed]", w, r, u)
   439  			}
   440  			defer outFile.Close()
   441  
   442  			_, err = io.Copy(outFile, inFile)
   443  			if err != nil {
   444  				return "", LocalError("Upload failed [Copy Failed]", w, r, u)
   445  			}
   446  		}
   447  	}
   448  	if ext == "" {
   449  		return "", LocalError("No file", w, r, u)
   450  	}
   451  	return ext, nil
   452  }
   453  
   454  func ChangeAvatar(path string, w http.ResponseWriter, r *http.Request, u *User) RouteError {
   455  	e := u.ChangeAvatar(path)
   456  	if e != nil {
   457  		return InternalError(e, w, r)
   458  	}
   459  
   460  	// Clean up the old avatar data, so we don't end up with too many dead files in /uploads/
   461  	if len(u.RawAvatar) > 2 {
   462  		if u.RawAvatar[0] == '.' && u.RawAvatar[1] == '.' {
   463  			e := os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_tmp" + u.RawAvatar[1:])
   464  			if e != nil && !os.IsNotExist(e) {
   465  				LogWarning(e)
   466  				return LocalError("Something went wrong", w, r, u)
   467  			}
   468  			e = os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_w48" + u.RawAvatar[1:])
   469  			if e != nil && !os.IsNotExist(e) {
   470  				LogWarning(e)
   471  				return LocalError("Something went wrong", w, r, u)
   472  			}
   473  		}
   474  	}
   475  
   476  	return nil
   477  }
   478  
   479  // SuperAdminOnly makes sure that only super admin can access certain critical panel routes
   480  func SuperAdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   481  	if !u.IsSuperAdmin {
   482  		return NoPermissions(w, r, u)
   483  	}
   484  	return nil
   485  }
   486  
   487  // AdminOnly makes sure that only admins can access certain panel routes
   488  func AdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   489  	if !u.IsAdmin {
   490  		return NoPermissions(w, r, u)
   491  	}
   492  	return nil
   493  }
   494  
   495  // SuperModeOnly makes sure that only super mods or higher can access the panel routes
   496  func SuperModOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   497  	if !u.IsSuperMod {
   498  		return NoPermissions(w, r, u)
   499  	}
   500  	return nil
   501  }
   502  
   503  // MemberOnly makes sure that only logged in users can access this route
   504  func MemberOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   505  	if !u.Loggedin {
   506  		return LoginRequired(w, r, u)
   507  	}
   508  	return nil
   509  }
   510  
   511  // NoBanned stops any banned users from accessing this route
   512  func NoBanned(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   513  	if u.IsBanned {
   514  		return Banned(w, r, u)
   515  	}
   516  	return nil
   517  }
   518  
   519  func ParseForm(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   520  	if e := r.ParseForm(); e != nil {
   521  		return LocalError("Bad Form", w, r, u)
   522  	}
   523  	return nil
   524  }
   525  
   526  func NoSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   527  	if e := r.ParseForm(); e != nil {
   528  		return LocalError("Bad Form", w, r, u)
   529  	}
   530  	if len(u.Session) == 0 {
   531  		return SecurityError(w, r, u)
   532  	}
   533  	// TODO: Try to eliminate some of these allocations
   534  	sess := []byte(u.Session)
   535  	if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 {
   536  		return SecurityError(w, r, u)
   537  	}
   538  	return nil
   539  }
   540  
   541  func ReqIsJson(r *http.Request) bool {
   542  	return r.Header.Get("Content-type") == "application/json"
   543  }
   544  
   545  func HandleUploadRoute(w http.ResponseWriter, r *http.Request, u *User, maxFileSize int) RouteError {
   546  	// TODO: Reuse this code more
   547  	if r.ContentLength > int64(maxFileSize) {
   548  		size, unit := ConvertByteUnit(float64(maxFileSize))
   549  		return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, u)
   550  	}
   551  	r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength)
   552  
   553  	e := r.ParseMultipartForm(int64(Megabyte))
   554  	if e != nil {
   555  		return LocalError("Bad Form", w, r, u)
   556  	}
   557  	return nil
   558  }
   559  
   560  func NoUploadSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   561  	if len(u.Session) == 0 {
   562  		return SecurityError(w, r, u)
   563  	}
   564  	// TODO: Try to eliminate some of these allocations
   565  	sess := []byte(u.Session)
   566  	if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 {
   567  		return SecurityError(w, r, u)
   568  	}
   569  	return nil
   570  }