|
@@ -34,22 +34,30 @@ type HTTPContext struct {
|
|
|
AuthUser *database.User
|
|
AuthUser *database.User
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// writeError writes an HTTP error response.
|
|
|
|
|
+func writeError(w http.ResponseWriter, status int, text string) {
|
|
|
|
|
+ w.WriteHeader(status)
|
|
|
|
|
+ if text != "" {
|
|
|
|
|
+ w.Write([]byte(text))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// askCredentials responses HTTP header and status which informs client to provide credentials.
|
|
// askCredentials responses HTTP header and status which informs client to provide credentials.
|
|
|
func askCredentials(c flamego.Context, status int, text string) {
|
|
func askCredentials(c flamego.Context, status int, text string) {
|
|
|
- c.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
|
|
|
|
|
- c.Error(status, text)
|
|
|
|
|
|
|
+ c.ResponseWriter().Header().Set("WWW-Authenticate", "Basic realm=\".\"")
|
|
|
|
|
+ writeError(c.ResponseWriter(), status, text)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func HTTPContexter(store Store) flamego.Handler {
|
|
func HTTPContexter(store Store) flamego.Handler {
|
|
|
return func(c flamego.Context) {
|
|
return func(c flamego.Context) {
|
|
|
if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
|
|
if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
|
|
|
// Set CORS headers for browser-based git clients
|
|
// Set CORS headers for browser-based git clients
|
|
|
- c.Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
|
|
|
|
|
- c.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
|
|
|
|
|
|
|
+ c.ResponseWriter().Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
|
|
|
|
|
+ c.ResponseWriter().Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
|
|
|
|
|
|
|
|
// Handle preflight OPTIONS request
|
|
// Handle preflight OPTIONS request
|
|
|
- if c.Req.Method == "OPTIONS" {
|
|
|
|
|
- c.Status(http.StatusOK)
|
|
|
|
|
|
|
+ if c.Request().Method == "OPTIONS" {
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusOK)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -59,26 +67,26 @@ func HTTPContexter(store Store) flamego.Handler {
|
|
|
repoName = strings.TrimSuffix(repoName, ".wiki")
|
|
repoName = strings.TrimSuffix(repoName, ".wiki")
|
|
|
|
|
|
|
|
isPull := c.Query("service") == "git-upload-pack" ||
|
|
isPull := c.Query("service") == "git-upload-pack" ||
|
|
|
- strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") ||
|
|
|
|
|
- c.Req.Method == "GET"
|
|
|
|
|
|
|
+ strings.HasSuffix(c.Request().URL.Path, "git-upload-pack") ||
|
|
|
|
|
+ c.Request().Method == "GET"
|
|
|
|
|
|
|
|
- owner, err := store.GetUserByUsername(c.Req.Context(), ownerName)
|
|
|
|
|
|
|
+ owner, err := store.GetUserByUsername(c.Request().Context(), ownerName)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
if database.IsErrUserNotExist(err) {
|
|
if database.IsErrUserNotExist(err) {
|
|
|
- c.Status(http.StatusNotFound)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusNotFound)
|
|
|
} else {
|
|
} else {
|
|
|
- c.Status(http.StatusInternalServerError)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusInternalServerError)
|
|
|
log.Error("Failed to get user [name: %s]: %v", ownerName, err)
|
|
log.Error("Failed to get user [name: %s]: %v", ownerName, err)
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- repo, err := store.GetRepositoryByName(c.Req.Context(), owner.ID, repoName)
|
|
|
|
|
|
|
+ repo, err := store.GetRepositoryByName(c.Request().Context(), owner.ID, repoName)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
if database.IsErrRepoNotExist(err) {
|
|
if database.IsErrRepoNotExist(err) {
|
|
|
- c.Status(http.StatusNotFound)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusNotFound)
|
|
|
} else {
|
|
} else {
|
|
|
- c.Status(http.StatusInternalServerError)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusInternalServerError)
|
|
|
log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, repoName, err)
|
|
log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, repoName, err)
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
@@ -98,12 +106,12 @@ func HTTPContexter(store Store) flamego.Handler {
|
|
|
!strings.Contains(action, "info/") &&
|
|
!strings.Contains(action, "info/") &&
|
|
|
!strings.Contains(action, "HEAD") &&
|
|
!strings.Contains(action, "HEAD") &&
|
|
|
!strings.Contains(action, "objects/") {
|
|
!strings.Contains(action, "objects/") {
|
|
|
- c.Error(http.StatusBadRequest, fmt.Sprintf("Unrecognized action %q", action))
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusBadRequest, fmt.Sprintf("Unrecognized action %q", action))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Handle HTTP Basic Authentication
|
|
// Handle HTTP Basic Authentication
|
|
|
- authHead := c.Req.Header.Get("Authorization")
|
|
|
|
|
|
|
+ authHead := c.Request().Header.Get("Authorization")
|
|
|
if authHead == "" {
|
|
if authHead == "" {
|
|
|
askCredentials(c, http.StatusUnauthorized, "")
|
|
askCredentials(c, http.StatusUnauthorized, "")
|
|
|
return
|
|
return
|
|
@@ -120,9 +128,9 @@ func HTTPContexter(store Store) flamego.Handler {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- authUser, err := store.AuthenticateUser(c.Req.Context(), authUsername, authPassword, -1)
|
|
|
|
|
|
|
+ authUser, err := store.AuthenticateUser(c.Request().Context(), authUsername, authPassword, -1)
|
|
|
if err != nil && !auth.IsErrBadCredentials(err) {
|
|
if err != nil && !auth.IsErrBadCredentials(err) {
|
|
|
- c.Status(http.StatusInternalServerError)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusInternalServerError)
|
|
|
log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)
|
|
log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -130,25 +138,25 @@ func HTTPContexter(store Store) flamego.Handler {
|
|
|
// If username and password combination failed, try again using either username
|
|
// If username and password combination failed, try again using either username
|
|
|
// or password as the token.
|
|
// or password as the token.
|
|
|
if authUser == nil {
|
|
if authUser == nil {
|
|
|
- authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authUsername)
|
|
|
|
|
|
|
+ authUser, err = context.AuthenticateByToken(store, c.Request().Context(), authUsername)
|
|
|
if err != nil && !database.IsErrAccessTokenNotExist(err) {
|
|
if err != nil && !database.IsErrAccessTokenNotExist(err) {
|
|
|
- c.Status(http.StatusInternalServerError)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusInternalServerError)
|
|
|
log.Error("Failed to authenticate by access token via username: %v", err)
|
|
log.Error("Failed to authenticate by access token via username: %v", err)
|
|
|
return
|
|
return
|
|
|
} else if database.IsErrAccessTokenNotExist(err) {
|
|
} else if database.IsErrAccessTokenNotExist(err) {
|
|
|
// Try again using the password field as the token.
|
|
// Try again using the password field as the token.
|
|
|
- authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authPassword)
|
|
|
|
|
|
|
+ authUser, err = context.AuthenticateByToken(store, c.Request().Context(), authPassword)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
if database.IsErrAccessTokenNotExist(err) {
|
|
if database.IsErrAccessTokenNotExist(err) {
|
|
|
askCredentials(c, http.StatusUnauthorized, "")
|
|
askCredentials(c, http.StatusUnauthorized, "")
|
|
|
} else {
|
|
} else {
|
|
|
- c.Status(http.StatusInternalServerError)
|
|
|
|
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusInternalServerError)
|
|
|
log.Error("Failed to authenticate by access token via password: %v", err)
|
|
log.Error("Failed to authenticate by access token via password: %v", err)
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } else if store.IsTwoFactorEnabled(c.Req.Context(), authUser.ID) {
|
|
|
|
|
|
|
+ } else if store.IsTwoFactorEnabled(c.Request().Context(), authUser.ID) {
|
|
|
askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password
|
|
askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password
|
|
|
Please create and use personal access token on user settings page`)
|
|
Please create and use personal access token on user settings page`)
|
|
|
return
|
|
return
|
|
@@ -160,7 +168,7 @@ Please create and use personal access token on user settings page`)
|
|
|
if isPull {
|
|
if isPull {
|
|
|
mode = database.AccessModeRead
|
|
mode = database.AccessModeRead
|
|
|
}
|
|
}
|
|
|
- if !database.Handle.Permissions().Authorize(c.Req.Context(), authUser.ID, repo.ID, mode,
|
|
|
|
|
|
|
+ if !database.Handle.Permissions().Authorize(c.Request().Context(), authUser.ID, repo.ID, mode,
|
|
|
database.AccessModeOptions{
|
|
database.AccessModeOptions{
|
|
|
OwnerID: repo.OwnerID,
|
|
OwnerID: repo.OwnerID,
|
|
|
Private: repo.IsPrivate,
|
|
Private: repo.IsPrivate,
|
|
@@ -171,7 +179,7 @@ Please create and use personal access token on user settings page`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if !isPull && repo.IsMirror {
|
|
if !isPull && repo.IsMirror {
|
|
|
- c.Error(http.StatusForbidden, "Mirror repository is read-only")
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusForbidden, "Mirror repository is read-only")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -388,7 +396,7 @@ func getGitRepoPath(dir string) (string, error) {
|
|
|
|
|
|
|
|
func HTTP(c *HTTPContext) {
|
|
func HTTP(c *HTTPContext) {
|
|
|
for _, route := range routes {
|
|
for _, route := range routes {
|
|
|
- reqPath := strings.ToLower(c.Req.URL.Path)
|
|
|
|
|
|
|
+ reqPath := strings.ToLower(c.Request().URL.Path)
|
|
|
m := route.re.FindStringSubmatch(reqPath)
|
|
m := route.re.FindStringSubmatch(reqPath)
|
|
|
if m == nil {
|
|
if m == nil {
|
|
|
continue
|
|
continue
|
|
@@ -398,19 +406,19 @@ func HTTP(c *HTTPContext) {
|
|
|
// but we only want to output this message only if user is really trying to access
|
|
// but we only want to output this message only if user is really trying to access
|
|
|
// Git HTTP endpoints.
|
|
// Git HTTP endpoints.
|
|
|
if conf.Repository.DisableHTTPGit {
|
|
if conf.Repository.DisableHTTPGit {
|
|
|
- c.Error(http.StatusForbidden, "Interacting with repositories by HTTP protocol is disabled")
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusForbidden, "Interacting with repositories by HTTP protocol is disabled")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if route.method != c.Req.Method {
|
|
|
|
|
- c.Error(http.StatusNotFound)
|
|
|
|
|
|
|
+ if route.method != c.Request().Method {
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusNotFound)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 🚨 SECURITY: Prevent path traversal.
|
|
// 🚨 SECURITY: Prevent path traversal.
|
|
|
cleaned := pathutil.Clean(m[1])
|
|
cleaned := pathutil.Clean(m[1])
|
|
|
if m[1] != "/"+cleaned {
|
|
if m[1] != "/"+cleaned {
|
|
|
- c.Error(http.StatusBadRequest, "Request path contains suspicious characters")
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusBadRequest, "Request path contains suspicious characters")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -418,13 +426,13 @@ func HTTP(c *HTTPContext) {
|
|
|
dir, err := getGitRepoPath(cleaned)
|
|
dir, err := getGitRepoPath(cleaned)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
log.Warn("HTTP.getGitRepoPath: %v", err)
|
|
log.Warn("HTTP.getGitRepoPath: %v", err)
|
|
|
- c.Error(http.StatusNotFound)
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusNotFound)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
route.handler(serviceHandler{
|
|
route.handler(serviceHandler{
|
|
|
w: c.Resp,
|
|
w: c.Resp,
|
|
|
- r: c.Req.Request,
|
|
|
|
|
|
|
+ r: c.Request().Request,
|
|
|
dir: dir,
|
|
dir: dir,
|
|
|
file: file,
|
|
file: file,
|
|
|
|
|
|
|
@@ -437,5 +445,5 @@ func HTTP(c *HTTPContext) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- c.Error(http.StatusNotFound)
|
|
|
|
|
|
|
+ writeError(c.ResponseWriter(), http.StatusNotFound)
|
|
|
}
|
|
}
|