Ctrl K
ComunidadGuías…

coderd/httpapi/httpapi.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package httpapi

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"time"

	"github.com/go-playground/validator/v10"
	"golang.org/x/xerrors"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
	"github.com/coder/coder/v2/coderd/tracing"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/quartz"
	"github.com/coder/websocket"
	"github.com/coder/websocket/wsjson"
)

var Validate *validator.Validate

// This init is used to create a validator and register validation-specific
// functionality for the HTTP API.
//
// A single validator instance is used, because it caches struct parsing.
func init() {
	Validate = validator.New()
	Validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
		name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
		if name == "-" {
			return ""
		}
		return name
	})

	nameValidator := func(fl validator.FieldLevel) bool {
		f := fl.Field().Interface()
		str, ok := f.(string)
		if !ok {
			return false
		}
		valid := codersdk.NameValid(str)
		return valid == nil
	}
	for _, tag := range []string{"username", "organization_name", "template_name", "workspace_name"} {
		err := Validate.RegisterValidation(tag, nameValidator)
		if err != nil {
			panic(err)
		}
	}

	oauth2AppNameValidator := func(fl validator.FieldLevel) bool {
		str, ok := fl.Field().Interface().(string)
		if !ok {
			return false
		}
		return codersdk.OAuth2AppNameValid(str) == nil
	}
	err := Validate.RegisterValidation("oauth2_app_name", oauth2AppNameValidator)
	if err != nil {
		panic(err)
	}

	displayNameValidator := func(fl validator.FieldLevel) bool {
		f := fl.Field().Interface()
		str, ok := f.(string)
		if !ok {
			return false
		}
		valid := codersdk.DisplayNameValid(str)
		return valid == nil
	}
	for _, displayNameTag := range []string{"organization_display_name", "template_display_name", "group_display_name"} {
		err := Validate.RegisterValidation(displayNameTag, displayNameValidator)
		if err != nil {
			panic(err)
		}
	}

	templateVersionNameValidator := func(fl validator.FieldLevel) bool {
		f := fl.Field().Interface()
		str, ok := f.(string)
		if !ok {
			return false
		}
		valid := codersdk.TemplateVersionNameValid(str)
		return valid == nil
	}
	err = Validate.RegisterValidation("template_version_name", templateVersionNameValidator)
	if err != nil {
		panic(err)
	}

	userRealNameValidator := func(fl validator.FieldLevel) bool {
		f := fl.Field().Interface()
		str, ok := f.(string)
		if !ok {
			return false
		}
		valid := codersdk.UserRealNameValid(str)
		return valid == nil
	}
	err = Validate.RegisterValidation("user_real_name", userRealNameValidator)
	if err != nil {
		panic(err)
	}

	groupNameValidator := func(fl validator.FieldLevel) bool {
		f := fl.Field().Interface()
		str, ok := f.(string)
		if !ok {
			return false
		}
		valid := codersdk.GroupNameValid(str)
		return valid == nil
	}
	err = Validate.RegisterValidation("group_name", groupNameValidator)
	if err != nil {
		panic(err)
	}
}

// Is404Error returns true if the given error should return a 404 status code.
// Both actual 404s and unauthorized errors should return 404s to not leak
// information about the existence of resources.
func Is404Error(err error) bool {
	if err == nil {
		return false
	}

	// This tests for dbauthz.IsNotAuthorizedError and rbac.IsUnauthorizedError.
	if IsUnauthorizedError(err) {
		return true
	}
	return xerrors.Is(err, sql.ErrNoRows)
}

func IsUnauthorizedError(err error) bool {
	if err == nil {
		return false
	}

	// This tests for dbauthz.IsNotAuthorizedError and rbac.IsUnauthorizedError.
	var unauthorized httpapiconstraints.IsUnauthorizedError
	if errors.As(err, &unauthorized) && unauthorized.IsUnauthorized() {
		return true
	}
	return false
}

// Convenience error functions don't take contexts since their responses are
// static, it doesn't make much sense to trace them.

var ResourceNotFoundResponse = codersdk.Response{Message: "Resource not found or you do not have access to this resource"}

// ResourceNotFound is intentionally vague. All 404 responses should be identical
// to prevent leaking existence of resources.
func ResourceNotFound(rw http.ResponseWriter) {
	Write(context.Background(), rw, http.StatusNotFound, ResourceNotFoundResponse)
}

var ResourceForbiddenResponse = codersdk.Response{
	Message: "Forbidden.",
	Detail:  "You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.",
}

func Forbidden(rw http.ResponseWriter) {
	Write(context.Background(), rw, http.StatusForbidden, ResourceForbiddenResponse)
}

func InternalServerError(rw http.ResponseWriter, err error) {
	var details string
	if err != nil {
		details = err.Error()
	}

	Write(context.Background(), rw, http.StatusInternalServerError, codersdk.Response{
		Message: "An internal server error occurred.",
		Detail:  details,
	})
}

func RouteNotFound(rw http.ResponseWriter) {
	Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{
		Message: "Route not found.",
	})
}

// Write outputs a standardized format to an HTTP response body. ctx is used for
// tracing and can be nil for tracing to be disabled. Tracing this function is
// helpful because JSON marshaling can sometimes take a non-insignificant amount
// of time, and could help us catch outliers. Additionally, we can enrich span
// data a bit more since we have access to the actual interface{} we're
// marshaling, such as the number of elements in an array, which could help us
// spot routes that need to be paginated.
func Write(ctx context.Context, rw http.ResponseWriter, status int, response interface{}) {
	// Pretty up JSON when testing.
	if flag.Lookup("test.v") != nil {
		WriteIndent(ctx, rw, status, response)
		return
	}

	_, span := tracing.StartSpan(ctx)
	defer span.End()

	SetAuthzCheckRecorderHeader(ctx, rw)

	rw.Header().Set("Content-Type", "application/json; charset=utf-8")
	rw.WriteHeader(status)

	enc := json.NewEncoder(rw)
	enc.SetEscapeHTML(true)

	// We can't really do much about these errors, it's probably due to a
	// dropped connection.
	_ = enc.Encode(response)
}

func WriteIndent(ctx context.Context, rw http.ResponseWriter, status int, response interface{}) {
	_, span := tracing.StartSpan(ctx)
	defer span.End()

	SetAuthzCheckRecorderHeader(ctx, rw)

	rw.Header().Set("Content-Type", "application/json; charset=utf-8")
	rw.WriteHeader(status)

	enc := json.NewEncoder(rw)
	enc.SetEscapeHTML(true)
	enc.SetIndent("", "\t")

	// We can't really do much about these errors, it's probably due to a
	// dropped connection.
	_ = enc.Encode(response)
}

// DefaultMaxRequestBodyBytes bounds the request body that a JSON endpoint will
// decode. It exists so that a single request, including an unauthenticated one,
// cannot exhaust server memory with an oversized body. Endpoints that need a
// different limit must call ReadLimit rather than change this constant.
const DefaultMaxRequestBodyBytes = 4 << 20 // 4 MiB

// Read decodes JSON from the HTTP request into the value provided, reading at
// most DefaultMaxRequestBodyBytes from the body. It uses go-validator to
// validate the incoming request body. ctx is used for tracing and can be nil.
// Although tracing this function isn't likely too helpful, it was done to be
// consistent with Write.
func Read(ctx context.Context, rw http.ResponseWriter, r *http.Request, value interface{}) bool {
	return ReadLimit(ctx, rw, r, DefaultMaxRequestBodyBytes, value)
}

// ReadLimit is Read with an explicit request body size limit, for endpoints
// that need one above or below DefaultMaxRequestBodyBytes. Most callers set a
// tighter one.
//
// Callers must use this rather than wrapping r.Body in an http.MaxBytesReader
// themselves. Read installs its own limit, and nested readers compose as
// tightest-wins, so the default would override a larger caller-supplied limit.
func ReadLimit(ctx context.Context, rw http.ResponseWriter, r *http.Request, limit int64, value interface{}) bool {
	ctx, span := tracing.StartSpan(ctx)
	defer span.End()

	r.Body = http.MaxBytesReader(rw, r.Body, limit)

	err := json.NewDecoder(r.Body).Decode(value)
	if err != nil {
		// Report the limit the error carries, not the one this call installed.
		// Nested readers compose as tightest-wins and the error carries the
		// winner, so a caller that wrapped r.Body tighter would otherwise be
		// told a limit far looser than the one that rejected it.
		if mbe, ok := errors.AsType[*http.MaxBytesError](err); ok {
			// Must be r.Context(), not ctx: ctx is the caller's and need not
			// be the request's, but the tracker rides the request's.
			RecordRequestBodyLimit(r.Context(), mbe.Limit)
			Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{
				Message: "Request body too large.",
				Detail:  fmt.Sprintf("Maximum request body size is %d bytes.", mbe.Limit),
			})
			return false
		}
		Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message: "Request body must be valid JSON.",
			Detail:  err.Error(),
		})
		return false
	}
	err = Validate.Struct(value)
	var validationErrors validator.ValidationErrors
	if errors.As(err, &validationErrors) {
		apiErrors := make([]codersdk.ValidationError, 0, len(validationErrors))
		for _, validationError := range validationErrors {
			apiErrors = append(apiErrors, codersdk.ValidationError{
				Field:  validationError.Field(),
				Detail: fmt.Sprintf("Validation failed for tag %q with value: \"%v\"", validationError.Tag(), validationError.Value()),
			})
		}
		Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message:     "Validation failed.",
			Validations: apiErrors,
		})
		return false
	}
	if err != nil {
		Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error validating request body payload.",
			Detail:  err.Error(),
		})
		return false
	}
	return true
}

const websocketCloseMaxLen = 123

// WebsocketCloseSprintf formats a websocket close message and ensures it is
// truncated to the maximum allowed length.
func WebsocketCloseSprintf(format string, vars ...any) string {
	msg := fmt.Sprintf(format, vars...)

	// Cap msg length at 123 bytes. coder/websocket only allows close messages
	// of this length.
	if len(msg) > websocketCloseMaxLen {
		// Trim the string to 123 bytes. If we accidentally cut in the middle of
		// a UTF-8 character, remove it from the string.
		return strings.ToValidUTF8(msg[:websocketCloseMaxLen], "")
	}

	return msg
}

type EventSender func(rw http.ResponseWriter, r *http.Request) (
	sendEvent func(sse codersdk.ServerSentEvent) error,
	done <-chan struct{},
	err error,
)

// ServerSentEventSender establishes a Server-Sent Event connection and allows
// the consumer to send messages to the client.
//
// The function returned allows you to send a single message to the client,
// while the channel lets you listen for when the connection closes.
//
// As much as possible, this function should be avoided in favor of using the
// OneWayWebSocket function. See OneWayWebSocket for more context.
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (
	func(sse codersdk.ServerSentEvent) error,
	<-chan struct{},
	error,
) {
	return newServerSentEventSender(quartz.NewReal(), rw, r)
}

// ServerSentEventSenderWithClock is ServerSentEventSender with the heartbeat
// ticker driven by clk.
func ServerSentEventSenderWithClock(clk quartz.Clock) EventSender {
	return func(rw http.ResponseWriter, r *http.Request) (
		func(sse codersdk.ServerSentEvent) error,
		<-chan struct{},
		error,
	) {
		return newServerSentEventSender(clk, rw, r)
	}
}

func newServerSentEventSender(clk quartz.Clock, rw http.ResponseWriter, r *http.Request) (
	func(sse codersdk.ServerSentEvent) error,
	<-chan struct{},
	error,
) {
	h := rw.Header()
	h.Set("Content-Type", "text/event-stream")
	h.Set("Cache-Control", "no-cache")
	h.Set("Connection", "keep-alive")
	h.Set("X-Accel-Buffering", "no")

	f, ok := rw.(http.Flusher)
	if !ok {
		panic("http.ResponseWriter is not http.Flusher")
	}

	ctx := r.Context()
	closed := make(chan struct{})
	type sseEvent struct {
		payload []byte
		errC    chan error
	}
	eventC := make(chan sseEvent)

	// Synchronized handling of events (no guarantee of order).
	go func() {
		defer close(closed)
		ticker := clk.NewTicker(HeartbeatInterval, "ServerSentEventSender")
		defer ticker.Stop()

		for {
			var event sseEvent
			select {
			case <-ctx.Done():
				return
			case event = <-eventC:
			case <-ticker.C:
				event = sseEvent{
					payload: []byte(fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing)),
				}
			}

			_, err := rw.Write(event.payload)
			if event.errC != nil {
				event.errC <- err
			}
			if err != nil {
				return
			}
			f.Flush()
		}
	}()

	sendEvent := func(newEvent codersdk.ServerSentEvent) error {
		buf := &bytes.Buffer{}
		_, err := fmt.Fprintf(buf, "event: %s\n", newEvent.Type)
		if err != nil {
			return err
		}

		if newEvent.Data != nil {
			_, err = buf.WriteString("data: ")
			if err != nil {
				return err
			}

			enc := json.NewEncoder(buf)
			err = enc.Encode(newEvent.Data)
			if err != nil {
				return err
			}
		}

		err = buf.WriteByte('\n')
		if err != nil {
			return err
		}

		event := sseEvent{
			payload: buf.Bytes(),
			errC:    make(chan error, 1), // Buffered to prevent deadlock.
		}

		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-closed:
			return xerrors.New("server sent event sender closed")
		case eventC <- event:
			// Re-check closure signals after sending the event to allow
			// for early exit. We don't check closed here because it
			// can't happen while processing the event.
			select {
			case <-ctx.Done():
				return ctx.Err()
			case err := <-event.errC:
				return err
			}
		}
	}

	return sendEvent, closed, nil
}

// OneWayWebSocketEventSender establishes a new WebSocket connection that
// enforces one-way communication from the server to the client.
//
// The function returned allows you to send a single message to the client,
// while the channel lets you listen for when the connection closes.
//
// We must use an approach like this instead of Server-Sent Events for the
// browser, because on HTTP/1.1 connections, browsers are locked to no more than
// six HTTP connections for a domain total, across all tabs. If a user were to
// open a workspace in multiple tabs, the entire UI can start to lock up.
// WebSockets have no such limitation, no matter what HTTP protocol was used to
// establish the connection.
func OneWayWebSocketEventSender(log slog.Logger, watcher *WSWatcher) func(rw http.ResponseWriter, r *http.Request) (
	func(event codersdk.ServerSentEvent) error,
	<-chan struct{},
	error,
) {
	return func(rw http.ResponseWriter, r *http.Request) (
		func(event codersdk.ServerSentEvent) error,
		<-chan struct{},
		error,
	) {
		ctx, cancel := context.WithCancel(r.Context())
		r = r.WithContext(ctx)
		socket, err := websocket.Accept(rw, r, nil)
		if err != nil {
			cancel()
			return nil, nil, xerrors.Errorf("cannot establish connection: %w", err)
		}
		ctx = watcher.Watch(ctx, log, socket)

		eventC := make(chan codersdk.ServerSentEvent, 64)
		socketErrC := make(chan websocket.CloseError, 1)
		closed := make(chan struct{})
		go func() {
			defer cancel()
			defer close(closed)

			for {
				select {
				case event := <-eventC:
					writeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
					err := wsjson.Write(writeCtx, socket, event)
					cancel()
					if err == nil {
						continue
					}
					_ = socket.Close(websocket.StatusInternalError, "Unable to send newest message")
				case err := <-socketErrC:
					_ = socket.Close(err.Code, err.Reason)
				case <-ctx.Done():
					_ = socket.Close(websocket.StatusNormalClosure, "Connection closed")
				}
				return
			}
		}()

		// We have some tools in the UI code to help enforce one-way WebSocket
		// connections, but there's still the possibility that the client could send
		// a message when it's not supposed to. If that happens, the client likely
		// forgot to use those tools, and communication probably can't be trusted.
		// Better to just close the socket and force the UI to fix its mess
		go func() {
			_, _, err := socket.Read(ctx)
			if errors.Is(err, context.Canceled) {
				return
			}
			if err != nil {
				socketErrC <- websocket.CloseError{
					Code:   websocket.StatusInternalError,
					Reason: "Unable to process invalid message from client",
				}
				return
			}
			socketErrC <- websocket.CloseError{
				Code:   websocket.StatusProtocolError,
				Reason: "Clients cannot send messages for one-way WebSockets",
			}
		}()

		sendEvent := func(event codersdk.ServerSentEvent) error {
			// Prioritize context cancellation over sending to the
			// buffered channel. Without this check, both cases in
			// the select below can fire simultaneously when the
			// context is already done and the channel has capacity,
			// making the result nondeterministic.
			select {
			case <-ctx.Done():
				return ctx.Err()
			default:
			}
			select {
			case eventC <- event:
			case <-ctx.Done():
				return ctx.Err()
			}
			return nil
		}

		return sendEvent, closed, nil
	}
}

// WriteOAuth2Error writes an OAuth2-compliant error response per RFC 6749.
// This should be used for all OAuth2 endpoints (/oauth2/*) to ensure compliance.
func WriteOAuth2Error(ctx context.Context, rw http.ResponseWriter, status int, errorCode codersdk.OAuth2ErrorCode, description string) {
	// RFC 6749 §5.2: invalid_client SHOULD use 401 and MUST include a
	// WWW-Authenticate response header.
	if status == http.StatusUnauthorized && errorCode == codersdk.OAuth2ErrorCodeInvalidClient {
		rw.Header().Set("WWW-Authenticate", `Basic realm="coder"`)
	}

	Write(ctx, rw, status, codersdk.OAuth2Error{
		Error:            errorCode,
		ErrorDescription: description,
	})
}

// WriteOAuth2RequestTooLarge reports a request body over limit as an RFC 6749
// error, for the OAuth2 endpoints that read a form body rather than decoding
// through Read. RFC 6749 defines no error code for a transport rejection, so
// invalid_request is the closest compliant framing.
//
// The limit is the one carried by the *http.MaxBytesError that tripped, so a
// caller of this function reports the bound that actually applied rather than
// the one it assumes applied.
func WriteOAuth2RequestTooLarge(ctx context.Context, rw http.ResponseWriter, limit int64) {
	RecordRequestBodyLimit(ctx, limit)
	WriteOAuth2Error(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.OAuth2ErrorCodeInvalidRequest,
		fmt.Sprintf("Maximum request body size is %d bytes.", limit))
}