diff options
-rw-r--r-- | modules/base/tool.go | 10 | ||||
-rw-r--r-- | modules/middleware/auth.go | 58 | ||||
-rw-r--r-- | modules/middleware/context.go | 107 | ||||
-rw-r--r-- | modules/middleware/render.go | 5 | ||||
-rw-r--r-- | public/js/app.js | 33 | ||||
-rw-r--r-- | templates/admin/users/edit.tmpl | 1 | ||||
-rw-r--r-- | templates/admin/users/new.tmpl | 1 | ||||
-rw-r--r-- | templates/base/head.tmpl | 1 | ||||
-rw-r--r-- | templates/repo/create.tmpl | 1 | ||||
-rw-r--r-- | templates/repo/setting.tmpl | 1 | ||||
-rw-r--r-- | templates/user/active.tmpl | 3 | ||||
-rw-r--r-- | templates/user/delete.tmpl | 1 | ||||
-rw-r--r-- | templates/user/password.tmpl | 4 | ||||
-rw-r--r-- | templates/user/publickey.tmpl | 1 | ||||
-rw-r--r-- | templates/user/setting.tmpl | 1 | ||||
-rw-r--r-- | templates/user/signin.tmpl | 1 | ||||
-rw-r--r-- | templates/user/signup.tmpl | 1 | ||||
-rw-r--r-- | web.go | 24 |
18 files changed, 208 insertions, 46 deletions
diff --git a/modules/base/tool.go b/modules/base/tool.go index 4f368aa58c..c7ee2ee857 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -25,13 +25,17 @@ func EncodeMd5(str string) string { return hex.EncodeToString(m.Sum(nil)) } -// Random generate string -func GetRandomString(n int) string { +// GetRandomString generate random string by specify chars. +func GetRandomString(n int, alphabets ...byte) string { const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var bytes = make([]byte, n) rand.Read(bytes) for i, b := range bytes { - bytes[i] = alphanum[b%byte(len(alphanum))] + if len(alphabets) == 0 { + bytes[i] = alphanum[b%byte(len(alphanum))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } } return string(bytes) } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index f211de32b9..b557188ee9 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -10,39 +10,45 @@ import ( "github.com/gogits/gogs/modules/base" ) -// SignInRequire requires user to sign in. -func SignInRequire(redirect bool) martini.Handler { - return func(ctx *Context) { - if !ctx.IsSigned { - if redirect { - ctx.Redirect("/user/login") - } - return - } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { - ctx.Data["Title"] = "Activate Your Account" - ctx.HTML(200, "user/active") - return - } - } +type ToggleOptions struct { + SignInRequire bool + SignOutRequire bool + AdminRequire bool + DisableCsrf bool } -// SignOutRequire requires user to sign out. -func SignOutRequire() martini.Handler { +func Toggle(options *ToggleOptions) martini.Handler { return func(ctx *Context) { - if ctx.IsSigned { + if options.SignOutRequire && ctx.IsSigned { ctx.Redirect("/") return } - } -} -// AdminRequire requires user signed in as administor. -func AdminRequire() martini.Handler { - return func(ctx *Context) { - if !ctx.User.IsAdmin { - ctx.Error(403) - return + if !options.DisableCsrf { + if ctx.Req.Method == "POST" { + if !ctx.CsrfTokenValid() { + ctx.Error(403, "CSRF token does not match") + return + } + } + } + + if options.SignInRequire { + if !ctx.IsSigned { + ctx.Redirect("/user/login") + return + } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { + ctx.Data["Title"] = "Activate Your Account" + ctx.HTML(200, "user/active") + return + } + } + + if options.AdminRequire { + if !ctx.User.IsAdmin { + ctx.Error(403) + return + } } - ctx.Data["PageIsAdmin"] = true } } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index c958c1d6cd..b28953fc0e 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -6,6 +6,7 @@ package middleware import ( "fmt" + "html/template" "net/http" "time" @@ -32,6 +33,8 @@ type Context struct { User *models.User IsSigned bool + csrfToken string + Repo struct { IsValid bool IsOwner bool @@ -90,6 +93,95 @@ func (ctx *Context) Handle(status int, title string, err error) { ctx.HTML(status, fmt.Sprintf("status/%d", status)) } +func (ctx *Context) GetCookie(name string) string { + cookie, err := ctx.Req.Cookie(name) + if err != nil { + return "" + } + return cookie.Value +} + +func (ctx *Context) SetCookie(name string, value string, others ...interface{}) { + cookie := http.Cookie{} + cookie.Name = name + cookie.Value = value + + if len(others) > 0 { + switch v := others[0].(type) { + case int: + cookie.MaxAge = v + case int64: + cookie.MaxAge = int(v) + case int32: + cookie.MaxAge = int(v) + } + } + + // default "/" + if len(others) > 1 { + if v, ok := others[1].(string); ok && len(v) > 0 { + cookie.Path = v + } + } else { + cookie.Path = "/" + } + + // default empty + if len(others) > 2 { + if v, ok := others[2].(string); ok && len(v) > 0 { + cookie.Domain = v + } + } + + // default empty + if len(others) > 3 { + switch v := others[3].(type) { + case bool: + cookie.Secure = v + default: + if others[3] != nil { + cookie.Secure = true + } + } + } + + // default false. for session cookie default true + if len(others) > 4 { + if v, ok := others[4].(bool); ok && v { + cookie.HttpOnly = true + } + } + + ctx.Res.Header().Add("Set-Cookie", cookie.String()) +} + +func (ctx *Context) CsrfToken() string { + if len(ctx.csrfToken) > 0 { + return ctx.csrfToken + } + + token := ctx.GetCookie("_csrf") + if len(token) == 0 { + token = base.GetRandomString(30) + ctx.SetCookie("_csrf", token) + } + ctx.csrfToken = token + return token +} + +func (ctx *Context) CsrfTokenValid() bool { + token := ctx.Query("_csrf") + if token == "" { + token = ctx.Req.Header.Get("X-Csrf-Token") + } + if token == "" { + return false + } else if ctx.csrfToken != token { + return false + } + return true +} + // InitContext initializes a classic context for a request. func InitContext() martini.Handler { return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { @@ -103,11 +195,14 @@ func InitContext() martini.Handler { Render: rd, } + ctx.Data["PageStartTime"] = time.Now() + // start session ctx.Session = base.SessionManager.SessionStart(res, r) - defer func() { + rw := res.(martini.ResponseWriter) + rw.Before(func(martini.ResponseWriter) { ctx.Session.SessionRelease(res) - }() + }) // Get user from session if logined. user := auth.SignedInUser(ctx.Session) @@ -121,9 +216,15 @@ func InitContext() martini.Handler { ctx.Data["SignedUserId"] = user.Id ctx.Data["SignedUserName"] = user.LowerName ctx.Data["IsAdmin"] = ctx.User.IsAdmin + + if ctx.User.IsAdmin { + ctx.Data["PageIsAdmin"] = true + } } - ctx.Data["PageStartTime"] = time.Now() + // get or create csrf token + ctx.Data["CsrfToken"] = ctx.CsrfToken() + ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`) c.Map(ctx) diff --git a/modules/middleware/render.go b/modules/middleware/render.go index 8a54183135..869ef9abaa 100644 --- a/modules/middleware/render.go +++ b/modules/middleware/render.go @@ -242,8 +242,11 @@ func (r *Render) HTMLString(name string, binding interface{}, htmlOpt ...HTMLOpt } } -func (r *Render) Error(status int) { +func (r *Render) Error(status int, message ...string) { r.WriteHeader(status) + if len(message) > 0 { + r.Write([]byte(message[0])) + } } func (r *Render) Redirect(location string, status ...int) { diff --git a/public/js/app.js b/public/js/app.js index f179342f4b..df755727b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,39 @@ var Gogits = { "PageIsSignup": false }; +(function($){ + // extend jQuery ajax, set csrf token value + var ajax = $.ajax; + $.extend({ + ajax: function(url, options) { + if (typeof url === 'object') { + options = url; + url = undefined; + } + options = options || {}; + url = options.url; + var csrftoken = $('meta[name=_csrf]').attr('content'); + var headers = options.headers || {}; + var domain = document.domain.replace(/\./ig, '\\.'); + if (!/^(http:|https:).*/.test(url) || eval('/^(http:|https:)\\/\\/(.+\\.)*' + domain + '.*/').test(url)) { + headers = $.extend(headers, {'X-Csrf-Token':csrftoken}); + } + options.headers = headers; + var callback = options.success; + options.success = function(data){ + if(data.once){ + // change all _once value if ajax data.once exist + $('[name=_once]').val(data.once); + } + if(callback){ + callback.apply(this, arguments); + } + }; + return ajax(url, options); + } + }); +}(jQuery)); + (function ($) { Gogits.showTab = function (selector, index) { diff --git a/templates/admin/users/edit.tmpl b/templates/admin/users/edit.tmpl index 2a9882423a..08f11fcb12 100644 --- a/templates/admin/users/edit.tmpl +++ b/templates/admin/users/edit.tmpl @@ -12,6 +12,7 @@ <br/> <form action="/admin/users/{{.User.Id}}" method="post" class="form-horizontal"> {{if .IsSuccess}}<p class="alert alert-success">Account profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} + {{.CsrfTokenHtml}} <input type="hidden" value="{{.User.Id}}" name="userId"/> <div class="form-group"> <label class="col-md-3 control-label">Username: </label> diff --git a/templates/admin/users/new.tmpl b/templates/admin/users/new.tmpl index 01d976caa0..7b41ae43a7 100644 --- a/templates/admin/users/new.tmpl +++ b/templates/admin/users/new.tmpl @@ -11,6 +11,7 @@ <div class="panel-body"> <br/> <form action="/admin/users/new" method="post" class="form-horizontal"> + {{.CsrfTokenHtml}} <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}"> <label class="col-md-3 control-label">Username: </label> diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index f02ea095ca..7f56ed7080 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -8,6 +8,7 @@ <meta name="author" content="Gogs - Go Git Service" /> <meta name="description" content="Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language" /> <meta name="keywords" content="go, git"> + <meta name="_csrf" content="{{.CsrfToken}}" /> <!-- Stylesheets --> <link href="/css/bootstrap.min.css" rel="stylesheet" /> diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2de92f515f..a43f510484 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body"> <form action="/repo/create" method="post" class="form-horizontal gogs-card" id="gogs-repo-create"> + {{.CsrfTokenHtml}} <h3>Create New Repository</h3> <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group"> diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index a2fb1771d4..38c3fd3bcc 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -40,6 +40,7 @@ <div class="modal fade" id="delete-repository-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form action="/{{.Owner.Name}}/{{.Repository.Name}}/settings" method="post" class="modal-content"> + {{.CsrfTokenHtml}} <input type="hidden" name="action" value="delete"> <div class="modal-header"> diff --git a/templates/user/active.tmpl b/templates/user/active.tmpl index 47c87a591c..0df116cb40 100644 --- a/templates/user/active.tmpl +++ b/templates/user/active.tmpl @@ -1,7 +1,8 @@ {{template "base/head" .}} {{template "base/navbar" .}} <div id="gogs-body" class="container"> - <form action="/user/activate" method="get" class="form-horizontal gogs-card" id="gogs-login-card"> + <form action="/user/activate" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} <h3>Activate Your Account</h3> {{if .IsActivatePage}} {{if .ServiceNotEnabled}} diff --git a/templates/user/delete.tmpl b/templates/user/delete.tmpl index 397ea8cc09..46376672d4 100644 --- a/templates/user/delete.tmpl +++ b/templates/user/delete.tmpl @@ -22,6 +22,7 @@ <div class="modal fade" id="delete-account-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form action="/user/delete" method="post" class="modal-content" id="gogs-user-delete"> + {{.CsrfTokenHtml}} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Delete Account</h4> diff --git a/templates/user/password.tmpl b/templates/user/password.tmpl index 2ee178a3fc..936ec4b124 100644 --- a/templates/user/password.tmpl +++ b/templates/user/password.tmpl @@ -5,7 +5,9 @@ <div id="gogs-user-setting-container" class="col-md-9"> <div id="gogs-setting-pwd"> <h4>Password</h4> - <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password">{{if .IsSuccess}} + <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password"> + {{.CsrfTokenHtml}} + {{if .IsSuccess}} <p class="alert alert-success">Password is changed successfully. You can now sign in via new password.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} <div class="form-group"> <label class="col-md-3 control-label">Old Password<strong class="text-danger">*</strong></label> diff --git a/templates/user/publickey.tmpl b/templates/user/publickey.tmpl index 72467659be..e645e1a84b 100644 --- a/templates/user/publickey.tmpl +++ b/templates/user/publickey.tmpl @@ -22,6 +22,7 @@ <div class="modal fade" id="ssh-add-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form class="modal-content form-horizontal" id="gogs-ssh-form" method="post" action="/user/setting/ssh/"> + {{.CsrfTokenHtml}} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Add SSH Key</h4> diff --git a/templates/user/setting.tmpl b/templates/user/setting.tmpl index 222ddd895b..30c9529b12 100644 --- a/templates/user/setting.tmpl +++ b/templates/user/setting.tmpl @@ -6,6 +6,7 @@ <div id="gogs-setting-pwd"> <h4>Account Profile</h4> <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting"> + {{.CsrfTokenHtml}} {{if .IsSuccess}}<p class="alert alert-success">Your profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} <p>Your Email will be public and used for Account related notifications and any web based operations made via the web.</p> <div class="form-group"> diff --git a/templates/user/signin.tmpl b/templates/user/signin.tmpl index a49bf11405..8dc7292ff8 100644 --- a/templates/user/signin.tmpl +++ b/templates/user/signin.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body" data-page="user-signin"> <form action="/user/login" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} <h3>Log in</h3> <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}"> diff --git a/templates/user/signup.tmpl b/templates/user/signup.tmpl index 069d34a5b2..fbfc4cadcc 100644 --- a/templates/user/signup.tmpl +++ b/templates/user/signup.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body" data-page="user-signup"> <form action="/user/sign_up" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} {{if .DisenableRegisteration}} Sorry, registeration has been disenabled, you can only get account from administrator. {{else}} @@ -82,9 +82,10 @@ func runWeb(*cli.Context) { m.Use(middleware.InitContext()) - reqSignIn := middleware.SignInRequire(true) - ignSignIn := middleware.SignInRequire(base.Service.RequireSignInView) - reqSignOut := middleware.SignOutRequire() + reqSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true}) + ignSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: base.Service.RequireSignInView}) + reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true}) + // Routers. m.Get("/", ignSignIn, routers.Home) m.Get("/issues", reqSignIn, user.Issues) @@ -109,14 +110,15 @@ func runWeb(*cli.Context) { m.Get("/help", routers.Help) - adminReq := middleware.AdminRequire() - m.Get("/admin", reqSignIn, adminReq, admin.Dashboard) - m.Get("/admin/users", reqSignIn, adminReq, admin.Users) - m.Any("/admin/users/new", reqSignIn, adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) - m.Any("/admin/users/:userid", reqSignIn, adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) - m.Any("/admin/users/:userid/delete", reqSignIn, adminReq, admin.DeleteUser) - m.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories) - m.Get("/admin/config", reqSignIn, adminReq, admin.Config) + adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true}) + + m.Get("/admin", adminReq, admin.Dashboard) + m.Get("/admin/users", adminReq, admin.Users) + m.Any("/admin/users/new", adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) + m.Any("/admin/users/:userid", adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) + m.Any("/admin/users/:userid/delete", adminReq, admin.DeleteUser) + m.Get("/admin/repos", adminReq, admin.Repositories) + m.Get("/admin/config", adminReq, admin.Config) m.Post("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.SettingPost) m.Get("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.Setting) |