mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
111 lines
4.8 KiB
Go
111 lines
4.8 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage.
|
|
package private
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/private"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/web"
|
|
"github.com/hanzoai/git/routers/common"
|
|
"github.com/hanzoai/git/routers/web/misc"
|
|
"github.com/hanzoai/git/services/context"
|
|
|
|
"github.com/hanzoai/git/modules/binding"
|
|
)
|
|
|
|
func authInternal(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if setting.InternalToken == "" {
|
|
log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
tokens := req.Header.Get("X-Internal-Auth") // TODO: use something like JWT or HMAC to avoid passing the token in the clear
|
|
after, found := strings.CutPrefix(tokens, "Bearer ")
|
|
authSucceeded := found && subtle.ConstantTimeCompare([]byte(after), []byte(setting.InternalToken)) == 1
|
|
if !authSucceeded {
|
|
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// bind binding an obj to a handler
|
|
func bind[T any](_ T) any {
|
|
return func(ctx *context.PrivateContext) {
|
|
theObj := new(T) // create a new form obj for every request but not use obj directly
|
|
binding.Bind(ctx.Req, theObj)
|
|
web.SetForm(ctx, theObj)
|
|
}
|
|
}
|
|
|
|
// setRealIP sets RemoteAddr from the trusted X-Real-IP header set by the internal API
|
|
// client (see modules/private.NewInternalRequest); the internal API is gated by InternalToken.
|
|
// It replaces chi's deprecated middleware.RealIP, which is unsafe on public-facing endpoints.
|
|
func setRealIP(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if ip := req.Header.Get("X-Real-IP"); net.ParseIP(ip) != nil {
|
|
req.RemoteAddr = ip
|
|
}
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// Routes registers all internal APIs routes to web application.
|
|
// These APIs will be invoked by internal commands for example `gitd serv` and etc.
|
|
func Routes() *web.Router {
|
|
r := web.NewRouter()
|
|
r.AfterRouting(context.PrivateContexter())
|
|
r.AfterRouting(authInternal)
|
|
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
|
|
// Since internal API will be sent only from the forge's sub commands and it's under control (checked by InternalToken), we can trust the headers.
|
|
r.AfterRouting(setRealIP)
|
|
|
|
r.Get("/dummy", misc.DummyOK)
|
|
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
|
|
r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
|
|
r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
|
|
r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive)
|
|
r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive)
|
|
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
|
|
r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
|
|
r.Get("/serv/none/{keyid}", ServNoCommand)
|
|
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
|
|
r.Post("/manager/shutdown", Shutdown)
|
|
r.Post("/manager/restart", Restart)
|
|
r.Post("/manager/reload-templates", ReloadTemplates)
|
|
r.Post("/manager/flush-queues", bind(private.FlushOptions{}), FlushQueues)
|
|
r.Post("/manager/pause-logging", PauseLogging)
|
|
r.Post("/manager/resume-logging", ResumeLogging)
|
|
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
|
|
r.Post("/manager/set-log-sql", SetLogSQL)
|
|
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
|
|
r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger)
|
|
r.Get("/manager/processes", Processes)
|
|
r.Post("/mail/send", SendEmail)
|
|
r.Post("/restore_repo", RestoreRepo)
|
|
r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken)
|
|
|
|
r.Group("/repo", func() {
|
|
// FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext
|
|
// Fortunately, the LFS handlers are able to handle requests without a complete web context
|
|
common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
|
|
webContext := &context.Context{Base: ctx.Base} // see above, it shouldn't manually construct the web context
|
|
ctx.SetContextValue(context.WebContextKey, webContext) // FIXME: this is not ideal but no other way at the moment
|
|
})
|
|
})
|
|
|
|
return r
|
|
}
|