mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
It's possible for websocket close messages to be too long, which cause them to silently fail without a proper close message. See error below: ``` 2022-03-31 17:08:34.862 [INFO] (stdlib) <close_notjs.go:72> "2022/03/31 17:08:34 websocket: failed to marshal close frame: reason string max is 123 but got \"insert provisioner daemon:Cannot encode []database.ProvisionerType into oid 19098 - []database.ProvisionerType must implement Encoder or be converted to a string\" with length 161" ```
136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
)
|
|
|
|
var (
|
|
validate *validator.Validate
|
|
usernameRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
|
|
)
|
|
|
|
// 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
|
|
})
|
|
err := validate.RegisterValidation("username", func(fl validator.FieldLevel) bool {
|
|
f := fl.Field().Interface()
|
|
str, ok := f.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if len(str) > 32 {
|
|
return false
|
|
}
|
|
if len(str) < 1 {
|
|
return false
|
|
}
|
|
return usernameRegex.MatchString(str)
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Response represents a generic HTTP response.
|
|
type Response struct {
|
|
Message string `json:"message" validate:"required"`
|
|
Errors []Error `json:"errors,omitempty" validate:"required"`
|
|
}
|
|
|
|
// Error represents a scoped error to a user input.
|
|
type Error struct {
|
|
Field string `json:"field" validate:"required"`
|
|
Code string `json:"code" validate:"required"`
|
|
}
|
|
|
|
// Write outputs a standardized format to an HTTP response body.
|
|
func Write(rw http.ResponseWriter, status int, response Response) {
|
|
buf := &bytes.Buffer{}
|
|
enc := json.NewEncoder(buf)
|
|
enc.SetEscapeHTML(true)
|
|
err := enc.Encode(response)
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
rw.WriteHeader(status)
|
|
_, err = rw.Write(buf.Bytes())
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read decodes JSON from the HTTP request into the value provided.
|
|
// It uses go-validator to validate the incoming request body.
|
|
func Read(rw http.ResponseWriter, r *http.Request, value interface{}) bool {
|
|
err := json.NewDecoder(r.Body).Decode(value)
|
|
if err != nil {
|
|
Write(rw, http.StatusBadRequest, Response{
|
|
Message: fmt.Sprintf("read body: %s", err.Error()),
|
|
})
|
|
return false
|
|
}
|
|
err = validate.Struct(value)
|
|
var validationErrors validator.ValidationErrors
|
|
if errors.As(err, &validationErrors) {
|
|
apiErrors := make([]Error, 0, len(validationErrors))
|
|
for _, validationError := range validationErrors {
|
|
apiErrors = append(apiErrors, Error{
|
|
Field: validationError.Field(),
|
|
Code: validationError.Tag(),
|
|
})
|
|
}
|
|
Write(rw, http.StatusBadRequest, Response{
|
|
Message: "Validation failed",
|
|
Errors: apiErrors,
|
|
})
|
|
return false
|
|
}
|
|
if err != nil {
|
|
Write(rw, http.StatusInternalServerError, Response{
|
|
Message: fmt.Sprintf("validation: %s", 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. nhooyr/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(string(msg[123]), "")
|
|
}
|
|
|
|
return msg
|
|
}
|