Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-21 05:11:15 |
Commit | 0893c1e6ee7e521b4f36ee08c69daf4ce211cfd3 |
Parent | a0ba6ae56e9aa32404aeaa74ab9792b3525eb81b |
Integrate UID into session tokens
Diffstat
M | main.go | | | 2 | +- |
A | res/admin/index.html | | | 16 | ++++++++++++++++ |
M | res/admin/repo_edit.html | | | 38 | ++++++++++++++++++++------------------ |
M | res/admin/repos.html | | | 10 | ++++++++-- |
M | res/admin/user_create.html | | | 28 | +++++++++++++++------------- |
M | res/admin/user_edit.html | | | 32 | +++++++++++++++++--------------- |
M | res/admin/users.html | | | 10 | ++++++++-- |
M | res/base/header.html | | | 2 | +- |
M | res/res.go | | | 3 | +++ |
M | src/admin.go | | | 38 | ++++++++++++++++++++++++++------------ |
M | src/auth.go | | | 140 | ++++++++++++++++++++++++++++++++++++++++++++------------------------------------ |
M | src/http.go | | | 1 | + |
M | src/repo.go | | | 27 | +++++++++++++++++++-------- |
M | src/user.go | | | 32 | ++++++++++++++++---------------- |
M | src/util/util.go | | | 9 | +++++---- |
15 files changed, 233 insertions, 155 deletions
diff --git a/main.go b/main.go index f3db37f..7debc01 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,7 @@ func main() { h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin) h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout) h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate) - // h.Path("/admin").Methods("GET").HandlerFunc() + h.Path("/admin").Methods("GET").HandlerFunc(goit.HandleAdminIndex) h.Path("/admin/users").Methods("GET").HandlerFunc(goit.HandleAdminUsers) h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserCreate) h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc(goit.HandleAdminUserEdit) diff --git a/res/admin/index.html b/res/admin/index.html new file mode 100644 index 0000000..257aed7 --- /dev/null +++ b/res/admin/index.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<head>{{template "base/head" .}}</head> +<body> + <table> + <tr> + <td rowspan="2"> + <a href="/"><img src="/static/favicon.png" style="max-height: 24px"></a> + </td> + <td><h1>{{.Title}}</h1></td> + </tr> + <tr><td> + <a href="/admin/users">Users</a> + | <a href="/admin/repos">Repos</a> + </td></tr> + </table> +</body> diff --git a/res/admin/repo_edit.html b/res/admin/repo_edit.html index 7556e12..43d177e 100644 --- a/res/admin/repo_edit.html +++ b/res/admin/repo_edit.html @@ -1,22 +1,24 @@ <!DOCTYPE html> <head>{{template "base/head" .}}</head> <body> - <h1>{{.Title}}</h1> - <form action="/admin/repo/edit?repo={{.Id}}" method="post"> - <label for="id">ID:</label> - <input type="text" name="id" value="{{.Id}}" disabled><br> - <label for="id">Owner:</label> - <input type="text" name="owner" value="{{.Owner}}" disabled><br> - <label for="reponame">Name:</label> - <input type="text" name="reponame" value="{{.Name}}"><br> - <label for="description">Description:</label> - <input type="text" name="description" value="{{.Description}}"><br> - <label for="visibility">Visibility:</label> - <select name="visibility"> - <option value="public">Public</option> - <option value="private" {{if .IsPrivate}}selected{{end}}>Private</option> - </select><br> - <input type="submit" value="Update"> - </form> - <span>{{.Message}}</span> + <main> + <h1>{{.Title}}</h1> + <form action="/admin/repo/edit?repo={{.Id}}" method="post"> + <label for="id">ID:</label> + <input type="text" name="id" value="{{.Id}}" disabled><br> + <label for="id">Owner:</label> + <input type="text" name="owner" value="{{.Owner}}" disabled><br> + <label for="reponame">Name:</label> + <input type="text" name="reponame" value="{{.Name}}"><br> + <label for="description">Description:</label> + <input type="text" name="description" value="{{.Description}}"><br> + <label for="visibility">Visibility:</label> + <select name="visibility"> + <option value="public">Public</option> + <option value="private" {{if .IsPrivate}}selected{{end}}>Private</option> + </select><br> + <input type="submit" value="Update"> + </form> + <span>{{.Message}}</span> + </main> </body> diff --git a/res/admin/repos.html b/res/admin/repos.html index db8b797..5458c8c 100644 --- a/res/admin/repos.html +++ b/res/admin/repos.html @@ -4,11 +4,17 @@ <header> <table> <tr> - <td><img src = "/static/favicon.png" style="max-height: 24px"></td> + <td rowspan="2"> + <a href="/"><img src="/static/favicon.png" style="max-height: 24px"></a> + </td> <td><h1>{{.Title}}</h1></td> </tr> + <tr><td> + <a href="/admin/users">Users</a> + | <a href="/admin/repos">Repos</a> + </td></tr> </table> - </header> + </header><hr> <main> <table> <thead> diff --git a/res/admin/user_create.html b/res/admin/user_create.html index c19b879..cea4814 100644 --- a/res/admin/user_create.html +++ b/res/admin/user_create.html @@ -1,17 +1,19 @@ <!DOCTYPE html> <head>{{template "base/head" .}}</head> <body> - <h1>{{.Title}}</h1> - <form action="/admin/user/create" method="post"> - <label for="username">Username:</label> - <input type="text" name="username"><br> - <label for="fullname">Full Name:</label> - <input type="text" name="fullname"><br> - <label for="password">Password:</label> - <input type="password" name="password"><br> - <label for="admin">Admin:</label> - <input type="checkbox" name="admin" value="true"><br> - <input type="submit" value="Create"> - </form> - <span>{{.Message}}</span> + <main> + <h1>{{.Title}}</h1> + <form action="/admin/user/create" method="post"> + <label for="username">Username:</label> + <input type="text" name="username"><br> + <label for="fullname">Full Name:</label> + <input type="text" name="fullname"><br> + <label for="password">Password:</label> + <input type="password" name="password"><br> + <label for="admin">Admin:</label> + <input type="checkbox" name="admin" value="true"><br> + <input type="submit" value="Create"> + </form> + <span>{{.Message}}</span> + </main> </body> diff --git a/res/admin/user_edit.html b/res/admin/user_edit.html index 877a7b7..13ca379 100644 --- a/res/admin/user_edit.html +++ b/res/admin/user_edit.html @@ -1,19 +1,21 @@ <!DOCTYPE html> <head>{{template "base/head" .}}</head> <body> - <h1>{{.Title}}</h1> - <form action="/admin/user/edit?user={{.Id}}" method="post"> - <label for="id">ID:</label> - <input type="text" name="id" value="{{.Id}}" disabled><br> - <label for="username">Username:</label> - <input type="text" name="username" value="{{.Name}}"><br> - <label for="fullname">Full Name:</label> - <input type="text" name="fullname" value="{{.FullName}}"><br> - <label for="password">Password:</label> - <input type="password" name="password" placeholder="Unchanged"><br> - <label for="admin">Admin:</label> - <input type="checkbox" name="admin" value="true" {{if .IsAdmin}}checked{{end}}><br> - <input type="submit" value="Update"> - </form> - <span>{{.Message}}</span> + <main> + <h1>{{.Title}}</h1> + <form action="/admin/user/edit?user={{.Id}}" method="post"> + <label for="id">ID:</label> + <input type="text" name="id" value="{{.Id}}" disabled><br> + <label for="username">Username:</label> + <input type="text" name="username" value="{{.Name}}"><br> + <label for="fullname">Full Name:</label> + <input type="text" name="fullname" value="{{.FullName}}"><br> + <label for="password">Password:</label> + <input type="password" name="password" placeholder="Unchanged"><br> + <label for="admin">Admin:</label> + <input type="checkbox" name="admin" value="true" {{if .IsAdmin}}checked{{end}}><br> + <input type="submit" value="Update"> + </form> + <span>{{.Message}}</span> + </main> </body> diff --git a/res/admin/users.html b/res/admin/users.html index b1d88bf..f224ae1 100644 --- a/res/admin/users.html +++ b/res/admin/users.html @@ -4,11 +4,17 @@ <header> <table> <tr> - <td><img src = "/static/favicon.png" style="max-height: 24px"></td> + <td rowspan="2"> + <a href="/"><img src="/static/favicon.png" style="max-height: 24px"></a> + </td> <td><h1>{{.Title}}</h1></td> </tr> + <tr><td> + <a href="/admin/users">Users</a> + | <a href="/admin/repos">Repos</a> + </td></tr> </table> - </header> + </header><hr> <main> <table> <thead> diff --git a/res/base/header.html b/res/base/header.html index 1645db9..da113bb 100644 --- a/res/base/header.html +++ b/res/base/header.html @@ -13,7 +13,7 @@ | <a href="/admin">Admin</a> {{end}} {{if .Auth}} - | <a href="/user/logout">Logout</a> + | <a href="/user/logout">Logout</a>{{if .Username}} ({{.Username}}){{end}} {{else}} | <a href="/user/login">Login</a> {{end}} diff --git a/res/res.go b/res/res.go index f8a1f73..837f577 100644 --- a/res/res.go +++ b/res/res.go @@ -14,6 +14,9 @@ var BaseHead string //go:embed base/header.html var BaseHeader string +//go:embed admin/index.html +var AdminIndex string + //go:embed admin/users.html var AdminUsers string diff --git a/src/admin.go b/src/admin.go index e646691..c14579a 100644 --- a/src/admin.go +++ b/src/admin.go @@ -15,8 +15,19 @@ import ( "github.com/dustin/go-humanize" ) +func HandleAdminIndex(w http.ResponseWriter, r *http.Request) { + if _, admin, _ := AuthCookieAdmin(r); !admin { + HttpError(w, http.StatusNotFound) + return + } + + if err := tmpl.ExecuteTemplate(w, "admin/index", struct{ Title string }{"Admin"}); err != nil { + log.Println("[/admin/index]", err.Error()) + } +} + func HandleAdminUsers(w http.ResponseWriter, r *http.Request) { - if _, admin, _ := AuthHttpAdmin(r); !admin { + if _, admin, _ := AuthCookieAdmin(r); !admin { HttpError(w, http.StatusNotFound) return } @@ -34,7 +45,7 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) { data := struct { Title string Users []row - }{Title: "Users"} + }{Title: "Admin - Users"} for rows.Next() { d := User{} @@ -61,12 +72,12 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) { } func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { - if _, admin, _ := AuthHttpAdmin(r); !admin { + if _, admin, _ := AuthCookieAdmin(r); !admin { HttpError(w, http.StatusNotFound) return } - data := struct{ Title, Message string }{"Create User", ""} + data := struct{ Title, Message string }{"Admin - Create User", ""} if r.Method == http.MethodPost { username := strings.ToLower(r.FormValue("username")) @@ -106,12 +117,12 @@ func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { } func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) { - if _, admin, _ := AuthHttpAdmin(r); !admin { + if _, admin, _ := AuthCookieAdmin(r); !admin { HttpError(w, http.StatusNotFound) return } - id, err := strconv.ParseUint(r.URL.Query().Get("user"), 10, 64) + id, err := strconv.ParseInt(r.URL.Query().Get("user"), 10, 64) if err != nil { HttpError(w, http.StatusNotFound) return @@ -130,7 +141,10 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) { data := struct { Title, Id, Name, FullName, Message string IsAdmin bool - }{Title: "Edit User", Id: fmt.Sprint(user.Id), Name: user.Name, FullName: user.FullName, IsAdmin: user.IsAdmin} + }{ + Title: "Admin - Edit User", Id: fmt.Sprint(user.Id), Name: user.Name, FullName: user.FullName, + IsAdmin: user.IsAdmin, + } if r.Method == http.MethodPost { data.Name = strings.ToLower(r.FormValue("username")) @@ -181,7 +195,7 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) { } func HandleAdminRepos(w http.ResponseWriter, r *http.Request) { - if _, admin, _ := AuthHttpAdmin(r); !admin { + if _, admin, _ := AuthCookieAdmin(r); !admin { HttpError(w, http.StatusNotFound) return } @@ -199,7 +213,7 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) { data := struct { Title string Repos []row - }{Title: "Repositories"} + }{Title: "Admin - Repositories"} for rows.Next() { d := Repo{} @@ -236,12 +250,12 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) { } func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) { - if _, admin, _ := AuthHttpAdmin(r); !admin { + if _, admin, _ := AuthCookieAdmin(r); !admin { HttpError(w, http.StatusNotFound) return } - id, err := strconv.ParseUint(r.URL.Query().Get("repo"), 10, 64) + id, err := strconv.ParseInt(r.URL.Query().Get("repo"), 10, 64) if err != nil { HttpError(w, http.StatusNotFound) return @@ -261,7 +275,7 @@ func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) { Title, Id, Owner, Name, Description, Message string IsPrivate bool }{ - Title: "Edit Repository", Id: fmt.Sprint(repo.Id), Name: repo.Name, Description: repo.Description, + Title: "Admin - Edit Repository", Id: fmt.Sprint(repo.Id), Name: repo.Name, Description: repo.Description, IsPrivate: repo.IsPrivate, } diff --git a/src/auth.go b/src/auth.go index 5888e02..8a28428 100644 --- a/src/auth.go +++ b/src/auth.go @@ -9,110 +9,124 @@ import ( "encoding/base64" "fmt" "log" - "math" "net/http" + "strconv" + "strings" "time" "github.com/Jamozed/Goit/src/util" "golang.org/x/crypto/argon2" ) -type session struct { - id uint64 - expiry time.Time +type Session struct { + Token, Ip string + Expiry time.Time } -var sessions = map[string]session{} +var Sessions = map[int64]map[string]Session{} -/* Hash a password with a salt using Argon2. */ -func Hash(pass string, salt []byte) []byte { - return argon2.IDKey([]byte(pass), salt, 3, 64*1024, 4, 32) -} - -/* Generate a random Base64 salt. */ -func Salt() ([]byte, error) { - b := make([]byte, 16) +func NewSession(uid int64, ip string, expiry time.Time) (Session, error) { + b := make([]byte, 24) if _, err := rand.Read(b); err != nil { - return nil, err + return Session{}, err } - return b, nil -} - -func NewSession(id uint64, expiry time.Time) (string, error) { - b := make([]byte, 24) - if _, err := rand.Read(b); err != nil { - return "", err + if Sessions[uid] == nil { + Sessions[uid] = map[string]Session{} } - s := base64.StdEncoding.EncodeToString(b) - sessions[s] = session{id, expiry} - return s, nil + t := base64.StdEncoding.EncodeToString(b) + Sessions[uid][t] = Session{t, ip, expiry} + return Sessions[uid][t], nil } -func EndSession(s string) { - delete(sessions, s) +func EndSession(id int64, token string) { + delete(Sessions[id], token) + if len(Sessions[id]) == 0 { + delete(Sessions, id) + } } -func Auth(s string) (bool, uint64) { - if v, ok := sessions[s]; ok { - if v.expiry.After(time.Now()) { - return true, v.id - } else { - delete(sessions, s) +func CleanupSessions() { + var n uint64 = 0 + + for k, v := range Sessions { + for k1, v1 := range v { + if v1.Expiry.Before(time.Now()) { + EndSession(k, k1) + n += 1 + } } } - return false, math.MaxUint64 -} - -func AuthHttp(r *http.Request) (bool, uint64) { - if c := util.Cookie(r, "session"); c != nil { - return Auth(c.Value) + if n > 0 { + log.Println("[Cleanup] cleaned up", n, "expired sessions") } +} - return false, math.MaxUint64 +func SetSessionCookie(w http.ResponseWriter, uid int64, s Session) { + http.SetCookie(w, &http.Cookie{ + Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry, + }) } -func AuthHttpAdmin(r *http.Request) (auth bool, admin bool, uid uint64) { - if ok, uid := AuthHttp(r); ok { - if user, err := GetUser(uid); err == nil && user.IsAdmin { - return true, true, uid +func GetSessionCookie(r *http.Request) (int64, Session) { + if c := util.Cookie(r, "session"); c != nil { + ss := strings.SplitN(c.Value, ".", 2) + if len(ss) != 2 { + return -1, Session{} } - return true, false, uid + id, err := strconv.ParseInt(ss[0], 10, 64) + if err != nil { + return -1, Session{} + } + + return id, Sessions[id][ss[1]] } - return false, false, math.MaxUint64 + return -1, Session{} } -func SessionCookie(r *http.Request) string { - if c := util.Cookie(r, "session"); c != nil { - return c.Value +func EndSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1}) +} + +func AuthCookie(r *http.Request) (auth bool, uid int64) { + if uid, s := GetSessionCookie(r); s != (Session{}) { + if s.Expiry.After(time.Now()) { + return true, uid + } + + EndSession(uid, s.Token) } - return "" + return false, -1 } -func GetSessions() (s string) { - for k, v := range sessions { - s += fmt.Sprint(k, v.id, v.expiry) +func AuthCookieAdmin(r *http.Request) (auth bool, admin bool, uid int64) { + if ok, uid := AuthCookie(r); ok { + if user, err := GetUser(uid); err == nil && user.IsAdmin { + return true, true, uid + } + + return true, false, uid } - return s + return false, false, -1 } -func CleanupSessions() { - n := 0 +/* Hash a password with a salt using Argon2. */ +func Hash(pass string, salt []byte) []byte { + return argon2.IDKey([]byte(pass), salt, 3, 64*1024, 4, 32) +} - for k, v := range sessions { - if v.expiry.Before(time.Now()) { - delete(sessions, k) - n += 1 - } +/* Generate a random Base64 salt. */ +func Salt() ([]byte, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return nil, err } - if n > 0 { - log.Println("[Sessions] Cleaned up", n, "expired sessions") - } + return b, nil } diff --git a/src/http.go b/src/http.go index 29e858e..918646e 100644 --- a/src/http.go +++ b/src/http.go @@ -19,6 +19,7 @@ func init() { template.Must(tmpl.New("base/head").Parse(res.BaseHead)) template.Must(tmpl.New("base/header").Parse(res.BaseHeader)) + template.Must(tmpl.New("admin/index").Parse(res.AdminIndex)) template.Must(tmpl.New("admin/users").Parse(res.AdminUsers)) template.Must(tmpl.New("admin/user/create").Parse(res.AdminUserCreate)) template.Must(tmpl.New("admin/user/edit").Parse(res.AdminUserEdit)) diff --git a/src/repo.go b/src/repo.go index eae9078..0f7b260 100644 --- a/src/repo.go +++ b/src/repo.go @@ -22,8 +22,8 @@ import ( ) type Repo struct { - Id uint64 - OwnerId uint64 + Id int64 + OwnerId int64 Name string Description string DefaultBranch string @@ -31,7 +31,14 @@ type Repo struct { } func HandleIndex(w http.ResponseWriter, r *http.Request) { - auth, admin, uid := AuthHttpAdmin(r) + auth, admin, uid := AuthCookieAdmin(r) + + user, err := GetUser(uid) + if err != nil { + log.Println("[/]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return + } if rows, err := db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil { log.Println("[/]", err.Error()) @@ -41,11 +48,15 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) { type row struct{ Name, Description, Owner, Visibility, LastCommit string } data := struct { - Title string - Admin, Auth bool - Repos []row + Title, Username string + Admin, Auth bool + Repos []row }{Title: "Repositories", Admin: admin, Auth: auth} + if user != nil { + data.Username = user.Name + } + for rows.Next() { d := Repo{} if err := rows.Scan(&d.Id, &d.OwnerId, &d.Name, &d.Description, &d.IsPrivate); err != nil { @@ -75,7 +86,7 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) { } func HandleRepoCreate(w http.ResponseWriter, r *http.Request) { - if ok, uid := AuthHttp(r); !ok { + if ok, uid := AuthCookie(r); !ok { HttpError(w, http.StatusUnauthorized) } else if r.Method == http.MethodPost { name := r.FormValue("reponame") @@ -216,7 +227,7 @@ func HandleRepoRefs(w http.ResponseWriter, r *http.Request) { } } -func GetRepo(id uint64) (*Repo, error) { +func GetRepo(id int64) (*Repo, error) { r := &Repo{} if err := db.QueryRow( diff --git a/src/user.go b/src/user.go index 77c11a9..cd6bec8 100644 --- a/src/user.go +++ b/src/user.go @@ -16,7 +16,7 @@ import ( ) type User struct { - Id uint64 + Id int64 Name string FullName string Pass []byte @@ -28,7 +28,7 @@ type User struct { var reserved []string = []string{"admin", "repo", "static", "user"} func HandleUserLogin(w http.ResponseWriter, r *http.Request) { - if ok, _ := AuthHttp(r); ok { + if ok, _ := AuthCookie(r); ok { http.Redirect(w, r, "/", http.StatusFound) return } @@ -56,30 +56,30 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) { return } else if !bytes.Equal(Hash(password, u.Salt), u.Pass) { data.Message = "Invalid credentials" + } else if s, err := NewSession(u.Id, r.RemoteAddr, time.Now().Add(15*time.Minute)); err != nil { + log.Println("[User:Login:Session]", err.Error()) + HttpError(w, http.StatusInternalServerError) + return } else { - expiry := time.Now().Add(15 * time.Minute) - if s, err := NewSession(u.Id, expiry); err != nil { - log.Println("[User:Login:Session]", err.Error()) - HttpError(w, http.StatusInternalServerError) - return - } else { - http.SetCookie(w, &http.Cookie{Name: "session", Value: s, Path: "/", Expires: expiry}) - http.Redirect(w, r, "/", http.StatusFound) - return - } + SetSessionCookie(w, u.Id, s) + http.Redirect(w, r, "/", http.StatusFound) + return } } - tmpl.ExecuteTemplate(w, "user_login", data) + if err := tmpl.ExecuteTemplate(w, "user_login", data); err != nil { + log.Println("[/user/login]", err.Error()) + } } func HandleUserLogout(w http.ResponseWriter, r *http.Request) { - EndSession(SessionCookie(r)) - http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1}) + id, s := GetSessionCookie(r) + EndSession(id, s.Token) + EndSessionCookie(w) http.Redirect(w, r, "/", http.StatusFound) } -func GetUser(id uint64) (*User, error) { +func GetUser(id int64) (*User, error) { u := User{} if err := db.QueryRow( diff --git a/src/util/util.go b/src/util/util.go index 065fd85..bc3e587 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -26,11 +26,12 @@ func SliceContains[T comparable](s []T, e T) bool { return false } -/* Return the named cookie or nil if not found. */ +/* Return the named cookie or nil if not found or invalid. */ func Cookie(r *http.Request, name string) *http.Cookie { - if c, err := r.Cookie(name); err != nil { - return nil - } else { + c, err := r.Cookie(name) + if err == nil && c.Valid() == nil { return c } + + return nil }