Lindenii Project Forge
misc: Move url.go into the misc package
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "errors" "net/http" "path/filepath" "strconv" "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 // of its subgroups and repos, as well as a form for group maintainers to // create repos. func httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var groupPath []string var repos []nameDesc var subgroups []nameDesc var err error var groupID int var groupDesc string groupPath = params["group_path"].([]string) // The group itself err = database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( SELECT id, parent_group, name, 1 AS depth FROM groups WHERE name = ($1::text[])[1] AND parent_group IS NULL UNION ALL SELECT g.id, g.parent_group, g.name, group_path_cte.depth + 1 FROM groups g JOIN group_path_cte ON g.parent_group = group_path_cte.id WHERE g.name = ($1::text[])[group_path_cte.depth + 1] AND group_path_cte.depth + 1 <= cardinality($1::text[]) ) SELECT c.id, COALESCE(g.description, '') FROM group_path_cte c JOIN groups g ON g.id = c.id WHERE c.depth = cardinality($1::text[]) `, pgtype.FlatArray[string](groupPath), ).Scan(&groupID, &groupDesc) if errors.Is(err, pgx.ErrNoRows) { errorPage404(writer, params) return } else if err != nil { errorPage500(writer, params, "Error getting group: "+err.Error()) return } // ACL var count int err = database.QueryRow(request.Context(), ` SELECT COUNT(*) FROM user_group_roles WHERE user_id = $1 AND group_id = $2 `, params["user_id"].(int), groupID).Scan(&count) if err != nil { errorPage500(writer, params, "Error checking access: "+err.Error()) return } directAccess := (count > 0) if request.Method == http.MethodPost { if !directAccess { errorPage403(writer, params, "You do not have direct access to this group") return } repoName := request.FormValue("repo_name") repoDesc := request.FormValue("repo_desc") contribReq := request.FormValue("repo_contrib") if repoName == "" { errorPage400(writer, params, "Repo name is required") return } var newRepoID int err := database.QueryRow( request.Context(), `INSERT INTO repos (name, description, group_id, contrib_requirements) VALUES ($1, $2, $3, $4) RETURNING id`, repoName, repoDesc, groupID, contribReq, ).Scan(&newRepoID) if err != nil { errorPage500(writer, params, "Error creating repo: "+err.Error()) return } filePath := filepath.Join(config.Git.RepoDir, strconv.Itoa(newRepoID)+".git") _, err = database.Exec( request.Context(), `UPDATE repos SET filesystem_path = $1 WHERE id = $2`, filePath, newRepoID, ) if err != nil { errorPage500(writer, params, "Error updating repo path: "+err.Error()) return } if err = gitInit(filePath); err != nil { errorPage500(writer, params, "Error initializing repo: "+err.Error()) return }
redirectUnconditionally(writer, request)
misc.RedirectUnconditionally(writer, request)
return } // Repos var rows pgx.Rows rows, err = database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM repos WHERE group_id = $1 `, groupID) if err != nil { errorPage500(writer, params, "Error getting repos: "+err.Error()) return } defer rows.Close() for rows.Next() { var name, description string if err = rows.Scan(&name, &description); err != nil { errorPage500(writer, params, "Error getting repos: "+err.Error()) return } repos = append(repos, nameDesc{name, description}) } if err = rows.Err(); err != nil { errorPage500(writer, params, "Error getting repos: "+err.Error()) return } // Subgroups rows, err = database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM groups WHERE parent_group = $1 `, groupID) if err != nil { errorPage500(writer, params, "Error getting subgroups: "+err.Error()) return } defer rows.Close() for rows.Next() { var name, description string if err = rows.Scan(&name, &description); err != nil { errorPage500(writer, params, "Error getting subgroups: "+err.Error()) return } subgroups = append(subgroups, nameDesc{name, description}) } if err = rows.Err(); err != nil { errorPage500(writer, params, "Error getting subgroups: "+err.Error()) return } params["repos"] = repos params["subgroups"] = subgroups params["description"] = groupDesc params["direct_access"] = directAccess renderTemplate(writer, "group", params) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "fmt" "html/template" "net/http" "strings" "go.lindenii.runxiyu.org/forge/git2c"
"go.lindenii.runxiyu.org/forge/misc"
) // httpHandleRepoRaw serves raw files, or directory listings that point to raw // files. func httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "") client, err := git2c.NewClient(config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return } defer client.Close() files, content, err := client.Cmd2(repoPath, pathSpec) if err != nil { errorPage500(writer, params, err.Error()) return } switch { case files != nil: params["files"] = files params["readme_filename"] = "README.md" params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // 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") fmt.Fprint(writer, content) default: errorPage500(writer, params, "Unknown error fetching repo raw data") } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "errors" "log/slog" "net/http" "net/url" "strconv" "strings" "github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/misc"
) type forgeHTTPRouter struct{} // ServeHTTP handles all incoming HTTP requests and routes them to the correct // location. // // TODO: This function is way too large. func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var remoteAddr string if config.HTTP.ReverseProxy { remoteAddrs, ok := request.Header["X-Forwarded-For"] if ok && len(remoteAddrs) == 1 { remoteAddr = remoteAddrs[0] } else { remoteAddr = request.RemoteAddr } } else { remoteAddr = request.RemoteAddr } slog.Info("incoming http", "addr", remoteAddr, "method", request.Method, "uri", request.RequestURI) var segments []string var err error 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 } dirMode := false if segments[len(segments)-1] == "" { dirMode = true segments = segments[:len(segments)-1] } params["url_segments"] = segments params["dir_mode"] = dirMode params["global"] = globalData var userID int // 0 for none userID, params["username"], err = getUserFromRequest(request) params["user_id"] = userID if err != nil && !errors.Is(err, http.ErrNoCookie) && !errors.Is(err, pgx.ErrNoRows) { errorPage500(writer, params, "Error getting user info from request: "+err.Error()) return } if userID == 0 { params["user_id_string"] = "" } else { params["user_id_string"] = strconv.Itoa(userID) } for _, v := range segments { if strings.Contains(v, ":") { errorPage400Colon(writer, params) return } } if len(segments) == 0 { httpHandleIndex(writer, request, params) return } if segments[0] == "-" { 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 } switch segments[1] { case "static": staticHandler.ServeHTTP(writer, request) return case "source": sourceHandler.ServeHTTP(writer, request) return } } if segments[0] == "-" { switch segments[1] { case "login": httpHandleLogin(writer, request, params) return case "users": httpHandleUsers(writer, request, params) return default: errorPage404(writer, params) return } } sepIndex = -1 for i, part := range segments { if part == "-" { sepIndex = i break } } params["separator_index"] = sepIndex var groupPath []string var moduleType string var moduleName string if sepIndex > 0 { groupPath = segments[:sepIndex] } else { groupPath = segments } params["group_path"] = groupPath switch { case sepIndex == -1:
if redirectDir(writer, request) {
if misc.RedirectDir(writer, request) {
return } httpHandleGroupIndex(writer, request, params) case len(segments) == sepIndex+1: errorPage404(writer, params) return case len(segments) == sepIndex+2: errorPage404(writer, params) return default: moduleType = segments[sepIndex+1] moduleName = segments[sepIndex+2] switch moduleType { case "repos": params["repo_name"] = moduleName if len(segments) > sepIndex+3 { switch segments[sepIndex+3] { case "info": if err = httpHandleRepoInfo(writer, request, params); err != nil { errorPage500(writer, params, err.Error()) } return case "git-upload-pack": if err = httpHandleUploadPack(writer, request, params); err != nil { errorPage500(writer, params, err.Error()) } return } }
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()) return } } if params["repo"], params["repo_description"], params["repo_id"], _, err = openRepo(request.Context(), groupPath, moduleName); err != nil { errorPage500(writer, params, "Error opening repo: "+err.Error()) return } repoURLRoot := "/" for _, part := range segments[:sepIndex+3] { repoURLRoot = repoURLRoot + url.PathEscape(part) + "/" } params["repo_url_root"] = repoURLRoot params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + config.LMTP.Domain params["http_clone_url"] = genHTTPRemoteURL(groupPath, moduleName) 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) return } 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 } if dirMode { params["rest"] = strings.Join(segments[sepIndex+4:], "/") + "/" } 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 } if dirMode { params["rest"] = strings.Join(segments[sepIndex+4:], "/") + "/" } 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) case "log": if len(segments) > sepIndex+4 { errorPage400(writer, params, "Too many parameters") return }
if redirectDir(writer, request) {
if misc.RedirectDir(writer, request) {
return } httpHandleRepoLog(writer, request, params) case "commit": if len(segments) != sepIndex+5 { 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) { case sepIndex + 4: httpHandleRepoContribIndex(writer, request, params) case sepIndex + 5: params["mr_id"] = segments[sepIndex+4] httpHandleRepoContribOne(writer, request, params) default: errorPage400(writer, params, "Too many parameters") } default: errorPage404(writer, params) return } default: errorPage404(writer, params) return } } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // SPDX-FileCopyrightText: Copyright (c) 2024 Robin Jarry <robin@jarry.cc> package main import ( "bytes" "context" "errors" "fmt" "io" "log/slog" "net" "strings" "time" "github.com/emersion/go-message" "github.com/emersion/go-smtp"
"go.lindenii.runxiyu.org/forge/misc"
) type lmtpHandler struct{} type lmtpSession struct { from string to []string ctx context.Context cancel context.CancelFunc } func (session *lmtpSession) Reset() { session.from = "" session.to = nil } func (session *lmtpSession) Logout() error { session.cancel() return nil } func (session *lmtpSession) AuthPlain(_, _ string) error { return nil } func (session *lmtpSession) Mail(from string, _ *smtp.MailOptions) error { session.from = from return nil } func (session *lmtpSession) Rcpt(to string, _ *smtp.RcptOptions) error { session.to = append(session.to, to) return nil } func (*lmtpHandler) NewSession(_ *smtp.Conn) (smtp.Session, error) { ctx, cancel := context.WithCancel(context.Background()) session := &lmtpSession{ ctx: ctx, cancel: cancel, } return session, nil } func serveLMTP(listener net.Listener) error { smtpServer := smtp.NewServer(&lmtpHandler{}) smtpServer.LMTP = true smtpServer.Domain = config.LMTP.Domain smtpServer.Addr = config.LMTP.Socket smtpServer.WriteTimeout = time.Duration(config.LMTP.WriteTimeout) * time.Second smtpServer.ReadTimeout = time.Duration(config.LMTP.ReadTimeout) * time.Second smtpServer.EnableSMTPUTF8 = true return smtpServer.Serve(listener) } func (session *lmtpSession) Data(r io.Reader) error { var ( email *message.Entity from string to []string err error buf bytes.Buffer data []byte n int64 ) n, err = io.CopyN(&buf, r, config.LMTP.MaxSize) switch { case n == config.LMTP.MaxSize: err = errors.New("Message too big.") // drain whatever is left in the pipe _, _ = io.Copy(io.Discard, r) goto end case errors.Is(err, io.EOF): // message was smaller than max size break case err != nil: goto end } data = buf.Bytes() email, err = message.Read(bytes.NewReader(data)) if err != nil && message.IsUnknownCharset(err) { goto end } switch strings.ToLower(email.Header.Get("Auto-Submitted")) { case "auto-generated", "auto-replied": // Disregard automatic emails like OOO replies. slog.Info("ignoring automatic message", "from", session.from, "to", strings.Join(session.to, ","), "message-id", email.Header.Get("Message-Id"), "subject", email.Header.Get("Subject"), ) goto end } slog.Info("message received", "from", session.from, "to", strings.Join(session.to, ","), "message-id", email.Header.Get("Message-Id"), "subject", email.Header.Get("Subject"), ) // Make local copies of the values before to ensure the references will // still be valid when the task is run. from = session.from to = session.to _ = from for _, to := range to { if !strings.HasSuffix(to, "@"+config.LMTP.Domain) { continue } 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? err = fmt.Errorf("cannot parse path: %w", err) goto end } sepIndex := -1 for i, part := range segments { if part == "-" { sepIndex = i break } } if segments[len(segments)-1] == "" { segments = segments[:len(segments)-1] // We don't care about dir or not. } if sepIndex == -1 || len(segments) <= sepIndex+2 { err = errors.New("illegal path") goto end } mbox := bytes.Buffer{} if _, err = fmt.Fprint(&mbox, "From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001\r\n"); err != nil { slog.Error("error handling patch... malloc???", "error", err) goto end } data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) if _, err = mbox.Write(data); err != nil { slog.Error("error handling patch... malloc???", "error", err) goto end } // TODO: Is mbox's From escaping necessary here? groupPath := segments[:sepIndex] moduleType := segments[sepIndex+1] moduleName := segments[sepIndex+2] switch moduleType { case "repos": err = lmtpHandlePatch(session, groupPath, moduleName, &mbox) if err != nil { slog.Error("error handling patch", "error", err) goto end } default: err = errors.New("Emailing any endpoint other than repositories, is not supported yet.") // TODO goto end } } end: session.to = nil session.from = "" switch err { case nil: return nil default: return &smtp.SMTPError{ Code: 550, Message: "Permanent failure: " + err.Error(), EnhancedCode: [3]int{5, 7, 1}, } } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> 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. // 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)
}
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "context" "errors" "fmt" "io" "net/url" "go.lindenii.runxiyu.org/forge/ansiec"
"go.lindenii.runxiyu.org/forge/misc"
) var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") // getRepoInfo2 also fetches repo information... it should be deprecated and // implemented in individual handlers. func getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) { var segments []string var sepIndex int var moduleType, moduleName string
segments, err = pathToSegments(sshPath)
segments, err = misc.PathToSegments(sshPath)
if err != nil { return } for i, segment := range segments { var err error segments[i], err = url.PathUnescape(segment) if err != nil { return []string{}, "", 0, "", false, "", "", 0, err } } if segments[0] == "-" { return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath } sepIndex = -1 for i, part := range segments { if part == "-" { sepIndex = i break } } if segments[len(segments)-1] == "" { segments = segments[:len(segments)-1] } switch { case sepIndex == -1: return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath case len(segments) <= sepIndex+2: return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath } groupPath = segments[:sepIndex] moduleType = segments[sepIndex+1] moduleName = segments[sepIndex+2] repoName = moduleName switch moduleType { case "repos": _1, _2, _3, _4, _5, _6, _7 := getRepoInfo(ctx, groupPath, moduleName, sshPubkey) return groupPath, repoName, _1, _2, _3, _4, _5, _6, _7 default: return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath } } // writeRedError is a helper function that basically does a Fprintf but makes // the entire thing red, in terms of ANSI escape sequences. It's useful when // producing error messages on SSH connections. func writeRedError(w io.Writer, format string, args ...any) { fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
package misc
import ( "errors" "net/http" "net/url" "strings" ) var (
errDupRefSpec = errors.New("duplicate ref spec") errNoRefSpec = errors.New("no ref spec")
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) {
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
err = ErrDupRefSpec
return } done = true if len(refName) != 1 {
err = errDupRefSpec
err = ErrDupRefSpec
return } retRefName = refName[0] retRefType = refType } } if !done {
err = errNoRefSpec
err = ErrNoRefSpec
} return }
// parseReqURI parses an HTTP request URL, and returns a slice of path segments
// 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) {
func ParseReqURI(requestURI string) (segments []string, params url.Values, err error) {
path, paramsStr, _ := strings.Cut(requestURI, "?")
segments, err = pathToSegments(path)
segments, err = PathToSegments(path)
if err != nil { return } params, err = url.ParseQuery(paramsStr) return }
func pathToSegments(path string) (segments []string, err error) {
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
// 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 {
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
// 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 {
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
// RedirectUnconditionally unconditionally redirects the user back to the
// current page while preserving query parameters.
func redirectUnconditionally(writer http.ResponseWriter, request *http.Request) {
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.
// SegmentsToURL joins URL segments to the path component of a URL.
// Each segment is escaped properly first.
func segmentsToURL(segments []string) string {
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 {
// 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 }