From 54a19febc0c7c49caa014254cabab571abad60ab Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 5 Apr 2025 19:45:17 +0800 Subject: misc: Move url.go into the misc package --- http_handle_group_index.go | 3 +- http_handle_repo_raw.go | 3 +- http_server.go | 29 ++++----- lmtp_server.go | 3 +- misc/url.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ remote_url.go | 6 +- ssh_utils.go | 3 +- url.go | 154 --------------------------------------------- 8 files changed, 181 insertions(+), 174 deletions(-) create mode 100644 misc/url.go delete mode 100644 url.go diff --git a/http_handle_group_index.go b/http_handle_group_index.go index 67cffd8..568a38e 100644 --- a/http_handle_group_index.go +++ b/http_handle_group_index.go @@ -11,6 +11,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "go.lindenii.runxiyu.org/forge/misc" ) // httpHandleGroupIndex provides index pages for groups, which includes a list @@ -130,7 +131,7 @@ func httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, par return } - redirectUnconditionally(writer, request) + misc.RedirectUnconditionally(writer, request) return } diff --git a/http_handle_repo_raw.go b/http_handle_repo_raw.go index ea2925c..3a4e152 100644 --- a/http_handle_repo_raw.go +++ b/http_handle_repo_raw.go @@ -10,6 +10,7 @@ import ( "strings" "go.lindenii.runxiyu.org/forge/git2c" + "go.lindenii.runxiyu.org/forge/misc" ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw @@ -43,7 +44,7 @@ func httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params params["readme"] = template.HTML("

README rendering here is WIP again

") // TODO renderTemplate(writer, "repo_raw_dir", params) case content != "": - if redirectNoDir(writer, request) { + if misc.RedirectNoDir(writer, request) { return } writer.Header().Set("Content-Type", "application/octet-stream") diff --git a/http_server.go b/http_server.go index 5d45d00..3f8e36c 100644 --- a/http_server.go +++ b/http_server.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/jackc/pgx/v5" + "go.lindenii.runxiyu.org/forge/misc" ) type forgeHTTPRouter struct{} @@ -39,7 +40,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht var sepIndex int params := make(map[string]any) - if segments, _, err = parseReqURI(request.RequestURI); err != nil { + if segments, _, err = misc.ParseReqURI(request.RequestURI); err != nil { errorPage400(writer, params, "Error parsing request URI: "+err.Error()) return } @@ -82,7 +83,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht if len(segments) < 2 { errorPage404(writer, params) return - } else if len(segments) == 2 && redirectDir(writer, request) { + } else if len(segments) == 2 && misc.RedirectDir(writer, request) { return } @@ -133,7 +134,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht switch { case sepIndex == -1: - if redirectDir(writer, request) { + if misc.RedirectDir(writer, request) { return } httpHandleGroupIndex(writer, request, params) @@ -165,8 +166,8 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht } } - if params["ref_type"], params["ref_name"], err = getParamRefTypeName(request); err != nil { - if errors.Is(err, errNoRefSpec) { + if params["ref_type"], params["ref_name"], err = misc.GetParamRefTypeName(request); err != nil { + if errors.Is(err, misc.ErrNoRefSpec) { params["ref_type"] = "" } else { errorPage400(writer, params, "Error querying ref type: "+err.Error()) @@ -189,7 +190,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht params["ssh_clone_url"] = genSSHRemoteURL(groupPath, moduleName) if len(segments) == sepIndex+3 { - if redirectDir(writer, request) { + if misc.RedirectDir(writer, request) { return } httpHandleRepoIndex(writer, request, params) @@ -199,7 +200,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht repoFeature := segments[sepIndex+3] switch repoFeature { case "tree": - if anyContain(segments[sepIndex+4:], "/") { + if misc.AnyContain(segments[sepIndex+4:], "/") { errorPage400(writer, params, "Repo tree paths may not contain slashes in any segments") return } @@ -208,18 +209,18 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht } else { params["rest"] = strings.Join(segments[sepIndex+4:], "/") } - if len(segments) < sepIndex+5 && redirectDir(writer, request) { + if len(segments) < sepIndex+5 && misc.RedirectDir(writer, request) { return } httpHandleRepoTree(writer, request, params) case "branches": - if redirectDir(writer, request) { + if misc.RedirectDir(writer, request) { return } httpHandleRepoBranches(writer, request, params) return case "raw": - if anyContain(segments[sepIndex+4:], "/") { + if misc.AnyContain(segments[sepIndex+4:], "/") { errorPage400(writer, params, "Repo tree paths may not contain slashes in any segments") return } @@ -228,7 +229,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht } else { params["rest"] = strings.Join(segments[sepIndex+4:], "/") } - if len(segments) < sepIndex+5 && redirectDir(writer, request) { + if len(segments) < sepIndex+5 && misc.RedirectDir(writer, request) { return } httpHandleRepoRaw(writer, request, params) @@ -237,7 +238,7 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht errorPage400(writer, params, "Too many parameters") return } - if redirectDir(writer, request) { + if misc.RedirectDir(writer, request) { return } httpHandleRepoLog(writer, request, params) @@ -246,13 +247,13 @@ func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *ht errorPage400(writer, params, "Incorrect number of parameters") return } - if redirectNoDir(writer, request) { + if misc.RedirectNoDir(writer, request) { return } params["commit_id"] = segments[sepIndex+4] httpHandleRepoCommit(writer, request, params) case "contrib": - if redirectDir(writer, request) { + if misc.RedirectDir(writer, request) { return } switch len(segments) { diff --git a/lmtp_server.go b/lmtp_server.go index cdfcffb..fc3d92d 100644 --- a/lmtp_server.go +++ b/lmtp_server.go @@ -17,6 +17,7 @@ import ( "github.com/emersion/go-message" "github.com/emersion/go-smtp" + "go.lindenii.runxiyu.org/forge/misc" ) type lmtpHandler struct{} @@ -136,7 +137,7 @@ func (session *lmtpSession) Data(r io.Reader) error { } localPart := to[:len(to)-len("@"+config.LMTP.Domain)] var segments []string - segments, err = pathToSegments(localPart) + segments, err = misc.PathToSegments(localPart) if err != nil { // TODO: Should the entire email fail or should we just // notify them out of band? diff --git a/misc/url.go b/misc/url.go new file mode 100644 index 0000000..b77d8ce --- /dev/null +++ b/misc/url.go @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +package misc + +import ( + "errors" + "net/http" + "net/url" + "strings" +) + +var ( + ErrDupRefSpec = errors.New("duplicate ref spec") + ErrNoRefSpec = errors.New("no ref spec") +) + +// getParamRefTypeName looks at the query parameters in an HTTP request and +// returns its ref name and type, if any. +func GetParamRefTypeName(request *http.Request) (retRefType, retRefName string, err error) { + rawQuery := request.URL.RawQuery + queryValues, err := url.ParseQuery(rawQuery) + if err != nil { + return + } + done := false + for _, refType := range []string{"commit", "branch", "tag"} { + refName, ok := queryValues[refType] + if ok { + if done { + err = ErrDupRefSpec + return + } + done = true + if len(refName) != 1 { + err = ErrDupRefSpec + return + } + retRefName = refName[0] + retRefType = refType + } + } + if !done { + err = ErrNoRefSpec + } + return +} + +// ParseReqURI parses an HTTP request URL, and returns a slice of path segments +// and the query parameters. It handles %2F correctly. +func ParseReqURI(requestURI string) (segments []string, params url.Values, err error) { + path, paramsStr, _ := strings.Cut(requestURI, "?") + + segments, err = PathToSegments(path) + if err != nil { + return + } + + params, err = url.ParseQuery(paramsStr) + return +} + +func PathToSegments(path string) (segments []string, err error) { + segments = strings.Split(strings.TrimPrefix(path, "/"), "/") + + for i, segment := range segments { + segments[i], err = url.PathUnescape(segment) + if err != nil { + return + } + } + + return +} + +// RedirectDir returns true and redirects the user to a version of the URL with +// a trailing slash, if and only if the request URL does not already have a +// trailing slash. +func RedirectDir(writer http.ResponseWriter, request *http.Request) bool { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + if !strings.HasSuffix(path, "/") { + http.Redirect(writer, request, path+"/"+rest, http.StatusSeeOther) + return true + } + return false +} + +// RedirectNoDir returns true and redirects the user to a version of the URL +// without a trailing slash, if and only if the request URL has a trailing +// slash. +func RedirectNoDir(writer http.ResponseWriter, request *http.Request) bool { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + if strings.HasSuffix(path, "/") { + http.Redirect(writer, request, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther) + return true + } + return false +} + +// RedirectUnconditionally unconditionally redirects the user back to the +// current page while preserving query parameters. +func RedirectUnconditionally(writer http.ResponseWriter, request *http.Request) { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + http.Redirect(writer, request, path+rest, http.StatusSeeOther) +} + +// SegmentsToURL joins URL segments to the path component of a URL. +// Each segment is escaped properly first. +func SegmentsToURL(segments []string) string { + for i, segment := range segments { + segments[i] = url.PathEscape(segment) + } + return strings.Join(segments, "/") +} + +// AnyContain returns true if and only if ss contains a string that contains c. +func AnyContain(ss []string, c string) bool { + for _, s := range ss { + if strings.Contains(s, c) { + return true + } + } + return false +} diff --git a/remote_url.go b/remote_url.go index 5c980f5..f227dbf 100644 --- a/remote_url.go +++ b/remote_url.go @@ -6,6 +6,8 @@ package main import ( "net/url" "strings" + + "go.lindenii.runxiyu.org/forge/misc" ) // We don't use path.Join because it collapses multiple slashes into one. @@ -13,11 +15,11 @@ import ( // genSSHRemoteURL generates SSH remote URLs from a given group path and repo // name. func genSSHRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(config.SSH.Root, "/") + "/" + segmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) + return strings.TrimSuffix(config.SSH.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } // genHTTPRemoteURL generates HTTP remote URLs from a given group path and repo // name. func genHTTPRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + segmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) + return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } diff --git a/ssh_utils.go b/ssh_utils.go index 476fc31..c906ab3 100644 --- a/ssh_utils.go +++ b/ssh_utils.go @@ -11,6 +11,7 @@ import ( "net/url" "go.lindenii.runxiyu.org/forge/ansiec" + "go.lindenii.runxiyu.org/forge/misc" ) var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") @@ -22,7 +23,7 @@ func getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []s var sepIndex int var moduleType, moduleName string - segments, err = pathToSegments(sshPath) + segments, err = misc.PathToSegments(sshPath) if err != nil { return } diff --git a/url.go b/url.go deleted file mode 100644 index ad5c8bb..0000000 --- a/url.go +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package main - -import ( - "errors" - "net/http" - "net/url" - "strings" -) - -var ( - errDupRefSpec = errors.New("duplicate ref spec") - errNoRefSpec = errors.New("no ref spec") -) - -// getParamRefTypeName looks at the query parameters in an HTTP request and -// returns its ref name and type, if any. -func getParamRefTypeName(request *http.Request) (retRefType, retRefName string, err error) { - rawQuery := request.URL.RawQuery - queryValues, err := url.ParseQuery(rawQuery) - if err != nil { - return - } - done := false - for _, refType := range []string{"commit", "branch", "tag"} { - refName, ok := queryValues[refType] - if ok { - if done { - err = errDupRefSpec - return - } - done = true - if len(refName) != 1 { - err = errDupRefSpec - return - } - retRefName = refName[0] - retRefType = refType - } - } - if !done { - err = errNoRefSpec - } - return -} - -// parseReqURI parses an HTTP request URL, and returns a slice of path segments -// and the query parameters. It handles %2F correctly. -func parseReqURI(requestURI string) (segments []string, params url.Values, err error) { - path, paramsStr, _ := strings.Cut(requestURI, "?") - - segments, err = pathToSegments(path) - if err != nil { - return - } - - params, err = url.ParseQuery(paramsStr) - return -} - -func pathToSegments(path string) (segments []string, err error) { - segments = strings.Split(strings.TrimPrefix(path, "/"), "/") - - for i, segment := range segments { - segments[i], err = url.PathUnescape(segment) - if err != nil { - return - } - } - - return -} - -// redirectDir returns true and redirects the user to a version of the URL with -// a trailing slash, if and only if the request URL does not already have a -// trailing slash. -func redirectDir(writer http.ResponseWriter, request *http.Request) bool { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - if !strings.HasSuffix(path, "/") { - http.Redirect(writer, request, path+"/"+rest, http.StatusSeeOther) - return true - } - return false -} - -// redirectNoDir returns true and redirects the user to a version of the URL -// without a trailing slash, if and only if the request URL has a trailing -// slash. -func redirectNoDir(writer http.ResponseWriter, request *http.Request) bool { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - if strings.HasSuffix(path, "/") { - http.Redirect(writer, request, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther) - return true - } - return false -} - -// redirectUnconditionally unconditionally redirects the user back to the -// current page while preserving query parameters. -func redirectUnconditionally(writer http.ResponseWriter, request *http.Request) { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - http.Redirect(writer, request, path+rest, http.StatusSeeOther) -} - -// segmentsToURL joins URL segments to the path component of a URL. -// Each segment is escaped properly first. -func segmentsToURL(segments []string) string { - for i, segment := range segments { - segments[i] = url.PathEscape(segment) - } - return strings.Join(segments, "/") -} - -// anyContain returns true if and only if ss contains a string that contains c. -func anyContain(ss []string, c string) bool { - for _, s := range ss { - if strings.Contains(s, c) { - return true - } - } - return false -} -- cgit v1.2.3