After this commit, dev auth would start working with the `REMARK_URL` hostname instead of the previously hardcoded 127.0.0.1. Breaks development setup where `REMARK_URL` was set to a non-standard value and dev auth was running on 127.0.0.1 and working, as, after that change, it would stop working.
235 lines
6.8 KiB
Go
235 lines
6.8 KiB
Go
package render
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
)
|
|
|
|
// M is a convenience alias for quickly building a map structure that is going
|
|
// out to a responder. Just a short-hand.
|
|
type M map[string]interface{}
|
|
|
|
// Respond is a package-level variable set to our default Responder. We do this
|
|
// because it allows you to set render.Respond to another function with the
|
|
// same function signature, while also utilizing the render.Responder() function
|
|
// itself. Effectively, allowing you to easily add your own logic to the package
|
|
// defaults. For example, maybe you want to test if v is an error and respond
|
|
// differently, or log something before you respond.
|
|
var Respond = DefaultResponder
|
|
|
|
// StatusCtxKey is a context key to record a future HTTP response status code.
|
|
var StatusCtxKey = &contextKey{"Status"}
|
|
|
|
// Status sets a HTTP response status code hint into request context at any point
|
|
// during the request life-cycle. Before the Responder sends its response header
|
|
// it will check the StatusCtxKey
|
|
func Status(r *http.Request, status int) {
|
|
*r = *r.WithContext(context.WithValue(r.Context(), StatusCtxKey, status))
|
|
}
|
|
|
|
// Respond handles streaming JSON and XML responses, automatically setting the
|
|
// Content-Type based on request headers. It will default to a JSON response.
|
|
func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
if v != nil {
|
|
switch reflect.TypeOf(v).Kind() {
|
|
case reflect.Chan:
|
|
switch GetAcceptedContentType(r) {
|
|
case ContentTypeEventStream:
|
|
channelEventStream(w, r, v)
|
|
return
|
|
default:
|
|
v = channelIntoSlice(w, r, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format response based on request Accept header.
|
|
switch GetAcceptedContentType(r) {
|
|
case ContentTypeJSON:
|
|
JSON(w, r, v)
|
|
case ContentTypeXML:
|
|
XML(w, r, v)
|
|
default:
|
|
JSON(w, r, v)
|
|
}
|
|
}
|
|
|
|
// PlainText writes a string to the response, setting the Content-Type as
|
|
// text/plain.
|
|
func PlainText(w http.ResponseWriter, r *http.Request, v string) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write([]byte(v)) //nolint:errcheck
|
|
}
|
|
|
|
// Data writes raw bytes to the response, setting the Content-Type as
|
|
// application/octet-stream.
|
|
func Data(w http.ResponseWriter, r *http.Request, v []byte) {
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write(v) //nolint:errcheck
|
|
}
|
|
|
|
// HTML writes a string to the response, setting the Content-Type as text/html.
|
|
func HTML(w http.ResponseWriter, r *http.Request, v string) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write([]byte(v)) //nolint:errcheck
|
|
}
|
|
|
|
// JSON marshals 'v' to JSON, automatically escaping HTML and setting the
|
|
// Content-Type as application/json.
|
|
func JSON(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
buf := &bytes.Buffer{}
|
|
enc := json.NewEncoder(buf)
|
|
enc.SetEscapeHTML(true)
|
|
if err := enc.Encode(v); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
w.Write(buf.Bytes()) //nolint:errcheck
|
|
}
|
|
|
|
// XML marshals 'v' to JSON, setting the Content-Type as application/xml. It
|
|
// will automatically prepend a generic XML header (see encoding/xml.Header) if
|
|
// one is not found in the first 100 bytes of 'v'.
|
|
func XML(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
b, err := xml.Marshal(v)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
|
if status, ok := r.Context().Value(StatusCtxKey).(int); ok {
|
|
w.WriteHeader(status)
|
|
}
|
|
|
|
// Try to find <?xml header in first 100 bytes (just in case there're some XML comments).
|
|
findHeaderUntil := len(b)
|
|
if findHeaderUntil > 100 {
|
|
findHeaderUntil = 100
|
|
}
|
|
if !bytes.Contains(b[:findHeaderUntil], []byte("<?xml")) {
|
|
// No header found. Print it out first.
|
|
w.Write([]byte(xml.Header)) //nolint:errcheck
|
|
}
|
|
|
|
w.Write(b) //nolint:errcheck
|
|
}
|
|
|
|
// NoContent returns a HTTP 204 "No Content" response.
|
|
func NoContent(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func channelEventStream(w http.ResponseWriter, r *http.Request, v interface{}) {
|
|
if reflect.TypeOf(v).Kind() != reflect.Chan {
|
|
panic(fmt.Sprintf("render: event stream expects a channel, not %v", reflect.TypeOf(v).Kind()))
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
if r.ProtoMajor == 1 {
|
|
// An endpoint MUST NOT generate an HTTP/2 message containing connection-specific header fields.
|
|
// Source: RFC7540
|
|
w.Header().Set("Connection", "keep-alive")
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
ctx := r.Context()
|
|
for {
|
|
switch chosen, recv, ok := reflect.Select([]reflect.SelectCase{
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())},
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(v)},
|
|
}); chosen {
|
|
case 0: // equivalent to: case <-ctx.Done()
|
|
w.Write([]byte("event: error\ndata: {\"error\":\"Server Timeout\"}\n\n")) //nolint:errcheck
|
|
return
|
|
|
|
default: // equivalent to: case v, ok := <-stream
|
|
if !ok {
|
|
w.Write([]byte("event: EOF\n\n")) //nolint:errcheck
|
|
return
|
|
}
|
|
v := recv.Interface()
|
|
|
|
// Build each channel item.
|
|
if rv, ok := v.(Renderer); ok {
|
|
err := renderer(w, r, rv)
|
|
if err != nil {
|
|
v = err
|
|
} else {
|
|
v = rv
|
|
}
|
|
}
|
|
|
|
bytes, err := json.Marshal(v)
|
|
if err != nil {
|
|
w.Write([]byte(fmt.Sprintf("event: error\ndata: {\"error\":\"%v\"}\n\n", err))) //nolint:errcheck
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
continue
|
|
}
|
|
w.Write([]byte(fmt.Sprintf("event: data\ndata: %s\n\n", bytes))) //nolint:errcheck
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// channelIntoSlice buffers channel data into a slice.
|
|
func channelIntoSlice(w http.ResponseWriter, r *http.Request, from interface{}) interface{} {
|
|
ctx := r.Context()
|
|
|
|
var to []interface{}
|
|
for {
|
|
switch chosen, recv, ok := reflect.Select([]reflect.SelectCase{
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())},
|
|
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(from)},
|
|
}); chosen {
|
|
case 0: // equivalent to: case <-ctx.Done()
|
|
http.Error(w, "Server Timeout", http.StatusGatewayTimeout)
|
|
return nil
|
|
|
|
default: // equivalent to: case v, ok := <-stream
|
|
if !ok {
|
|
return to
|
|
}
|
|
v := recv.Interface()
|
|
|
|
// Render each channel item.
|
|
if rv, ok := v.(Renderer); ok {
|
|
err := renderer(w, r, rv)
|
|
if err != nil {
|
|
v = err
|
|
} else {
|
|
v = rv
|
|
}
|
|
}
|
|
|
|
to = append(to, v)
|
|
}
|
|
}
|
|
}
|