sourcegraph/cmd/frontend/internal/httpapi/httpapi.go

324 lines
12 KiB
Go

package httpapi
import (
"context"
"log"
"net/http"
"os"
"reflect"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/graph-gophers/graphql-go"
sglog "github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
"github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise"
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/updatecheck"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/handlerutil"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/httpapi/releasecache"
apirouter "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/httpapi/router"
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/httpapi/webhookhandlers"
frontendsearch "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/search"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api"
"github.com/sourcegraph/sourcegraph/cmd/frontend/webhooks"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/encryption/keyring"
"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/search"
"github.com/sourcegraph/sourcegraph/internal/search/searchcontexts"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type Handlers struct {
// Repo sync
GitHubSyncWebhook webhooks.Registerer
GitLabSyncWebhook webhooks.Registerer
BitbucketServerSyncWebhook webhooks.Registerer
BitbucketCloudSyncWebhook webhooks.Registerer
// Permissions
PermissionsGitHubWebhook webhooks.Registerer
// Batch changes
BatchesGitHubWebhook webhooks.Registerer
BatchesGitLabWebhook webhooks.RegistererHandler
BatchesBitbucketServerWebhook webhooks.RegistererHandler
BatchesBitbucketCloudWebhook webhooks.RegistererHandler
BatchesChangesFileGetHandler http.Handler
BatchesChangesFileExistsHandler http.Handler
BatchesChangesFileUploadHandler http.Handler
// Code intel
NewCodeIntelUploadHandler enterprise.NewCodeIntelUploadHandler
// Compute
NewComputeStreamHandler enterprise.NewComputeStreamHandler
// Code Insights
CodeInsightsDataExportHandler http.Handler
}
// NewHandler returns a new API handler that uses the provided API
// router, which must have been created by httpapi/router.New, or
// creates a new one if nil.
//
// 🚨 SECURITY: The caller MUST wrap the returned handler in middleware that checks authentication
// and sets the actor in the request context.
func NewHandler(
db database.DB,
m *mux.Router,
schema *graphql.Schema,
rateLimiter graphqlbackend.LimitWatcher,
handlers *Handlers,
) http.Handler {
logger := sglog.Scoped("Handler", "frontend HTTP API handler")
if m == nil {
m = apirouter.New(nil)
}
m.StrictSlash(true)
handler := jsonMiddleware(&errorHandler{
Logger: logger,
// Only display error message to admins when in debug mode, since it
// may contain sensitive info (like API keys in net/http error
// messages).
WriteErrBody: env.InsecureDev,
})
// Set handlers for the installed routes.
m.Get(apirouter.RepoShield).Handler(trace.Route(handler(serveRepoShield())))
m.Get(apirouter.RepoRefresh).Handler(trace.Route(handler(serveRepoRefresh(db))))
webhookMiddleware := webhooks.NewLogMiddleware(
db.WebhookLogs(keyring.Default().WebhookLogKey),
)
wh := webhooks.Router{
Logger: logger.Scoped("webhooks.Router", "handling webhook requests and dispatching them to handlers"),
DB: db,
}
webhookhandlers.Init(&wh)
handlers.BatchesGitHubWebhook.Register(&wh)
handlers.BatchesGitLabWebhook.Register(&wh)
handlers.BitbucketServerSyncWebhook.Register(&wh)
handlers.BitbucketCloudSyncWebhook.Register(&wh)
handlers.BatchesBitbucketServerWebhook.Register(&wh)
handlers.BatchesBitbucketCloudWebhook.Register(&wh)
handlers.GitHubSyncWebhook.Register(&wh)
handlers.GitLabSyncWebhook.Register(&wh)
handlers.PermissionsGitHubWebhook.Register(&wh)
// 🚨 SECURITY: This handler implements its own secret-based auth
webhookHandler := webhooks.NewHandler(logger, db, &wh)
gitHubWebhook := webhooks.GitHubWebhook{Router: &wh}
// New UUID based webhook handler
m.Get(apirouter.Webhooks).Handler(trace.Route(webhookMiddleware.Logger(webhookHandler)))
// Old, soon to be deprecated, webhook handlers
m.Get(apirouter.GitHubWebhooks).Handler(trace.Route(webhookMiddleware.Logger(&gitHubWebhook)))
m.Get(apirouter.GitLabWebhooks).Handler(trace.Route(webhookMiddleware.Logger(handlers.BatchesGitLabWebhook)))
m.Get(apirouter.BitbucketServerWebhooks).Handler(trace.Route(webhookMiddleware.Logger(handlers.BatchesBitbucketServerWebhook)))
m.Get(apirouter.BitbucketCloudWebhooks).Handler(trace.Route(webhookMiddleware.Logger(handlers.BatchesBitbucketCloudWebhook)))
m.Get(apirouter.BatchesFileGet).Handler(trace.Route(handlers.BatchesChangesFileGetHandler))
m.Get(apirouter.BatchesFileExists).Handler(trace.Route(handlers.BatchesChangesFileExistsHandler))
m.Get(apirouter.BatchesFileUpload).Handler(trace.Route(handlers.BatchesChangesFileUploadHandler))
m.Get(apirouter.LSIFUpload).Handler(trace.Route(handlers.NewCodeIntelUploadHandler(true)))
m.Get(apirouter.SCIPUpload).Handler(trace.Route(handlers.NewCodeIntelUploadHandler(true)))
m.Get(apirouter.SCIPUploadExists).Handler(trace.Route(noopHandler))
m.Get(apirouter.ComputeStream).Handler(trace.Route(handlers.NewComputeStreamHandler()))
m.Get(apirouter.CodeInsightsDataExport).Handler(trace.Route(handlers.CodeInsightsDataExportHandler))
if envvar.SourcegraphDotComMode() {
m.Path("/updates").Methods("GET", "POST").Name("updatecheck").Handler(trace.Route(http.HandlerFunc(updatecheck.HandlerWithLog(logger))))
}
m.Get(apirouter.GraphQL).Handler(trace.Route(handler(serveGraphQL(logger, schema, rateLimiter, false))))
m.Get(apirouter.SearchStream).Handler(trace.Route(frontendsearch.StreamHandler(db)))
// Return the minimum src-cli version that's compatible with this instance
m.Get(apirouter.SrcCli).Handler(trace.Route(newSrcCliVersionHandler(logger)))
gsClient := gitserver.NewClient()
m.Get(apirouter.GitBlameStream).Handler(trace.Route(handleStreamBlame(logger, db, gsClient)))
// Set up the src-cli version cache handler (this will effectively be a
// no-op anywhere other than dot-com).
m.Get(apirouter.SrcCliVersionCache).Handler(trace.Route(releasecache.NewHandler(logger)))
m.Get(apirouter.Registry).Handler(trace.Route(handler(registry.HandleRegistry(db))))
m.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("API no route: %s %s from %s", r.Method, r.URL, r.Referer())
http.Error(w, "no route", http.StatusNotFound)
})
return m
}
// NewInternalHandler returns a new API handler for internal endpoints that uses
// the provided API router, which must have been created by httpapi/router.NewInternal.
//
// 🚨 SECURITY: This handler should not be served on a publicly exposed port. 🚨
// This handler is not guaranteed to provide the same authorization checks as
// public API handlers.
func NewInternalHandler(
m *mux.Router,
db database.DB,
schema *graphql.Schema,
newCodeIntelUploadHandler enterprise.NewCodeIntelUploadHandler,
rankingService enterprise.RankingService,
newComputeStreamHandler enterprise.NewComputeStreamHandler,
rateLimitWatcher graphqlbackend.LimitWatcher,
) http.Handler {
logger := sglog.Scoped("InternalHandler", "frontend internal HTTP API handler")
if m == nil {
m = apirouter.New(nil)
}
m.StrictSlash(true)
handler := jsonMiddleware(&errorHandler{
Logger: logger,
// Internal endpoints can expose sensitive errors
WriteErrBody: true,
})
m.Get(apirouter.ExternalServiceConfigs).Handler(trace.Route(handler(serveExternalServiceConfigs(db))))
// zoekt-indexserver endpoints
gsClient := gitserver.NewClient()
indexer := &searchIndexerServer{
db: db,
logger: logger.Scoped("searchIndexerServer", "zoekt-indexserver endpoints"),
gitserverClient: gsClient,
ListIndexable: backend.NewRepos(logger, db, gsClient).ListIndexable,
RepoStore: db.Repos(),
SearchContextsRepoRevs: func(ctx context.Context, repoIDs []api.RepoID) (map[api.RepoID][]string, error) {
return searchcontexts.RepoRevs(ctx, db, repoIDs)
},
Indexers: search.Indexers(),
Ranking: rankingService,
MinLastChangedDisabled: os.Getenv("SRC_SEARCH_INDEXER_EFFICIENT_POLLING_DISABLED") != "",
}
m.Get(apirouter.SearchConfiguration).Handler(trace.Route(handler(indexer.serveConfiguration)))
m.Get(apirouter.ReposIndex).Handler(trace.Route(handler(indexer.serveList)))
m.Get(apirouter.RepoRank).Handler(trace.Route(handler(indexer.serveRepoRank)))
m.Get(apirouter.DocumentRanks).Handler(trace.Route(handler(indexer.serveDocumentRanks)))
m.Get(apirouter.UpdateIndexStatus).Handler(trace.Route(handler(indexer.handleIndexStatusUpdate)))
m.Get(apirouter.ExternalURL).Handler(trace.Route(handler(serveExternalURL)))
m.Get(apirouter.SendEmail).Handler(trace.Route(handler(serveSendEmail)))
gitService := &gitServiceHandler{
Gitserver: gsClient,
}
m.Get(apirouter.GitInfoRefs).Handler(trace.Route(handler(gitService.serveInfoRefs())))
m.Get(apirouter.GitUploadPack).Handler(trace.Route(handler(gitService.serveGitUploadPack())))
m.Get(apirouter.Telemetry).Handler(trace.Route(telemetryHandler(db)))
m.Get(apirouter.GraphQL).Handler(trace.Route(handler(serveGraphQL(logger, schema, rateLimitWatcher, true))))
m.Get(apirouter.Configuration).Handler(trace.Route(handler(serveConfiguration)))
m.Path("/ping").Methods("GET").Name("ping").HandlerFunc(handlePing)
m.Get(apirouter.StreamingSearch).Handler(trace.Route(frontendsearch.StreamHandler(db)))
m.Get(apirouter.ComputeStream).Handler(trace.Route(newComputeStreamHandler()))
m.Get(apirouter.LSIFUpload).Handler(trace.Route(newCodeIntelUploadHandler(false)))
m.Get(apirouter.SCIPUpload).Handler(trace.Route(newCodeIntelUploadHandler(false)))
m.Get(apirouter.SCIPUploadExists).Handler(trace.Route(noopHandler))
m.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("API no route: %s %s from %s", r.Method, r.URL, r.Referer())
http.Error(w, "no route", http.StatusNotFound)
})
return m
}
var schemaDecoder = schema.NewDecoder()
func init() {
schemaDecoder.IgnoreUnknownKeys(true)
// Register a converter for unix timestamp strings -> time.Time values
// (needed for Appdash PageLoadEvent type).
schemaDecoder.RegisterConverter(time.Time{}, func(s string) reflect.Value {
ms, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return reflect.ValueOf(err)
}
return reflect.ValueOf(time.Unix(0, ms*int64(time.Millisecond)))
})
}
type errorHandler struct {
// Logger is required
Logger sglog.Logger
WriteErrBody bool
}
func (h *errorHandler) Handle(w http.ResponseWriter, r *http.Request, status int, err error) {
logger := trace.Logger(r.Context(), h.Logger)
trace.SetRequestErrorCause(r.Context(), err)
// Handle custom errors
var e *handlerutil.URLMovedError
if errors.As(err, &e) {
err := handlerutil.RedirectToNewRepoName(w, r, e.NewRepo)
if err != nil {
logger.Error("error redirecting to new URI",
sglog.Error(err),
sglog.String("new_url", string(e.NewRepo)))
}
return
}
// Never cache error responses.
w.Header().Set("cache-control", "no-cache, max-age=0")
errBody := err.Error()
var displayErrBody string
if h.WriteErrBody {
displayErrBody = errBody
}
http.Error(w, displayErrBody, status)
if status < 200 || status >= 500 {
logger.Error("API HTTP handler error response",
sglog.String("method", r.Method),
sglog.String("request_uri", r.URL.RequestURI()),
sglog.Int("status_code", status),
sglog.Error(err))
}
}
func jsonMiddleware(errorHandler *errorHandler) func(func(http.ResponseWriter, *http.Request) error) http.Handler {
return func(h func(http.ResponseWriter, *http.Request) error) http.Handler {
return handlerutil.HandlerWithErrorReturn{
Handler: func(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "application/json")
return h(w, r)
},
Error: errorHandler.Handle,
}
}
}
var noopHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})